54
shuffle/frontend/Dockerfile
Normal 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
|
20
shuffle/frontend/README.md
Normal 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
|
||||
```
|
3
shuffle/frontend/build.sh
Normal file
@ -0,0 +1,3 @@
|
||||
npm run build
|
||||
rm -rf ../backend/go-app/build
|
||||
cp -r build/ ../backend/go-app/build
|
19
shuffle/frontend/certs/certreq.csr
Normal 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-----
|
21
shuffle/frontend/certs/fullchain.pem
Normal 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-----
|
27
shuffle/frontend/certs/privkey.pem
Normal 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-----
|
9
shuffle/frontend/confd/conf.d/nginx.conf.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[template]
|
||||
src = "nginx.conf"
|
||||
dest = "/etc/nginx/nginx.conf"
|
||||
uid = 0
|
||||
gid = 0
|
||||
mode = "0644"
|
||||
keys = [
|
||||
"/",
|
||||
]
|
125
shuffle/frontend/confd/templates/nginx.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
7
shuffle/frontend/entrypoint.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# generate configs
|
||||
/usr/local/bin/confd -backend="env" -confdir="/etc/confd" -onetime
|
||||
|
||||
# run main command
|
||||
exec "$@"
|
116
shuffle/frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
shuffle/frontend/public/favicon.ico
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
shuffle/frontend/public/images/Arrow.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
shuffle/frontend/public/images/Shuffle_logo.png
Normal file
After Width: | Height: | Size: 217 KiB |
BIN
shuffle/frontend/public/images/Shuffle_logo_new.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -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 |
BIN
shuffle/frontend/public/images/demo1.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
shuffle/frontend/public/images/demo2.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
shuffle/frontend/public/images/demo3.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
shuffle/frontend/public/images/detectionframework.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
shuffle/frontend/public/images/experienced.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
shuffle/frontend/public/images/finalize.gif
Normal file
After Width: | Height: | Size: 154 KiB |
After Width: | Height: | Size: 5.2 KiB |
5
shuffle/frontend/public/images/logos/orange_logo.svg
Normal 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 |
BIN
shuffle/frontend/public/images/social/discord.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
shuffle/frontend/public/images/social/shuffle_logo_round.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
shuffle/frontend/public/images/testing.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
shuffle/frontend/public/images/welcome-to-shuffle.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
shuffle/frontend/public/images/welcome_cog.png
Normal file
After Width: | Height: | Size: 79 KiB |
19
shuffle/frontend/public/images/workflows/UserInput.svg
Normal 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 |
8
shuffle/frontend/public/images/workflows/UserInput2.svg
Normal 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 |
17
shuffle/frontend/public/index.html
Normal 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>
|
14
shuffle/frontend/public/manifest.json
Normal 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
@ -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
|
586
shuffle/frontend/src/App.jsx
Normal file
@ -0,0 +1,586 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { Link, Route, Routes, BrowserRouter, useNavigate } from "react-router-dom";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { removeCookies, useCookies } from "react-cookie";
|
||||
|
||||
import Workflows from "./views/Workflows";
|
||||
import GettingStarted from "./views/GettingStarted";
|
||||
import AngularWorkflow from "./views/AngularWorkflow.jsx";
|
||||
|
||||
import Header from "./components/Header.jsx";
|
||||
import theme from "./theme";
|
||||
import Apps from "./views/Apps";
|
||||
import AppCreator from "./views/AppCreator";
|
||||
|
||||
import Welcome from "./views/Welcome.jsx";
|
||||
import Dashboard from "./views/Dashboard.jsx";
|
||||
import DashboardView from "./views/DashboardViews.jsx";
|
||||
import AdminSetup from "./views/AdminSetup";
|
||||
import Admin from "./views/Admin";
|
||||
import Docs from "./views/Docs.jsx";
|
||||
//import Introduction from "./views/Introduction";
|
||||
import SetAuthentication from "./views/SetAuthentication";
|
||||
import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
|
||||
import Search from "./views/Search.jsx";
|
||||
import RunWorkflow from "./views/RunWorkflow.jsx";
|
||||
|
||||
import LoginPage from "./views/LoginPage";
|
||||
import SettingsPage from "./views/SettingsPage";
|
||||
import KeepAlive from "./views/KeepAlive.jsx";
|
||||
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
|
||||
import UpdateAuthentication from "./views/UpdateAuthentication.jsx";
|
||||
import FrameworkWrapper from "./views/FrameworkWrapper.jsx";
|
||||
import ScrollToTop from "./components/ScrollToTop";
|
||||
import AlertTemplate from "./components/AlertTemplate";
|
||||
import { useAlert, positions, Provider } from "react-alert";
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import Drift from "react-driftjs";
|
||||
|
||||
// Production - backend proxy forwarding in nginx
|
||||
var globalUrl = window.location.origin;
|
||||
|
||||
// CORS used for testing purposes. Should only happen with specific port and http
|
||||
if (window.location.port === "3000") {
|
||||
globalUrl = "http://localhost:5001";
|
||||
//globalUrl = "http://localhost:5002"
|
||||
}
|
||||
|
||||
// Development on Github Codespaces
|
||||
if (globalUrl.includes("app.github.dev")) {
|
||||
//globalUrl = globalUrl.replace("3000", "5001")
|
||||
globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.preview.app.github.dev"
|
||||
}
|
||||
//console.log("global: ", globalUrl)
|
||||
|
||||
const App = (message, props) => {
|
||||
|
||||
const [userdata, setUserData] = useState({});
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const [cookies, setCookie, removeCookie] = useCookies([])
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [dataset, setDataset] = useState(false)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const [curpath, setCurpath] = useState(typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (dataset === false) {
|
||||
getUserNotifications();
|
||||
checkLogin();
|
||||
setDataset(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (
|
||||
isLoaded &&
|
||||
!isLoggedIn &&
|
||||
!window.location.pathname.startsWith("/login") &&
|
||||
!window.location.pathname.startsWith("/docs") &&
|
||||
!window.location.pathname.startsWith("/support") &&
|
||||
!window.location.pathname.startsWith("/detectionframework") &&
|
||||
!window.location.pathname.startsWith("/appframework") &&
|
||||
!window.location.pathname.startsWith("/adminsetup") &&
|
||||
!window.location.pathname.startsWith("/usecases")
|
||||
) {
|
||||
window.location = "/login";
|
||||
}
|
||||
|
||||
const getUserNotifications = () => {
|
||||
fetch(`${globalUrl}/api/v1/users/notifications`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cors: "cors",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
if (
|
||||
responseJson.success === true &&
|
||||
responseJson.notifications !== null &&
|
||||
responseJson.notifications !== undefined &&
|
||||
responseJson.notifications.length > 0
|
||||
) {
|
||||
//console.log("RESP: ", responseJson)
|
||||
setNotifications(responseJson.notifications);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Failed getting notifications for user: ", error);
|
||||
});
|
||||
};
|
||||
|
||||
const checkLogin = () => {
|
||||
var baseurl = globalUrl;
|
||||
fetch(`${globalUrl}/api/v1/getinfo`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
var userInfo = {};
|
||||
if (responseJson.success === true) {
|
||||
//console.log("USER: ", responseJson);
|
||||
|
||||
userInfo = responseJson;
|
||||
setIsLoggedIn(true);
|
||||
//console.log("Cookies: ", cookies)
|
||||
// Updating cookie every request
|
||||
for (var key in responseJson["cookies"]) {
|
||||
setCookie(
|
||||
responseJson["cookies"][key].key,
|
||||
responseJson["cookies"][key].value,
|
||||
{ path: "/" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Handling Ethereum update
|
||||
|
||||
//console.log("USER: ", userInfo)
|
||||
setUserData(userInfo);
|
||||
setIsLoaded(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
setIsLoaded(true);
|
||||
});
|
||||
};
|
||||
|
||||
// Dumb for content load (per now), but good for making the site not suddenly reload parts (ajax thingies)
|
||||
|
||||
const options = {
|
||||
timeout: 9000,
|
||||
position: positions.BOTTOM_LEFT,
|
||||
};
|
||||
|
||||
const handleFirstInteraction = (event) => {
|
||||
console.log("First interaction: ", event)
|
||||
}
|
||||
|
||||
const includedData =
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
color: "rgba(255, 255, 255, 0.65)",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<ScrollToTop
|
||||
getUserNotifications={getUserNotifications}
|
||||
curpath={curpath}
|
||||
setCurpath={setCurpath}
|
||||
/>
|
||||
{!isLoaded ? null :
|
||||
userdata.chat_disabled === true ? null :
|
||||
<Drift
|
||||
appId="zfk9i7w3yizf"
|
||||
attributes={{
|
||||
name: userdata.username === undefined || userdata.username === null ? "OSS user" : `OSS ${userdata.username}`,
|
||||
}}
|
||||
eventHandlers={[
|
||||
{
|
||||
event: "conversation:firstInteraction",
|
||||
function: handleFirstInteraction
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
<Header
|
||||
notifications={notifications}
|
||||
setNotifications={setNotifications}
|
||||
checkLogin={checkLogin}
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
{...props}
|
||||
/>
|
||||
{/*
|
||||
<div style={{ height: 60 }} />
|
||||
*/}
|
||||
<Routes>
|
||||
<Route
|
||||
exact
|
||||
path="/login"
|
||||
element={
|
||||
<LoginPage
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
checkLogin={checkLogin}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/admin"
|
||||
element={
|
||||
<Admin
|
||||
userdata={userdata}
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
checkLogin={checkLogin}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/search" element={<Search serverside={false} isLoaded={isLoaded} userdata={userdata} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} {...props} /> } />
|
||||
<Route
|
||||
exact
|
||||
path="/admin/:key"
|
||||
element={
|
||||
<Admin
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{userdata.id !== undefined ? (
|
||||
<Route
|
||||
exact
|
||||
path="/settings"
|
||||
element={
|
||||
<SettingsPage
|
||||
isLoaded={isLoaded}
|
||||
setUserData={setUserData}
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Route
|
||||
exact
|
||||
path="/AdminSetup"
|
||||
element={
|
||||
<AdminSetup
|
||||
isLoaded={isLoaded}
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/detectionframework"
|
||||
element={
|
||||
<FrameworkWrapper
|
||||
selectedOption={"Draw"}
|
||||
showOptions={false}
|
||||
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/app"
|
||||
element={
|
||||
<FrameworkWrapper
|
||||
selectedOption={"Draw"}
|
||||
showOptions={false}
|
||||
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/usecases"
|
||||
element={
|
||||
<Dashboard
|
||||
userdata={userdata}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/apps/new"
|
||||
element={
|
||||
<AppCreator
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/apps/authentication" element={<UpdateAuthentication serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} register={true} isLoaded={isLoaded} globalUrl={globalUrl} setCookie={setCookie} cookies={cookies} {...props} />} />
|
||||
<Route
|
||||
exact
|
||||
path="/apps"
|
||||
element={
|
||||
<Apps
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/apps/edit/:appid"
|
||||
element={
|
||||
<AppCreator
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/workflows"
|
||||
element={
|
||||
<Workflows
|
||||
checkLogin={checkLogin}
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
cookies={cookies}
|
||||
userdata={userdata}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/getting-started"
|
||||
element={
|
||||
<GettingStarted
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
cookies={cookies}
|
||||
userdata={userdata}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/workflows/:key"
|
||||
element={
|
||||
<AngularWorkflow
|
||||
alert={alert}
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/workflows/:key/run" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
|
||||
<Route exact path="/workflows/:key/execute" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
|
||||
<Route
|
||||
exact
|
||||
path="/docs/:key"
|
||||
element={
|
||||
<Docs
|
||||
isMobile={isMobile}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/docs"
|
||||
element={
|
||||
//navigate(`/docs/about`)
|
||||
<Docs
|
||||
isMobile={isMobile}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/support"
|
||||
element={
|
||||
//navigate(`/docs/about`)
|
||||
<Docs
|
||||
isMobile={isMobile}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/set_authentication"
|
||||
element={
|
||||
<SetAuthentication
|
||||
userdata={userdata}
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/login_sso"
|
||||
element={
|
||||
<SetAuthenticationSSO
|
||||
userdata={userdata}
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/keepalive"
|
||||
element={
|
||||
<KeepAlive
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/dashboards"
|
||||
element={
|
||||
<DashboardView
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/welcome"
|
||||
element={
|
||||
<Welcome
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
cookies={cookies}
|
||||
userdata={userdata}
|
||||
checkLogin={checkLogin}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
element={
|
||||
<LoginPage
|
||||
isLoggedIn={isLoggedIn}
|
||||
setIsLoggedIn={setIsLoggedIn}
|
||||
register={true}
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
setCookie={setCookie}
|
||||
cookies={cookies}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<CookiesProvider>
|
||||
<BrowserRouter>
|
||||
<Provider template={AlertTemplate} {...options}>
|
||||
{includedData}
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
<ToastContainer
|
||||
position="bottom-center"
|
||||
autoClose={5000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="dark"
|
||||
/>
|
||||
</CookiesProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
1350
shuffle/frontend/src/__test__/appdata.js
Normal file
6
shuffle/frontend/src/__test__/environmentdata.js
Normal file
@ -0,0 +1,6 @@
|
||||
const data = [
|
||||
{ name: "cloud", type: "cloud" },
|
||||
{ name: "onprem", type: "onprem" },
|
||||
];
|
||||
|
||||
export default data;
|
29
shuffle/frontend/src/__test__/scheduledata.js
Normal file
@ -0,0 +1,29 @@
|
||||
const Data = {
|
||||
src: {
|
||||
name: "Get Tickets",
|
||||
description: "Get tickets",
|
||||
outputparameters: [
|
||||
{
|
||||
name: "SymptomDescription",
|
||||
schema: { type: "string" },
|
||||
},
|
||||
{ name: "DetailedDescription", schema: { type: "string" } },
|
||||
{ name: "EventSource", schema: { type: "string" } },
|
||||
],
|
||||
},
|
||||
dst: {
|
||||
name: "Create alert",
|
||||
description: "Create alert in TheHive",
|
||||
inputparameters: [
|
||||
{
|
||||
name: "title",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
},
|
||||
{ name: "description", required: true, schema: { type: "string" } },
|
||||
{ name: "source", required: true, schema: { type: "string" } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default Data;
|
15
shuffle/frontend/src/__test__/webhookdata.js
Normal file
@ -0,0 +1,15 @@
|
||||
const data = {
|
||||
id: "8ccf0bec1fde018771ab685d2a40bd52",
|
||||
info: {
|
||||
url: "",
|
||||
name: "testing",
|
||||
description: "wut",
|
||||
},
|
||||
transforms: {},
|
||||
actions: {},
|
||||
type: "webhook",
|
||||
status: "uninitialized",
|
||||
running: false,
|
||||
};
|
||||
|
||||
export default data;
|
169
shuffle/frontend/src/__test__/workflowdata.js
Normal file
@ -0,0 +1,169 @@
|
||||
const data = {
|
||||
actions: [
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "70574332-da82-cf17-c723-75fa7b8493c2",
|
||||
is_valid: true,
|
||||
label: "hello_world",
|
||||
environment: "onprem",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: 353.7438792397648, y: 260.6717930890377 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "30522433-56ed-53c3-575d-766e282e1d3e",
|
||||
is_valid: true,
|
||||
label: "random_number",
|
||||
environment: "cloud",
|
||||
name: "random_number",
|
||||
parameters: null,
|
||||
position: { x: 458.30040774503794, y: 104.27580103487651 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "5b7ac5b5-9514-02b9-ebe0-998c0843b104",
|
||||
is_valid: false,
|
||||
label: "hello_world_2",
|
||||
environment: "onprem",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: 414.7256019053981, y: -140.46450482659628 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "7e6e7a19-4636-cebc-91c4-052a3769a18b",
|
||||
is_valid: true,
|
||||
label: "hello_world_3",
|
||||
environment: "cloud",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: 83.59752786243806, y: 50.232317715020734 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "edbf927d-5a00-2405-28ed-47982cdf5110",
|
||||
is_valid: true,
|
||||
label: "hello_world_4",
|
||||
environment: "cloud",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: -147.30681300186404, y: 89.16690830150289 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "4844a855-1e2b-669d-fc72-5f398321ac5d",
|
||||
is_valid: false,
|
||||
label: "hello_world_5",
|
||||
environment: "onprem",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: 130.24982593523967, y: 233.8325632286361 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
|
||||
is_valid: true,
|
||||
label: "hello_world_6",
|
||||
environment: "cloud",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: 83.551088005629, y: -105.15867327274223 },
|
||||
priority: 0,
|
||||
},
|
||||
{
|
||||
app_name: "hello_world",
|
||||
app_version: "1.0.0",
|
||||
errors: null,
|
||||
id: "469d8c2b-52ac-e397-9a29-becccd04aed8",
|
||||
is_valid: true,
|
||||
label: "hello_world_7",
|
||||
environment: "cloud",
|
||||
name: "hello_world",
|
||||
parameters: null,
|
||||
position: { x: 314.4987657226086, y: 10.167183586257954 },
|
||||
priority: 0,
|
||||
},
|
||||
],
|
||||
branches: [
|
||||
{
|
||||
destination_id: "30522433-56ed-53c3-575d-766e282e1d3e",
|
||||
id: "4bcb9795-94e6-7d5f-2074-0d5b27784e0b",
|
||||
source_id: "70574332-da82-cf17-c723-75fa7b8493c2",
|
||||
},
|
||||
{
|
||||
destination_id: "5b7ac5b5-9514-02b9-ebe0-998c0843b104",
|
||||
id: "fe0ab8e4-a535-61cd-3c09-8fd3d8e40769",
|
||||
source_id: "30522433-56ed-53c3-575d-766e282e1d3e",
|
||||
},
|
||||
{
|
||||
destination_id: "469d8c2b-52ac-e397-9a29-becccd04aed8",
|
||||
id: "8b9ee9bc-b0ab-0bb6-af61-46d4594b2663",
|
||||
source_id: "30522433-56ed-53c3-575d-766e282e1d3e",
|
||||
},
|
||||
{
|
||||
destination_id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
|
||||
id: "c204d5ef-9cc1-d906-9988-86a624c57783",
|
||||
source_id: "469d8c2b-52ac-e397-9a29-becccd04aed8",
|
||||
},
|
||||
{
|
||||
destination_id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
|
||||
id: "1ffb3934-60ec-8f80-5cee-3ddc0a37fdb6",
|
||||
source_id: "5b7ac5b5-9514-02b9-ebe0-998c0843b104",
|
||||
},
|
||||
{
|
||||
destination_id: "edbf927d-5a00-2405-28ed-47982cdf5110",
|
||||
id: "9c7fb048-9d0d-cb84-9ba0-be729af9b4d1",
|
||||
source_id: "6d1d3f8a-1ac9-3db4-0e0f-2fe32e9d3c09",
|
||||
},
|
||||
{
|
||||
destination_id: "edbf927d-5a00-2405-28ed-47982cdf5110",
|
||||
id: "e3ab104e-fc8b-3af5-8daa-bfa57bcf9690",
|
||||
source_id: "7e6e7a19-4636-cebc-91c4-052a3769a18b",
|
||||
},
|
||||
{
|
||||
destination_id: "7e6e7a19-4636-cebc-91c4-052a3769a18b",
|
||||
id: "b6626081-22dd-3af3-b899-480f60d886ca",
|
||||
source_id: "30522433-56ed-53c3-575d-766e282e1d3e",
|
||||
},
|
||||
{
|
||||
destination_id: "4844a855-1e2b-669d-fc72-5f398321ac5d",
|
||||
id: "4275cf97-0447-bbda-0c80-ab20d389de1a",
|
||||
source_id: "edbf927d-5a00-2405-28ed-47982cdf5110",
|
||||
},
|
||||
],
|
||||
conditions: [],
|
||||
triggers: [],
|
||||
transforms: [],
|
||||
description: "asd",
|
||||
id: "2f299808-0f1b-4ae0-97fc-ac17483dfcf7",
|
||||
id: "2f299808-0f1b-4ae0-97fc-ac17483dfcf7",
|
||||
is_valid: true,
|
||||
name: "test2",
|
||||
start: "70574332-da82-cf17-c723-75fa7b8493c2",
|
||||
owner: { username: "", id: "", orgs: "" },
|
||||
execution_org: { name: "", org: "", users: null, id: "" },
|
||||
workflow_variables: null,
|
||||
};
|
||||
|
||||
export default data;
|
9
shuffle/frontend/src/assets/img/bag.svg
Normal file
After Width: | Height: | Size: 14 KiB |
9
shuffle/frontend/src/assets/img/book.svg
Normal file
After Width: | Height: | Size: 20 KiB |
1
shuffle/frontend/src/assets/img/default-monochrome.svg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
shuffle/frontend/src/assets/img/github_phishing_email.png
Normal file
After Width: | Height: | Size: 449 KiB |
BIN
shuffle/frontend/src/assets/img/github_shuffle_img.png
Normal file
After Width: | Height: | Size: 431 KiB |
BIN
shuffle/frontend/src/assets/img/icpl_logo.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
shuffle/frontend/src/assets/img/kafka.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
shuffle/frontend/src/assets/img/logo.png
Normal file
After Width: | Height: | Size: 58 KiB |
26
shuffle/frontend/src/assets/img/logo.svg
Normal file
@ -0,0 +1,26 @@
|
||||
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
|
||||
<!---->
|
||||
<defs>
|
||||
<linearGradient y2="0%" x2="100%" y1="0%" x1="0%" id="30c29011-a081-4741-b6bb-e06d8873e7b7" gradientTransform="rotate(25)">
|
||||
<stop stop-color=" rgb(169, 37, 128)" offset="0%"/>
|
||||
<stop stop-color=" rgb(247, 188, 0)" offset="100%"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
<g>
|
||||
<title>background</title>
|
||||
<rect fill="none" id="canvas_background" height="202" width="202" y="-1" x="-1"/>
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 1</title>
|
||||
<g fill="url(#30c29011-a081-4741-b6bb-e06d8873e7b7)" transform="matrix(1.1414221157695137,0,0,1.1414221157695137,-10.343879888974278,-11.465698853151771) " id="1085c47b-b6bc-4e6d-b1f9-4301cfb34be7">
|
||||
<switch transform="translate(2.6282968521118164,0) translate(0.0000033420565159758553,0) translate(-0.2776981592178345,0) translate(44.68110275268555,47.30940246582031) ">
|
||||
<g id="svg_2">
|
||||
<path id="svg_3" d="m89.141,35.617c-2.891,-12.765 -11.297,-21.212 -20.442,-25.253c11.371,10.018 21.846,29.405 8.814,47.069c-5.406,7.331 -16.217,10.228 -20.746,8.184c0,0 7.002,-1.487 11.357,-5.295c8.469,-8.017 11.299,-20.932 4.52,-32.673a25.839,25.839 0 0 0 -3.357,-4.575c-0.018,-0.021 -0.033,-0.042 -0.051,-0.062c-4.764,-5.683 -11.307,-9.075 -18.337,-10.116c-9.127,-1.715 -20.896,0.515 -29.71,8.666c-9.609,8.888 -12.721,20.391 -11.647,30.333c2.989,-14.858 14.542,-33.622 36.355,-31.171c9.053,1.019 16.965,8.932 17.461,13.877c0,0 -5.277,-8.281 -18.365,-8.281c-0.24,0.004 -0.747,0.024 -0.761,0.024l-0.167,0.01c-8.51,0.333 -16.783,4.784 -21.83,13.526a25.819,25.819 0 0 0 -2.284,5.195l-0.028,0.074c-2.539,6.968 -2.205,14.33 0.407,20.938c3.079,8.763 10.896,17.841 22.361,21.397c12.502,3.878 24.02,0.82 32.092,-5.079c-14.363,4.837 -36.389,4.216 -45.173,-15.899c-3.645,-8.35 -0.749,-19.159 3.287,-22.061c0,0 -4.534,8.71 2.012,20.044c4.482,7.484 12.617,12.658 22.949,12.658c0.498,0 4.488,-0.311 5.926,-0.633l0.08,-0.011c7.303,-1.286 13.512,-5.256 17.928,-10.822c6.05,-7.046 10.003,-18.356 7.349,-30.064z"/>
|
||||
</g>
|
||||
</switch>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
BIN
shuffle/frontend/src/assets/img/logo.webp
Normal file
After Width: | Height: | Size: 5.7 KiB |
9
shuffle/frontend/src/assets/img/mobile.svg
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
shuffle/frontend/src/assets/img/shuffle_adminaccount.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
shuffle/frontend/src/assets/img/shuffle_architecture.png
Normal file
After Width: | Height: | Size: 143 KiB |
BIN
shuffle/frontend/src/assets/img/shuffle_webhook.png
Normal file
After Width: | Height: | Size: 440 KiB |
3
shuffle/frontend/src/assets/img/transform.sh
Normal file
@ -0,0 +1,3 @@
|
||||
# resize: convert schedule.png -resize 100x100\> schedule100.png
|
||||
# base64: - cat picture.png | base64 -w 0
|
||||
# js insert: data:image/png;base64,<base64>
|
BIN
shuffle/frontend/src/assets/img/webhook.png
Normal file
After Width: | Height: | Size: 16 KiB |
427
shuffle/frontend/src/charts.js
Normal file
@ -0,0 +1,427 @@
|
||||
/*!
|
||||
|
||||
=========================================================
|
||||
* Black Dashboard React v1.1.0
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/black-dashboard-react
|
||||
* Copyright 2020 Creative Tim (https://www.creative-tim.com)
|
||||
* Licensed under MIT (https://github.com/creativetimofficial/black-dashboard-react/blob/master/LICENSE.md)
|
||||
|
||||
* Coded by Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
*/
|
||||
// ##############################
|
||||
// // // Chart variables
|
||||
// #############################
|
||||
|
||||
// chartExample1 and chartExample2 options
|
||||
let chart1_2_options = {
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltips: {
|
||||
backgroundColor: "#f5f5f5",
|
||||
titleFontColor: "#333",
|
||||
bodyFontColor: "#666",
|
||||
bodySpacing: 4,
|
||||
xPadding: 12,
|
||||
mode: "nearest",
|
||||
intersect: 0,
|
||||
position: "nearest",
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
barPercentage: 1.6,
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(29,140,248,0.0)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: 60,
|
||||
suggestedMax: 125,
|
||||
padding: 20,
|
||||
fontColor: "#9a9a9a",
|
||||
},
|
||||
},
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.6,
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(29,140,248,0.1)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
padding: 20,
|
||||
fontColor: "#9a9a9a",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// #########################################
|
||||
// // // used inside src/views/Dashboard.js
|
||||
// #########################################
|
||||
let chartExample1 = {
|
||||
data1: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
|
||||
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
|
||||
|
||||
return {
|
||||
labels: [
|
||||
"JAN",
|
||||
"FEB",
|
||||
"MAR",
|
||||
"APR",
|
||||
"MAY",
|
||||
"JUN",
|
||||
"JUL",
|
||||
"AUG",
|
||||
"SEP",
|
||||
"OCT",
|
||||
"NOV",
|
||||
"DEC",
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: "My First dataset",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
borderColor: "#1f8ef1",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
pointBackgroundColor: "#1f8ef1",
|
||||
pointBorderColor: "rgba(255,255,255,0)",
|
||||
pointHoverBackgroundColor: "#1f8ef1",
|
||||
pointBorderWidth: 20,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 15,
|
||||
pointRadius: 4,
|
||||
data: [100, 70, 90, 70, 85, 60, 75, 60, 90, 80, 110, 100],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
data2: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
|
||||
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
|
||||
|
||||
return {
|
||||
labels: [
|
||||
"JAN",
|
||||
"FEB",
|
||||
"MAR",
|
||||
"APR",
|
||||
"MAY",
|
||||
"JUN",
|
||||
"JUL",
|
||||
"AUG",
|
||||
"SEP",
|
||||
"OCT",
|
||||
"NOV",
|
||||
"DEC",
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: "My First dataset",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
borderColor: "#1f8ef1",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
pointBackgroundColor: "#1f8ef1",
|
||||
pointBorderColor: "rgba(255,255,255,0)",
|
||||
pointHoverBackgroundColor: "#1f8ef1",
|
||||
pointBorderWidth: 20,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 15,
|
||||
pointRadius: 4,
|
||||
data: [80, 120, 105, 110, 95, 105, 90, 100, 80, 95, 70, 120],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
data3: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
|
||||
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
|
||||
|
||||
return {
|
||||
labels: [
|
||||
"JAN",
|
||||
"FEB",
|
||||
"MAR",
|
||||
"APR",
|
||||
"MAY",
|
||||
"JUN",
|
||||
"JUL",
|
||||
"AUG",
|
||||
"SEP",
|
||||
"OCT",
|
||||
"NOV",
|
||||
"DEC",
|
||||
],
|
||||
datasets: [
|
||||
{
|
||||
label: "My First dataset",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
borderColor: "#1f8ef1",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
pointBackgroundColor: "#1f8ef1",
|
||||
pointBorderColor: "rgba(255,255,255,0)",
|
||||
pointHoverBackgroundColor: "#1f8ef1",
|
||||
pointBorderWidth: 20,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 15,
|
||||
pointRadius: 4,
|
||||
data: [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
options: chart1_2_options,
|
||||
};
|
||||
|
||||
// #########################################
|
||||
// // // used inside src/views/Dashboard.js
|
||||
// #########################################
|
||||
let chartExample2 = {
|
||||
data: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
|
||||
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
|
||||
|
||||
return {
|
||||
labels: ["JUL", "AUG", "SEP", "OCT", "NOV", "DEC"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Data",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
borderColor: "#1f8ef1",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
pointBackgroundColor: "#1f8ef1",
|
||||
pointBorderColor: "rgba(255,255,255,0)",
|
||||
pointHoverBackgroundColor: "#1f8ef1",
|
||||
pointBorderWidth: 20,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 15,
|
||||
pointRadius: 4,
|
||||
data: [80, 100, 70, 80, 120, 80],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
options: chart1_2_options,
|
||||
};
|
||||
|
||||
// #########################################
|
||||
// // // used inside src/views/Dashboard.js
|
||||
// #########################################
|
||||
let chartExample3 = {
|
||||
data: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(72,72,176,0.1)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(72,72,176,0.0)");
|
||||
gradientStroke.addColorStop(0, "rgba(119,52,169,0)"); //purple colors
|
||||
|
||||
return {
|
||||
labels: ["USA", "GER", "AUS", "UK", "RO", "BR"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Countries",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
hoverBackgroundColor: gradientStroke,
|
||||
borderColor: "#d048b6",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
data: [53, 20, 10, 80, 100, 45],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltips: {
|
||||
backgroundColor: "#f5f5f5",
|
||||
titleFontColor: "#333",
|
||||
bodyFontColor: "#666",
|
||||
bodySpacing: 4,
|
||||
xPadding: 12,
|
||||
mode: "nearest",
|
||||
intersect: 0,
|
||||
position: "nearest",
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(225,78,202,0.1)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: 60,
|
||||
suggestedMax: 120,
|
||||
padding: 20,
|
||||
fontColor: "#9e9e9e",
|
||||
},
|
||||
},
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(225,78,202,0.1)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
padding: 20,
|
||||
fontColor: "#9e9e9e",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// #########################################
|
||||
// // // used inside src/views/Dashboard.js
|
||||
// #########################################
|
||||
const chartExample4 = {
|
||||
data: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(66,134,121,0.15)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(66,134,121,0.0)"); //green colors
|
||||
gradientStroke.addColorStop(0, "rgba(66,134,121,0)"); //green colors
|
||||
|
||||
return {
|
||||
labels: ["JUL", "AUG", "SEP", "OCT", "NOV"],
|
||||
datasets: [
|
||||
{
|
||||
label: "My First dataset",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
borderColor: "#00d6b4",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
pointBackgroundColor: "#00d6b4",
|
||||
pointBorderColor: "rgba(255,255,255,0)",
|
||||
pointHoverBackgroundColor: "#00d6b4",
|
||||
pointBorderWidth: 20,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 15,
|
||||
pointRadius: 4,
|
||||
data: [90, 27, 60, 12, 80],
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
|
||||
tooltips: {
|
||||
backgroundColor: "#f5f5f5",
|
||||
titleFontColor: "#333",
|
||||
bodyFontColor: "#666",
|
||||
bodySpacing: 4,
|
||||
xPadding: 12,
|
||||
mode: "nearest",
|
||||
intersect: 0,
|
||||
position: "nearest",
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
barPercentage: 1.6,
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(29,140,248,0.0)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: 50,
|
||||
suggestedMax: 125,
|
||||
padding: 20,
|
||||
fontColor: "#9e9e9e",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.6,
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(0,242,195,0.1)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
padding: 20,
|
||||
fontColor: "#9e9e9e",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
chartExample1, // in src/views/Dashboard.js
|
||||
chartExample2, // in src/views/Dashboard.js
|
||||
chartExample3, // in src/views/Dashboard.js
|
||||
chartExample4, // in src/views/Dashboard.js
|
||||
};
|
19
shuffle/frontend/src/components/AlertPopup.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const Popup = (props) => {
|
||||
const { data } = props;
|
||||
|
||||
const popupStyle = {
|
||||
position: "fixed",
|
||||
width: "300px",
|
||||
height: "50px",
|
||||
backgroundColor: "black",
|
||||
color: "white",
|
||||
};
|
||||
|
||||
const popupData = <div>HEY</div>;
|
||||
|
||||
return <div>{popupData}</div>;
|
||||
};
|
||||
|
||||
export default Popup;
|
53
shuffle/frontend/src/components/AlertTemplate.js
Normal file
@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Info as InfoIcon,
|
||||
Check as CheckIcon,
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
Close as CloseIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import {
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
|
||||
const alertStyle = {
|
||||
backgroundColor: "rgba(0,0,0,0.9)",
|
||||
color: "white",
|
||||
padding: 15,
|
||||
textTransform: "uppercase",
|
||||
borderRadius: "3px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
boxShadow: "0px 2px 2px 2px rgba(0, 0, 0, 0.03)",
|
||||
width: 300,
|
||||
boxSizing: "border-box",
|
||||
zIndex: 100001,
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
marginLeft: "20px",
|
||||
border: "none",
|
||||
backgroundColor: "transparent",
|
||||
cursor: "pointer",
|
||||
color: "#FFFFFF",
|
||||
};
|
||||
|
||||
const AlertTemplate = ({ message, options, style, close }) => {
|
||||
return (
|
||||
<div style={{ ...alertStyle, ...style }}>
|
||||
{options.type === "info" && <InfoIcon style={{ color: "white" }} />}
|
||||
{options.type === "success" && <CheckIcon style={{ color: "green" }} />}
|
||||
{options.type === "error" && (
|
||||
<ErrorOutlineIcon style={{ color: "red" }} />
|
||||
)}
|
||||
<Typography style={{ marginLeft: 15, flex: 2 }}>{message}</Typography>
|
||||
<button onClick={close} style={buttonStyle}>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertTemplate;
|
2319
shuffle/frontend/src/components/AppFramework.jsx
Normal file
385
shuffle/frontend/src/components/AppGrid.jsx
Normal file
@ -0,0 +1,385 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
|
||||
import {
|
||||
Search as SearchIcon,
|
||||
CloudQueue as CloudQueueIcon,
|
||||
Code as CodeIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, connectHitInsights } from 'react-instantsearch-dom';
|
||||
|
||||
import aa from 'search-insights'
|
||||
|
||||
import {
|
||||
Zoom,
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
|
||||
const AppGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata } = props
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integrate any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
//useEffect(() => {
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery)
|
||||
refine(foundQuery)
|
||||
defaultSearch = foundQuery
|
||||
}
|
||||
}
|
||||
//}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
defaultValue={defaultSearch}
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
placeholder="Find Apps..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
// Remove "q" from URL
|
||||
removeQuery("q")
|
||||
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
const Hits = ({ hits, insights }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
//console.log(hits)
|
||||
//var curhits = hits
|
||||
//if (hits.length > 0 && defaultApps.length === 0) {
|
||||
// setDefaultApps(hits)
|
||||
//}
|
||||
|
||||
//const [defaultApps, setDefaultApps] = React.useState([])
|
||||
//console.log(hits)
|
||||
//if (hits.length > 0 && hits.length !== innerHits.length) {
|
||||
// setInnerHits(hits)
|
||||
//}
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{hits.map((data, index) => {
|
||||
|
||||
workflowDelay += 50
|
||||
|
||||
const paperStyle = {
|
||||
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor,
|
||||
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
|
||||
border: `1px solid ${innerColor}`,
|
||||
padding: 15,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
minHeight: 116,
|
||||
}
|
||||
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
var parsedname = ""
|
||||
for (var key = 0; key < data.name.length; key++) {
|
||||
var character = data.name.charAt(key)
|
||||
if (character === character.toUpperCase()) {
|
||||
//console.log(data.name[key], data.name[key+1])
|
||||
if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
|
||||
} else {
|
||||
parsedname += " "
|
||||
}
|
||||
}
|
||||
|
||||
parsedname += character
|
||||
}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
|
||||
const appUrl = isCloud ? `/apps/${data.objectID}?queryID=${data.__queryID}` : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Grid item xs={xs} key={index}>
|
||||
<a href={appUrl} rel="noopener noreferrer" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>
|
||||
<Paper elevation={0} style={paperStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
/*
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `search_bar_click`,
|
||||
label: "",
|
||||
})
|
||||
*/
|
||||
}} onMouseOut={() => {
|
||||
setMouseHoverIndex(-1)
|
||||
}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `app_${parsedname}_${data.id}_click`,
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
|
||||
console.log(searchClient)
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Product Clicked',
|
||||
index: 'appsearch',
|
||||
objectIDs: [data.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: data.__queryID,
|
||||
positions: [data.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
}}>
|
||||
<ButtonBase style={{padding: 5, borderRadius: 3, minHeight: 100, minWidth: 100,}}>
|
||||
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 100, minWidth: 100, minHeight: 100, maxHeight: 100, display: "block", margin: "0 auto"}} />
|
||||
</ButtonBase>
|
||||
<div/>
|
||||
{index === mouseHoverIndex || showName === true ?
|
||||
parsedname
|
||||
:
|
||||
null
|
||||
}
|
||||
{data.generated ?
|
||||
<Tooltip title={"Created with App editor"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
|
||||
{data.invalid ?
|
||||
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: theme.palette.primary.main }}/>
|
||||
:
|
||||
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
|
||||
}
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={"Created with python (custom app)"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
|
||||
<CodeIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
|
||||
</Tooltip>
|
||||
}
|
||||
</Paper>
|
||||
</a>
|
||||
</Grid>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
//const CustomHits = connectHitInsights(aa)(Hits)
|
||||
const selectButtonStyle = {
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
minHeight: 50,
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
|
||||
{/*
|
||||
<div style={{padding: 10, }}>
|
||||
<Button
|
||||
style={selectButtonStyle}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
const searchField = document.createElement("shuffle_search_field")
|
||||
console.log("Field: ", searchField)
|
||||
if (searchField !== null & searchField !== undefined) {
|
||||
console.log("Set field.")
|
||||
searchField.value = "WHAT WABALABA"
|
||||
searchField.setAttribute("value", "WHAT WABALABA")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cases
|
||||
</Button>
|
||||
</div>
|
||||
*/}
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
<Configure clickAnalytics />
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What apps do you want to see?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: isMobile?"":120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppGrid;
|
403
shuffle/frontend/src/components/AppSearchButtons.jsx
Normal file
@ -0,0 +1,403 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import theme from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
|
||||
import aa from 'search-insights'
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ShowChartIcon from '@mui/icons-material/ShowChart';
|
||||
import ExploreIcon from '@mui/icons-material/Explore';
|
||||
import LightbulbIcon from "@mui/icons-material/Lightbulb";
|
||||
import NewReleasesIcon from "@mui/icons-material/NewReleases";
|
||||
import ExtensionIcon from "@mui/icons-material/Extension";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import FingerprintIcon from '@mui/icons-material/Fingerprint';
|
||||
import AppSearch from "../components/Appsearch.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Zoom,
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
Collapse,
|
||||
IconButton,
|
||||
Avatar,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
|
||||
const AppSearchButtons = (props) => {
|
||||
const { userdata, globalUrl, appFramework, defaultSearch, finishedApps, onNodeSelect, setDiscoveryData, appName, AppImage, setDefaultSearch, discoveryData } = props
|
||||
const ref = useRef()
|
||||
const [moreButton, setMoreButton] = useState(false);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const sizing = moreButton ? 510 : 480;
|
||||
const buttonWidth = 450;
|
||||
const buttonMargin = 10;
|
||||
const bottomButtonStyle = {
|
||||
borderRadius: 200,
|
||||
marginTop: moreButton ? 44 : "",
|
||||
height: 51,
|
||||
width: 510,
|
||||
fontSize: 16,
|
||||
// background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
|
||||
background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
|
||||
padding: "16px 24px",
|
||||
// top: 20,
|
||||
// margin: "auto",
|
||||
textTransform: 'capitalize',
|
||||
itemAlign: "center",
|
||||
// marginTop: 25
|
||||
// marginLeft: "65px",
|
||||
};
|
||||
const buttonStyle = {
|
||||
flex: 1,
|
||||
width: 224,
|
||||
padding: 25,
|
||||
margin: buttonMargin,
|
||||
color: "var(--White-text, #F1F1F1)",
|
||||
fontWeight: 400,
|
||||
fontSize: 17,
|
||||
background: "rgba(33, 33, 33, 1)",
|
||||
textTransform: 'capitalize',
|
||||
border: "1px solid rgba(33, 33, 33, 1)",
|
||||
borderRadius: 8,
|
||||
marginRight: 8,
|
||||
};
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
const mouseOver = (e) => {
|
||||
e.target.style.border = "1px solid #f85a3e";
|
||||
}
|
||||
const mouseOut = (e) => {
|
||||
e.target.style.border = "1px solid rgb(33, 33, 33)";
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid item xs={11} style={{ display: "flex" }}>
|
||||
|
||||
{/*<FormLabel style={{ color: "#B9B9BA" }}>Find your integrations!</FormLabel>*/}
|
||||
{/* <div style={{ display: "flex", width: 510, height:100 }}>
|
||||
<Button
|
||||
disabled={finishedApps.includes("CASES")}
|
||||
variant={
|
||||
defaultSearch === "CASES" ? "contained" : "outlined"
|
||||
}
|
||||
color="secondary"
|
||||
style={{
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
padding: 25,
|
||||
margin: buttonMargin,
|
||||
fontSize: 18,
|
||||
color: "var(--White-text, #F1F1F1)",
|
||||
fontWeight: 400,
|
||||
background: "rgba(33, 33, 33, 1)",
|
||||
borderRadius: 8,
|
||||
textTransform: 'capitalize',
|
||||
border: "1px solid rgba(33, 33, 33, 1)" ,
|
||||
}}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
// startIcon = {defaultSearch === "CASES" ? newSelectedApp.image_url : <LightbulbIcon/>}
|
||||
onClick={(event) => {
|
||||
onNodeSelect("CASES");
|
||||
setDefaultSearch(discoveryData.label)
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.cases === undefined || appFramework.cases.large_image === undefined ||
|
||||
appFramework === null || appFramework.cases === null || appFramework.cases.large_image === null || appFramework.cases.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<LightbulbIcon style={{ marginTop: 6 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={AppImage} />}
|
||||
<div style={{marginLeft: 8, }}>
|
||||
<Typography style={{display:"flex",border:"none"}} >Case Management</Typography>
|
||||
{appFramework === undefined || appFramework.cases === undefined || appFramework.cases.name === undefined ||
|
||||
appFramework === null || appFramework.cases === null || appFramework.cases.name === null || appFramework.cases.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{AppName}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: "flex", width: 510, height: 100 }}>
|
||||
<Button
|
||||
disabled={finishedApps.includes("SIEM")}
|
||||
variant={
|
||||
defaultSearch === "SIEM" ? "contained" : "outlined"
|
||||
}
|
||||
style={buttonStyle}
|
||||
// startIcon={<SearchIcon />}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("SIEM");
|
||||
setDefaultSearch(discoveryData.label)
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.siem === undefined || appFramework.siem.large_image === undefined ||
|
||||
appFramework === null || appFramework.siem === null || appFramework.siem.large_image === null || appFramework.siem.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<SearchIcon style={{ marginTop: 6 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.siem.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>SIEM</Typography>
|
||||
{appFramework === undefined || appFramework.siem === undefined || appFramework.siem.name === undefined ||
|
||||
appFramework === null || appFramework.siem === null || appFramework.siem.name === null || appFramework.siem.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.siem.name.split('_').join(' ')}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
finishedApps.includes("EDR & AV") ||
|
||||
finishedApps.includes("ERADICATION")
|
||||
}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
variant={
|
||||
defaultSearch === "Eradication" ? "contained" : "outlined"
|
||||
}
|
||||
style={buttonStyle}
|
||||
// startIcon={<NewReleasesIcon />}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("ERADICATION");
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.edr === undefined || appFramework.edr.large_image === undefined ||
|
||||
appFramework === null || appFramework.edr === null || appFramework.edr.large_image === null || appFramework.edr.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<NewReleasesIcon style={{marginTop: 8 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.edr.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>Endpoint</Typography>
|
||||
{appFramework === undefined || appFramework.edr === undefined || appFramework.edr.name === undefined ||
|
||||
appFramework === null || appFramework.edr === null || appFramework.edr.name === null || appFramework.edr.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.edr.name.split('_').join(' ')}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{ display: "flex", width: 510, height: 100 }}>
|
||||
<Button
|
||||
disabled={finishedApps.includes("INTEL")}
|
||||
variant={
|
||||
defaultSearch === "INTEL" ? "contained" : "outlined"
|
||||
}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
style={buttonStyle}
|
||||
// startIcon={<ExtensionIcon />}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("INTEL");
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.intel === undefined || appFramework.intel.large_image === undefined ||
|
||||
appFramework === null || appFramework.intel === null || appFramework.intel.large_image === null || appFramework.intel.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<ExtensionIcon style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.intel.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>Intel</Typography>
|
||||
{appFramework === undefined || appFramework.intel === undefined || appFramework.intel.name === undefined ||
|
||||
appFramework === null || appFramework.intel === null || appFramework.intel.name === null || appFramework.intel.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.intel.name.split('_').join(' ')}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
finishedApps.includes("COMMS") ||
|
||||
finishedApps.includes("EMAIL")
|
||||
}
|
||||
variant={
|
||||
defaultSearch === "EMAIL" ? "contained" : "outlined"
|
||||
}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
style={buttonStyle}
|
||||
// startIcon={<EmailIcon />}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("EMAIL");
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.communication === undefined || appFramework.communication.large_image === undefined ||
|
||||
appFramework === null || appFramework.communication === null || appFramework.communication.large_image === null || appFramework.communication.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<EmailIcon style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.communication.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>Email</Typography>
|
||||
{appFramework === undefined || appFramework.communication === undefined || appFramework.communication.name === undefined ||
|
||||
appFramework === null || appFramework.communication === null || appFramework.communication.name === null || appFramework.communication.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 12, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.communication.name.split('_').join(' ')}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{moreButton ? (
|
||||
<div style={{ display: "flex", width: 510, height: 100, marginBottom: 20 }}>
|
||||
<Button
|
||||
disabled={finishedApps.includes("NETWORK")}
|
||||
variant={
|
||||
defaultSearch === "NETWORK" ? "contained" : "outlined"
|
||||
}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
style={buttonStyle}
|
||||
// startIcon={<ExtensionIcon />}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("NETWORK");
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.network === undefined || appFramework.network.large_image === undefined ||
|
||||
appFramework === null || appFramework.network === null || appFramework.network.large_image === null || appFramework.network.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<ShowChartIcon style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.network.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>Network</Typography>
|
||||
{appFramework === undefined || appFramework.network === undefined || appFramework.network.name === undefined ||
|
||||
appFramework === null || appFramework.network === null || appFramework.network.name === null || appFramework.network.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 10, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.network.name}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
finishedApps.includes("ASSETS")
|
||||
}
|
||||
variant={
|
||||
defaultSearch === "ASSETS" ? "contained" : "outlined"
|
||||
}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
style={buttonStyle}
|
||||
// startIcon={<EmailIcon />}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("ASSETS");
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.assets === undefined || appFramework.assets.large_image === undefined ||
|
||||
appFramework === null || appFramework.assets === null || appFramework.assets.large_image === null || appFramework.assets.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<ExploreIcon style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.assets.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>Assets</Typography>
|
||||
{appFramework === undefined || appFramework.assets === undefined || appFramework.assets.name === undefined ||
|
||||
appFramework === null || appFramework.assets === null || appFramework.assets.name === null || appFramework.assets.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 10, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.assets.name}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={
|
||||
finishedApps.includes("IAM")
|
||||
}
|
||||
variant={
|
||||
defaultSearch === "IAM" ? "contained" : "outlined"
|
||||
}
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
style={buttonStyle}
|
||||
// startIcon={<EmailIcon />}
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
onNodeSelect("IAM");
|
||||
}}
|
||||
>
|
||||
{appFramework === undefined || appFramework.iam === undefined || appFramework.iam.large_image === undefined ||
|
||||
appFramework === null || appFramework.iam === null || appFramework.iam.large_image === null || appFramework.iam.large_image.length === 0 ?
|
||||
<div style={{width: 40, border: "1px solid rgba(33, 33, 33, 1) !importent", height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign:"center"}}>
|
||||
<FingerprintIcon style={{ marginTop: 8 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={appFramework.iam.large_image} />}
|
||||
<div style={{marginLeft: 8,}}>
|
||||
<Typography style={{display:"flex",}}>IAM</Typography>
|
||||
{appFramework === undefined || appFramework.iam === undefined || appFramework.iam.name === undefined ||
|
||||
appFramework === null || appFramework.iam === null || appFramework.iam.name === null || appFramework.iam.name.length === 0 ?
|
||||
"":<Typography style={{fontSize: 8, textAlign:"left", color:"var(--label-grey-text, #9E9E9E)" }} >{appFramework.iam.name}</Typography>}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
:
|
||||
<div style={{ display: "flex", width: 510, paddingLeft: 165, }}>
|
||||
<Button
|
||||
style={{ color: "#f86a3e", textTransform: 'capitalize', border: 2, backgroundColor: "var(--Background-color, #1A1A1A)" }}
|
||||
className="btn btn-primary"
|
||||
onClick={(event) => {
|
||||
setMoreButton(true);
|
||||
}}
|
||||
>
|
||||
<Typography style={{ textDecorationLine: 'underline', }}>
|
||||
See more Categories
|
||||
</Typography>
|
||||
</Button>
|
||||
</div>} */}
|
||||
<div style={{ display: "flex", width: 510, height: 64, borderRadius: 8, background: "var(--Container, #212121)" }}
|
||||
>
|
||||
<div
|
||||
onMouseOver={mouseOver}
|
||||
onMouseOut={mouseOut}
|
||||
disabled={finishedApps.includes("CASES")}
|
||||
variant={
|
||||
defaultSearch === "CASES" ? "contained" : "outlined"
|
||||
}
|
||||
color="secondary"
|
||||
style={{
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
margin: buttonMargin,
|
||||
fontSize: 18,
|
||||
color: "var(--White-text, #F1F1F1)",
|
||||
fontWeight: 400,
|
||||
background: "rgba(33, 33, 33, 1)",
|
||||
borderRadius: 8,
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
// startIcon = {defaultSearch === "CASES" ? newSelectedApp.image_url : <LightbulbIcon/>}
|
||||
onClick={(event) => {
|
||||
onNodeSelect("CASES");
|
||||
setDefaultSearch(discoveryData.label)
|
||||
}}
|
||||
>
|
||||
<div style={{marginLeft: 20, display:"flex", textAlign:"center", alignItems:"center", marginLeft: 100, width: 320, marginRight: "auto" }}>
|
||||
{AppImage === undefined || AppImage === undefined ||
|
||||
AppImage === null || AppImage === null || AppImage.length === 0 ?
|
||||
<div style={{ width: 40, height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign: "center" }}>
|
||||
<LightbulbIcon style={{ marginTop: 6 }} />
|
||||
</div>
|
||||
: <img style={{ marginRight: 8, width: 40, height: 40, flexShrink: 0, borderRadius: 40, }} src={AppImage} />}
|
||||
<div style={{ marginLeft: 8, }}>
|
||||
<Typography style={{ display: "flex", border: "none" }} >Case Management</Typography>
|
||||
{appName === undefined || appName === undefined ||
|
||||
appName === null || appName === null || appName.length === 0 ?
|
||||
""
|
||||
:
|
||||
<Typography style={{ fontSize: 12, textAlign: "left", color: "var(--label-grey-text, #9E9E9E)" }} >{appName}</Typography>}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
export default AppSearchButtons
|
433
shuffle/frontend/src/components/AppSelection.jsx
Normal file
@ -0,0 +1,433 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import theme from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
|
||||
import aa from 'search-insights'
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ShowChartIcon from '@mui/icons-material/ShowChart';
|
||||
import ExploreIcon from '@mui/icons-material/Explore';
|
||||
import LightbulbIcon from "@mui/icons-material/Lightbulb";
|
||||
import NewReleasesIcon from "@mui/icons-material/NewReleases";
|
||||
import ExtensionIcon from "@mui/icons-material/Extension";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import FingerprintIcon from '@mui/icons-material/Fingerprint';
|
||||
import AppSearch from "../components/Appsearch.jsx";
|
||||
import AppSearchButtons from "../components/AppSearchButtons.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
Zoom,
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
Collapse,
|
||||
IconButton,
|
||||
Avatar,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
|
||||
const AppSelection = props => {
|
||||
const {
|
||||
userdata,
|
||||
globalUrl,
|
||||
appFramework,
|
||||
setActiveStep,
|
||||
defaultSearch,
|
||||
setDefaultSearch,
|
||||
checkLogin,
|
||||
} = props;
|
||||
const [discoveryData, setDiscoveryData] = React.useState({})
|
||||
const [selectionOpen, setSelectionOpen] = React.useState(false)
|
||||
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
||||
const [finishedApps, setFinishedApps] = React.useState([])
|
||||
const [appButtons, setAppButtons] = useState([])
|
||||
const [apps, setApps] = useState([])
|
||||
const [appName, setAppName] = React.useState();
|
||||
const [moreButton, setMoreButton] = useState(false);
|
||||
|
||||
// const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
const ref = useRef()
|
||||
let navigate = useNavigate();
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
|
||||
const setFrameworkItem = (data) => {
|
||||
console.log("Setting framework item: ", data, isCloud)
|
||||
// if (!isCloud) {
|
||||
// activateApp(data.id)
|
||||
// }
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for framework!");
|
||||
}
|
||||
|
||||
if (checkLogin !== undefined) {
|
||||
checkLogin()
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
toast("Failed updating: " + responseJson.reason)
|
||||
} else {
|
||||
toast("Failed to update framework for your org.")
|
||||
|
||||
}
|
||||
}
|
||||
//setFrameworkLoaded(true)
|
||||
//setFrameworkData(responseJson)
|
||||
})
|
||||
.catch((error) => {
|
||||
if (checkLogin !== undefined) {
|
||||
checkLogin()
|
||||
}
|
||||
|
||||
toast(error.toString());
|
||||
//setFrameworkLoaded(true)
|
||||
})
|
||||
}
|
||||
const GetApps = (data) => {
|
||||
console.log("Setting framework item: ", data, isCloud)
|
||||
// if (!isCloud) {
|
||||
// activateApp(data.id)
|
||||
// }
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson === null) {
|
||||
console.log("null-response from server")
|
||||
const pretend_apps = [{
|
||||
"description": "TBD",
|
||||
"id": "TBD",
|
||||
"large_image": "",
|
||||
"name": "TBD",
|
||||
"type": "TBD"
|
||||
}]
|
||||
|
||||
setApps(pretend_apps)
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.success === false) {
|
||||
console.log("error loading apps: ", responseJson)
|
||||
return
|
||||
}
|
||||
|
||||
setApps(responseJson);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("App loading error: " + error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
const onNodeSelect = (label) => {
|
||||
// if (setDiscoveryWrapper !== undefined) {
|
||||
// setDiscoveryWrapper({ id: label });
|
||||
// }
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: `click_${label}`,
|
||||
label: "",
|
||||
});
|
||||
}
|
||||
setDiscoveryData(label)
|
||||
setSelectionOpen(true)
|
||||
setNewSelectedApp({})
|
||||
setDefaultSearch(label.charAt(0).toUpperCase() + (label.substring(1)).toLowerCase())
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
var tempApps = []
|
||||
if (tempApps.length === 0) {
|
||||
const tempApps =
|
||||
[{
|
||||
"description": newSelectedApp.description,
|
||||
"id": newSelectedApp.objectID,
|
||||
"large_image": newSelectedApp.image_url,
|
||||
"name": newSelectedApp.name,
|
||||
"type": discoveryData
|
||||
},
|
||||
//{
|
||||
// // description: newSelectedApp.siem.description,
|
||||
// id: newSelectedApp.siem.objectID,
|
||||
// large_image: newSelectedApp.siem.image_url,
|
||||
// name: newSelectedApp.siem.name,
|
||||
// type: discoveryData.siem
|
||||
// },{
|
||||
// // description: newSelectedApp.edr.description,
|
||||
// id: newSelectedApp.edr.objectID,
|
||||
// large_image: newSelectedApp.edr.image_url,
|
||||
// name: newSelectedApp.edr.name,
|
||||
// type: discoveryData.edr
|
||||
// }
|
||||
]
|
||||
setAppButtons(tempApps)
|
||||
GetApps()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
|
||||
return
|
||||
}
|
||||
const submitNewApp = {
|
||||
description: newSelectedApp.description,
|
||||
id: newSelectedApp.objectID,
|
||||
large_image: newSelectedApp.image_url,
|
||||
name: newSelectedApp.name,
|
||||
type: discoveryData
|
||||
}
|
||||
if (discoveryData === "CASES") {
|
||||
appFramework.cases = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "SIEM") {
|
||||
appFramework.siem = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "ERADICATION") {
|
||||
appFramework.edr = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "INTEL") {
|
||||
appFramework.intel = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "EMAIL") {
|
||||
appFramework.communication = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "NETWORK") {
|
||||
appFramework.network = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "ASSETS") {
|
||||
appFramework.assets = submitNewApp
|
||||
}
|
||||
else if (discoveryData === "IAM") {
|
||||
appFramework.iam = submitNewApp
|
||||
}
|
||||
setFrameworkItem(submitNewApp);
|
||||
setSelectionOpen(false);
|
||||
console.log("Selected app changed (effect)");
|
||||
}, [newSelectedApp]);
|
||||
|
||||
const sizing = moreButton ? 510 : 480;
|
||||
const buttonWidth = 450;
|
||||
const buttonMargin = 10;
|
||||
const bottomButtonStyle = {
|
||||
borderRadius: 200,
|
||||
marginTop: moreButton ? 44 : "",
|
||||
height: 51,
|
||||
width: 510,
|
||||
fontSize: 16,
|
||||
// background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
|
||||
background: "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
|
||||
padding: "16px 24px",
|
||||
// top: 20,
|
||||
// margin: "auto",
|
||||
textTransform: 'capitalize',
|
||||
itemAlign: "center",
|
||||
// marginTop: 25
|
||||
// marginLeft: "65px",
|
||||
};
|
||||
const buttonStyle = {
|
||||
flex: 1,
|
||||
width: 224,
|
||||
padding: 25,
|
||||
margin: buttonMargin,
|
||||
color: "var(--White-text, #F1F1F1)",
|
||||
fontWeight: 400,
|
||||
fontSize: 17,
|
||||
background: "rgba(33, 33, 33, 1)",
|
||||
textTransform: 'capitalize',
|
||||
border: "1px solid rgba(33, 33, 33, 1)",
|
||||
borderRadius: 8,
|
||||
marginRight: 8,
|
||||
};
|
||||
// console.log("appFramework",appFramework.cases.name)
|
||||
return (
|
||||
<Collapse in={true}>
|
||||
<div
|
||||
style={{
|
||||
minHeight: sizing,
|
||||
maxHeight: sizing,
|
||||
marginTop: 10,
|
||||
width: 500,
|
||||
}}
|
||||
>
|
||||
{selectionOpen ? (
|
||||
<div
|
||||
style={{
|
||||
width: 319,
|
||||
height: 395,
|
||||
flexShrink: 0,
|
||||
marginLeft: 70,
|
||||
marginTop: 68,
|
||||
position: "absolute",
|
||||
zIndex: 100,
|
||||
borderRadius: 6,
|
||||
border: "1px solid var(--Container-Stroke, #494949)",
|
||||
background: "var(--Container, #212121)",
|
||||
boxShadow: "8px 8px 32px 24px rgba(0, 0, 0, 0.16)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ display: "flex", textAlign: "center", textTransform: "capitalize" }}>
|
||||
<Typography style={{ padding: 16, color: "#FFFFFF", textTransform: "capitalize" }}> {discoveryData} </Typography>
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Tooltip
|
||||
title="Close"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
flex: 1,
|
||||
// width: 224,
|
||||
marginLeft: discoveryData === ('ERADICATION') ? 120 : 177,
|
||||
width: "100%",
|
||||
marginBottom: 23,
|
||||
fontSize: 16,
|
||||
background: "rgba(33, 33, 33, 1)",
|
||||
borderColor: "rgba(33, 33, 33, 1)",
|
||||
borderRadius: 8,
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectionOpen(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ width: 16 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title="Delete app"
|
||||
placement="bottom"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 12501, position: "absolute", top: 32, right: 16 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setSelectionOpen(false)
|
||||
setDefaultSearch("")
|
||||
const submitDeletedApp = {
|
||||
"description": "",
|
||||
"id": "remove",
|
||||
"name": "",
|
||||
"type": discoveryData
|
||||
}
|
||||
setFrameworkItem(submitDeletedApp)
|
||||
setNewSelectedApp({})
|
||||
setTimeout(() => {
|
||||
setDiscoveryData({})
|
||||
setFrameworkItem(submitDeletedApp)
|
||||
setNewSelectedApp({})
|
||||
}, 1000)
|
||||
//setAppName(discoveryData.cases.name)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{ width: "100%", border: "1px #494949 solid" }}
|
||||
/>
|
||||
<AppSearch
|
||||
defaultSearch={defaultSearch}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
userdata={userdata}
|
||||
// cy={cy}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<Typography
|
||||
variant="h4"
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
marginTop: 40,
|
||||
marginRight: 30,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
color="rgba(241, 241, 241, 1)"
|
||||
>
|
||||
Find your apps
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
marginTop: 10,
|
||||
marginRight: 30,
|
||||
marginBottom: 40,
|
||||
}}
|
||||
color="rgba(158, 158, 158, 1)"
|
||||
>
|
||||
Select the apps you work with and we will connect them for you.
|
||||
</Typography>
|
||||
{appButtons.map((appData, index) => {
|
||||
|
||||
const appName = appData.name
|
||||
const AppImage = appData.large_image
|
||||
const appType = appData.type
|
||||
|
||||
return (
|
||||
|
||||
<AppSearchButtons
|
||||
appFramework={appFramework}
|
||||
appName={appName}
|
||||
appType = {appType}
|
||||
AppImage={AppImage}
|
||||
defaultSearch={defaultSearch}
|
||||
finishedApps={finishedApps}
|
||||
onNodeSelect={onNodeSelect}
|
||||
discoveryData={discoveryData}
|
||||
setDiscoveryData={setDiscoveryData}
|
||||
setDefaultSearch={setDefaultSearch}
|
||||
apps={apps}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div style={{ flexDirection: "row", }}>
|
||||
<Button variant="contained" type="submit" fullWidth style={bottomButtonStyle} onClick={() => {
|
||||
navigate("/welcome?tab=3")
|
||||
setActiveStep(2)
|
||||
}}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppSelection;
|
220
shuffle/frontend/src/components/Appsearch.jsx
Normal file
@ -0,0 +1,220 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactGA from 'react-ga4';
|
||||
import theme from '../theme.jsx';
|
||||
import {Link} from 'react-router-dom';
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
//import algoliasearch from 'algoliasearch/lite';
|
||||
import algoliasearch from 'algoliasearch';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip
|
||||
} from '@mui/material';
|
||||
|
||||
import aa from 'search-insights'
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
|
||||
//const theme = useTheme();
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
const [selectedApp, setSelectedApp] = React.useState({});
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integration any app"
|
||||
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
//console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement)
|
||||
if (defaultSearch !== undefined && defaultSearch !== null) {
|
||||
refine(defaultSearch)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: "#2F2F2F", borderRadius: borderRadius, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
endAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='on'
|
||||
type="search"
|
||||
color="primary"
|
||||
defaultValue={defaultSearch}
|
||||
// placeholder={`Find ${defaultSearch} Apps...`}
|
||||
placeholder= {defaultSearch ? `${defaultSearch}` : "Search Cases "}
|
||||
id="shuffle_workflow_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
//value={currentRefinement}
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{hits.map((data, index) => {
|
||||
const paperStyle = {
|
||||
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : "#2F2F2F",
|
||||
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
|
||||
// border: newSelectedApp.objectID !== data.objectID ? `1px solid rgba(255,255,255,0.2)` : "2px solid #f86a3e",
|
||||
textAlign: "left",
|
||||
padding: 10,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
minHeight: 37,
|
||||
maxHeight: 52,
|
||||
}
|
||||
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
var parsedname = data.name.valueOf()
|
||||
//for (var key = 0; key < data.name.length; key++) {
|
||||
// var character = data.name.charAt(key)
|
||||
// if (character === character.toUpperCase()) {
|
||||
// //console.log(data.name[key], data.name[key+1])
|
||||
// if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
|
||||
// } else {
|
||||
// parsedname += " "
|
||||
// }
|
||||
// }
|
||||
|
||||
// parsedname += character
|
||||
//}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
|
||||
|
||||
return (
|
||||
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
/*
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `search_bar_click`,
|
||||
label: "",
|
||||
})
|
||||
*/
|
||||
}} onMouseOut={() => {
|
||||
setMouseHoverIndex(-1)
|
||||
}} onClick={() => {
|
||||
if(isCreatorPage === true){
|
||||
if (setNewSelectedApp !== undefined && setUserSpecialzedApp !== undefined) {
|
||||
setUserSpecialzedApp(userdata.id, data)
|
||||
}
|
||||
}
|
||||
if (setNewSelectedApp !== undefined) {
|
||||
setNewSelectedApp(data)
|
||||
}
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "app_search",
|
||||
action: `app_${parsedname}_${data.id}_personalize_click`,
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
const queryID = ""
|
||||
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
try {
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.headers["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'conversion',
|
||||
eventName: 'App Framework Activation',
|
||||
index: 'appsearch',
|
||||
objectIDs: [data.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: queryID,
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
} catch (e) {
|
||||
console.log("Failed algolia search update: ", e)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<div style={{display: "flex"}}>
|
||||
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, borderRadius: 40, maxHeight: 30, display: "block", }} />
|
||||
<Typography variant="body1" style={{marginTop: 2, marginLeft: 10, }}>
|
||||
{parsedname}
|
||||
</Typography>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(InputHits)
|
||||
|
||||
return (
|
||||
<div style={{width: 287, height: 295, padding: "16px 16px 267px 16px", alignItems: "center", gap: 138,}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
{/* showSearch === false ? null :
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
*/}
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Appsearch;
|
190
shuffle/frontend/src/components/AppsearchPopout.jsx
Normal file
@ -0,0 +1,190 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import AppSearch from './Appsearch.jsx';
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
Badge,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const AppSearchPopout = (props) => {
|
||||
const {
|
||||
cy,
|
||||
paperTitle,
|
||||
setPaperTitle,
|
||||
newSelectedApp,
|
||||
setNewSelectedApp,
|
||||
selectionOpen,
|
||||
setSelectionOpen,
|
||||
discoveryData,
|
||||
setDiscoveryData,
|
||||
userdata,
|
||||
} = props;
|
||||
|
||||
const [defaultSearch, setDefaultSearch] = React.useState(paperTitle !== undefined ? paperTitle : "")
|
||||
|
||||
if (selectionOpen !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
// <Paper style={{width: 275, maxHeight: 400, zIndex: 100000, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, }}>
|
||||
return (
|
||||
<Paper style={{minWidth: 275, width: 275, minHeight: 400, maxHeight: 400, zIndex: 100000, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, }}>
|
||||
{paperTitle !== undefined && paperTitle.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="h6" style={{textAlign: "center"}}>
|
||||
{paperTitle}
|
||||
</Typography>
|
||||
<Divider style={{marginTop: 5, marginBottom: 5 }} />
|
||||
</span>
|
||||
: null}
|
||||
<Tooltip
|
||||
title="Close window"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 12501, position: "absolute", top: 10, right: 10}}
|
||||
onClick={(e) => {
|
||||
//cy.elements().unselectify();
|
||||
if (cy !== undefined) {
|
||||
cy.elements().unselect()
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
setSelectionOpen(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* {/*Causes errors in Cytoscape. Removing for now.}
|
||||
<Tooltip
|
||||
title="Unselect app"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 12501, position: "absolute", top: 32, right: 10}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDiscoveryData({
|
||||
"id": discoveryData.id,
|
||||
"label": discoveryData.label,
|
||||
"name": ""
|
||||
})
|
||||
setNewSelectedApp({
|
||||
"image_url": "",
|
||||
"name": "",
|
||||
"id": "",
|
||||
"objectID": "remove",
|
||||
})
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch("")
|
||||
|
||||
const foundelement = cy.getElementById(discoveryData.id)
|
||||
if (foundelement !== undefined && foundelement !== null) {
|
||||
console.log("element: ", foundelement)
|
||||
foundelement.data("large_image", discoveryData.large_image)
|
||||
foundelement.data("text_margin_y", "14px")
|
||||
foundelement.data("margin_x", "32px")
|
||||
foundelement.data("margin_y", "19x")
|
||||
foundelement.data("width", "45px")
|
||||
foundelement.data("height", "45px")
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setDiscoveryData({})
|
||||
setNewSelectedApp({})
|
||||
}, 1000)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
*/}
|
||||
<div style={{display: "flex"}}>
|
||||
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
<div style={{border: "1px solid rgba(255,255,255,0.2)", borderRadius: 25, height: 40, width: 40, textAlign: "center", overflow: "hidden",}}>
|
||||
|
||||
<img alt={discoveryData.id} src={newSelectedApp.image_url !== undefined && newSelectedApp.image_url !== null && newSelectedApp.image_url.length > 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/>
|
||||
</div>
|
||||
:
|
||||
<img alt={discoveryData.id} src={discoveryData.large_image} style={{height: 40,}}/>
|
||||
}
|
||||
<Typography variant="body1" style={{marginLeft: 10, marginTop: 6}}>
|
||||
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
discoveryData.name
|
||||
:
|
||||
newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ?
|
||||
newSelectedApp.name
|
||||
:
|
||||
`No ${discoveryData.label} app chosen`
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
{discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginBottom: 10, maxHeight: 75, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{discoveryData.description}
|
||||
</Typography>
|
||||
{/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ?
|
||||
{<
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
defaultSearch={defaultSearch}
|
||||
/>}
|
||||
:
|
||||
null
|
||||
*/}
|
||||
</span>
|
||||
:
|
||||
selectionOpen
|
||||
?
|
||||
<span>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10}}>
|
||||
Click an app below to select it
|
||||
</Typography>
|
||||
</span>
|
||||
:
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginTop: 10, }}
|
||||
onClick={() => {
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch(discoveryData.label)
|
||||
}}
|
||||
>
|
||||
Choose {discoveryData.label} app
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
{selectionOpen ?
|
||||
<AppSearch
|
||||
defaultSearch={defaultSearch}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
userdata={userdata}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppSearchPopout;
|
278
shuffle/frontend/src/components/AuthenticationItem.jsx
Normal file
@ -0,0 +1,278 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
FormGroup,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormLabel,
|
||||
FormControlLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
Zoom,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
SelectAll as SelectAllIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const AuthenticationItem = (props) => {
|
||||
const { data, index, globalUrl, getAppAuthentication } = props
|
||||
|
||||
const [selectedAuthentication, setSelectedAuthentication] = React.useState({})
|
||||
const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false);
|
||||
const [authenticationFields, setAuthenticationFields] = React.useState([]);
|
||||
|
||||
//const alert = useAlert();
|
||||
var bgColor = "#27292d";
|
||||
if (index % 2 === 0) {
|
||||
bgColor = "#1f2023";
|
||||
}
|
||||
|
||||
//console.log("Auth data: ", data)
|
||||
if (data.type === "oauth2") {
|
||||
data.fields = [
|
||||
{
|
||||
key: "url",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
{
|
||||
key: "client_id",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
{
|
||||
key: "client_secret",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
{
|
||||
key: "scope",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deleteAuthentication = (data) => {
|
||||
toast("Deleting auth " + data.label);
|
||||
|
||||
// Just use this one?
|
||||
const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
|
||||
console.log("URL: ", url);
|
||||
fetch(url, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
console.log("RESP: ", responseJson);
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed deleting auth");
|
||||
} else {
|
||||
// Need to wait because query in ES is too fast
|
||||
setTimeout(() => {
|
||||
getAppAuthentication();
|
||||
}, 1000);
|
||||
//toast("Successfully deleted authentication!")
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log("Error in userdata: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
const editAuthenticationConfig = (id) => {
|
||||
const data = {
|
||||
id: id,
|
||||
action: "assign_everywhere",
|
||||
};
|
||||
const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config";
|
||||
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed overwriting appauth in workflows");
|
||||
} else {
|
||||
toast("Successfully updated auth everywhere!");
|
||||
//setSelectedUserModalOpen(false);
|
||||
setTimeout(() => {
|
||||
getAppAuthentication();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const updateAppAuthentication = (field) => {
|
||||
setSelectedAuthenticationModalOpen(true);
|
||||
setSelectedAuthentication(field);
|
||||
//{selectedAuthentication.fields.map((data, index) => {
|
||||
var newfields = [];
|
||||
for (var key in field.fields) {
|
||||
newfields.push({
|
||||
key: field.fields[key].key,
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
setAuthenticationFields(newfields);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor }}>
|
||||
<ListItemText
|
||||
primary=<img
|
||||
alt=""
|
||||
src={data.app.large_image}
|
||||
style={{
|
||||
maxWidth: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
/>
|
||||
style={{ minWidth: 75, maxWidth: 75 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.label}
|
||||
style={{
|
||||
minWidth: 225,
|
||||
maxWidth: 225,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.app.name}
|
||||
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
|
||||
/>
|
||||
{/*
|
||||
<ListItemText
|
||||
primary={data.defined === false ? "No" : "Yes"}
|
||||
style={{ minWidth: 100, maxWidth: 100, }}
|
||||
/>
|
||||
*/}
|
||||
<ListItemText
|
||||
primary={
|
||||
data.workflow_count === null ? 0 : data.workflow_count
|
||||
}
|
||||
style={{
|
||||
minWidth: 100,
|
||||
maxWidth: 100,
|
||||
textAlign: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
<ListItemText
|
||||
primary={data.node_count}
|
||||
style={{
|
||||
minWidth: 110,
|
||||
maxWidth: 110,
|
||||
textAlign: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
*/}
|
||||
<ListItemText
|
||||
primary={
|
||||
data.fields === null || data.fields === undefined
|
||||
? ""
|
||||
: data.fields
|
||||
.map((data) => {
|
||||
return data.key;
|
||||
})
|
||||
.join(", ")
|
||||
}
|
||||
style={{
|
||||
minWidth: 125,
|
||||
maxWidth: 125,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 230,
|
||||
minWidth: 230,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
primary={new Date(data.created * 1000).toISOString()}
|
||||
/>
|
||||
<ListItemText>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
updateAppAuthentication(data);
|
||||
}}
|
||||
>
|
||||
<EditIcon color="primary" />
|
||||
</IconButton>
|
||||
{data.defined ? (
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Set in EVERY workflow"
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
style={{ marginRight: 10 }}
|
||||
disabled={data.defined === false}
|
||||
onClick={() => {
|
||||
editAuthenticationConfig(data.id);
|
||||
}}
|
||||
>
|
||||
<SelectAllIcon
|
||||
color={data.defined ? "primary" : "secondary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Must edit before you can set in all workflows"
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
style={{ marginRight: 10 }}
|
||||
onClick={() => {}}
|
||||
>
|
||||
<SelectAllIcon
|
||||
color={data.defined ? "primary" : "secondary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
deleteAuthentication(data);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon color="primary" />
|
||||
</IconButton>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthenticationItem
|
366
shuffle/frontend/src/components/AuthenticationNormal.jsx
Normal file
@ -0,0 +1,366 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import theme from '../theme.jsx';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
TextField,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
LockOpen as LockOpenIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const AuthenticationData = (props) => {
|
||||
const {
|
||||
globalUrl,
|
||||
saveWorkflow,
|
||||
selectedApp,
|
||||
workflow,
|
||||
selectedAction,
|
||||
authenticationType,
|
||||
getAppAuthentication,
|
||||
appAuthentication,
|
||||
setSelectedAction,
|
||||
setAuthenticationModalOpen,
|
||||
isCloud,
|
||||
} = props;
|
||||
|
||||
const setNewAppAuth = (appAuthData) => {
|
||||
console.log("DAta: ", appAuthData);
|
||||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(appAuthData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for setting app auth :O!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success) {
|
||||
toast("Failed to set app auth: " + responseJson.reason);
|
||||
} else {
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication()
|
||||
}
|
||||
|
||||
if (setAuthenticationModalOpen !== undefined) {
|
||||
setAuthenticationModalOpen(false)
|
||||
}
|
||||
|
||||
// Needs a refresh with the new authentication..
|
||||
//toast("Successfully saved new app auth")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast(error.toString());
|
||||
console.log("New auth error: ", error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||||
fields: {},
|
||||
label: "",
|
||||
usage: [
|
||||
{
|
||||
workflow_id: workflow.id,
|
||||
},
|
||||
],
|
||||
id: uuidv4(),
|
||||
active: true,
|
||||
});
|
||||
|
||||
if (
|
||||
selectedApp.authentication === undefined ||
|
||||
selectedApp.authentication.parameters === null ||
|
||||
selectedApp.authentication.parameters === undefined ||
|
||||
selectedApp.authentication.parameters.length === 0
|
||||
) {
|
||||
return (
|
||||
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
|
||||
<Typography variant="h4" id="draggable-dialog-title" style={{cursor: "move",}}>
|
||||
{selectedApp.name} does not require authentication
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
authenticationOption.app.actions = [];
|
||||
|
||||
for (var key in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] === undefined
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = "";
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitCheck = () => {
|
||||
console.log("NEW AUTH: ", authenticationOption);
|
||||
if (authenticationOption.label.length === 0) {
|
||||
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||||
}
|
||||
|
||||
// Automatically mapping fields that already exist (predefined).
|
||||
// Warning if fields are NOT filled
|
||||
for (var key in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
].length === 0
|
||||
) {
|
||||
if (
|
||||
selectedApp.authentication.parameters[key].value !== undefined &&
|
||||
selectedApp.authentication.parameters[key].value !== null &&
|
||||
selectedApp.authentication.parameters[key].value.length > 0
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = selectedApp.authentication.parameters[key].value;
|
||||
} else {
|
||||
if (
|
||||
selectedApp.authentication.parameters[key].schema.type === "bool"
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = "false";
|
||||
} else {
|
||||
toast(
|
||||
"Field " +
|
||||
selectedApp.authentication.parameters[key].name +
|
||||
" can't be empty"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Action: ", selectedAction);
|
||||
selectedAction.authentication_id = authenticationOption.id;
|
||||
selectedAction.selectedAuthentication = authenticationOption;
|
||||
if (
|
||||
selectedAction.authentication === undefined ||
|
||||
selectedAction.authentication === null
|
||||
) {
|
||||
selectedAction.authentication = [authenticationOption];
|
||||
} else {
|
||||
selectedAction.authentication.push(authenticationOption);
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction);
|
||||
|
||||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||||
var newFields = [];
|
||||
for (const key in newAuthOption.fields) {
|
||||
const value = newAuthOption.fields[key];
|
||||
newFields.push({
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("FIELDS: ", newFields);
|
||||
newAuthOption.fields = newFields;
|
||||
setNewAppAuth(newAuthOption);
|
||||
|
||||
//if (configureWorkflowModalOpen) {
|
||||
// setSelectedAction({});
|
||||
//}
|
||||
|
||||
//setUpdate(authenticationOption.id);
|
||||
};
|
||||
|
||||
if (
|
||||
authenticationOption.label === null ||
|
||||
authenticationOption.label === undefined
|
||||
) {
|
||||
authenticationOption.label = selectedApp.name + " authentication";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle id="draggable-dialog-title" style={{cursor: "move",}}>
|
||||
<div style={{ color: "white" }}>
|
||||
Authentication for {selectedApp.name}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://shuffler.io/docs/apps#authentication"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
What is app authentication?
|
||||
</a>
|
||||
<div />
|
||||
These are required fields for authenticating with {selectedApp.name}
|
||||
<div style={{ marginTop: 15 }} />
|
||||
<b>Name - what is this used for?</b>
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
marginLeft: "5px",
|
||||
maxWidth: "95%",
|
||||
height: 50,
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Auth july 2020"}
|
||||
defaultValue={`Auth for ${selectedApp.name}`}
|
||||
onChange={(event) => {
|
||||
authenticationOption.label = event.target.value;
|
||||
}}
|
||||
/>
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
backgroundColor: "rgb(91, 96, 100)",
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
{selectedApp.authentication.parameters.map((data, index) => {
|
||||
return (
|
||||
<div key={index} style={{ marginTop: 10 }}>
|
||||
<LockOpenIcon style={{ marginRight: 10 }} />
|
||||
<b>{data.name}</b>
|
||||
|
||||
{data.schema !== undefined &&
|
||||
data.schema !== null &&
|
||||
data.schema.type === "bool" ? (
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
}}
|
||||
defaultValue={"false"}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
console.log("Value: ", e.target.value);
|
||||
authenticationOption.fields[data.name] = e.target.value;
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
height: 50,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
key={"false"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"false"}
|
||||
>
|
||||
false
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
key={"true"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"true"}
|
||||
>
|
||||
true
|
||||
</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
marginLeft: "5px",
|
||||
maxWidth: "95%",
|
||||
height: 50,
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
type={
|
||||
data.example !== undefined && data.example.includes("***")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
color="primary"
|
||||
defaultValue={
|
||||
data.value !== undefined && data.value !== null
|
||||
? data.value
|
||||
: ""
|
||||
}
|
||||
placeholder={data.example}
|
||||
onChange={(event) => {
|
||||
authenticationOption.fields[data.name] =
|
||||
event.target.value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption);
|
||||
handleSubmitCheck();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticationData
|
469
shuffle/frontend/src/components/AuthenticationWindow.jsx
Normal file
@ -0,0 +1,469 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import {
|
||||
Divider,
|
||||
MenuItem,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
Textfield,
|
||||
TextField,
|
||||
Typography,
|
||||
Select,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
LockOpen as LockOpenIcon,
|
||||
Close as CloseIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import PaperComponent from "../components/PaperComponent.jsx"
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
|
||||
const AuthenticationData = (props) => {
|
||||
const {
|
||||
globalUrl,
|
||||
selectedApp,
|
||||
getAppAuthentication,
|
||||
authenticationModalOpen,
|
||||
setAuthenticationModalOpen,
|
||||
|
||||
configureWorkflowModalOpen,
|
||||
workflow,
|
||||
setUpdate,
|
||||
selectedAction,
|
||||
setSelectedAction,
|
||||
isLoggedIn,
|
||||
authFieldsOnly,
|
||||
} = props
|
||||
|
||||
//const alert = useAlert()
|
||||
let navigate = useNavigate();
|
||||
const [submitSuccessful, setSubmitSuccessful] = useState(false)
|
||||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||||
fields: {},
|
||||
label: "",
|
||||
usage: [
|
||||
{
|
||||
workflow_id: workflow === undefined ? "" : workflow.id,
|
||||
},
|
||||
],
|
||||
id: uuidv4(),
|
||||
active: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn === false && authFieldsOnly !== true) {
|
||||
navigate(`/login?view=${window.location.pathname}&message=Log in to authenticate this app`)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const setNewAppAuth = (appAuthData) => {
|
||||
var headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
// Find org_id and authorization from queries and add to headers
|
||||
if (window.location.search !== "") {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const org_id = params.get("org_id")
|
||||
const authorization = params.get("authorization")
|
||||
if (org_id !== null && authorization !== null) {
|
||||
headers["Org-Id"] = org_id
|
||||
headers["Authorization"] = "Bearer " + authorization
|
||||
}
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||
method: "PUT",
|
||||
headers: headers,
|
||||
body: JSON.stringify(appAuthData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for setting app auth :O!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success) {
|
||||
if (responseJson.reason === undefined) {
|
||||
toast("Failed to set app auth. Are you logged in?")
|
||||
} else {
|
||||
toast("Failed to set app auth: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
setSubmitSuccessful(true)
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication(true, false);
|
||||
}
|
||||
|
||||
if (setAuthenticationModalOpen !== undefined) {
|
||||
setAuthenticationModalOpen(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast(error.toString());
|
||||
console.log("New auth error: ", error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
if (selectedApp.authentication === undefined || selectedApp.authentication.parameters === null ||
|
||||
selectedApp.authentication.parameters === undefined || selectedApp.authentication.parameters.length === 0) {
|
||||
|
||||
return (
|
||||
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
|
||||
<Typography variant="h4" id="draggable-dialog-title" style={{ cursor: "move", }}>
|
||||
{selectedApp.name} does not require authentication
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
authenticationOption.app.actions = [];
|
||||
|
||||
for (let paramkey in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] === undefined
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] = "";
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitCheck = () => {
|
||||
if (authenticationOption.label.length === 0) {
|
||||
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||||
}
|
||||
|
||||
// Automatically mapping fields that already exist (predefined).
|
||||
// Warning if fields are NOT filled
|
||||
for (let paramkey in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
].length === 0
|
||||
) {
|
||||
if (
|
||||
selectedApp.authentication.parameters[paramkey].value !== undefined &&
|
||||
selectedApp.authentication.parameters[paramkey].value !== null &&
|
||||
selectedApp.authentication.parameters[paramkey].value.length > 0
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] = selectedApp.authentication.parameters[paramkey].value;
|
||||
} else {
|
||||
if (
|
||||
selectedApp.authentication.parameters[paramkey].schema.type === "bool"
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[paramkey].name
|
||||
] = "false";
|
||||
} else {
|
||||
toast(
|
||||
"Field " +
|
||||
selectedApp.authentication.parameters[paramkey].name +
|
||||
" can't be empty"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Action: ", selectedAction);
|
||||
|
||||
if (selectedAction !== undefined) {
|
||||
selectedAction.authentication_id = authenticationOption.id;
|
||||
selectedAction.selectedAuthentication = authenticationOption;
|
||||
if (
|
||||
selectedAction.authentication === undefined ||
|
||||
selectedAction.authentication === null
|
||||
) {
|
||||
selectedAction.authentication = [authenticationOption];
|
||||
} else {
|
||||
selectedAction.authentication.push(authenticationOption);
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction);
|
||||
}
|
||||
|
||||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||||
var newFields = [];
|
||||
console.log("Fields: ", newAuthOption.fields)
|
||||
for (let authkey in newAuthOption.fields) {
|
||||
const value = newAuthOption.fields[authkey];
|
||||
newFields.push({
|
||||
"key": authkey,
|
||||
"value": value,
|
||||
});
|
||||
}
|
||||
|
||||
newAuthOption.fields = newFields;
|
||||
setNewAppAuth(newAuthOption);
|
||||
|
||||
if (configureWorkflowModalOpen === true) {
|
||||
setSelectedAction({});
|
||||
}
|
||||
|
||||
if (setUpdate !== undefined) {
|
||||
setUpdate(authenticationOption.id);
|
||||
}
|
||||
};
|
||||
|
||||
if (authenticationOption.label === null || authenticationOption.label === undefined) {
|
||||
authenticationOption.label = selectedApp.name + " authentication";
|
||||
}
|
||||
|
||||
const authenticationParameters = selectedApp.authentication.parameters.map((data, index) => {
|
||||
return (
|
||||
<div key={index} style={{ marginTop: 10 }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<LockOpenIcon style={{ marginRight: 10, }} />
|
||||
<Typography variant="body1" style={{}}>
|
||||
{data.name.replace("_basic", "", -1).replace("_", " ", -1)}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
{data.schema !== undefined &&
|
||||
data.schema !== null &&
|
||||
data.schema.type === "bool" ? (
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
}}
|
||||
defaultValue={"false"}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
console.log("Value: ", e.target.value);
|
||||
authenticationOption.fields[data.name] = e.target.value;
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
height: 50,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
key={"false"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"false"}
|
||||
>
|
||||
false
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
key={"true"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"true"}
|
||||
>
|
||||
true
|
||||
</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
disableUnderline: true,
|
||||
}}
|
||||
fullWidth
|
||||
type={
|
||||
data.example !== undefined && data.example.includes("***")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
color="primary"
|
||||
defaultValue={
|
||||
data.value !== undefined && data.value !== null
|
||||
? data.value
|
||||
: ""
|
||||
}
|
||||
placeholder={data.example}
|
||||
onChange={(event) => {
|
||||
authenticationOption.fields[data.name] =
|
||||
event.target.value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
const authenticationButtons = <span>
|
||||
<Button
|
||||
style={{ borderRadius: theme.palette.borderRadius, marginTop: authFieldsOnly ? 20 : 0 }}
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption);
|
||||
handleSubmitCheck();
|
||||
}}
|
||||
variant={"contained"}
|
||||
disabled={submitSuccessful}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
{authFieldsOnly === true ? null :
|
||||
<Button
|
||||
style={{ borderRadius: 0 }}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
</span>
|
||||
|
||||
// Check if only the auth items should show
|
||||
if (authFieldsOnly === true) {
|
||||
return (
|
||||
<div>
|
||||
{submitSuccessful === true ?
|
||||
<Typography variant="h6" style={{ marginTop: 10 }}>
|
||||
App succesfully configured! You may close this window.
|
||||
</Typography>
|
||||
:
|
||||
<span>
|
||||
{authenticationParameters}
|
||||
{authenticationButtons}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
PaperComponent={PaperComponent}
|
||||
aria-labelledby="draggable-dialog-title"
|
||||
hideBackdrop={true}
|
||||
disableEnforceFocus={true}
|
||||
disableBackdropClick={true}
|
||||
style={{ pointerEvents: "none" }}
|
||||
open={authenticationModalOpen}
|
||||
onClose={() => {
|
||||
//if (configureWorkflowModalOpen) {
|
||||
// setSelectedAction({});
|
||||
//}
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
color: "white",
|
||||
minWidth: 600,
|
||||
minHeight: 600,
|
||||
maxHeight: 600,
|
||||
padding: 15,
|
||||
overflow: "hidden",
|
||||
zIndex: 10012,
|
||||
border: theme.palette.defaultBorder,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 14,
|
||||
right: 18,
|
||||
color: "grey",
|
||||
}}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
if (configureWorkflowModalOpen === true) {
|
||||
setSelectedAction({});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<DialogTitle id="draggable-dialog-title" style={{ cursor: "move", }}>
|
||||
<div style={{ color: "white" }}>
|
||||
Authentication for {selectedApp.name}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://shuffler.io/docs/apps#authentication"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
What is app authentication?
|
||||
</a>
|
||||
<div />
|
||||
These are required fields for authenticating with {selectedApp.name}
|
||||
<div style={{ marginTop: 15 }} />
|
||||
<b>Name - what is this used for?</b>
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Auth july 2020"}
|
||||
defaultValue={`Auth for ${selectedApp.name}`}
|
||||
onChange={(event) => {
|
||||
authenticationOption.label = event.target.value;
|
||||
}}
|
||||
/>
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
backgroundColor: "rgb(91, 96, 100)",
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
{authenticationParameters}
|
||||
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{authenticationButtons}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticationData;
|
1011
shuffle/frontend/src/components/Billing.jsx
Normal file
280
shuffle/frontend/src/components/BillingStats.jsx
Normal file
@ -0,0 +1,280 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import classNames from "classnames";
|
||||
import theme from '../theme.jsx';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TextField,
|
||||
IconButton,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
Chip,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
RadialBarChart,
|
||||
RadialAreaChart,
|
||||
RadialAxis,
|
||||
StackedBarSeries,
|
||||
TooltipArea,
|
||||
ChartTooltip,
|
||||
TooltipTemplate,
|
||||
RadialAreaSeries,
|
||||
RadialPointSeries,
|
||||
RadialArea,
|
||||
RadialLine,
|
||||
TreeMap,
|
||||
TreeMapSeries,
|
||||
TreeMapLabel,
|
||||
TreeMapRect,
|
||||
Line,
|
||||
LineChart,
|
||||
LineSeries,
|
||||
LinearYAxis,
|
||||
LinearXAxis,
|
||||
LinearYAxisTickSeries,
|
||||
LinearXAxisTickSeries,
|
||||
Area,
|
||||
AreaChart,
|
||||
AreaSeries,
|
||||
AreaSparklineChart,
|
||||
PointSeries,
|
||||
GridlineSeries,
|
||||
Gridline,
|
||||
Stripes,
|
||||
Gradient,
|
||||
GradientStop,
|
||||
LinearXAxisTickLabel,
|
||||
} from 'reaviz';
|
||||
|
||||
const LineChartWrapper = ({keys, inputname, height, width}) => {
|
||||
const [hovered, setHovered] = useState("");
|
||||
const inputdata = keys.data === undefined ? keys : keys.data
|
||||
|
||||
return (
|
||||
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: 30, marginTop: 15, }}>
|
||||
<Typography variant="h6" style={{marginBotton: 15, }}>
|
||||
{inputname}
|
||||
</Typography>
|
||||
<BarChart
|
||||
width={"100%"}
|
||||
height={height}
|
||||
data={inputdata}
|
||||
gridlines={
|
||||
<GridlineSeries line={<Gridline direction="all" />} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const AppStats = (defaultprops) => {
|
||||
const { globalUrl, selectedOrganization, userdata, } = defaultprops;
|
||||
const [keys, setKeys] = useState([])
|
||||
const [searches, setSearches] = useState([]);
|
||||
const [clickData, setClickData] = useState(undefined);
|
||||
const [conversionData, setConversionData] = useState(undefined);
|
||||
const [statistics, setStatistics] = useState(undefined);
|
||||
const [appRuns, setAppruns] = useState(undefined);
|
||||
const [workflowRuns, setWorkflowRuns] = useState(undefined);
|
||||
const [subflowRuns, setSubflowRuns] = useState(undefined);
|
||||
|
||||
const handleDataSetting = (inputdata, grouping) => {
|
||||
if (inputdata === undefined || inputdata === null) {
|
||||
return
|
||||
}
|
||||
|
||||
const dailyStats = inputdata.daily_statistics
|
||||
if (dailyStats === undefined || dailyStats === null) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Looking at daily data: ", inputdata)
|
||||
|
||||
var appRuns = {
|
||||
"key": "App Runs",
|
||||
"data": []
|
||||
}
|
||||
|
||||
var workflowRuns = {
|
||||
"key": "Workflow Runs (includes subflows)",
|
||||
"data": []
|
||||
}
|
||||
|
||||
var subflowRuns = {
|
||||
"key": "Subflow Runs",
|
||||
"data": []
|
||||
}
|
||||
|
||||
for (let key in dailyStats) {
|
||||
// Always skips first one as it has accumulated data in it
|
||||
if (key === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const item = dailyStats[key]
|
||||
|
||||
if (item["date"] === undefined) {
|
||||
console.log("No date: ", item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if app_executions key in item
|
||||
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
|
||||
appRuns["data"].push({
|
||||
key: new Date(item["date"]),
|
||||
data: item["app_executions"]
|
||||
})
|
||||
}
|
||||
|
||||
// Check if workflow_executions key in item
|
||||
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
|
||||
workflowRuns["data"].push({
|
||||
key: new Date(item["date"]),
|
||||
data: item["workflow_executions"]
|
||||
})
|
||||
}
|
||||
|
||||
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
|
||||
subflowRuns["data"].push({
|
||||
key: new Date(item["date"]),
|
||||
data: item["subflow_executions"]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Adds data for today
|
||||
console.log("Inputdata: ", inputdata)
|
||||
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
|
||||
appRuns["data"].push({
|
||||
key: new Date(),
|
||||
data: inputdata["daily_app_executions"]
|
||||
})
|
||||
}
|
||||
|
||||
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
|
||||
workflowRuns["data"].push({
|
||||
key: new Date(),
|
||||
data: inputdata["daily_workflow_executions"]
|
||||
})
|
||||
}
|
||||
|
||||
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
|
||||
subflowRuns["data"].push({
|
||||
key: new Date(),
|
||||
data: inputdata["daily_subflow_executions"]
|
||||
})
|
||||
}
|
||||
|
||||
setSubflowRuns(subflowRuns)
|
||||
setWorkflowRuns(workflowRuns)
|
||||
setAppruns(appRuns)
|
||||
}
|
||||
|
||||
const getStats = () => {
|
||||
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
return
|
||||
}
|
||||
|
||||
setStatistics(responseJson)
|
||||
handleDataSetting(responseJson, "day")
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("error: ", error)
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getStats()
|
||||
}, [])
|
||||
|
||||
const paperStyle = {
|
||||
textAlign: "center",
|
||||
padding: 40,
|
||||
margin: 5,
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
maxWidth: 300,
|
||||
}
|
||||
|
||||
const data = (
|
||||
<div className="content" style={{width: "100%", margin: "auto", }}>
|
||||
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }}>
|
||||
All Stat widgets are monthly and gathered from <a
|
||||
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "#f85a3e",}}
|
||||
>Your Organization Statistics. </a>
|
||||
This is a feature to help give you more insight into Shuffle, and will be populating over time.
|
||||
</Typography>
|
||||
{statistics !== undefined ?
|
||||
<div style={{display: "flex", textAlign: "center",}}>
|
||||
<Paper style={paperStyle}>
|
||||
<Typography variant="h4">
|
||||
{statistics.monthly_workflow_executions}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
Workflow Runs
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Paper style={paperStyle}>
|
||||
<Typography variant="h4">
|
||||
{statistics.monthly_app_executions}
|
||||
</Typography>
|
||||
<Typography variant="h6">
|
||||
App Runs
|
||||
</Typography>
|
||||
</Paper>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{appRuns === undefined ?
|
||||
null
|
||||
:
|
||||
<LineChartWrapper keys={appRuns} height={300} width={"100%"} inputname={"Daily App Runs"}/>
|
||||
}
|
||||
|
||||
{workflowRuns === undefined ?
|
||||
null
|
||||
:
|
||||
<LineChartWrapper keys={workflowRuns} height={300} width={"100%"} inputname={"Daily Workflow Runs (including subflows)"}/>
|
||||
}
|
||||
|
||||
{subflowRuns === undefined ?
|
||||
null
|
||||
:
|
||||
<LineChartWrapper keys={subflowRuns} height={300} width={"100%"} inputname={"Subflow Runs"}/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
const dataWrapper = (
|
||||
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
|
||||
);
|
||||
|
||||
return dataWrapper;
|
||||
}
|
||||
|
||||
export default AppStats;
|
166
shuffle/frontend/src/components/Branding.jsx
Normal file
@ -0,0 +1,166 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ReactGA from 'react-ga4';
|
||||
import theme from "../theme.jsx";
|
||||
import { ToastContainer, toast } from "react-toastify"
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
} from "@mui/material";
|
||||
|
||||
//import { useAlert
|
||||
|
||||
const Branding = (props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props;
|
||||
//const alert = useAlert();
|
||||
const [publishingInfo, setPublishingInfo] = useState("");
|
||||
const [publishRequirements, setPublishRequirements] = useState([])
|
||||
|
||||
|
||||
const handleEditOrg = (joinStatus) => {
|
||||
const data = {
|
||||
"org_id": selectedOrganization.id,
|
||||
"creator_config": joinStatus,
|
||||
};
|
||||
|
||||
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
if (joinStatus == "join") {
|
||||
setPublishingInfo("Your organization is now part of the Creator Incentive Program. You can now create and publish content to your organization's page. You can also create a creator account to manage your organization's content.")
|
||||
} else {
|
||||
setPublishingInfo("Your organization is no longer part of the Creator Incentive Program. You can still create a creator account to manage your organization's content.")
|
||||
}
|
||||
handleGetOrg(selectedOrganization.id);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
// Should enable / disable org branding
|
||||
const handleChangePublishing = () => {
|
||||
console.log("Handle change publishing");
|
||||
|
||||
if (selectedOrganization.creator_id == "") {
|
||||
handleEditOrg("join")
|
||||
} else {
|
||||
handleEditOrg("leave")
|
||||
}
|
||||
}
|
||||
|
||||
const isOrganizationReady = () => {
|
||||
console.log("Is organization ready?")
|
||||
|
||||
// A simple checklist to ensure the button shows up properly
|
||||
if (selectedOrganization.name === selectedOrganization.org) {
|
||||
const comment = "Change the name of your organization"
|
||||
if (!publishRequirements.includes(comment)) {
|
||||
setPublishRequirements([...publishRequirements, comment])
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if it's a suborg
|
||||
if (selectedOrganization.creator_org !== "") {
|
||||
const comment = "Child orgs can't become creators"
|
||||
if (!publishRequirements.includes(comment)) {
|
||||
setPublishRequirements([...publishRequirements, comment])
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedOrganization.large_image === "" || selectedOrganization.large_image === theme.palette.defaultImage) {
|
||||
const comment = "Add a logo for your organization"
|
||||
if (!publishRequirements.includes(comment)) {
|
||||
setPublishRequirements([...publishRequirements, comment])
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>
|
||||
Branding
|
||||
</h2>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10 }}>
|
||||
You can customize your organization's branding by uploading a logo, changing the color scheme and a lot more.
|
||||
</Typography>
|
||||
|
||||
<Divider style={{marginTop: 50, marginBottom: 50, }} />
|
||||
<h2>
|
||||
Creator Incentive Program
|
||||
</h2>
|
||||
<div style={{ display: "flex", width: 900, }}>
|
||||
<div>
|
||||
<span>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
By changing publishing settings, you agree to our <a href="/docs/terms_of_service" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Terms of Service</a>, and acknowledge that your organization's non-sensitive data will be added as a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/creators">creator account</a>. None of your existing workflows, apps, or other stored data will be published. Any admin in your organization can manage the creator configuration. Becoming a creator organization is reversible.<div/>Support: <a href="mailto:support@shuffler.io"target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>support@shuffler.io</a>
|
||||
</Typography>
|
||||
{selectedOrganization.creator_id == "" ?
|
||||
<Typography variant="h6" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
|
||||
|
||||
</Typography>
|
||||
:
|
||||
<Typography variant="h6" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
|
||||
|
||||
<a href={`/creators/${selectedOrganization.creator_id}`} target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Modify your creator organization</a>
|
||||
</Typography>
|
||||
}
|
||||
|
||||
<Button
|
||||
style={{ height: 40, marginTop: 10, width: 300, }}
|
||||
variant={selectedOrganization.creator_id == "" ? "contained" : "outlined"}
|
||||
color={selectedOrganization.creator_id == "" ? "primary" : "secondary"}
|
||||
disabled={!isOrganizationReady()}
|
||||
onClick={() => {
|
||||
handleChangePublishing();
|
||||
}}
|
||||
>
|
||||
{selectedOrganization.creator_id == "" ? "Join" : "Leave"} Creators
|
||||
|
||||
</Button>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "white", }}>
|
||||
{publishingInfo}
|
||||
</Typography>
|
||||
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
|
||||
{publishRequirements.map((item) => {
|
||||
return (
|
||||
<div>
|
||||
Required: {item}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Branding;
|
504
shuffle/frontend/src/components/CacheView.jsx
Normal file
@ -0,0 +1,504 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import theme from "../theme.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
Divider,
|
||||
TextField,
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
FileCopy as FileCopyIcon,
|
||||
SelectAll as SelectAllIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
CloudDownload as CloudDownloadIcon,
|
||||
Description as DescriptionIcon,
|
||||
Polymer as PolymerIcon,
|
||||
CheckCircle as CheckCircleIcon,
|
||||
Close as CloseIcon,
|
||||
Apps as AppsIcon,
|
||||
Image as ImageIcon,
|
||||
Delete as DeleteIcon,
|
||||
Cached as CachedIcon,
|
||||
AccessibilityNew as AccessibilityNewIcon,
|
||||
Lock as LockIcon,
|
||||
Eco as EcoIcon,
|
||||
Schedule as ScheduleIcon,
|
||||
Cloud as CloudIcon,
|
||||
Business as BusinessIcon,
|
||||
Visibility as VisibilityIcon,
|
||||
VisibilityOff as VisibilityOffIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const scrollStyle1 = {
|
||||
height: 100,
|
||||
width: 225,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
}
|
||||
|
||||
const scrollStyle2 = {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: "-20px",
|
||||
right: "-20px",
|
||||
overflow: "scroll",
|
||||
}
|
||||
|
||||
const CacheView = (props) => {
|
||||
const { globalUrl, userdata, serverside, orgId } = props;
|
||||
const [orgCache, setOrgCache] = React.useState("");
|
||||
const [listCache, setListCache] = React.useState([]);
|
||||
const [addCache, setAddCache] = React.useState("");
|
||||
const [editedCache, setEditedCache] = React.useState("");
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [key, setKey] = React.useState("");
|
||||
const [value, setValue] = React.useState("");
|
||||
const [cacheInput, setCacheInput] = React.useState("");
|
||||
const [cacheCursor, setCacheCursor] = React.useState("");
|
||||
const [dataValue, setDataValue] = React.useState({});
|
||||
const [editCache, setEditCache] = React.useState(false);
|
||||
const [show, setShow] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
listOrgCache(orgId);
|
||||
console.log("orgid", orgId);
|
||||
}, []);
|
||||
|
||||
const listOrgCache = (orgId) => {
|
||||
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
setListCache(responseJson.keys);
|
||||
}
|
||||
|
||||
if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") {
|
||||
setCacheCursor(responseJson.cursor);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
// const getCacheList = (orgId) => {
|
||||
// fetch(`${globalUrl}/api/v1/orgs/${orgId}/get_cache`, {
|
||||
// method: "GET",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// Accept: "application/json",
|
||||
// },
|
||||
// credentials: "include",
|
||||
// })
|
||||
// .then((response) => {
|
||||
// if (response.status !== 200) {
|
||||
// console.log("Status not 200 for WORKFLOW EXECUTION :O!");
|
||||
// }
|
||||
|
||||
|
||||
// return response.json();
|
||||
// })
|
||||
// .then((responseJson) => {
|
||||
// if (responseJson.success !== false) {
|
||||
// console.log("Found cache: ", responseJson)
|
||||
// setListCache(responseJson)
|
||||
// } else {
|
||||
// console.log("Couldn't find the creator profile (rerun?): ", responseJson)
|
||||
// // If the current user is any of the Shuffle Creators
|
||||
// // AND the workflow doesn't have an owner: allow editing.
|
||||
// // else: Allow suggestions?
|
||||
// //console.log("User: ", userdata)
|
||||
// //if (rerun !== true) {
|
||||
// // getUserProfile(userdata.id, true)
|
||||
// //}
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.log("Get userprofile error: ", error);
|
||||
// })
|
||||
// }
|
||||
|
||||
|
||||
const deleteCache = (orgId, key) => {
|
||||
toast("Attempting to delete Cache");
|
||||
fetch(globalUrl + `/api/v1/orgs/${orgId}/cache/${key}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
toast("Successfully deleted Cache");
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId);
|
||||
}, 1000);
|
||||
} else {
|
||||
toast("Failed deleting Cache. Does it still exist?");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const editOrgCache = (orgId) => {
|
||||
const cache = { key: dataValue.key , value: value };
|
||||
setCacheInput([cache]);
|
||||
console.log("cache:", cache)
|
||||
console.log("cache input: ", cacheInput)
|
||||
|
||||
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
|
||||
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(cache),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for Cache :O!");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setAddCache(responseJson);
|
||||
toast("Cache Edited Successfully!");
|
||||
listOrgCache(orgId);
|
||||
setModalOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const addOrgCache = (orgId) => {
|
||||
const cache = { key: key, value: value };
|
||||
setCacheInput([cache]);
|
||||
console.log("cache input:", cacheInput)
|
||||
|
||||
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
|
||||
|
||||
method: "POST",
|
||||
body: JSON.stringify(cache),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setAddCache(responseJson);
|
||||
toast("New Cache Added Successfully!");
|
||||
listOrgCache(orgId);
|
||||
setModalOpen(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const modalView = (
|
||||
// console.log("key:", dataValue.key),
|
||||
//console.log("value:",dataValue.value),
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
minWidth: "800px",
|
||||
minHeight: "320px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<span style={{ color: "white" }}>
|
||||
{ editCache ? "Edit Cache" : "Add Cache" }
|
||||
</span>
|
||||
</DialogTitle>
|
||||
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
||||
Key
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{ backgroundColor: theme.palette.inputColor }}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
style: {
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
required
|
||||
fullWidth={true}
|
||||
autoComplete="Key"
|
||||
placeholder="abc"
|
||||
id="keyfield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
value={editCache ? dataValue.key : key}
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
||||
Value
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{ backgroundColor: theme.palette.inputColor }}
|
||||
InputProps={{
|
||||
style: {
|
||||
height: "50px",
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
required
|
||||
fullWidth={true}
|
||||
autoComplete="Value"
|
||||
placeholder="123"
|
||||
id="Valuefield"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
defaultValue={editCache ? dataValue.value : ""}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => setModalOpen(false)}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => {
|
||||
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{editCache ? "Edit":"Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
{modalView}
|
||||
<div style={{ marginTop: 20, marginBottom: 20 }}>
|
||||
<h2 style={{ display: "inline" }}>Shuffle Datastore</h2>
|
||||
<span style={{ marginLeft: 25 }}>
|
||||
Datastore is a key-value store for storing data that can be used cross-workflow.
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/docs/organizations#datastore"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
style={{}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() =>{
|
||||
setEditCache(false)
|
||||
setModalOpen(true)
|
||||
}
|
||||
}
|
||||
>
|
||||
Add Cache
|
||||
</Button>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => listOrgCache(orgId)}
|
||||
>
|
||||
<CachedIcon />
|
||||
</Button>
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
/>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Key"
|
||||
// style={{ minWidth: 150, maxWidth: 150 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="value"
|
||||
// style={{ minWidth: 150, maxWidth: 150 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Updated"
|
||||
// style={{ minWidth: 150, maxWidth: 150 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Actions"
|
||||
// style={{ minWidth: 150, maxWidth: 150 }}
|
||||
/>
|
||||
</ListItem>
|
||||
{listCache === undefined || listCache === null
|
||||
? null
|
||||
: listCache.map((data, index) => {
|
||||
var bgColor = "#27292d";
|
||||
if (index % 2 === 0) {
|
||||
bgColor = "#1f2023";
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor }}>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 225,
|
||||
minWidth: 225,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
primary={data.key}
|
||||
/>
|
||||
<div style={scrollStyle1}>
|
||||
<ListItemText
|
||||
// style={{
|
||||
// maxWidth: 225,
|
||||
// maxHeight: 150,
|
||||
// // overflow: "hidden",
|
||||
// paddingLeft: "52px",
|
||||
// overflow: "scroll",
|
||||
|
||||
// }}
|
||||
style={scrollStyle2}
|
||||
// style={{ maxWidth: 100, minWidth: 100 }}
|
||||
// onMouseOver={() =>
|
||||
// setShow((prevState) => ({ ...prevState, [data.value]: true }))
|
||||
// }
|
||||
// onMouseLeave={() =>
|
||||
// setShow((prevState) => ({ ...prevState, [data.value]: false }))
|
||||
// }
|
||||
//primary={show[data.value] ? data.value : `${data.value.substring(0, 5)}...`}
|
||||
primary={data.value}
|
||||
/>
|
||||
</div>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 225,
|
||||
minWidth: 225,
|
||||
overflow: "hidden",
|
||||
marginLeft: "42px",
|
||||
}}
|
||||
primary={new Date(data.edited * 1000).toISOString()}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
overflow: "hidden",
|
||||
paddingLeft: "155px",
|
||||
}}
|
||||
primary=<span style={{ display: "inline" }}>
|
||||
<Tooltip
|
||||
title="Edit"
|
||||
style={{}}
|
||||
aria-label={"Edit"}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
style={{ padding: "6px" }}
|
||||
onClick={() => {
|
||||
setEditCache(true)
|
||||
setDataValue({"key":data.key,"value":data.value})
|
||||
setModalOpen(true)
|
||||
}}
|
||||
>
|
||||
<EditIcon
|
||||
style={{ color: "white" }}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Delete Cache"}
|
||||
style={{ marginLeft: 15, }}
|
||||
aria-label={"Delete"}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
style={{ padding: "6px" }}
|
||||
onClick={() => {
|
||||
deleteCache(orgId, data.key);
|
||||
//deleteFile(orgId);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon
|
||||
style={{ color: "white" }}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
export default CacheView;
|
1357
shuffle/frontend/src/components/ConfigureWorkflow.jsx
Normal file
434
shuffle/frontend/src/components/Countries.jsx
Normal file
@ -0,0 +1,434 @@
|
||||
const countries = [
|
||||
{ code: 'GB', label: 'United Kingdom', phone: '44' },
|
||||
{
|
||||
code: 'US',
|
||||
label: 'United States',
|
||||
phone: '1',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'IN', label: 'India', phone: '91' },
|
||||
{ code: 'AD', label: 'Andorra', phone: '376' },
|
||||
{
|
||||
code: 'AE',
|
||||
label: 'United Arab Emirates',
|
||||
phone: '971',
|
||||
},
|
||||
{ code: 'AF', label: 'Afghanistan', phone: '93' },
|
||||
{
|
||||
code: 'AG',
|
||||
label: 'Antigua and Barbuda',
|
||||
phone: '1-268',
|
||||
},
|
||||
{ code: 'AI', label: 'Anguilla', phone: '1-264' },
|
||||
{ code: 'AL', label: 'Albania', phone: '355' },
|
||||
{ code: 'AM', label: 'Armenia', phone: '374' },
|
||||
{ code: 'AO', label: 'Angola', phone: '244' },
|
||||
{ code: 'AQ', label: 'Antarctica', phone: '672' },
|
||||
{ code: 'AR', label: 'Argentina', phone: '54' },
|
||||
{ code: 'AS', label: 'American Samoa', phone: '1-684' },
|
||||
{ code: 'AT', label: 'Austria', phone: '43' },
|
||||
{
|
||||
code: 'AU',
|
||||
label: 'Australia',
|
||||
phone: '61',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'AW', label: 'Aruba', phone: '297' },
|
||||
{ code: 'AX', label: 'Alland Islands', phone: '358' },
|
||||
{ code: 'AZ', label: 'Azerbaijan', phone: '994' },
|
||||
{
|
||||
code: 'BA',
|
||||
label: 'Bosnia and Herzegovina',
|
||||
phone: '387',
|
||||
},
|
||||
{ code: 'BB', label: 'Barbados', phone: '1-246' },
|
||||
{ code: 'BD', label: 'Bangladesh', phone: '880' },
|
||||
{ code: 'BE', label: 'Belgium', phone: '32' },
|
||||
{ code: 'BF', label: 'Burkina Faso', phone: '226' },
|
||||
{ code: 'BG', label: 'Bulgaria', phone: '359' },
|
||||
{ code: 'BH', label: 'Bahrain', phone: '973' },
|
||||
{ code: 'BI', label: 'Burundi', phone: '257' },
|
||||
{ code: 'BJ', label: 'Benin', phone: '229' },
|
||||
{ code: 'BL', label: 'Saint Barthelemy', phone: '590' },
|
||||
{ code: 'BM', label: 'Bermuda', phone: '1-441' },
|
||||
{ code: 'BN', label: 'Brunei Darussalam', phone: '673' },
|
||||
{ code: 'BO', label: 'Bolivia', phone: '591' },
|
||||
{ code: 'BR', label: 'Brazil', phone: '55' },
|
||||
{ code: 'BS', label: 'Bahamas', phone: '1-242' },
|
||||
{ code: 'BT', label: 'Bhutan', phone: '975' },
|
||||
{ code: 'BV', label: 'Bouvet Island', phone: '47' },
|
||||
{ code: 'BW', label: 'Botswana', phone: '267' },
|
||||
{ code: 'BY', label: 'Belarus', phone: '375' },
|
||||
{ code: 'BZ', label: 'Belize', phone: '501' },
|
||||
{
|
||||
code: 'CA',
|
||||
label: 'Canada',
|
||||
phone: '1',
|
||||
suggested: true,
|
||||
},
|
||||
{
|
||||
code: 'CC',
|
||||
label: 'Cocos (Keeling) Islands',
|
||||
phone: '61',
|
||||
},
|
||||
{
|
||||
code: 'CD',
|
||||
label: 'Congo, Democratic Republic of the',
|
||||
phone: '243',
|
||||
},
|
||||
{
|
||||
code: 'CF',
|
||||
label: 'Central African Republic',
|
||||
phone: '236',
|
||||
},
|
||||
{
|
||||
code: 'CG',
|
||||
label: 'Congo, Republic of the',
|
||||
phone: '242',
|
||||
},
|
||||
{ code: 'CH', label: 'Switzerland', phone: '41' },
|
||||
{ code: 'CI', label: "Cote d'Ivoire", phone: '225' },
|
||||
{ code: 'CK', label: 'Cook Islands', phone: '682' },
|
||||
{ code: 'CL', label: 'Chile', phone: '56' },
|
||||
{ code: 'CM', label: 'Cameroon', phone: '237' },
|
||||
{ code: 'CN', label: 'China', phone: '86' },
|
||||
{ code: 'CO', label: 'Colombia', phone: '57' },
|
||||
{ code: 'CR', label: 'Costa Rica', phone: '506' },
|
||||
{ code: 'CU', label: 'Cuba', phone: '53' },
|
||||
{ code: 'CV', label: 'Cape Verde', phone: '238' },
|
||||
{ code: 'CW', label: 'Curacao', phone: '599' },
|
||||
{ code: 'CX', label: 'Christmas Island', phone: '61' },
|
||||
{ code: 'CY', label: 'Cyprus', phone: '357' },
|
||||
{ code: 'CZ', label: 'Czech Republic', phone: '420' },
|
||||
{
|
||||
code: 'DE',
|
||||
label: 'Germany',
|
||||
phone: '49',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'DJ', label: 'Djibouti', phone: '253' },
|
||||
{ code: 'DK', label: 'Denmark', phone: '45' },
|
||||
{ code: 'DM', label: 'Dominica', phone: '1-767' },
|
||||
{
|
||||
code: 'DO',
|
||||
label: 'Dominican Republic',
|
||||
phone: '1-809',
|
||||
},
|
||||
{ code: 'DZ', label: 'Algeria', phone: '213' },
|
||||
{ code: 'EC', label: 'Ecuador', phone: '593' },
|
||||
{ code: 'EE', label: 'Estonia', phone: '372' },
|
||||
{ code: 'EG', label: 'Egypt', phone: '20' },
|
||||
{ code: 'EH', label: 'Western Sahara', phone: '212' },
|
||||
{ code: 'ER', label: 'Eritrea', phone: '291' },
|
||||
{ code: 'ES', label: 'Spain', phone: '34' },
|
||||
{ code: 'ET', label: 'Ethiopia', phone: '251' },
|
||||
{ code: 'FI', label: 'Finland', phone: '358' },
|
||||
{ code: 'FJ', label: 'Fiji', phone: '679' },
|
||||
{
|
||||
code: 'FK',
|
||||
label: 'Falkland Islands (Malvinas)',
|
||||
phone: '500',
|
||||
},
|
||||
{
|
||||
code: 'FM',
|
||||
label: 'Micronesia, Federated States of',
|
||||
phone: '691',
|
||||
},
|
||||
{ code: 'FO', label: 'Faroe Islands', phone: '298' },
|
||||
{
|
||||
code: 'FR',
|
||||
label: 'France',
|
||||
phone: '33',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'GA', label: 'Gabon', phone: '241' },
|
||||
{ code: 'GB', label: 'United Kingdom', phone: '44' },
|
||||
{ code: 'GD', label: 'Grenada', phone: '1-473' },
|
||||
{ code: 'GE', label: 'Georgia', phone: '995' },
|
||||
{ code: 'GF', label: 'French Guiana', phone: '594' },
|
||||
{ code: 'GG', label: 'Guernsey', phone: '44' },
|
||||
{ code: 'GH', label: 'Ghana', phone: '233' },
|
||||
{ code: 'GI', label: 'Gibraltar', phone: '350' },
|
||||
{ code: 'GL', label: 'Greenland', phone: '299' },
|
||||
{ code: 'GM', label: 'Gambia', phone: '220' },
|
||||
{ code: 'GN', label: 'Guinea', phone: '224' },
|
||||
{ code: 'GP', label: 'Guadeloupe', phone: '590' },
|
||||
{ code: 'GQ', label: 'Equatorial Guinea', phone: '240' },
|
||||
{ code: 'GR', label: 'Greece', phone: '30' },
|
||||
{
|
||||
code: 'GS',
|
||||
label: 'South Georgia and the South Sandwich Islands',
|
||||
phone: '500',
|
||||
},
|
||||
{ code: 'GT', label: 'Guatemala', phone: '502' },
|
||||
{ code: 'GU', label: 'Guam', phone: '1-671' },
|
||||
{ code: 'GW', label: 'Guinea-Bissau', phone: '245' },
|
||||
{ code: 'GY', label: 'Guyana', phone: '592' },
|
||||
{ code: 'HK', label: 'Hong Kong', phone: '852' },
|
||||
{
|
||||
code: 'HM',
|
||||
label: 'Heard Island and McDonald Islands',
|
||||
phone: '672',
|
||||
},
|
||||
{ code: 'HN', label: 'Honduras', phone: '504' },
|
||||
{ code: 'HR', label: 'Croatia', phone: '385' },
|
||||
{ code: 'HT', label: 'Haiti', phone: '509' },
|
||||
{ code: 'HU', label: 'Hungary', phone: '36' },
|
||||
{ code: 'ID', label: 'Indonesia', phone: '62' },
|
||||
{ code: 'IE', label: 'Ireland', phone: '353' },
|
||||
{ code: 'IL', label: 'Israel', phone: '972' },
|
||||
{ code: 'IM', label: 'Isle of Man', phone: '44' },
|
||||
{ code: 'IN', label: 'India', phone: '91' },
|
||||
{
|
||||
code: 'IO',
|
||||
label: 'British Indian Ocean Territory',
|
||||
phone: '246',
|
||||
},
|
||||
{ code: 'IQ', label: 'Iraq', phone: '964' },
|
||||
{
|
||||
code: 'IR',
|
||||
label: 'Iran, Islamic Republic of',
|
||||
phone: '98',
|
||||
},
|
||||
{ code: 'IS', label: 'Iceland', phone: '354' },
|
||||
{ code: 'IT', label: 'Italy', phone: '39' },
|
||||
{ code: 'JE', label: 'Jersey', phone: '44' },
|
||||
{ code: 'JM', label: 'Jamaica', phone: '1-876' },
|
||||
{ code: 'JO', label: 'Jordan', phone: '962' },
|
||||
{
|
||||
code: 'JP',
|
||||
label: 'Japan',
|
||||
phone: '81',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'KE', label: 'Kenya', phone: '254' },
|
||||
{ code: 'KG', label: 'Kyrgyzstan', phone: '996' },
|
||||
{ code: 'KH', label: 'Cambodia', phone: '855' },
|
||||
{ code: 'KI', label: 'Kiribati', phone: '686' },
|
||||
{ code: 'KM', label: 'Comoros', phone: '269' },
|
||||
{
|
||||
code: 'KN',
|
||||
label: 'Saint Kitts and Nevis',
|
||||
phone: '1-869',
|
||||
},
|
||||
{
|
||||
code: 'KP',
|
||||
label: "Korea, Democratic People's Republic of",
|
||||
phone: '850',
|
||||
},
|
||||
{ code: 'KR', label: 'Korea, Republic of', phone: '82' },
|
||||
{ code: 'KW', label: 'Kuwait', phone: '965' },
|
||||
{ code: 'KY', label: 'Cayman Islands', phone: '1-345' },
|
||||
{ code: 'KZ', label: 'Kazakhstan', phone: '7' },
|
||||
{
|
||||
code: 'LA',
|
||||
label: "Lao People's Democratic Republic",
|
||||
phone: '856',
|
||||
},
|
||||
{ code: 'LB', label: 'Lebanon', phone: '961' },
|
||||
{ code: 'LC', label: 'Saint Lucia', phone: '1-758' },
|
||||
{ code: 'LI', label: 'Liechtenstein', phone: '423' },
|
||||
{ code: 'LK', label: 'Sri Lanka', phone: '94' },
|
||||
{ code: 'LR', label: 'Liberia', phone: '231' },
|
||||
{ code: 'LS', label: 'Lesotho', phone: '266' },
|
||||
{ code: 'LT', label: 'Lithuania', phone: '370' },
|
||||
{ code: 'LU', label: 'Luxembourg', phone: '352' },
|
||||
{ code: 'LV', label: 'Latvia', phone: '371' },
|
||||
{ code: 'LY', label: 'Libya', phone: '218' },
|
||||
{ code: 'MA', label: 'Morocco', phone: '212' },
|
||||
{ code: 'MC', label: 'Monaco', phone: '377' },
|
||||
{
|
||||
code: 'MD',
|
||||
label: 'Moldova, Republic of',
|
||||
phone: '373',
|
||||
},
|
||||
{ code: 'ME', label: 'Montenegro', phone: '382' },
|
||||
{
|
||||
code: 'MF',
|
||||
label: 'Saint Martin (French part)',
|
||||
phone: '590',
|
||||
},
|
||||
{ code: 'MG', label: 'Madagascar', phone: '261' },
|
||||
{ code: 'MH', label: 'Marshall Islands', phone: '692' },
|
||||
{
|
||||
code: 'MK',
|
||||
label: 'Macedonia, the Former Yugoslav Republic of',
|
||||
phone: '389',
|
||||
},
|
||||
{ code: 'ML', label: 'Mali', phone: '223' },
|
||||
{ code: 'MM', label: 'Myanmar', phone: '95' },
|
||||
{ code: 'MN', label: 'Mongolia', phone: '976' },
|
||||
{ code: 'MO', label: 'Macao', phone: '853' },
|
||||
{
|
||||
code: 'MP',
|
||||
label: 'Northern Mariana Islands',
|
||||
phone: '1-670',
|
||||
},
|
||||
{ code: 'MQ', label: 'Martinique', phone: '596' },
|
||||
{ code: 'MR', label: 'Mauritania', phone: '222' },
|
||||
{ code: 'MS', label: 'Montserrat', phone: '1-664' },
|
||||
{ code: 'MT', label: 'Malta', phone: '356' },
|
||||
{ code: 'MU', label: 'Mauritius', phone: '230' },
|
||||
{ code: 'MV', label: 'Maldives', phone: '960' },
|
||||
{ code: 'MW', label: 'Malawi', phone: '265' },
|
||||
{ code: 'MX', label: 'Mexico', phone: '52' },
|
||||
{ code: 'MY', label: 'Malaysia', phone: '60' },
|
||||
{ code: 'MZ', label: 'Mozambique', phone: '258' },
|
||||
{ code: 'NA', label: 'Namibia', phone: '264' },
|
||||
{ code: 'NC', label: 'New Caledonia', phone: '687' },
|
||||
{ code: 'NE', label: 'Niger', phone: '227' },
|
||||
{ code: 'NF', label: 'Norfolk Island', phone: '672' },
|
||||
{ code: 'NG', label: 'Nigeria', phone: '234' },
|
||||
{ code: 'NI', label: 'Nicaragua', phone: '505' },
|
||||
{ code: 'NL', label: 'Netherlands', phone: '31' },
|
||||
{ code: 'NO', label: 'Norway', phone: '47' },
|
||||
{ code: 'NP', label: 'Nepal', phone: '977' },
|
||||
{ code: 'NR', label: 'Nauru', phone: '674' },
|
||||
{ code: 'NU', label: 'Niue', phone: '683' },
|
||||
{ code: 'NZ', label: 'New Zealand', phone: '64' },
|
||||
{ code: 'OM', label: 'Oman', phone: '968' },
|
||||
{ code: 'PA', label: 'Panama', phone: '507' },
|
||||
{ code: 'PE', label: 'Peru', phone: '51' },
|
||||
{ code: 'PF', label: 'French Polynesia', phone: '689' },
|
||||
{ code: 'PG', label: 'Papua New Guinea', phone: '675' },
|
||||
{ code: 'PH', label: 'Philippines', phone: '63' },
|
||||
{ code: 'PK', label: 'Pakistan', phone: '92' },
|
||||
{ code: 'PL', label: 'Poland', phone: '48' },
|
||||
{
|
||||
code: 'PM',
|
||||
label: 'Saint Pierre and Miquelon',
|
||||
phone: '508',
|
||||
},
|
||||
{ code: 'PN', label: 'Pitcairn', phone: '870' },
|
||||
{ code: 'PR', label: 'Puerto Rico', phone: '1' },
|
||||
{
|
||||
code: 'PS',
|
||||
label: 'Palestine, State of',
|
||||
phone: '970',
|
||||
},
|
||||
{ code: 'PT', label: 'Portugal', phone: '351' },
|
||||
{ code: 'PW', label: 'Palau', phone: '680' },
|
||||
{ code: 'PY', label: 'Paraguay', phone: '595' },
|
||||
{ code: 'QA', label: 'Qatar', phone: '974' },
|
||||
{ code: 'RE', label: 'Reunion', phone: '262' },
|
||||
{ code: 'RO', label: 'Romania', phone: '40' },
|
||||
{ code: 'RS', label: 'Serbia', phone: '381' },
|
||||
{ code: 'RU', label: 'Russian Federation', phone: '7' },
|
||||
{ code: 'RW', label: 'Rwanda', phone: '250' },
|
||||
{ code: 'SA', label: 'Saudi Arabia', phone: '966' },
|
||||
{ code: 'SB', label: 'Solomon Islands', phone: '677' },
|
||||
{ code: 'SC', label: 'Seychelles', phone: '248' },
|
||||
{ code: 'SD', label: 'Sudan', phone: '249' },
|
||||
{ code: 'SE', label: 'Sweden', phone: '46' },
|
||||
{ code: 'SG', label: 'Singapore', phone: '65' },
|
||||
{ code: 'SH', label: 'Saint Helena', phone: '290' },
|
||||
{ code: 'SI', label: 'Slovenia', phone: '386' },
|
||||
{
|
||||
code: 'SJ',
|
||||
label: 'Svalbard and Jan Mayen',
|
||||
phone: '47',
|
||||
},
|
||||
{ code: 'SK', label: 'Slovakia', phone: '421' },
|
||||
{ code: 'SL', label: 'Sierra Leone', phone: '232' },
|
||||
{ code: 'SM', label: 'San Marino', phone: '378' },
|
||||
{ code: 'SN', label: 'Senegal', phone: '221' },
|
||||
{ code: 'SO', label: 'Somalia', phone: '252' },
|
||||
{ code: 'SR', label: 'Suriname', phone: '597' },
|
||||
{ code: 'SS', label: 'South Sudan', phone: '211' },
|
||||
{
|
||||
code: 'ST',
|
||||
label: 'Sao Tome and Principe',
|
||||
phone: '239',
|
||||
},
|
||||
{ code: 'SV', label: 'El Salvador', phone: '503' },
|
||||
{
|
||||
code: 'SX',
|
||||
label: 'Sint Maarten (Dutch part)',
|
||||
phone: '1-721',
|
||||
},
|
||||
{
|
||||
code: 'SY',
|
||||
label: 'Syrian Arab Republic',
|
||||
phone: '963',
|
||||
},
|
||||
{ code: 'SZ', label: 'Swaziland', phone: '268' },
|
||||
{
|
||||
code: 'TC',
|
||||
label: 'Turks and Caicos Islands',
|
||||
phone: '1-649',
|
||||
},
|
||||
{ code: 'TD', label: 'Chad', phone: '235' },
|
||||
{
|
||||
code: 'TF',
|
||||
label: 'French Southern Territories',
|
||||
phone: '262',
|
||||
},
|
||||
{ code: 'TG', label: 'Togo', phone: '228' },
|
||||
{ code: 'TH', label: 'Thailand', phone: '66' },
|
||||
{ code: 'TJ', label: 'Tajikistan', phone: '992' },
|
||||
{ code: 'TK', label: 'Tokelau', phone: '690' },
|
||||
{ code: 'TL', label: 'Timor-Leste', phone: '670' },
|
||||
{ code: 'TM', label: 'Turkmenistan', phone: '993' },
|
||||
{ code: 'TN', label: 'Tunisia', phone: '216' },
|
||||
{ code: 'TO', label: 'Tonga', phone: '676' },
|
||||
{ code: 'TR', label: 'Turkey', phone: '90' },
|
||||
{
|
||||
code: 'TT',
|
||||
label: 'Trinidad and Tobago',
|
||||
phone: '1-868',
|
||||
},
|
||||
{ code: 'TV', label: 'Tuvalu', phone: '688' },
|
||||
{
|
||||
code: 'TW',
|
||||
label: 'Taiwan, Province of China',
|
||||
phone: '886',
|
||||
},
|
||||
{
|
||||
code: 'TZ',
|
||||
label: 'United Republic of Tanzania',
|
||||
phone: '255',
|
||||
},
|
||||
{ code: 'UA', label: 'Ukraine', phone: '380' },
|
||||
{ code: 'UG', label: 'Uganda', phone: '256' },
|
||||
{
|
||||
code: 'US',
|
||||
label: 'United States',
|
||||
phone: '1',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'UY', label: 'Uruguay', phone: '598' },
|
||||
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
|
||||
{
|
||||
code: 'VA',
|
||||
label: 'Holy See (Vatican City State)',
|
||||
phone: '379',
|
||||
},
|
||||
{
|
||||
code: 'VC',
|
||||
label: 'Saint Vincent and the Grenadines',
|
||||
phone: '1-784',
|
||||
},
|
||||
{ code: 'VE', label: 'Venezuela', phone: '58' },
|
||||
{
|
||||
code: 'VG',
|
||||
label: 'British Virgin Islands',
|
||||
phone: '1-284',
|
||||
},
|
||||
{
|
||||
code: 'VI',
|
||||
label: 'US Virgin Islands',
|
||||
phone: '1-340',
|
||||
},
|
||||
{ code: 'VN', label: 'Vietnam', phone: '84' },
|
||||
{ code: 'VU', label: 'Vanuatu', phone: '678' },
|
||||
{ code: 'WF', label: 'Wallis and Futuna', phone: '681' },
|
||||
{ code: 'WS', label: 'Samoa', phone: '685' },
|
||||
{ code: 'XK', label: 'Kosovo', phone: '383' },
|
||||
{ code: 'YE', label: 'Yemen', phone: '967' },
|
||||
{ code: 'YT', label: 'Mayotte', phone: '262' },
|
||||
{ code: 'ZA', label: 'South Africa', phone: '27' },
|
||||
{ code: 'ZM', label: 'Zambia', phone: '260' },
|
||||
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
|
||||
];
|
||||
|
||||
export default countries
|
304
shuffle/frontend/src/components/CreatorGrid.jsx
Normal file
@ -0,0 +1,304 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
import theme from '../theme.jsx';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
|
||||
import {
|
||||
SkipNext as SkipNextIcon,
|
||||
SkipPrevious as SkipPreviousIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
VerifiedUser as VerifiedUserIcon,
|
||||
Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
Card,
|
||||
Box,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Zoom,
|
||||
CardMedia,
|
||||
CardActionArea,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const CreatorGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Workflows | Discover your use-case"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
refine(foundQuery)
|
||||
defaultSearch = foundQuery
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
defaultValue={defaultSearch}
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
placeholder="Find Creators..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const paperAppContainer = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={4} style={paperAppContainer}>
|
||||
{hits.map((data, index) => {
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
const creatorUrl = !isCloud ? `https://shuffler.io/creators/${data.username}` : `/creators/${data.username}`
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{}}>
|
||||
<Grid item xs={xs} style={{ padding: "12px 10px 12px 10px", }}>
|
||||
<Card style={{border: "1px solid rgba(255,255,255,0.3)", minHeight: 177, maxHeight: 177,}}>
|
||||
<a href={creatorUrl} rel="noopener noreferrer" target={isCloud ? "" : "_blank"} style={{textDecoration: "none", color: "inherit",}}>
|
||||
<CardActionArea style={{padding: "5px 10px 5px 10px", minHeight: 177, maxHeight: 177,}}>
|
||||
<CardContent sx={{ flex: '1 0 auto', minWidth: 160, maxWidth: 160, overflow: "hidden", padding: 0, }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<img style={{height: 74, width: 74, borderRadius: 100, }} alt={"Creator profile of "+data.username} src={data.image} />
|
||||
<Typography component="div" variant="body1" style={{marginTop: 20, marginLeft: 15, }}>
|
||||
@{data.username}
|
||||
</Typography>
|
||||
<span style={{marginTop: "auto", marginBottom: "auto", marginLeft: 10, }}>
|
||||
{data.verified === true ?
|
||||
<Tooltip title="Verified and earning from Shuffle contributions" placement="top">
|
||||
<VerifiedUserIcon style={{}}/>
|
||||
</Tooltip>
|
||||
:
|
||||
null
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
|
||||
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
|
||||
</Typography>
|
||||
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
|
||||
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
|
||||
{data.specialized_apps.map((app, index) => {
|
||||
// Putting all this in secondary of ListItemText looked weird.
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
console.log("Click")
|
||||
//navigate("/apps/"+app.id)
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={app.name} placement="bottom">
|
||||
<Avatar alt={app.name} src={app.image} style={{width: 24, height: 24}}/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
:
|
||||
null}
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</a>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="creators">
|
||||
<Configure clickAnalytics />
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={100}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatorGrid;
|
357
shuffle/frontend/src/components/DocsGrid.jsx
Normal file
@ -0,0 +1,357 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
|
||||
import aa from 'search-insights'
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Zoom,
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
Avatar,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const DocsGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integrate any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery)
|
||||
refine(foundQuery)
|
||||
defaultSearch = foundQuery
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
defaultValue={defaultSearch}
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
placeholder="Search our Documentation..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
//console.log(hits)
|
||||
//var curhits = hits
|
||||
//if (hits.length > 0 && defaultApps.length === 0) {
|
||||
// setDefaultApps(hits)
|
||||
//}
|
||||
|
||||
//const [defaultApps, setDefaultApps] = React.useState([])
|
||||
//console.log(hits)
|
||||
//if (hits.length > 0 && hits.length !== innerHits.length) {
|
||||
// setInnerHits(hits)
|
||||
//}
|
||||
|
||||
var counted = 0
|
||||
return (
|
||||
<List>
|
||||
{hits.map((data, index) => {
|
||||
workflowDelay += 50
|
||||
|
||||
const innerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
if (counted >= 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
|
||||
var name = data.name === undefined ?
|
||||
data.filename.charAt(0).toUpperCase() + data.filename.slice(1).replaceAll("_", " ") + " - " + data.title :
|
||||
(data.name.charAt(0).toUpperCase()+data.name.slice(1)).replaceAll("_", " ")
|
||||
|
||||
if (name.length > 96) {
|
||||
name = name.slice(0, 96)+"..."
|
||||
}
|
||||
|
||||
//const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : ""
|
||||
const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : ""
|
||||
const baseImage = <CodeIcon/>
|
||||
const avatar = data.image_url === undefined ?
|
||||
baseImage
|
||||
:
|
||||
<Avatar
|
||||
src={data.image_url}
|
||||
variant="rounded"
|
||||
/>
|
||||
|
||||
var parsedUrl = data.urlpath !== undefined ? data.urlpath : ""
|
||||
parsedUrl += `?queryID=${data.__queryID}`
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Link key={data.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Product Clicked Appgrid',
|
||||
index: 'documentation',
|
||||
objectIDs: [data.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: data.__queryID,
|
||||
positions: [data.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
console.log("CLICK")
|
||||
}}>
|
||||
<ListItem key={data.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={secondaryText}
|
||||
/>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
const selectButtonStyle = {
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
minHeight: 50,
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
|
||||
{/*
|
||||
<div style={{padding: 10, }}>
|
||||
<Button
|
||||
style={selectButtonStyle}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
const searchField = document.createElement("shuffle_search_field")
|
||||
console.log("Field: ", searchField)
|
||||
if (searchField !== null & searchField !== undefined) {
|
||||
console.log("Set field.")
|
||||
searchField.value = "WHAT WABALABA"
|
||||
searchField.setAttribute("value", "WHAT WABALABA")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cases
|
||||
</Button>
|
||||
</div>
|
||||
*/}
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="documentation">
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<Configure clickAnalytics />
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocsGrid;
|
94
shuffle/frontend/src/components/Dropzone.jsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
Backup as BackupIcon
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const dragOverStyle = {
|
||||
backgroundColor: "rgba(0,0,0,0.8)",
|
||||
border: "5px dashed white",
|
||||
borderRadius: "8px",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
overflow: "hidden",
|
||||
zIndex: 100,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
};
|
||||
|
||||
const Dropzone = ({ children, style, onDrop }) => {
|
||||
const dropzoneRef = useRef(null);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
let dragCounter = 0;
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter++;
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0)
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter--;
|
||||
if (dragCounter === 0) setDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDragging(false);
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
onDrop(e);
|
||||
e.dataTransfer.clearData();
|
||||
dragCounter = 0;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dropzoneRef === null || dropzoneRef === undefined || dropzoneRef.current === null || dropzoneRef.current === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if event listene exists for dropzoneRef.current
|
||||
|
||||
dropzoneRef.current.addEventListener("dragover", handleDragOver);
|
||||
dropzoneRef.current.addEventListener("dragenter", handleDragEnter);
|
||||
dropzoneRef.current.addEventListener("dragleave", handleDragLeave);
|
||||
dropzoneRef.current.addEventListener("drop", handleDrop);
|
||||
|
||||
return () => {
|
||||
if (dropzoneRef.current === null || dropzoneRef.current === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
dropzoneRef.current.removeEventListener("dragover", handleDragOver);
|
||||
dropzoneRef.current.removeEventListener("dragenter", handleDragEnter);
|
||||
dropzoneRef.current.removeEventListener("dragleave", handleDragLeave);
|
||||
dropzoneRef.current.removeEventListener("drop", handleDrop);
|
||||
};
|
||||
}, [dropzoneRef]);
|
||||
|
||||
return (
|
||||
<div ref={dropzoneRef} style={{ position: "relative", ...style }}>
|
||||
{dragging && (
|
||||
<div style={dragOverStyle}>
|
||||
<BackupIcon fontSize="large" />
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dropzone;
|
601
shuffle/frontend/src/components/EditWorkflow.jsx
Normal file
@ -0,0 +1,601 @@
|
||||
import React, { useEffect, useContext } from "react";
|
||||
import theme from '../theme.jsx';
|
||||
import { isMobile } from "react-device-detect"
|
||||
import { MuiChipsInput } from "mui-chips-input";
|
||||
import UsecaseSearch from "../components/UsecaseSearch.jsx"
|
||||
import WorkflowGrid from "../components/WorkflowGrid.jsx"
|
||||
import dayjs from 'dayjs';
|
||||
import WorkflowTemplatePopup from "./WorkflowTemplatePopup.jsx";
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Avatar,
|
||||
Grid,
|
||||
InputLabel,
|
||||
Select,
|
||||
ListSubheader,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Divider,
|
||||
Button,
|
||||
TextField,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
FormControlLabel,
|
||||
Chip,
|
||||
Switch,
|
||||
Typography,
|
||||
Zoom,
|
||||
CircularProgress,
|
||||
Drawer,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
ListItemText,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
DatePicker,
|
||||
LocalizationProvider,
|
||||
} from '@mui/x-date-pickers'
|
||||
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
||||
|
||||
import {
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Publish as PublishIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const EditWorkflow = (props) => {
|
||||
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, } = props
|
||||
|
||||
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
|
||||
|
||||
const [submitLoading, setSubmitLoading] = React.useState(false);
|
||||
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
|
||||
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
|
||||
|
||||
const [newWorkflowTags, setNewWorkflowTags] = React.useState(workflow.tags !== undefined && workflow.tags !== null ? JSON.parse(JSON.stringify(workflow.tags)) : [])
|
||||
const [description, setDescription] = React.useState(workflow.description !== undefined ? workflow.description : "")
|
||||
|
||||
const [selectedUsecases, setSelectedUsecases] = React.useState(workflow.usecase_ids !== undefined && workflow.usecase_ids !== null ? JSON.parse(JSON.stringify(workflow.usecase_ids)) : []);
|
||||
const [foundWorkflowId, setFoundWorkflowId] = React.useState("")
|
||||
const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "")
|
||||
const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day'))
|
||||
|
||||
// Gets the generated workflow
|
||||
const getGeneratedWorkflow = (workflow_id) => {
|
||||
fetch(globalUrl + "/api/v1/workflows/" + workflow_id, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 when getting workflow");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.id === workflow_id) {
|
||||
console.log("GOT WORKFLOW: ", responseJson)
|
||||
if (name === "") {
|
||||
innerWorkflow.name = responseJson.name
|
||||
setName(responseJson.name)
|
||||
}
|
||||
|
||||
if (description === "") {
|
||||
innerWorkflow.description = responseJson.description
|
||||
setDescription(description)
|
||||
}
|
||||
|
||||
if (newWorkflowTags === []) {
|
||||
innerWorkflow.tags = responseJson.tags
|
||||
setNewWorkflowTags(responseJson.tags)
|
||||
}
|
||||
|
||||
if (selectedUsecases === []) {
|
||||
selectedUsecases = responseJson.usecase_ids
|
||||
}
|
||||
|
||||
innerWorkflow.id = responseJson.id
|
||||
innerWorkflow.blogpost = responseJson.blogpost
|
||||
innerWorkflow.actions = responseJson.actions
|
||||
innerWorkflow.triggers = responseJson.triggers
|
||||
innerWorkflow.branches = responseJson.branches
|
||||
innerWorkflow.comments = responseJson.comments
|
||||
innerWorkflow.workflow_variables = responseJson.workflow_variables
|
||||
innerWorkflow.execution_variables = responseJson.execution_variables
|
||||
|
||||
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//toast(error.toString());
|
||||
console.log("Get workflow error: ", error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
if (foundWorkflowId.length > 0) {
|
||||
getGeneratedWorkflow(foundWorkflowId)
|
||||
|
||||
setFoundWorkflowId("")
|
||||
} else {
|
||||
}
|
||||
|
||||
if (modalOpen !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
const newWorkflow = isEditing === true ? false : true
|
||||
const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true)
|
||||
console.log("PRIO: ", priority)
|
||||
|
||||
var upload = "";
|
||||
var total_count = 0
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
minWidth: isMobile ? "90%" : 650,
|
||||
maxWidth: isMobile ? "90%" : 650,
|
||||
minHeight: 400,
|
||||
paddingTop: 25,
|
||||
paddingLeft: 50,
|
||||
//minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
|
||||
//maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{padding: 30, paddingBottom: 0, zIndex: 1000,}}>
|
||||
<div style={{display: "flex"}}>
|
||||
<div style={{flex: 1, color: "rgba(255,255,255,0.9)" }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<Typography variant="h4" style={{flex: 9, }}>
|
||||
{newWorkflow ? "New" : "Editing"} workflow
|
||||
</Typography>
|
||||
{newWorkflow === true ? null :
|
||||
<div style={{ marginLeft: 5, flex: 1 }}>
|
||||
<Tooltip title="Open Workflow Form for 'normal' users">
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
href={`/workflows/${workflow.id}/run`}
|
||||
target="_blank"
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "#f85a3e",
|
||||
marginLeft: 5,
|
||||
marginTop: 10,
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 20, maxWidth: 440,}}>
|
||||
Workflows can be built from scratch, or from templates. <a href="/usecases" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
|
||||
</Typography>
|
||||
{showUpload === true ?
|
||||
<div style={{ float: "right" }}>
|
||||
<Tooltip color="primary" title={"Import manually"} placement="top">
|
||||
<Button
|
||||
color="primary"
|
||||
style={{}}
|
||||
variant="text"
|
||||
onClick={() => upload.click()}
|
||||
>
|
||||
<PublishIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
{/*newWorkflow === true ?
|
||||
<div style={{flex: 1, marginLeft: 45, }}>
|
||||
<Typography variant="h6">
|
||||
Use a Template
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{maxWidth: 440,}}>
|
||||
Start your workflow from our templating system. This uses publied workflows from our <a href="/creators" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Creators</a> to generate full Usecases or parts of your Workflow.
|
||||
</Typography>
|
||||
</div>
|
||||
: null*/}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<FormControl>
|
||||
<DialogContent style={{paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, }}>
|
||||
<div style={{minWidth: newWorkflow ? 500 : 550, maxWidth: newWorkflow ? 450 : 500, }}>
|
||||
<TextField
|
||||
onChange={(event) => {
|
||||
setName(event.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
placeholder="Name"
|
||||
required
|
||||
margin="dense"
|
||||
defaultValue={innerWorkflow.name}
|
||||
label="Name"
|
||||
autoFocus
|
||||
fullWidth
|
||||
/>
|
||||
<div style={{display: "flex", }}>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
setDescription(event.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
maxRows={4}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.description}
|
||||
placeholder="Description"
|
||||
multiline
|
||||
label="Description"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<div style={{display: "flex", marginTop: 10, }}>
|
||||
<MuiChipsInput
|
||||
style={{ flex: 1, maxHeight: 40, }}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
placeholder="Tags"
|
||||
color="primary"
|
||||
fullWidth
|
||||
value={newWorkflowTags}
|
||||
onChange={(chip) => {
|
||||
console.log("Chip: ", chip)
|
||||
//newWorkflowTags.push(chip);
|
||||
setNewWorkflowTags(chip);
|
||||
}}
|
||||
onAdd={(chip) => {
|
||||
newWorkflowTags.push(chip);
|
||||
setNewWorkflowTags(newWorkflowTags);
|
||||
}}
|
||||
onDelete={(chip, index) => {
|
||||
console.log("Deleting: ", chip, index)
|
||||
newWorkflowTags.splice(index, 1);
|
||||
setNewWorkflowTags(newWorkflowTags);
|
||||
setUpdate(Math.random());
|
||||
}}
|
||||
/>
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<FormControl style={{flex: 1, marginLeft: 5, }}>
|
||||
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
|
||||
<Select
|
||||
defaultValue=""
|
||||
id="grouped-select"
|
||||
label="Matching Usecase"
|
||||
multiple
|
||||
value={selectedUsecases}
|
||||
renderValue={(selected) => selected.join(', ')}
|
||||
onChange={(event) => {
|
||||
console.log("Changed: ", event)
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{usecases.map((usecase, index) => {
|
||||
//console.log(usecase)
|
||||
return (
|
||||
<span key={index}>
|
||||
<ListSubheader
|
||||
style={{color: usecase.color}}
|
||||
>
|
||||
{usecase.name}
|
||||
</ListSubheader>
|
||||
{usecase.list.map((subcase, subindex) => {
|
||||
//console.log(subcase)
|
||||
total_count += 1
|
||||
return (
|
||||
<MenuItem key={subindex} value={total_count} onClick={(event) => {
|
||||
if (selectedUsecases.includes(subcase.name)) {
|
||||
const itemIndex = selectedUsecases.indexOf(subcase.name)
|
||||
if (itemIndex > -1) {
|
||||
selectedUsecases.splice(itemIndex, 1)
|
||||
}
|
||||
} else {
|
||||
selectedUsecases.push(subcase.name)
|
||||
}
|
||||
|
||||
setUpdate(Math.random());
|
||||
setSelectedUsecases(selectedUsecases)
|
||||
}}>
|
||||
<Checkbox style={{color: selectedUsecases.includes(subcase.name) ? usecase.color : theme.palette.inputColor}} checked={selectedUsecases.includes(subcase.name)} />
|
||||
<ListItemText primary={subcase.name} />
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{showMoreClicked === true ?
|
||||
<span style={{marginTop: 25, }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<FormControl style={{marginTop: 15, }}>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Status</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="row-radio-buttons-group"
|
||||
defaultValue={innerWorkflow.status}
|
||||
onChange={(e) => {
|
||||
console.log("Data: ", e.target.value)
|
||||
|
||||
innerWorkflow.workflow_type = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="test" control={<Radio />} label="Test" />
|
||||
<FormControlLabel value="production" control={<Radio />} label="Production" />
|
||||
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DatePicker
|
||||
sx={{
|
||||
marginTop: 3,
|
||||
marginLeft: 3,
|
||||
}}
|
||||
value={dueDate}
|
||||
label="Due Date"
|
||||
format="YYYY-MM-DD"
|
||||
onChange={(newValue) => {
|
||||
setDueDate(newValue)
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
<div />
|
||||
|
||||
<FormControl style={{marginTop: 15, }}>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Type</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="row-radio-buttons-group"
|
||||
defaultValue={innerWorkflow.workflow_type}
|
||||
onChange={(e) => {
|
||||
console.log("Data: ", e.target.value)
|
||||
|
||||
innerWorkflow.workflow_type = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="trigger" control={<Radio />} label="Trigger" />
|
||||
<FormControlLabel value="subflow" control={<Radio />} label="Subflow" />
|
||||
<FormControlLabel value="standalone" control={<Radio />} label="Standalone" />
|
||||
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
innerWorkflow.blogpost = event.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.blogpost}
|
||||
placeholder="A blogpost or other reference for how this work workflow was built, and what it's for."
|
||||
rows="1"
|
||||
label="blogpost"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
innerWorkflow.video = event.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.video}
|
||||
placeholder="A youtube or loom link to the video"
|
||||
rows="1"
|
||||
label="Video"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
innerWorkflow.default_return_value = event.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.default_return_value}
|
||||
placeholder="Default return value (used for Subflows if the subflow fails)"
|
||||
rows="3"
|
||||
multiline
|
||||
label="Default return value"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
</span>
|
||||
: null}
|
||||
<Tooltip color="primary" title={"Add more details"} placement="top">
|
||||
<IconButton
|
||||
style={{ color: "white", margin: "auto", marginTop: 10, textAlign: "center", width: 50,}}
|
||||
onClick={() => {
|
||||
setShowMoreClicked(!showMoreClicked);
|
||||
}}
|
||||
>
|
||||
{showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon/>}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions style={{paddingRight: 100, }}>
|
||||
<Button
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
if (setNewWorkflow !== undefined) {
|
||||
setWorkflow({})
|
||||
}
|
||||
|
||||
setModalOpen(false)
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{}}
|
||||
disabled={name.length === 0 || submitLoading === true}
|
||||
onClick={() => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
innerWorkflow.name = name
|
||||
innerWorkflow.description = description
|
||||
if (newWorkflowTags.length > 0) {
|
||||
innerWorkflow.tags = newWorkflowTags
|
||||
}
|
||||
|
||||
if (selectedUsecases.length > 0) {
|
||||
innerWorkflow.usecase_ids = selectedUsecases
|
||||
}
|
||||
|
||||
if (dueDate > 0) {
|
||||
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"]+1}-${dueDate["$D"]}`).getTime()/1000
|
||||
}
|
||||
|
||||
if (setNewWorkflow !== undefined) {
|
||||
setNewWorkflow(
|
||||
innerWorkflow.name,
|
||||
innerWorkflow.description,
|
||||
innerWorkflow.tags,
|
||||
innerWorkflow.default_return_value,
|
||||
innerWorkflow,
|
||||
newWorkflow,
|
||||
innerWorkflow.usecase_ids,
|
||||
innerWorkflow.blogpost,
|
||||
innerWorkflow.status,
|
||||
)
|
||||
setWorkflow({})
|
||||
} else {
|
||||
setWorkflow(innerWorkflow)
|
||||
console.log("editing workflow: ", innerWorkflow)
|
||||
}
|
||||
|
||||
setSubmitLoading(true)
|
||||
|
||||
// If new workflow, don't close it
|
||||
if (isEditing) {
|
||||
setModalOpen(false)
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{submitLoading ? <CircularProgress color="secondary" /> : "Done"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
{newWorkflow === true ?
|
||||
<span style={{marginTop: 30, }}>
|
||||
<Typography variant="h6" style={{marginLeft: 30, paddingBottom: 0, }}>
|
||||
Relevant Workflows
|
||||
</Typography>
|
||||
|
||||
{priority === null || priority === undefined ? null :
|
||||
<div style={{marginLeft: 30, }}>
|
||||
<WorkflowTemplatePopup
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
|
||||
srcapp={priority.description.split("&").length > 2 ? priority.description.split("&")[0] : ""}
|
||||
img1={priority.description.split("&").length > 2 ? priority.description.split("&")[1] : ""}
|
||||
|
||||
dstapp={priority.description.split("&").length > 3 ? priority.description.split("&")[2] : ""}
|
||||
img2={priority.description.split("&").length > 3 ? priority.description.split("&")[3] : ""}
|
||||
title={priority.name}
|
||||
description={priority.description.split("&").length > 4 ? priority.description.split("&")[4] : ""}
|
||||
|
||||
apps={apps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
</span>
|
||||
: null}
|
||||
|
||||
{newWorkflow === true && name.length > 2 ?
|
||||
<div style={{marginLeft: 30, }}>
|
||||
<WorkflowGrid
|
||||
maxRows={1}
|
||||
globalUrl={globalUrl}
|
||||
showSuggestions={false}
|
||||
isMobile={isMobile}
|
||||
userdata={userdata}
|
||||
inputsearch={name+description+newWorkflowTags.join(" ")}
|
||||
|
||||
parsedXs={6}
|
||||
alternativeView={false}
|
||||
onlyResults={true}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
</FormControl>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditWorkflow;
|
382
shuffle/frontend/src/components/ExploreWorkflow.jsx
Normal file
@ -0,0 +1,382 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
import 'react-alice-carousel/lib/alice-carousel.css';
|
||||
import TrendingFlatIcon from '@mui/icons-material/TrendingFlat';
|
||||
import theme from '../theme.jsx';
|
||||
import CheckBoxSharpIcon from '@mui/icons-material/CheckBoxSharp';
|
||||
import { findSpecificApp } from "../components/AppFramework.jsx"
|
||||
import {
|
||||
Checkbox,
|
||||
Button,
|
||||
Collapse,
|
||||
IconButton,
|
||||
FormGroup,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormLabel,
|
||||
FormControlLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
Zoom,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Chip,
|
||||
ButtonGroup,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx";
|
||||
|
||||
const ExploreWorkflow = (props) => {
|
||||
const { userdata, globalUrl, appFramework } = props
|
||||
const [activeUsecases, setActiveUsecases] = useState(0);
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [suggestedUsecases, setSuggestedUsecases] = useState([])
|
||||
const [usecasesSet, setUsecasesSet] = useState(false)
|
||||
const [apps, setApps] = useState([])
|
||||
const sizing = 475
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
const imagestyle = {
|
||||
height: 40,
|
||||
borderRadius: 40,
|
||||
//border: "2px solid rgba(255,255,255,0.3)",
|
||||
}
|
||||
|
||||
const loadApps = () => {
|
||||
fetch(`${globalUrl}/api/v1/apps`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson === null) {
|
||||
console.log("null-response from server")
|
||||
const pretend_apps = [{
|
||||
"name": "TBD",
|
||||
"app_name": "TBD",
|
||||
"app_version": "TBD",
|
||||
"description": "TBD",
|
||||
"version": "TBD",
|
||||
"large_image": "",
|
||||
}]
|
||||
|
||||
setApps(pretend_apps)
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.success === false) {
|
||||
console.log("error loading apps: ", responseJson)
|
||||
return
|
||||
}
|
||||
|
||||
setApps(responseJson);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("App loading error: " + error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
// Find priorities in userdata.priorities and check if the item.type === "usecase"
|
||||
// If so, set the item.isActive to true
|
||||
if (usecasesSet === false && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0 && suggestedUsecases.length === 0) {
|
||||
|
||||
var tmpUsecases = []
|
||||
for (let i = 0; i < userdata.priorities.length; i++) {
|
||||
if (userdata.priorities[i].type !== "usecase" || userdata.priorities[i].active === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
const descsplit = userdata.priorities[i].description.split("&")
|
||||
if (descsplit.length === 5) {
|
||||
console.log("descsplit: ", descsplit)
|
||||
if (descsplit[1] === "") {
|
||||
const item = findSpecificApp(appFramework, descsplit[0])
|
||||
console.log("item: ", item)
|
||||
if (item !== null) {
|
||||
descsplit[1] = item.large_image
|
||||
}
|
||||
}
|
||||
|
||||
if (descsplit[3] === "") {
|
||||
const item = findSpecificApp(appFramework, descsplit[2])
|
||||
console.log("item: ", item)
|
||||
if (item !== null) {
|
||||
descsplit[3] = item.large_image
|
||||
}
|
||||
}
|
||||
|
||||
console.log("descsplit: ", descsplit)
|
||||
userdata.priorities[i].description = descsplit.join("&")
|
||||
}
|
||||
|
||||
tmpUsecases.push(userdata.priorities[i])
|
||||
}
|
||||
|
||||
console.log("USECASES: ", tmpUsecases)
|
||||
if (tmpUsecases.length === 0) {
|
||||
console.log("Add some random ones, as everything is done")
|
||||
|
||||
const comms = findSpecificApp(appFramework, "communication")
|
||||
const cases = findSpecificApp(appFramework, "cases")
|
||||
const edr = findSpecificApp(appFramework, "edr")
|
||||
const siem = findSpecificApp(appFramework, "siem")
|
||||
|
||||
tmpUsecases = [{
|
||||
"name": "Suggested Usecase: Email management",
|
||||
"description": comms.name+"&"+comms.large_image+"&"+cases.name+"&"+cases.large_image,
|
||||
"type": "usecase",
|
||||
"url": "/usecases?selected_object=Email management",
|
||||
"severity": 0,
|
||||
"active": false,
|
||||
},{
|
||||
"name": "Suggested Usecase: EDR to ticket",
|
||||
"description": edr.name+"&"+edr.large_image+"&"+cases.name+"&"+cases.large_image,
|
||||
"type": "usecase",
|
||||
"url": "/usecases?selected_object=EDR to ticket",
|
||||
"severity": 0,
|
||||
"active": false,
|
||||
},{
|
||||
"name": "Suggested Usecase: SIEM to ticket",
|
||||
"description": siem.name+"&"+siem.large_image+"&"+cases.name+"&"+cases.large_image,
|
||||
"type": "usecase",
|
||||
"url": "/usecases?selected_object=SIEM to ticket",
|
||||
"severity": 0,
|
||||
"active": false,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
setSuggestedUsecases(tmpUsecases)
|
||||
setUsecasesSet(true)
|
||||
loadApps()
|
||||
}
|
||||
|
||||
const modalView = (
|
||||
// console.log("key:", dataValue.key),
|
||||
//console.log("value:",dataValue.value),
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
minWidth: "800px",
|
||||
minHeight: "320px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{}}>
|
||||
<div style={{ color: "white", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
|
||||
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Sign Up</span>
|
||||
<div style={{ borderTop: "1px solid rgba(255, 132, 68, 1)", width: 85, marginLeft: 8, marginRight: 8 }} />
|
||||
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
|
||||
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Setup</span>
|
||||
<div style={{ borderTop: "1px solid rgba(255, 132, 68, 1)", width: 85, marginRight: 8 }} />
|
||||
<CheckBoxSharpIcon sx={{ borderRadius: 4, color: "rgba(255, 132, 68, 1)" }} style={{ width: 24 }} />
|
||||
<span style={{ marginLeft: 8, color: "rgba(255, 132, 68, 1)", fontSize: 16, width: 60 }}>Explore</span>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<Typography style={{ fontSize: 16, width: 252, marginLeft: 167 }}>
|
||||
Here’s a recommended workflow:
|
||||
</Typography>
|
||||
{/* <div style={{ marginTop: 0, maxWidth: 700, minWidth: 700, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
|
||||
<div style={{ marginTop: 0, }}>
|
||||
<div className="thumbs" style={{ display: "flex" }}>
|
||||
<Tooltip title={"Previous usecase"}>
|
||||
<IconButton
|
||||
style={{
|
||||
// backgroundColor: thumbIndex === 0 ? "inherit" : "white",
|
||||
zIndex: 5000,
|
||||
minHeight: 50,
|
||||
maxHeight: 50,
|
||||
color: "grey",
|
||||
marginTop: 150,
|
||||
borderRadius: 50,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
onClick={() => {
|
||||
slidePrev()
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
|
||||
<AliceCarousel
|
||||
style={{ backgroundColor: theme.palette.surfaceColor, minHeight: 750, maxHeight: 750, }}
|
||||
items={formattedCarousel}
|
||||
activeIndex={thumbIndex}
|
||||
infiniteLoop
|
||||
mouseTracking={false}
|
||||
responsive={responsive}
|
||||
// activeIndex={activeIndex}
|
||||
controlsStrategy="responsive"
|
||||
autoPlay={false}
|
||||
infinite={true}
|
||||
animationType="fadeout"
|
||||
animationDuration={800}
|
||||
disableButtonsControls
|
||||
|
||||
/>
|
||||
</div>
|
||||
<Tooltip title={"Next usecase"}>
|
||||
<IconButton
|
||||
style={{
|
||||
backgroundColor: thumbIndex === usecaseButtons.length - 1 ? "inherit" : "white",
|
||||
zIndex: 5000,
|
||||
minHeight: 50,
|
||||
maxHeight: 50,
|
||||
color: "grey",
|
||||
marginTop: 150,
|
||||
borderRadius: 50,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
onClick={() => {
|
||||
slideNext()
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => setModalOpen(false)}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => {
|
||||
console.log("hello")
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 0, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
|
||||
{modalView}
|
||||
<Typography variant="h4" style={{ marginLeft: 8, marginTop: 40, marginRight: 30, marginBottom: 0, }} color="rgba(241, 241, 241, 1)">
|
||||
Start using workflows
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ marginLeft: 8, marginTop: 10, marginRight: 30, marginBottom: 40, }} color="rgba(158, 158, 158, 1)">
|
||||
Based on what you selected workflows, here are our recommendations! You will see more of these later.
|
||||
</Typography>
|
||||
|
||||
<div style={{ marginTop: 0, }}>
|
||||
<div className="thumbs" style={{ display: "flex" }}>
|
||||
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
|
||||
<Grid item xs={11} style={{}}>
|
||||
{suggestedUsecases.length === 0 && usecasesSet ?
|
||||
<Typography variant="h6" style={{ marginTop: 30, marginBottom: 50, }} color="rgba(158, 158, 158, 1)">
|
||||
All Workflows are already added for your current apps!
|
||||
</Typography>
|
||||
:
|
||||
suggestedUsecases.map((priority, index) => {
|
||||
|
||||
const srcapp = priority.description.split("&")[0]
|
||||
var image1 = priority.description.split("&")[1]
|
||||
var image2 = ""
|
||||
var dstapp = ""
|
||||
if (priority.description.split("&").length > 3) {
|
||||
dstapp = priority.description.split("&")[2]
|
||||
image2 = priority.description.split("&")[3]
|
||||
}
|
||||
|
||||
const name = priority.name.replace("Suggested Usecase: ", "")
|
||||
|
||||
var description = ""
|
||||
if (priority.description.split("&").length > 4) {
|
||||
description = priority.description[4]
|
||||
}
|
||||
|
||||
// FIXME: Should have a proper description
|
||||
description = ""
|
||||
|
||||
return (
|
||||
<WorkflowTemplatePopup
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
img1={image1}
|
||||
srcapp={srcapp}
|
||||
img2={image2}
|
||||
dstapp={dstapp}
|
||||
title={name}
|
||||
description={description}
|
||||
|
||||
apps={apps}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
<div>
|
||||
<div style={{ marginTop: 32 }}>
|
||||
<Typography variant="body2" style={{ fontSize: 16, marginTop: 24 }} color="rgba(158, 158, 158, 1)">
|
||||
<Button variant="contained" type="submit"
|
||||
fullWidth style={{
|
||||
borderRadius: 200,
|
||||
height: 51,
|
||||
width: 464,
|
||||
fontSize: 16,
|
||||
padding: "16px 24px",
|
||||
margin: "auto",
|
||||
itemAlign: "center",
|
||||
background: activeUsecases === 0 ? "rgba(47, 47, 47, 1)" : "linear-gradient(90deg, #F86744 0%, #F34475 100%)",
|
||||
color: activeUsecases === 0? "rgba(158, 158, 158, 1)" : "rgba(241, 241, 241, 1)",
|
||||
border: activeUsecases === 0 ? "1px solid rgba(158, 158, 158, 1)" : "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/workflows?message="+activeUsecases+" workflows added")
|
||||
}}>
|
||||
Continue to workflows
|
||||
</Button>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="body2" style={{ fontSize: 16, marginTop: 24 }} color="rgba(158, 158, 158, 1)">
|
||||
<Link style={{ color: "#f86a3e", marginLeft: 145 }} to="/usecases" className="btn btn-primary">
|
||||
Explore usecases
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default ExploreWorkflow
|
27
shuffle/frontend/src/components/ExtraApps.jsx
Normal file
@ -0,0 +1,27 @@
|
||||
// Move this to the backend to be loaded in?
|
||||
const extraApps = [{
|
||||
"name": "Cases",
|
||||
"description": "Allows use of other Case Management apps without knowing how to use them.",
|
||||
"app_version": "1.0.0",
|
||||
"app_name": "Cases",
|
||||
"type": "ACTION",
|
||||
"large_image": encodeURI('data:image/svg+xml;utf-8,<svg fill="rgb(248,90,62)" width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" /></svg>'),
|
||||
"template": true,
|
||||
"actions": [{
|
||||
"name": "Create Alert",
|
||||
"description": "Create a ticket",
|
||||
"parameters": [{
|
||||
"name": "id",
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"multiline": true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}]
|
||||
|
||||
export default extraApps
|
23
shuffle/frontend/src/components/FAQ.jsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
|
||||
const FAQItem = (props) => {
|
||||
const { question, answer } = props
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<Paper onClick={() => {
|
||||
setIsExpanded(!isExpanded)
|
||||
}}>
|
||||
<Typography variant="body1">
|
||||
{question}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{answer}
|
||||
</Typography>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default FAQItem;
|
810
shuffle/frontend/src/components/Files.jsx
Normal file
@ -0,0 +1,810 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import {
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
Tooltip,
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
TextField,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
Edit as EditIcon,
|
||||
CloudDownload as CloudDownloadIcon,
|
||||
Delete as DeleteIcon,
|
||||
FileCopy as FileCopyIcon,
|
||||
Cached as CachedIcon,
|
||||
Publish as PublishIcon,
|
||||
Clear as ClearIcon,
|
||||
Add as AddIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
//import { useAlert
|
||||
import Dropzone from "../components/Dropzone.jsx";
|
||||
import CodeEditor from "../components/ShuffleCodeEditor.jsx";
|
||||
import theme from "../theme.jsx";
|
||||
|
||||
const Files = (props) => {
|
||||
const { globalUrl, userdata, serverside, selectedOrganization, isCloud, } = props;
|
||||
|
||||
const [files, setFiles] = React.useState([]);
|
||||
const [selectedNamespace, setSelectedNamespace] = React.useState("default");
|
||||
const [openFileId, setOpenFileId] = React.useState(false);
|
||||
const [fileNamespaces, setFileNamespaces] = React.useState([]);
|
||||
const [fileContent, setFileContent] = React.useState("");
|
||||
const [openEditor, setOpenEditor] = React.useState(false);
|
||||
const [renderTextBox, setRenderTextBox] = React.useState(false);
|
||||
|
||||
//const alert = useAlert();
|
||||
const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log"]
|
||||
var upload = "";
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key === 'Enter') {
|
||||
|
||||
console.log('do validate')
|
||||
console.log("new namespace name->",event.target.value);
|
||||
fileNamespaces.push(event.target.value);
|
||||
setSelectedNamespace(event.target.value);
|
||||
setRenderTextBox(false);
|
||||
}
|
||||
|
||||
if (event.key === 'Escape'){ // not working for some reasons
|
||||
console.log('escape pressed')
|
||||
setRenderTextBox(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const runUpdateText = (text) =>{
|
||||
fetch(`${globalUrl}/api/v1/files/${openFileId}/edit`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body:text,
|
||||
credentials: "include",
|
||||
}).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Can't update file");
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
//console.log(text);
|
||||
}
|
||||
|
||||
const getFiles = () => {
|
||||
fetch(globalUrl + "/api/v1/files", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.files !== undefined && responseJson.files !== null) {
|
||||
setFiles(responseJson.files);
|
||||
} else {
|
||||
setFiles([]);
|
||||
}
|
||||
|
||||
if (responseJson.namespaces !== undefined && responseJson.namespaces !== null) {
|
||||
setFileNamespaces(responseJson.namespaces);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getFiles();
|
||||
}, []);
|
||||
|
||||
const deleteFile = (file) => {
|
||||
fetch(globalUrl + "/api/v1/files/" + file.id, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for file delete :O!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success) {
|
||||
toast("Successfully deleted file " + file.name);
|
||||
} else if (
|
||||
responseJson.reason !== undefined &&
|
||||
responseJson.reason !== null
|
||||
) {
|
||||
toast("Failed to delete file: " + responseJson.reason);
|
||||
}
|
||||
setTimeout(() => {
|
||||
getFiles();
|
||||
}, 1500);
|
||||
|
||||
console.log(responseJson);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const readFileData = (file) => {
|
||||
fetch(globalUrl + "/api/v1/files/" + file.id + "/content", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for file :O!");
|
||||
return "";
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then((respdata) => {
|
||||
// console.log("respdata ->", respdata);
|
||||
// console.log("respdata type ->", typeof(respdata));
|
||||
|
||||
if (respdata.length === 0) {
|
||||
toast("Failed getting file. Is it deleted?");
|
||||
return;
|
||||
}
|
||||
return respdata
|
||||
})
|
||||
.then((responseData) => {
|
||||
|
||||
setFileContent(responseData);
|
||||
//console.log("filecontent state ",fileContent);
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const downloadFile = (file) => {
|
||||
fetch(globalUrl + "/api/v1/files/" + file.id + "/content", {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
return "";
|
||||
}
|
||||
|
||||
console.log("Resp: ", response)
|
||||
|
||||
return response.blob()
|
||||
})
|
||||
.then((respdata) => {
|
||||
if (respdata.length === 0) {
|
||||
toast("Failed getting file. Is it deleted?");
|
||||
return;
|
||||
}
|
||||
|
||||
var blob = new Blob([respdata], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
|
||||
var url = URL.createObjectURL(blob);
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("href", url);
|
||||
link.setAttribute("download", `${file.filename}`);
|
||||
var event = document.createEvent("MouseEvents");
|
||||
event.initMouseEvent(
|
||||
"click",
|
||||
true,
|
||||
true,
|
||||
window,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
null
|
||||
);
|
||||
link.dispatchEvent(event);
|
||||
|
||||
//return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//console.log(responseJson)
|
||||
//setSchedules(responseJson)
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateFile = (filename, file) => {
|
||||
var data = {
|
||||
filename: filename,
|
||||
org_id: selectedOrganization.id,
|
||||
workflow_id: "global",
|
||||
};
|
||||
|
||||
if (
|
||||
selectedNamespace !== undefined &&
|
||||
selectedNamespace !== null &&
|
||||
selectedNamespace.length > 0 &&
|
||||
selectedNamespace !== "default"
|
||||
) {
|
||||
data.namespace = selectedNamespace;
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/files/create", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//console.log("RESP: ", responseJson)
|
||||
if (responseJson.success === true) {
|
||||
handleFileUpload(responseJson.id, file);
|
||||
} else {
|
||||
toast("Failed to upload file ", filename);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast("Failed to upload file ", filename);
|
||||
console.log(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleFileUpload = (file_id, file) => {
|
||||
//console.log("FILE: ", file_id, file)
|
||||
fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: file,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
toast("File was created, but failed to upload.");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//console.log("RESPONSE: ", responseJson)
|
||||
//setFiles(responseJson)
|
||||
})
|
||||
.catch((error) => {
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const uploadFiles = (files) => {
|
||||
for (var key in files) {
|
||||
try {
|
||||
const filename = files[key].name;
|
||||
var filedata = new FormData();
|
||||
filedata.append("shuffle_file", files[key]);
|
||||
|
||||
if (typeof files[key] === "object") {
|
||||
handleCreateFile(filename, filedata);
|
||||
}
|
||||
|
||||
/*
|
||||
reader.addEventListener('load', (e) => {
|
||||
var data = e.target.result;
|
||||
setIsDropzone(false)
|
||||
console.log(filename)
|
||||
console.log(data)
|
||||
console.log(files[key])
|
||||
})
|
||||
reader.readAsText(files[key])
|
||||
*/
|
||||
} catch (e) {
|
||||
console.log("Error in dropzone: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
getFiles();
|
||||
}, 2500);
|
||||
};
|
||||
|
||||
const uploadFile = (e) => {
|
||||
const isDropzone =
|
||||
e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0;
|
||||
const files = isDropzone ? e.dataTransfer.files : e.target.files;
|
||||
|
||||
//const reader = new FileReader();
|
||||
//toast("Starting fileupload")
|
||||
uploadFiles(files);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropzone
|
||||
style={{
|
||||
maxWidth: window.innerWidth > 1366 ? 1366 : 1200,
|
||||
margin: "auto",
|
||||
padding: 20,
|
||||
}}
|
||||
onDrop={uploadFile}
|
||||
>
|
||||
<div>
|
||||
<div style={{ marginTop: 20, marginBottom: 20 }}>
|
||||
<h2 style={{ display: "inline" }}>Files</h2>
|
||||
<span style={{ marginLeft: 25 }}>
|
||||
Files from Workflows.{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://shuffler.io/docs/organizations#files"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
upload.click();
|
||||
}}
|
||||
>
|
||||
<PublishIcon /> Upload files
|
||||
</Button>
|
||||
{/* <FileCategoryInput
|
||||
isSet={renderTextBox} /> */}
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
ref={(ref) => (upload = ref)}
|
||||
onChange={(event) => {
|
||||
//const file = event.target.value
|
||||
//const fileObject = URL.createObjectURL(actualFile)
|
||||
//setFile(fileObject)
|
||||
//const files = event.target.files[0]
|
||||
uploadFiles(event.target.files);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => getFiles()}
|
||||
>
|
||||
<CachedIcon />
|
||||
</Button>
|
||||
|
||||
{fileNamespaces !== undefined &&
|
||||
fileNamespaces !== null &&
|
||||
fileNamespaces.length > 1 ? (
|
||||
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
|
||||
<InputLabel id="input-namespace-label">File Category</InputLabel>
|
||||
<Select
|
||||
labelId="input-namespace-select-label"
|
||||
id="input-namespace-select-id"
|
||||
style={{
|
||||
color: "white",
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
float: "right",
|
||||
}}
|
||||
value={selectedNamespace}
|
||||
onChange={(event) => {
|
||||
console.log("CHANGE NAMESPACE: ", event.target);
|
||||
setSelectedNamespace(event.target.value);
|
||||
}}
|
||||
>
|
||||
{fileNamespaces.map((data, index) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
value={data}
|
||||
style={{ color: "white" }}
|
||||
>
|
||||
{data}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
) : null}
|
||||
<div style={{display: "inline-flex", position:"relative"}}>
|
||||
{renderTextBox ?
|
||||
|
||||
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15 }}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setRenderTextBox(false);
|
||||
console.log(" close clicked")
|
||||
}}
|
||||
>
|
||||
<ClearIcon/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
|
||||
<Button
|
||||
style={{ marginLeft: 5, marginRight: 15 }}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setRenderTextBox(true);
|
||||
}}
|
||||
>
|
||||
<AddIcon/>
|
||||
</Button>
|
||||
</Tooltip> }
|
||||
{renderTextBox && <TextField
|
||||
onKeyPress={(event)=>{
|
||||
handleKeyDown(event);
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
placeholder="File category name"
|
||||
required
|
||||
margin="dense"
|
||||
defaultValue={""}
|
||||
autoFocus
|
||||
/>}</div>
|
||||
|
||||
<CodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openEditor}
|
||||
setExpansionModalOpen={setOpenEditor}
|
||||
setcodedata = {setFileContent}
|
||||
codedata={fileContent}
|
||||
isFileEditor = {true}
|
||||
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText = {runUpdateText}
|
||||
/>
|
||||
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 20,
|
||||
marginBottom: 20,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
/>
|
||||
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="Updated"
|
||||
style={{ maxWidth: 225, minWidth: 225 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Name"
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
minWidth: 150,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Workflow"
|
||||
style={{ maxWidth: 100, minWidth: 100, overflow: "hidden" }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Md5"
|
||||
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Status"
|
||||
style={{ minWidth: 75, maxWidth: 75, marginLeft: 10 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Filesize"
|
||||
style={{ minWidth: 125, maxWidth: 125 }}
|
||||
/>
|
||||
<ListItemText primary="Actions" />
|
||||
</ListItem>
|
||||
{files === undefined || files === null || files.length === 0 ? null :
|
||||
files.map((file, index) => {
|
||||
if (file.namespace === "") {
|
||||
file.namespace = "default";
|
||||
}
|
||||
|
||||
if (file.namespace !== selectedNamespace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var bgColor = "#27292d";
|
||||
if (index % 2 === 0) {
|
||||
bgColor = "#1f2023";
|
||||
}
|
||||
|
||||
const filenamesplit = file.filename.split(".")
|
||||
const iseditable = file.filesize < 2000000 && file.status === "active" && allowedFileTypes.includes(filenamesplit[filenamesplit.length-1])
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
maxHeight: 100,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 225,
|
||||
minWidth: 225,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
primary={new Date(file.updated_at * 1000).toISOString()}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 150,
|
||||
minWidth: 150,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
primary={file.filename}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={
|
||||
file.workflow_id === "global" ? (
|
||||
<IconButton
|
||||
disabled={file.workflow_id === "global"}
|
||||
>
|
||||
<OpenInNewIcon
|
||||
style={{
|
||||
color:
|
||||
file.workflow_id !== "global"
|
||||
? "white"
|
||||
: "grey",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip
|
||||
title={"Go to workflow"}
|
||||
style={{}}
|
||||
aria-label={"Download"}
|
||||
>
|
||||
<span>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
textDecoration: "none",
|
||||
color: "#f85a3e",
|
||||
}}
|
||||
href={`/workflows/${file.workflow_id}`}
|
||||
target="_blank"
|
||||
>
|
||||
<IconButton
|
||||
disabled={file.workflow_id === "global"}
|
||||
>
|
||||
<OpenInNewIcon
|
||||
style={{
|
||||
color:
|
||||
file.workflow_id !== "global"
|
||||
? "white"
|
||||
: "grey",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</a>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
style={{
|
||||
minWidth: 100,
|
||||
maxWidth: 100,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={file.md5_sum}
|
||||
style={{
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={file.status}
|
||||
style={{
|
||||
minWidth: 75,
|
||||
maxWidth: 75,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={file.filesize}
|
||||
style={{
|
||||
minWidth: 125,
|
||||
maxWidth: 125,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary=<span style={{ display:"inline"}}>
|
||||
<Tooltip
|
||||
title={`Edit File (${allowedFileTypes.join(", ")}). Max size 2MB`}
|
||||
style={{}}
|
||||
aria-label={"Edit"}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={!iseditable}
|
||||
style = {{padding: "6px"}}
|
||||
onClick={() => {
|
||||
setOpenEditor(true)
|
||||
setOpenFileId(file.id)
|
||||
readFileData(file)
|
||||
}}
|
||||
>
|
||||
<EditIcon
|
||||
style={{color: iseditable ? "white" : "grey",}}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Download file"}
|
||||
style={{}}
|
||||
aria-label={"Download"}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
style = {{padding: "6px"}}
|
||||
disabled={file.status !== "active"}
|
||||
onClick={() => {
|
||||
downloadFile(file);
|
||||
}}
|
||||
>
|
||||
<CloudDownloadIcon
|
||||
style={{
|
||||
color:
|
||||
file.status === "active"
|
||||
? "white"
|
||||
: "grey",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Copy file ID"}
|
||||
style={{}}
|
||||
aria-label={"copy"}
|
||||
>
|
||||
<IconButton
|
||||
style = {{padding: "6px"}}
|
||||
onClick={() => {
|
||||
const elementName = "copy_element_shuffle";
|
||||
var copyText =
|
||||
document.getElementById(elementName);
|
||||
if (
|
||||
copyText !== null &&
|
||||
copyText !== undefined
|
||||
) {
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
toast(
|
||||
"Can only copy over HTTPS (port 3443)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(file.id);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(
|
||||
0,
|
||||
99999
|
||||
); /* For mobile devices */
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
|
||||
toast(file.id + " copied to clipboard");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FileCopyIcon style={{ color: "white" }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={"Delete file"}
|
||||
style={{marginLeft: 15, }}
|
||||
aria-label={"Delete"}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
disabled={file.status !== "active"}
|
||||
style = {{padding: "6px"}}
|
||||
onClick={() => {
|
||||
deleteFile(file);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon
|
||||
style={{
|
||||
color:
|
||||
file.status === "active"
|
||||
? "white"
|
||||
: "grey",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
style={{
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
// overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
</Dropzone>
|
||||
)
|
||||
}
|
||||
|
||||
export default Files;
|
28
shuffle/frontend/src/components/FrameworkData.jsx
Normal file
1050
shuffle/frontend/src/components/Header.jsx
Normal file
245
shuffle/frontend/src/components/LandingpageUsecases.jsx
Normal file
@ -0,0 +1,245 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import AppFramework, { usecases } from "../components/AppFramework.jsx";
|
||||
import {Link} from 'react-router-dom';
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
import { Button, LinearProgress, Typography } from '@mui/material';
|
||||
|
||||
export const securityFramework = [
|
||||
{
|
||||
image: <path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" />,
|
||||
text: "Cases",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M6.93767 0C8.71083 0 10.4114 0.704386 11.6652 1.9582C12.919 3.21202 13.6234 4.91255 13.6234 6.68571C13.6234 8.34171 13.0165 9.864 12.0188 11.0366L12.2965 11.3143H13.1091L18.252 16.4571L16.7091 18L11.5662 12.8571V12.0446L11.2885 11.7669C10.116 12.7646 8.59367 13.3714 6.93767 13.3714C5.16451 13.3714 3.46397 12.667 2.21015 11.4132C0.956339 10.1594 0.251953 8.45888 0.251953 6.68571C0.251953 4.91255 0.956339 3.21202 2.21015 1.9582C3.46397 0.704386 5.16451 0 6.93767 0ZM6.93767 2.05714C4.36624 2.05714 2.3091 4.11429 2.3091 6.68571C2.3091 9.25714 4.36624 11.3143 6.93767 11.3143C9.5091 11.3143 11.5662 9.25714 11.5662 6.68571C11.5662 4.11429 9.5091 2.05714 6.93767 2.05714Z" />,
|
||||
text: "SIEM",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M11.223 10.971L3.85195 14.4L7.28095 7.029L14.652 3.6L11.223 10.971ZM9.25195 0C8.07006 0 6.89973 0.232792 5.8078 0.685084C4.71587 1.13738 3.72372 1.80031 2.88799 2.63604C1.20016 4.32387 0.251953 6.61305 0.251953 9C0.251953 11.3869 1.20016 13.6761 2.88799 15.364C3.72372 16.1997 4.71587 16.8626 5.8078 17.3149C6.89973 17.7672 8.07006 18 9.25195 18C11.6389 18 13.9281 17.0518 15.6159 15.364C17.3037 13.6761 18.252 11.3869 18.252 9C18.252 7.8181 18.0192 6.64778 17.5669 5.55585C17.1146 4.46392 16.4516 3.47177 15.6159 2.63604C14.7802 1.80031 13.788 1.13738 12.6961 0.685084C11.6042 0.232792 10.4338 0 9.25195 0ZM9.25195 8.01C8.98939 8.01 8.73758 8.1143 8.55192 8.29996C8.36626 8.48563 8.26195 8.73744 8.26195 9C8.26195 9.26256 8.36626 9.51437 8.55192 9.70004C8.73758 9.8857 8.98939 9.99 9.25195 9.99C9.51452 9.99 9.76633 9.8857 9.95199 9.70004C10.1376 9.51437 10.242 9.26256 10.242 9C10.242 8.73744 10.1376 8.48563 9.95199 8.29996C9.76633 8.1143 9.51452 8.01 9.25195 8.01Z" />,
|
||||
text: "Assets",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M13.3318 2.223C13.2598 2.223 13.1878 2.205 13.1248 2.169C11.3968 1.278 9.90284 0.9 8.11184 0.9C6.32984 0.9 4.63784 1.323 3.09884 2.169C2.88284 2.286 2.61284 2.205 2.48684 1.989C2.36984 1.773 2.45084 1.494 2.66684 1.377C4.34084 0.468 6.17684 0 8.11184 0C10.0288 0 11.7028 0.423 13.5388 1.368C13.7638 1.485 13.8448 1.755 13.7278 1.971C13.6468 2.133 13.4938 2.223 13.3318 2.223ZM0.452843 6.948C0.362843 6.948 0.272843 6.921 0.191843 6.867C-0.015157 6.723 -0.0601571 6.444 0.0838429 6.237C0.974843 4.977 2.10884 3.987 3.45884 3.294C6.28484 1.836 9.90284 1.827 12.7378 3.285C14.0878 3.978 15.2218 4.959 16.1128 6.21C16.2568 6.408 16.2118 6.696 16.0048 6.84C15.7978 6.984 15.5188 6.939 15.3748 6.732C14.5648 5.598 13.5388 4.707 12.3238 4.086C9.74084 2.763 6.43784 2.763 3.86384 4.095C2.63984 4.725 1.61384 5.625 0.803843 6.759C0.731843 6.885 0.596843 6.948 0.452843 6.948ZM6.07784 17.811C5.96084 17.811 5.84384 17.766 5.76284 17.676C4.97984 16.893 4.55684 16.389 3.95384 15.3C3.33284 14.193 3.00884 12.843 3.00884 11.394C3.00884 8.721 5.29484 6.543 8.10284 6.543C10.9108 6.543 13.1968 8.721 13.1968 11.394C13.1968 11.646 12.9988 11.844 12.7468 11.844C12.4948 11.844 12.2968 11.646 12.2968 11.394C12.2968 9.216 10.4158 7.443 8.10284 7.443C5.78984 7.443 3.90884 9.216 3.90884 11.394C3.90884 12.69 4.19684 13.887 4.74584 14.859C5.32184 15.894 5.71784 16.335 6.41084 17.037C6.58184 17.217 6.58184 17.496 6.41084 17.676C6.31184 17.766 6.19484 17.811 6.07784 17.811ZM12.5308 16.146C11.4598 16.146 10.5148 15.876 9.74084 15.345C8.39984 14.436 7.59884 12.96 7.59884 11.394C7.59884 11.142 7.79684 10.944 8.04884 10.944C8.30084 10.944 8.49884 11.142 8.49884 11.394C8.49884 12.663 9.14684 13.86 10.2448 14.598C10.8838 15.03 11.6308 15.237 12.5308 15.237C12.7468 15.237 13.1068 15.21 13.4668 15.147C13.7098 15.102 13.9438 15.264 13.9888 15.516C14.0338 15.759 13.8718 15.993 13.6198 16.038C13.1068 16.137 12.6568 16.146 12.5308 16.146ZM10.7218 18C10.6858 18 10.6408 17.991 10.6048 17.982C9.17384 17.586 8.23784 17.055 7.25684 16.092C5.99684 14.841 5.30384 13.176 5.30384 11.394C5.30384 9.936 6.54584 8.748 8.07584 8.748C9.60584 8.748 10.8478 9.936 10.8478 11.394C10.8478 12.357 11.6848 13.14 12.7198 13.14C13.7548 13.14 14.5918 12.357 14.5918 11.394C14.5918 8.001 11.6668 5.247 8.06684 5.247C5.51084 5.247 3.17084 6.669 2.11784 8.874C1.76684 9.603 1.58684 10.458 1.58684 11.394C1.58684 12.096 1.64984 13.203 2.18984 14.643C2.27984 14.877 2.16284 15.138 1.92884 15.219C1.69484 15.309 1.43384 15.183 1.35284 14.958C0.911843 13.779 0.695843 12.609 0.695843 11.394C0.695843 10.314 0.902843 9.333 1.30784 8.478C2.50484 5.967 5.15984 4.338 8.06684 4.338C12.1618 4.338 15.4918 7.497 15.4918 11.385C15.4918 12.843 14.2498 14.031 12.7198 14.031C11.1898 14.031 9.94784 12.843 9.94784 11.385C9.94784 10.422 9.11084 9.639 8.07584 9.639C7.04084 9.639 6.20384 10.422 6.20384 11.385C6.20384 12.924 6.79784 14.364 7.88684 15.444C8.74184 16.29 9.56084 16.758 10.8298 17.109C11.0728 17.172 11.2078 17.424 11.1448 17.658C11.0998 17.865 10.9108 18 10.7218 18Z" />,
|
||||
text: "IAM",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image: <path d="M16.1091 8.57143H14.8234V5.14286C14.8234 4.19143 14.052 3.42857 13.1091 3.42857H9.68052V2.14286C9.68052 1.57454 9.45476 1.02949 9.0529 0.627628C8.65103 0.225765 8.10599 0 7.53767 0C6.96935 0 6.4243 0.225765 6.02244 0.627628C5.62057 1.02949 5.39481 1.57454 5.39481 2.14286V3.42857H1.96624C1.51158 3.42857 1.07555 3.60918 0.754056 3.93067C0.432565 4.25216 0.251953 4.6882 0.251953 5.14286V8.4H1.53767C2.82338 8.4 3.85195 9.42857 3.85195 10.7143C3.85195 12 2.82338 13.0286 1.53767 13.0286H0.251953V16.2857C0.251953 16.7404 0.432565 17.1764 0.754056 17.4979C1.07555 17.8194 1.51158 18 1.96624 18H5.22338V16.7143C5.22338 15.4286 6.25195 14.4 7.53767 14.4C8.82338 14.4 9.85195 15.4286 9.85195 16.7143V18H13.1091C13.5638 18 13.9998 17.8194 14.3213 17.4979C14.6428 17.1764 14.8234 16.7404 14.8234 16.2857V12.8571H16.1091C16.6774 12.8571 17.2225 12.6314 17.6243 12.2295C18.0262 11.8277 18.252 11.2826 18.252 10.7143C18.252 10.146 18.0262 9.60092 17.6243 9.19906C17.2225 8.79719 16.6774 8.57143 16.1091 8.57143Z" />,
|
||||
text: "Intel",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z" />,
|
||||
text: "Comms",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M0.251953 10.6011H3.8391L9.38052 -4.92572e-08L10.8977 11.5696L15.0377 6.28838L19.3191 10.6011H23.3948V13.1836H18.252L15.2562 10.175L9.1491 18L7.88909 8.41894L5.39481 13.1836H0.251953V10.6011Z" />,
|
||||
text: "Network",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M19.1722 8.9957L17.0737 6.60487L17.3661 3.44004L14.2615 2.73483L12.6361 -3.28068e-08L9.71206 1.25561L6.78803 -3.28068e-08L5.16261 2.73483L2.05797 3.43144L2.35038 6.59627L0.251953 8.9957L2.35038 11.3865L2.05797 14.56L5.16261 15.2652L6.78803 18L9.71206 16.7358L12.6361 17.9914L14.2615 15.2566L17.3661 14.5514L17.0737 11.3865L19.1722 8.9957ZM10.5721 13.2957H8.85205V11.5757H10.5721V13.2957ZM10.5721 9.85571H8.85205V4.69565H10.5721V9.85571Z" />,
|
||||
text: "EDR & AV",
|
||||
description: "Case management"
|
||||
},
|
||||
]
|
||||
|
||||
const LandingpageUsecases = (props) => {
|
||||
const [selectedUsecase, setSelectedUsecase] = useState("Phishing")
|
||||
const usecasekeys = usecases === undefined || usecases === null ? [] : Object.keys(usecases)
|
||||
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"
|
||||
const buttonStyle = {borderRadius: 25, height: 50, width: 260, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18, backgroundImage: buttonBackground}
|
||||
|
||||
const HandleTitle = (props) => {
|
||||
const { usecases, selectedUsecase, setSelecedUsecase } = props
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setProgress((oldProgress) => {
|
||||
if (oldProgress >= 105) {
|
||||
const foundIndex = usecasekeys.findIndex(key => key === selectedUsecase)
|
||||
var newitem = usecasekeys[foundIndex+1]
|
||||
if (newitem === undefined || newitem === 0) {
|
||||
newitem = usecasekeys[1]
|
||||
}
|
||||
|
||||
setSelectedUsecase(newitem)
|
||||
return -18
|
||||
}
|
||||
|
||||
if (oldProgress >= 65) {
|
||||
return oldProgress + 3
|
||||
}
|
||||
|
||||
if (oldProgress >= 80) {
|
||||
return oldProgress + 1
|
||||
}
|
||||
|
||||
return oldProgress + 6
|
||||
})
|
||||
}, 165)
|
||||
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (usecases === null || usecases === undefined || usecases.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const modifier = isMobile ? 17 : 22
|
||||
return (
|
||||
<span style={{margin: "auto", textAlign: isMobile ? "center" : "left", width: isMobile ? 280 : "100%",}}>
|
||||
<b>Handle <br/>
|
||||
<span style={{marginBottom: 10}}>
|
||||
<i id="usecase-text">{selectedUsecase}</i>
|
||||
<LinearProgress variant="determinate" value={progress} style={{marginTop: 0, marginBottom: 0, height: 3, width: isMobile ? "100%" : selectedUsecase.length*modifier, borderRadius: 10, }} />
|
||||
</span>
|
||||
with confidence</b>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const parsedWidth = isMobile ? "100%" : 1100
|
||||
return (
|
||||
<div style={{width: isMobile ? null : parsedWidth, margin: isMobile ? "0px 0px 0px 0px" : "auto", color: "white", textAlign: isMobile ? "center" : "left",}}>
|
||||
<div style={{display: "flex", position: "relative",}}>
|
||||
<div style={{maxWidth: isMobile ? "100%" : 420, paddingTop: isMobile ? 0 : 120, zIndex: 1000, margin: "auto",}}>
|
||||
|
||||
<Typography variant="h1" style={{margin: "auto", width: isMobile ? 280 : "100%", marginTop: isMobile ? 50 : 0}}>
|
||||
<HandleTitle usecases={usecases} selectedUsecase={selectedUsecase} setSelectedUsecase={setSelectedUsecase} />
|
||||
|
||||
{/*<b>Security Automation <i>is Hard</i></b>*/}
|
||||
</Typography>
|
||||
<Typography variant="h6" style={{marginTop: isMobile ? 15 : 0,}}>
|
||||
Connecting your everchanging environment is hard. We get it! That's why we built Shuffle, where you can use and share your security workflows to everyones benefit.
|
||||
{/*Shuffle is an automation platform where you don't need to be an expert to automate. Get access to our large pool of security playbooks, apps and people.*/}
|
||||
</Typography>
|
||||
<div style={{display: "flex", textAlign: "center", itemAlign: "center",}}>
|
||||
{isMobile ? null :
|
||||
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_pricing",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground, marginRight: 10,
|
||||
}}>
|
||||
See Pricing
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
{isMobile ? null :
|
||||
<Link rel="noopener noreferrer" to={"/register?message=You'll need to sign up first. No name, company or credit card required."} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_try_it_out",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground,
|
||||
}}>
|
||||
Start for free
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{isMobile ? null :
|
||||
<div style={{marginLeft: 200, marginTop: 125, zIndex: 1000}}>
|
||||
<AppFramework showOptions={false} selectedOption={selectedUsecase} rolling={true} />
|
||||
</div>
|
||||
}
|
||||
{isMobile ? null :
|
||||
<div style={{position: "absolute", top: 50, right: -200, zIndex: 0, }}>
|
||||
<svg width="351" height="433" viewBox="0 0 351 433" fill="none" xmlns="http://www.w3.org/2000/svg" style={{zIndex: 0, }}>
|
||||
<path d="M167.781 184.839C167.781 235.244 208.625 276.104 259.03 276.104C309.421 276.104 350.28 235.244 350.28 184.839C350.28 134.448 309.421 93.5892 259.03 93.5892C208.625 93.5741 167.781 134.433 167.781 184.839ZM330.387 184.839C330.387 224.263 298.439 256.195 259.03 256.195C219.621 256.195 187.674 224.248 187.674 184.839C187.674 145.43 219.636 113.483 259.03 113.483C298.439 113.483 330.387 145.43 330.387 184.839Z" fill="white" fill-opacity="0.2"/>
|
||||
<path d="M167.781 387.368C167.781 412.578 188.203 433 213.398 433C238.593 433 259.03 412.578 259.03 387.368C259.03 362.157 238.608 341.735 213.398 341.735C188.187 341.735 167.781 362.172 167.781 387.368ZM249.076 387.368C249.076 407.08 233.095 423.046 213.398 423.046C193.686 423.046 177.72 407.065 177.72 387.368C177.72 367.671 193.686 351.69 213.398 351.69C233.095 351.705 249.076 367.671 249.076 387.368Z" fill="white" fill-opacity="0.2"/>
|
||||
<path d="M56.8637 0.738726C25.7052 0.738724 0.44632 25.9976 0.446317 57.1561C0.446314 88.3146 25.7052 113.573 56.8637 113.573C88.0221 113.573 113.281 88.3146 113.281 57.1561C113.281 25.9977 88.0222 0.738729 56.8637 0.738726Z" fill="white" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div style={{display: "flex", width: isMobile ? "100%" : 300, itemAlign: "center", margin: "auto", marginTop: 20, flexDirection: isMobile ? "column" : "row", textAlign: "center",}}>
|
||||
{isMobile ?
|
||||
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant={isMobile ? "contained" : "outlined"}
|
||||
color={isMobile ? "primary" : "secondary"}
|
||||
style={buttonStyle}
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_pricing",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
>
|
||||
See pricing
|
||||
</Button>
|
||||
</Link>
|
||||
: null
|
||||
}
|
||||
{/*isMobile ?
|
||||
<Link rel="noopener noreferrer" to={"/docs/features"} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_features",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
color="secondary"
|
||||
style={buttonStyle}>
|
||||
Features
|
||||
</Button>
|
||||
</Link>
|
||||
: null*/}
|
||||
</div>
|
||||
{isMobile ? null :
|
||||
<div style={{display: "flex", width: parsedWidth, margin: "auto", marginTop: 150}}>
|
||||
{securityFramework.map((data, index) => {
|
||||
return (
|
||||
<div key={index} style={{flex: 1, textAlign: "center",}}>
|
||||
<span style={{margin: "auto", width: 25,}}>
|
||||
<svg width="25" height="25" fill="white" xmlns="http://www.w3.org/2000/svg" >
|
||||
{data.image}
|
||||
</svg>
|
||||
</span>
|
||||
<Typography variant="body2" style={{color: "white", marginRight: 5}}>
|
||||
{data.text}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingpageUsecases;
|
106
shuffle/frontend/src/components/Newsletter.jsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React, {useState} from 'react';
|
||||
import { useTheme } from '@mui/styles';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
import {
|
||||
TextField,
|
||||
Typography,
|
||||
Button
|
||||
} from '@mui/material';
|
||||
|
||||
const Newsletter = (props) => {
|
||||
const { globalUrl, } = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const [email, setEmail] = useState("");
|
||||
const [msg, setMsg] = useState("");
|
||||
const [buttonActive, setButtonActive] = useState(true);
|
||||
const buttonStyle = {minWidth: 300, borderRadius: 30, height: 60, width: 140, margin: isMobile ? "15px auto 15px auto" : "20px 20px 20px 10px", fontSize: 18,}
|
||||
|
||||
const newsletterSignup = (inemail) => {
|
||||
if (inemail.length < 4) {
|
||||
setMsg("Invalid email")
|
||||
setButtonActive(true)
|
||||
return
|
||||
}
|
||||
|
||||
setButtonActive(false)
|
||||
const data = {"email": inemail}
|
||||
const url = globalUrl+'/api/v1/functions/newsletter_signup'
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then(response =>
|
||||
response.json().then(responseJson => {
|
||||
setButtonActive(true)
|
||||
setMsg(responseJson["reason"])
|
||||
if (responseJson["success"] === false) {
|
||||
} else {
|
||||
setEmail("")
|
||||
}
|
||||
}),
|
||||
)
|
||||
.catch(error => {
|
||||
setMsg("Something went wrong: ", error.toString())
|
||||
setButtonActive(true)
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{margin: "auto", color: "white", textAlign: "center",}}>
|
||||
<Typography variant="h4" style={{marginTop: 35,}}>
|
||||
Security Automation Newsletter
|
||||
</Typography>
|
||||
<Typography variant="h6" style={{color: "#7d7f82", marginTop: 20, }}>
|
||||
Defensive security is 99% noise. Join us to sift through it.
|
||||
</Typography>
|
||||
<div style={{}}>
|
||||
<TextField
|
||||
style={{minWidth: isMobile ? "90%" : 450, backgroundColor: theme.palette.inputColor, marginTop: 20, borderRadius: 10, }}
|
||||
InputProps={{
|
||||
style:{
|
||||
borderRadius: 10,
|
||||
height: 60,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value)
|
||||
}}
|
||||
placeholder="Your email"
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={!buttonActive}
|
||||
onClick={() => {
|
||||
newsletterSignup(email)
|
||||
ReactGA.event({
|
||||
category: "newsletter",
|
||||
action: `signup_click`,
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
<div/>
|
||||
{msg}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Newsletter;
|
937
shuffle/frontend/src/components/Oauth2Auth.jsx
Normal file
@ -0,0 +1,937 @@
|
||||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
||||
import { toast } from 'react-toastify';
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
import theme from '../theme.jsx';
|
||||
//import { useAlert
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
ListItemText,
|
||||
TextField,
|
||||
Drawer,
|
||||
Button,
|
||||
Paper,
|
||||
Grid,
|
||||
Tabs,
|
||||
InputAdornment,
|
||||
Tab,
|
||||
ButtonBase,
|
||||
Tooltip,
|
||||
Select,
|
||||
MenuItem,
|
||||
Divider,
|
||||
Dialog,
|
||||
Modal,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
InputLabel,
|
||||
DialogContent,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Menu,
|
||||
Input,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
Typography,
|
||||
Checkbox,
|
||||
Breadcrumbs,
|
||||
CircularProgress,
|
||||
Switch,
|
||||
Fade,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
LockOpen as LockOpenIcon,
|
||||
SupervisorAccount as SupervisorAccountIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const ITEM_HEIGHT = 55;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
minWidth: 500,
|
||||
maxWidth: 500,
|
||||
scrollX: "auto",
|
||||
},
|
||||
},
|
||||
variant: "menu",
|
||||
getContentAnchorEl: null,
|
||||
};
|
||||
|
||||
const registeredApps = [
|
||||
"gmail",
|
||||
"slack",
|
||||
"webex",
|
||||
"zoho_desk",
|
||||
"outlook_graph",
|
||||
"outlook_office365",
|
||||
"microsoft_teams",
|
||||
"microsoft_teams_user_access",
|
||||
"todoist",
|
||||
"microsoft_sentinel",
|
||||
"microsoft_365_defender",
|
||||
"google_chat",
|
||||
"google_sheets",
|
||||
"google_drive",
|
||||
"google_disk",
|
||||
"jira",
|
||||
"jira_service_desk",
|
||||
"jira_service_management",
|
||||
"github",
|
||||
]
|
||||
|
||||
const AuthenticationOauth2 = (props) => {
|
||||
const {
|
||||
saveWorkflow,
|
||||
selectedApp,
|
||||
workflow,
|
||||
selectedAction,
|
||||
authenticationType,
|
||||
getAppAuthentication,
|
||||
appAuthentication,
|
||||
setSelectedAction,
|
||||
setNewAppAuth,
|
||||
isCloud,
|
||||
autoAuth,
|
||||
authButtonOnly,
|
||||
isLoggedIn,
|
||||
} = props;
|
||||
|
||||
let navigate = useNavigate();
|
||||
//const alert = useAlert()
|
||||
|
||||
//const [update, setUpdate] = React.useState("|")
|
||||
const [defaultConfigSet, setDefaultConfigSet] = React.useState(
|
||||
authenticationType.client_id !== undefined &&
|
||||
authenticationType.client_id !== null &&
|
||||
authenticationType.client_id.length > 0 &&
|
||||
authenticationType.client_secret !== undefined &&
|
||||
authenticationType.client_secret !== null &&
|
||||
authenticationType.client_secret.length > 0
|
||||
);
|
||||
|
||||
const [clientId, setClientId] = React.useState(
|
||||
defaultConfigSet ? authenticationType.client_id : ""
|
||||
);
|
||||
const [clientSecret, setClientSecret] = React.useState(
|
||||
defaultConfigSet ? authenticationType.client_secret : ""
|
||||
);
|
||||
const [oauthUrl, setOauthUrl] = React.useState("");
|
||||
const [buttonClicked, setButtonClicked] = React.useState(false);
|
||||
|
||||
const [offlineAccess, setOfflineAccess] = React.useState(true);
|
||||
const allscopes = authenticationType.scope !== undefined ? authenticationType.scope : [];
|
||||
|
||||
|
||||
const [selectedScopes, setSelectedScopes] = React.useState(allscopes.length === 1 ? [allscopes[0]] : [])
|
||||
const [manuallyConfigure, setManuallyConfigure] = React.useState(
|
||||
defaultConfigSet ? false : true
|
||||
);
|
||||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||||
fields: {},
|
||||
label: "",
|
||||
usage: [
|
||||
{
|
||||
workflow_id: workflow !== undefined ? workflow.id : "",
|
||||
},
|
||||
],
|
||||
id: uuidv4(),
|
||||
active: true,
|
||||
});
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn === false) {
|
||||
navigate(`/login?view=${window.location.pathname}&message=Log in to authenticate this app`)
|
||||
}
|
||||
|
||||
console.log("Should automatically click the auto-auth button?: ", autoAuth)
|
||||
if (autoAuth === true && selectedApp !== undefined) {
|
||||
startOauth2Request()
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (selectedApp.authentication === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startOauth2Request = (admin_consent) => {
|
||||
// Admin consent also means to add refresh tokens
|
||||
console.log("Inside oauth2 request for app: ", selectedApp.name)
|
||||
selectedApp.name = selectedApp.name.replace(" ", "_").toLowerCase()
|
||||
|
||||
//console.log("APP: ", selectedApp)
|
||||
if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") {
|
||||
handleOauth2Request(
|
||||
"efe4c3fe-84a1-4821-a84f-23a6cfe8e72d",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["Mail.ReadWrite", "Mail.Send", "offline_access"],
|
||||
admin_consent,
|
||||
);
|
||||
} else if (selectedApp.name.toLowerCase() == "gmail") {
|
||||
handleOauth2Request(
|
||||
"253565968129-6ke8086pkp0at16m8t95rdcsas69ngt1.apps.googleusercontent.com",
|
||||
"",
|
||||
"https://gmail.googleapis.com",
|
||||
["https://www.googleapis.com/auth/gmail.modify",
|
||||
"https://www.googleapis.com/auth/gmail.send",
|
||||
"https://www.googleapis.com/auth/gmail.insert",
|
||||
"https://www.googleapis.com/auth/gmail.compose",
|
||||
],
|
||||
admin_consent,
|
||||
"select_account%20consent",
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase() == "zoho_desk") {
|
||||
handleOauth2Request(
|
||||
"1000.ZR5MHUW6B0L6W1VUENFGIATFS0TOJT",
|
||||
"",
|
||||
"https://desk.zoho.com",
|
||||
["Desk.tickets.READ",
|
||||
"Desk.tickets.UPDATE",
|
||||
"Desk.tickets.DELETE",
|
||||
"Desk.tickets.CREATE",
|
||||
"offline_access"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase() == "slack") {
|
||||
handleOauth2Request(
|
||||
"5155508477298.5168162485601",
|
||||
"",
|
||||
"https://slack.com",
|
||||
["chat:write:user", "im:read", "im:write", "search:read", "usergroups:read", "usergroups:write",],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase() == "webex") {
|
||||
handleOauth2Request(
|
||||
"Cab184f3d7271f540443c79b5b79845e3387abbbdb3db4233a87ea3a5432fb3d5",
|
||||
"",
|
||||
"https://webexapis.com",
|
||||
["spark:all"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("microsoft_teams")) {
|
||||
handleOauth2Request(
|
||||
"31cb4c84-658e-43d5-ae84-22c9142e967a",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["ChannelMessage.Edit", "ChannelMessage.Read.All", "ChannelMessage.Send", "Chat.Create", "Chat.ReadWrite", "Chat.Read", "offline_access", "Team.ReadBasic.All"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("todoist")) {
|
||||
handleOauth2Request(
|
||||
"35fa3a384040470db0c8527e90a3c2eb",
|
||||
"",
|
||||
"https://api.todoist.com",
|
||||
["task:add",],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("microsoft_sentinel")) {
|
||||
handleOauth2Request(
|
||||
"4c16e8c4-3d34-4aa1-ac94-262ea170b7f7",
|
||||
"",
|
||||
"https://management.azure.com",
|
||||
["https://management.azure.com/user_impersonation",],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("microsoft_365_defender")) {
|
||||
handleOauth2Request(
|
||||
"4c16e8c4-3d34-4aa1-ac94-262ea170b7f7",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["SecurityEvents.ReadWrite.All",],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("google_sheets")) {
|
||||
handleOauth2Request(
|
||||
"253565968129-mppu17aciek8slr3kpgnb37hp86dmvmb.apps.googleusercontent.com",
|
||||
"",
|
||||
"https://sheets.googleapis.com",
|
||||
["https://www.googleapis.com/auth/spreadsheets"],
|
||||
admin_consent,
|
||||
"consent",
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("google_drive") || selectedApp.name.toLowerCase().includes("google_disk")) {
|
||||
handleOauth2Request(
|
||||
"253565968129-6pij4g6ojim4gpum0h9m9u3bc357qsq7.apps.googleusercontent.com",
|
||||
"",
|
||||
"https://www.googleapis.com",
|
||||
["https://www.googleapis.com/auth/drive",],
|
||||
admin_consent,
|
||||
"consent",
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("google_chat") || selectedApp.name.toLowerCase().includes("google_hangout")) {
|
||||
handleOauth2Request(
|
||||
"253565968129-6pij4g6ojim4gpum0h9m9u3bc357qsq7.apps.googleusercontent.com",
|
||||
"",
|
||||
"https://www.googleapis.com",
|
||||
["https://www.googleapis.com/auth/chat.messages",],
|
||||
admin_consent,
|
||||
"consent",
|
||||
)
|
||||
|
||||
} else if (selectedApp.name.toLowerCase().includes("jira_service_desk") || selectedApp.name.toLowerCase().includes("jira") || selectedApp.name.toLowerCase().includes("jira_service_management")) {
|
||||
handleOauth2Request(
|
||||
"AI02egeCQh1Zskm1QAJaaR6dzjR97V2F",
|
||||
"",
|
||||
"https://api.atlassian.com",
|
||||
["read:jira-work", "write:jira-work", "read:servicedesk:jira-service-management", "write:servicedesk:jira-service-management", "read:request:jira-service-management", "write:request:jira-service-management",],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("github")) {
|
||||
handleOauth2Request(
|
||||
"3d272b1b782b100b1e61",
|
||||
"",
|
||||
"https://api.github.com",
|
||||
["repo","user","project","notifications",],
|
||||
admin_consent,
|
||||
)
|
||||
} else {
|
||||
console.log("No match found for: ", selectedApp.name)
|
||||
}
|
||||
// write:request:jira-service-management
|
||||
}
|
||||
|
||||
|
||||
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes, admin_consent, prompt) => {
|
||||
setButtonClicked(true);
|
||||
//console.log("SCOPES: ", scopes);
|
||||
|
||||
client_id = client_id.trim()
|
||||
client_secret = client_secret.trim()
|
||||
oauth_url = oauth_url.trim()
|
||||
|
||||
var resources = "";
|
||||
if (scopes !== undefined && (scopes !== null) & (scopes.length > 0)) {
|
||||
console.log("IN scope 1")
|
||||
if (offlineAccess === true && !scopes.includes("offline_access")) {
|
||||
|
||||
console.log("IN scope 2")
|
||||
if (!authenticationType.redirect_uri.includes("google")) {
|
||||
console.log("Appending offline access")
|
||||
scopes.push("offline_access")
|
||||
}
|
||||
}
|
||||
|
||||
resources = scopes.join(" ");
|
||||
//resources = scopes.join(",");
|
||||
}
|
||||
|
||||
const authentication_url = authenticationType.token_uri;
|
||||
//console.log("AUTH: ", authenticationType)
|
||||
//console.log("SCOPES2: ", resources)
|
||||
const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`;
|
||||
const workflowId = workflow !== undefined ? workflow.id : "";
|
||||
var state = `workflow_id%3D${workflowId}%26reference_action_id%3d${selectedAction.app_id}%26app_name%3d${selectedAction.app_name}%26app_id%3d${selectedAction.app_id}%26app_version%3d${selectedAction.app_version}%26authentication_url%3d${authentication_url}%26scope%3d${resources}%26client_id%3d${client_id}%26client_secret%3d${client_secret}`;
|
||||
|
||||
|
||||
// This is to make sure authorization can be handled WITHOUT being logged in,
|
||||
// kind of making it act like an api key
|
||||
// https://shuffler.io/authorization -> 3rd party integration auth
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const userAuth = urlParams.get("authorization");
|
||||
if (userAuth !== undefined && userAuth !== null && userAuth.length > 0) {
|
||||
console.log("Adding authorization from user side")
|
||||
state += `%26authorization%3d${userAuth}`;
|
||||
}
|
||||
|
||||
// Check for org_id
|
||||
const orgId = urlParams.get("org_id");
|
||||
if (orgId !== undefined && orgId !== null && orgId.length > 0) {
|
||||
console.log("Adding org_id from user side")
|
||||
state += `%26org_id%3d${orgId}`;
|
||||
}
|
||||
|
||||
if (oauth_url !== undefined && oauth_url !== null && oauth_url.length > 0) {
|
||||
state += `%26oauth_url%3d${oauth_url}`;
|
||||
console.log("ADDING OAUTH2 URL: ", state);
|
||||
}
|
||||
|
||||
|
||||
if (
|
||||
authenticationType.refresh_uri !== undefined &&
|
||||
authenticationType.refresh_uri !== null &&
|
||||
authenticationType.refresh_uri.length > 0
|
||||
) {
|
||||
state += `%26refresh_uri%3d${authenticationType.refresh_uri}`;
|
||||
} else {
|
||||
state += `%26refresh_uri%3d${authentication_url}`;
|
||||
}
|
||||
|
||||
// No prompt forcing
|
||||
//var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=login&scope=${resources}&state=${state}&access_type=offline`;
|
||||
var defaultPrompt = "login"
|
||||
if (prompt !== undefined && prompt !== null && prompt.length > 0) {
|
||||
defaultPrompt = prompt
|
||||
}
|
||||
|
||||
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
|
||||
|
||||
if (admin_consent === true) {
|
||||
console.log("Running Oauth2 WITH admin consent")
|
||||
//url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`;
|
||||
url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`;
|
||||
}
|
||||
|
||||
console.log("URL: ", url)
|
||||
|
||||
// Force new consent
|
||||
//const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`;
|
||||
|
||||
// Admin consent
|
||||
//const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent`
|
||||
|
||||
// &resource=https%3A%2F%2Fgraph.microsoft.com&
|
||||
|
||||
// FIXME: Awful, but works for prototyping
|
||||
// How can we get a callback properly realtime?
|
||||
// How can we properly try-catch without breaks on error?
|
||||
try {
|
||||
var newwin = window.open(url, "", "width=582,height=700");
|
||||
//console.log(newwin)
|
||||
|
||||
var open = true;
|
||||
const timer = setInterval(() => {
|
||||
if (newwin.closed) {
|
||||
console.log("Closing?")
|
||||
|
||||
|
||||
setButtonClicked(false);
|
||||
clearInterval(timer);
|
||||
//alert('"Secure Payment" window closed!');
|
||||
//
|
||||
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication(true, true, true);
|
||||
}
|
||||
} else {
|
||||
console.log("Not closed")
|
||||
}
|
||||
}, 1000);
|
||||
//do {
|
||||
// setTimeout(() => {
|
||||
// console.log(newwin)
|
||||
// console.log("CLOSED", newwin.closed)
|
||||
// if (newwin.closed) {
|
||||
|
||||
// open = false
|
||||
// }
|
||||
// }, 1000)
|
||||
//}
|
||||
//while(open === true)
|
||||
} catch (e) {
|
||||
toast(
|
||||
"Failed authentication - probably bad credentials. Try again"
|
||||
);
|
||||
setButtonClicked(false);
|
||||
}
|
||||
|
||||
return;
|
||||
//do {
|
||||
//} while (
|
||||
};
|
||||
|
||||
authenticationOption.app.actions = [];
|
||||
|
||||
for (var key in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] === undefined
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = "";
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitCheck = () => {
|
||||
console.log("NEW AUTH: ", authenticationOption);
|
||||
if (authenticationOption.label.length === 0) {
|
||||
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||||
//toast("Label can't be empty")
|
||||
//return
|
||||
}
|
||||
|
||||
// Automatically mapping fields that already exist (predefined).
|
||||
// Warning if fields are NOT filled
|
||||
for (var key in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
].length === 0
|
||||
) {
|
||||
if (
|
||||
selectedApp.authentication.parameters[key].value !== undefined &&
|
||||
selectedApp.authentication.parameters[key].value !== null &&
|
||||
selectedApp.authentication.parameters[key].value.length > 0
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = selectedApp.authentication.parameters[key].value;
|
||||
} else {
|
||||
if (
|
||||
selectedApp.authentication.parameters[key].schema.type === "bool"
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = "false";
|
||||
} else {
|
||||
toast(
|
||||
"Field " + selectedApp.authentication.parameters[key].name.replace("_basic", "", -1).replace("_", " ", -1) + " can't be empty"
|
||||
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Action: ", selectedAction);
|
||||
selectedAction.authentication_id = authenticationOption.id;
|
||||
selectedAction.selectedAuthentication = authenticationOption;
|
||||
if (
|
||||
selectedAction.authentication === undefined ||
|
||||
selectedAction.authentication === null
|
||||
) {
|
||||
selectedAction.authentication = [authenticationOption];
|
||||
} else {
|
||||
selectedAction.authentication.push(authenticationOption);
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction);
|
||||
|
||||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||||
var newFields = [];
|
||||
for (const key in newAuthOption.fields) {
|
||||
const value = newAuthOption.fields[key];
|
||||
newFields.push({
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("FIELDS: ", newFields);
|
||||
newAuthOption.fields = newFields;
|
||||
setNewAppAuth(newAuthOption);
|
||||
//appAuthentication.push(newAuthOption)
|
||||
//setAppAuthentication(appAuthentication)
|
||||
//
|
||||
|
||||
//if (configureWorkflowModalOpen) {
|
||||
// setSelectedAction({})
|
||||
//}
|
||||
//setUpdate(authenticationOption.id)
|
||||
|
||||
/*
|
||||
{selectedAction.authentication.map(data => (
|
||||
<MenuItem key={data.id} style={{backgroundColor: inputColor, color: "white"}} value={data}>
|
||||
*/
|
||||
};
|
||||
|
||||
const handleScopeChange = (event) => {
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
|
||||
console.log("VALUE: ", value);
|
||||
|
||||
// On autofill we get a the stringified value.
|
||||
setSelectedScopes(typeof value === "string" ? value.split(",") : value);
|
||||
};
|
||||
|
||||
if (
|
||||
authenticationOption.label === null ||
|
||||
authenticationOption.label === undefined
|
||||
) {
|
||||
authenticationOption.label = selectedApp.name + " authentication";
|
||||
}
|
||||
|
||||
|
||||
const autoAuthButton =
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{buttonClicked ? (
|
||||
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
|
||||
) : (
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAction.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
src={selectedAction.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5,}} variant="body1">
|
||||
One-click Login
|
||||
</Typography>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
if (authButtonOnly === true) {
|
||||
return autoAuthButton
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle>
|
||||
<div style={{ color: "white" }}>
|
||||
Authenticate {selectedApp.name.replaceAll("_", " ")}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<span style={{}}>
|
||||
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. Your redirect URL is <b>{window.location.origin}/set_authentication</b> -
|
||||
<a
|
||||
target="_blank"
|
||||
rel="norefferer"
|
||||
href="/docs/apps#authentication"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
{" "}
|
||||
Learn more about Oauth2 with Shuffle
|
||||
</a>
|
||||
<div />
|
||||
</span>
|
||||
|
||||
{isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ?
|
||||
<span>
|
||||
<span style={{display: "flex"}}>
|
||||
{autoAuthButton}
|
||||
|
||||
{buttonClicked ?
|
||||
null
|
||||
:
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Force Admin Consent"}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
style={{
|
||||
maxWidth: 50,
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
maxHeight: 50,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
//startOauth2Request(true)
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
<SupervisorAccountIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 10, }}>
|
||||
OR
|
||||
</Typography>
|
||||
</span>
|
||||
: null}
|
||||
{/*<TextField
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius,}}
|
||||
InputProps={{
|
||||
style:{
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Auth july 2020"}
|
||||
defaultValue={`Auth for ${selectedApp.name}`}
|
||||
onChange={(event) => {
|
||||
authenticationOption.label = event.target.value
|
||||
}}
|
||||
/>
|
||||
<Divider style={{marginTop: 15, marginBottom: 15, backgroundColor: "rgb(91, 96, 100)"}}/>
|
||||
*/}
|
||||
|
||||
{!manuallyConfigure ? null : (
|
||||
<span>
|
||||
{selectedApp.authentication.parameters.map((data, index) => {
|
||||
//console.log(data, index)
|
||||
if (data.name === "client_id" || data.name === "client_secret") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.name !== "url") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (oauthUrl.length === 0) {
|
||||
setOauthUrl(data.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} style={{ marginTop: 10 }}>
|
||||
<LockOpenIcon style={{ marginRight: 10 }} />
|
||||
<b>{data.name}</b>
|
||||
|
||||
{data.schema !== undefined &&
|
||||
data.schema !== null &&
|
||||
data.schema.type === "bool" ? (
|
||||
<Select
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
}}
|
||||
defaultValue={"false"}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
console.log("Value: ", e.target.value);
|
||||
authenticationOption.fields[data.name] = e.target.value;
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
height: 50,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
key={"false"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"false"}
|
||||
>
|
||||
false
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
key={"true"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"true"}
|
||||
>
|
||||
true
|
||||
</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
type={
|
||||
data.example !== undefined &&
|
||||
data.example.includes("***")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
color="primary"
|
||||
defaultValue={
|
||||
data.value !== undefined && data.value !== null
|
||||
? data.value
|
||||
: ""
|
||||
}
|
||||
placeholder={data.example}
|
||||
onChange={(event) => {
|
||||
authenticationOption.fields[data.name] =
|
||||
event.target.value;
|
||||
console.log("Setting oauth url");
|
||||
setOauthUrl(event.target.value);
|
||||
//const [oauthUrl, setOauthUrl] = React.useState("")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<TextField
|
||||
style={{
|
||||
marginTop: 20,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Client ID"}
|
||||
onChange={(event) => {
|
||||
setClientId(event.target.value);
|
||||
//authenticationOption.label = event.target.value
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Client Secret"}
|
||||
onChange={(event) => {
|
||||
setClientSecret(event.target.value);
|
||||
//authenticationOption.label = event.target.value
|
||||
}}
|
||||
/>
|
||||
{allscopes.length === 0 ? null : (
|
||||
<div style={{width: "100%", marginTop: 10, display: "flex"}}>
|
||||
<span>
|
||||
Scopes
|
||||
<Select
|
||||
multiple
|
||||
underline={false}
|
||||
value={selectedScopes}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
padding: 5,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
handleScopeChange(e)
|
||||
}}
|
||||
fullWidth
|
||||
input={<Input id="select-multiple-native" />}
|
||||
renderValue={(selected) => selected.join(", ")}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{allscopes.map((data, index) => {
|
||||
return (
|
||||
<MenuItem key={index} value={data}>
|
||||
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
|
||||
<ListItemText primary={data} />
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Automatic Refresh (default: true)"}
|
||||
placement="top"
|
||||
>
|
||||
<Checkbox style={{paddingTop: 20}} color="secondary" checked={offlineAccess} onClick={() => {
|
||||
setOfflineAccess(!offlineAccess)
|
||||
}}/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
style={{
|
||||
marginBottom: 40,
|
||||
marginTop: 20,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
disabled={
|
||||
clientSecret.length === 0 || clientId.length === 0 || buttonClicked || selectedScopes.length === 0
|
||||
}
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
handleOauth2Request(
|
||||
clientId,
|
||||
clientSecret,
|
||||
oauthUrl,
|
||||
selectedScopes
|
||||
);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{buttonClicked ? (
|
||||
<CircularProgress style={{ color: "white" }} />
|
||||
) : (
|
||||
"Manually Authenticate"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
{defaultConfigSet ? (
|
||||
<span style={{}}>
|
||||
... or
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
disabled={clientSecret.length === 0 || clientId.length === 0}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
setManuallyConfigure(!manuallyConfigure);
|
||||
|
||||
if (manuallyConfigure) {
|
||||
setClientId(authenticationType.client_id);
|
||||
setClientSecret(authenticationType.client_secret);
|
||||
} else {
|
||||
setClientId("");
|
||||
setClientSecret("");
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{manuallyConfigure
|
||||
? "Use auto-config"
|
||||
: "Manually configure Oauth2"}
|
||||
</Button>
|
||||
</span>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticationOauth2;
|
275
shuffle/frontend/src/components/OrgHeader.jsx
Normal file
873
shuffle/frontend/src/components/OrgHeaderexpanded.jsx
Normal file
@ -0,0 +1,873 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import theme from '../theme.jsx';
|
||||
import { toast } from "react-toastify"
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Paper,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
Card,
|
||||
Tooltip,
|
||||
FormControlLabel,
|
||||
Typography,
|
||||
Switch,
|
||||
Select,
|
||||
MenuItem,
|
||||
Divider,
|
||||
TextField,
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
Grid,
|
||||
IconButton,
|
||||
Autocomplete,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Save as SaveIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
borderColor: "#f85a3e !important",
|
||||
},
|
||||
});
|
||||
|
||||
const OrgHeaderexpanded = (props) => {
|
||||
const {
|
||||
userdata,
|
||||
selectedOrganization,
|
||||
setSelectedOrganization,
|
||||
globalUrl,
|
||||
isCloud,
|
||||
adminTab,
|
||||
} = props;
|
||||
|
||||
const classes = useStyles();
|
||||
const defaultBranch = "master";
|
||||
|
||||
const [orgName, setOrgName] = React.useState(selectedOrganization.name);
|
||||
const [orgDescription, setOrgDescription] = React.useState(
|
||||
selectedOrganization.description
|
||||
);
|
||||
|
||||
const [appDownloadUrl, setAppDownloadUrl] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? "https://github.com/frikky/shuffle-apps"
|
||||
: selectedOrganization.defaults.app_download_repo === undefined ||
|
||||
selectedOrganization.defaults.app_download_repo.length === 0
|
||||
? "https://github.com/frikky/shuffle-apps"
|
||||
: selectedOrganization.defaults.app_download_repo
|
||||
);
|
||||
const [appDownloadBranch, setAppDownloadBranch] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? defaultBranch
|
||||
: selectedOrganization.defaults.app_download_branch === undefined ||
|
||||
selectedOrganization.defaults.app_download_branch.length === 0
|
||||
? defaultBranch
|
||||
: selectedOrganization.defaults.app_download_branch
|
||||
);
|
||||
const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? "https://github.com/frikky/shuffle-apps"
|
||||
: selectedOrganization.defaults.workflow_download_repo === undefined ||
|
||||
selectedOrganization.defaults.workflow_download_repo.length === 0
|
||||
? "https://github.com/frikky/shuffle-workflows"
|
||||
: selectedOrganization.defaults.workflow_download_repo
|
||||
);
|
||||
const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? defaultBranch
|
||||
: selectedOrganization.defaults.workflow_download_branch === undefined ||
|
||||
selectedOrganization.defaults.workflow_download_branch.length === 0
|
||||
? defaultBranch
|
||||
: selectedOrganization.defaults.workflow_download_branch
|
||||
);
|
||||
const [ssoEntrypoint, setSsoEntrypoint] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.sso_entrypoint === undefined ||
|
||||
selectedOrganization.sso_config.sso_entrypoint.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.sso_entrypoint
|
||||
);
|
||||
const [ssoCertificate, setSsoCertificate] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.sso_certificate === undefined ||
|
||||
selectedOrganization.sso_config.sso_certificate.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.sso_certificate
|
||||
);
|
||||
const [notificationWorkflow, setNotificationWorkflow] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? ""
|
||||
: selectedOrganization.defaults.notification_workflow === undefined ||
|
||||
selectedOrganization.defaults.notification_workflow.length === 0
|
||||
? ""
|
||||
: selectedOrganization.defaults.notification_workflow
|
||||
);
|
||||
|
||||
const [documentationReference, setDocumentationReference] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? ""
|
||||
: selectedOrganization.defaults.documentation_reference === undefined ||
|
||||
selectedOrganization.defaults.documentation_reference.length === 0
|
||||
? ""
|
||||
: selectedOrganization.defaults.documentation_reference
|
||||
);
|
||||
const [openidClientId, setOpenidClientId] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_id === undefined ||
|
||||
selectedOrganization.sso_config.client_id.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_id
|
||||
);
|
||||
const [openidClientSecret, setOpenidClientSecret] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_secret === undefined ||
|
||||
selectedOrganization.sso_config.client_secret.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_secret
|
||||
);
|
||||
const [openidAuthorization, setOpenidAuthorization] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_authorization === undefined ||
|
||||
selectedOrganization.sso_config.openid_authorization.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_authorization
|
||||
);
|
||||
const [openidToken, setOpenidToken] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_token === undefined ||
|
||||
selectedOrganization.sso_config.openid_token.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_token
|
||||
)
|
||||
|
||||
const [workflows, setWorkflows] = React.useState([])
|
||||
const [workflow, setWorkflow] = React.useState({})
|
||||
|
||||
const getAvailableWorkflows = (trigger_index) => {
|
||||
fetch(globalUrl + "/api/v1/workflows", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!");
|
||||
return;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson !== undefined) {
|
||||
setWorkflows(responseJson)
|
||||
|
||||
if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) {
|
||||
|
||||
const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow)
|
||||
if (workflow !== undefined && workflow !== null) {
|
||||
setWorkflow(workflow)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error getting workflows: " + error);
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAvailableWorkflows()
|
||||
}, [])
|
||||
|
||||
const handleEditOrg = (
|
||||
name,
|
||||
description,
|
||||
orgId,
|
||||
image,
|
||||
defaults,
|
||||
sso_config
|
||||
) => {
|
||||
|
||||
const data = {
|
||||
name: name,
|
||||
description: description,
|
||||
org_id: orgId,
|
||||
image: image,
|
||||
defaults: defaults,
|
||||
sso_config: sso_config,
|
||||
};
|
||||
|
||||
const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`;
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
toast("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
toast("Successfully edited org!");
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
toast("Err: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const handleWorkflowSelectionUpdate = (e, isUserinput) => {
|
||||
if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) {
|
||||
console.log("Returning as there's no id")
|
||||
return null
|
||||
}
|
||||
|
||||
setWorkflow(e.target.value)
|
||||
setNotificationWorkflow(e.target.value.id)
|
||||
toast("Updated notification workflow. Don't forget to save!")
|
||||
}
|
||||
|
||||
const orgSaveButton = (
|
||||
<Tooltip title="Save any unsaved data" placement="bottom">
|
||||
<div>
|
||||
<Button
|
||||
style={{ width: 150, height: 55, flex: 1 }}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={
|
||||
userdata === undefined ||
|
||||
userdata === null ||
|
||||
userdata.admin !== "true"
|
||||
}
|
||||
onClick={() =>
|
||||
handleEditOrg(
|
||||
orgName,
|
||||
orgDescription,
|
||||
selectedOrganization.id,
|
||||
selectedOrganization.image,
|
||||
{
|
||||
app_download_repo: appDownloadUrl,
|
||||
app_download_branch: appDownloadBranch,
|
||||
workflow_download_repo: workflowDownloadUrl,
|
||||
workflow_download_branch: workflowDownloadBranch,
|
||||
notification_workflow: notificationWorkflow,
|
||||
documentation_reference: documentationReference,
|
||||
},
|
||||
{
|
||||
sso_entrypoint: ssoEntrypoint,
|
||||
sso_certificate: ssoCertificate,
|
||||
client_id: openidClientId,
|
||||
client_secret: openidClientSecret,
|
||||
openid_authorization: openidAuthorization,
|
||||
openid_token: openidToken,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
<SaveIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<Grid container spacing={3} style={{ textAlign: "left" }}>
|
||||
<Grid item xs={12} style={{}}>
|
||||
<span>
|
||||
<Typography>Notification Workflow</Typography>
|
||||
{/*
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows
|
||||
</Typography>
|
||||
*/}
|
||||
<div style={{display: "flex", flexDirection: "row", alignItems: "center"}}>
|
||||
{workflows !== undefined && workflows !== null && workflows.length > 0 ?
|
||||
<Autocomplete
|
||||
id="notification_workflow_search"
|
||||
autoHighlight
|
||||
freeSolo
|
||||
//autoSelect
|
||||
value={workflow}
|
||||
classes={{ inputRoot: classes.inputRoot }}
|
||||
ListboxProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
getOptionLabel={(option) => {
|
||||
if (
|
||||
option === undefined ||
|
||||
option === null ||
|
||||
option.name === undefined ||
|
||||
option.name === null
|
||||
) {
|
||||
return "No Workflow Selected";
|
||||
}
|
||||
|
||||
const newname = (
|
||||
option.name.charAt(0).toUpperCase() + option.name.substring(1)
|
||||
).replaceAll("_", " ");
|
||||
return newname;
|
||||
}}
|
||||
options={workflows}
|
||||
fullWidth
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
height: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
console.log("Found value: ", newValue)
|
||||
|
||||
var parsedinput = { target: { value: newValue } }
|
||||
|
||||
// For variables
|
||||
if (typeof newValue === 'string' && newValue.startsWith("$")) {
|
||||
parsedinput = {
|
||||
target: {
|
||||
value: {
|
||||
"name": newValue,
|
||||
"id": newValue,
|
||||
"actions": [],
|
||||
"triggers": [],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleWorkflowSelectionUpdate(parsedinput)
|
||||
}}
|
||||
renderOption={(props, data, state) => {
|
||||
if (data.id === workflow.id) {
|
||||
data = workflow;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip arrow placement="left" title={
|
||||
<span style={{}}>
|
||||
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
|
||||
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
|
||||
: null}
|
||||
<Typography>
|
||||
Choose {data.name}
|
||||
</Typography>
|
||||
</span>
|
||||
} placement="bottom">
|
||||
<MenuItem
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: data.id === workflow.id ? "red" : "white",
|
||||
}}
|
||||
value={data}
|
||||
onClick={(e) => {
|
||||
var parsedinput = { target: { value: data } }
|
||||
handleWorkflowSelectionUpdate(parsedinput)
|
||||
}}
|
||||
>
|
||||
{data.name}
|
||||
</MenuItem>
|
||||
</Tooltip>
|
||||
)
|
||||
}}
|
||||
renderInput={(params) => {
|
||||
return (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
{...params}
|
||||
label="Find a notification workflow"
|
||||
variant="outlined"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
:
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="ID of the workflow to receive notifications"
|
||||
value={notificationWorkflow}
|
||||
onChange={(e) => {
|
||||
setNotificationWorkflow(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
<div style={{minWidth: 150, maxWidth: 150, marginTop: 5, marginLeft: 10, }}>
|
||||
{orgSaveButton}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={12} style={{}}>
|
||||
<span>
|
||||
<Typography>Org Documentation reference</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="URL to an external reference for this implementation"
|
||||
value={documentationReference}
|
||||
onChange={(e) => {
|
||||
setDocumentationReference(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
{isCloud ? null :
|
||||
<Grid item xs={12} style={{marginTop: 50 }}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>OpenID connect</Typography>
|
||||
<Grid container style={{marginTop: 10, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Client ID</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client ID from the identity provider"
|
||||
value={openidClientId}
|
||||
onChange={(e) => {
|
||||
setOpenidClientId(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Client Secret (optional)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
|
||||
value={openidClientSecret}
|
||||
onChange={(e) => {
|
||||
setOpenidClientSecret(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container style={{marginTop: 10, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Authorization URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The OpenID authorization URL (usually ends with /authorize)"
|
||||
value={openidAuthorization}
|
||||
onChange={(e) => {
|
||||
setOpenidAuthorization(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Token URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The OpenID token URL (usually ends with /token)"
|
||||
value={openidToken}
|
||||
onChange={(e) => {
|
||||
setOpenidToken(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
{/*isCloud ? null : */}
|
||||
<Grid item xs={12} style={{marginTop: 50,}}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>SAML SSO (v1.1)</Typography>
|
||||
<Grid container style={{marginTop: 20, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>SSO Entrypoint (IdP)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The entrypoint URL from your provider"
|
||||
value={ssoEntrypoint}
|
||||
onChange={(e) => {
|
||||
setSsoEntrypoint(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>SSO Certificate (X509)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The X509 certificate to use"
|
||||
value={ssoCertificate}
|
||||
onChange={(e) => {
|
||||
setSsoCertificate(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{isCloud ?
|
||||
<Typography variant="body2" style={{textAlign: "left",}} color="textSecondary">
|
||||
IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso
|
||||
</Typography>
|
||||
: null}
|
||||
</Grid>
|
||||
{isCloud ? null : (
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>App Download URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="A description for the organization"
|
||||
value={appDownloadUrl}
|
||||
onChange={(e) => {
|
||||
setAppDownloadUrl(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
)}
|
||||
{isCloud ? null : (
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>App Download Branch</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="A description for the organization"
|
||||
value={appDownloadBranch}
|
||||
onChange={(e) => {
|
||||
setAppDownloadBranch(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
)}
|
||||
{isCloud ? null : (
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Workflow Download URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="A description for the organization"
|
||||
value={workflowDownloadUrl}
|
||||
onChange={(e) => {
|
||||
setWorkflowDownloadUrl(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
)}
|
||||
{isCloud ? null : (
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Workflow Download Branch</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="A description for the organization"
|
||||
value={workflowDownloadBranch}
|
||||
onChange={(e) => {
|
||||
setWorkflowDownloadBranch(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<div style={{ margin: "auto", textalign: "center", marginTop: 15, marginBottom: 15, }}>
|
||||
{orgSaveButton}
|
||||
</div>
|
||||
{/*
|
||||
<span style={{textAlign: "center"}}>
|
||||
{expanded ?
|
||||
<ExpandLessIcon />
|
||||
:
|
||||
<ExpandMoreIcon />
|
||||
}
|
||||
</span>
|
||||
*/}
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OrgHeaderexpanded;
|
19
shuffle/frontend/src/components/PaperComponent.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React, {useState, useEffect, useLayoutEffect} from 'react';
|
||||
|
||||
import Draggable from "react-draggable";
|
||||
import {
|
||||
Paper
|
||||
} from "@mui/material";
|
||||
|
||||
const PaperComponent = (props) => {
|
||||
return (
|
||||
<Draggable
|
||||
handle="#draggable-dialog-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
>
|
||||
<Paper {...props} />
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaperComponent;
|
3771
shuffle/frontend/src/components/ParsedAction.jsx
Normal file
93
shuffle/frontend/src/components/Priorities.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import theme from "../theme.jsx";
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
Switch,
|
||||
} from "@mui/material";
|
||||
|
||||
import Priority from "../components/Priority.jsx";
|
||||
//import { useAlert
|
||||
|
||||
const Priorities = (props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, } = props;
|
||||
const [showDismissed, setShowDismissed] = React.useState(false);
|
||||
const [showRead, setShowRead] = React.useState(false);
|
||||
|
||||
if (userdata === undefined || userdata === null) {
|
||||
return
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{maxWidth: 1000, }}>
|
||||
<h2 style={{ display: "inline" }}>Suggestions</h2>
|
||||
<span style={{ marginLeft: 25 }}>
|
||||
Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. These range from simple configurations in Shuffle to Usecases you may have missed.
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/docs/organizations#priorities"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
<div style={{marginTop: 10, }}/>
|
||||
<Switch
|
||||
checked={showDismissed}
|
||||
onChange={() => {
|
||||
setShowDismissed(!showDismissed);
|
||||
}}
|
||||
/> Show dismissed
|
||||
{userdata.priorities === null || userdata.priorities === undefined || userdata.priorities.length === 0 ?
|
||||
<Typography variant="h4">
|
||||
No Suggestions found
|
||||
</Typography>
|
||||
:
|
||||
userdata.priorities.map((priority, index) => {
|
||||
if (showDismissed === false && priority.active === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Priority
|
||||
key={index}
|
||||
globalUrl={globalUrl}
|
||||
priority={priority}
|
||||
checkLogin={checkLogin}
|
||||
setAdminTab={setAdminTab}
|
||||
setCurTab={setCurTab}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
<Divider style={{marginTop: 50, marginBottom: 50, }} />
|
||||
<h2 style={{ display: "inline" }}>Notifications</h2>
|
||||
<span style={{ marginLeft: 25 }}>
|
||||
Notifications help you find potential problems with your workflows and apps.
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="/docs/organizations#notifications"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
<div/>
|
||||
<Switch
|
||||
checked={showRead}
|
||||
onChange={() => {
|
||||
setShowRead(!showRead);
|
||||
}}
|
||||
/> Show read
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Priorities;
|
176
shuffle/frontend/src/components/Priority.jsx
Normal file
@ -0,0 +1,176 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ReactGA from 'react-ga4';
|
||||
import theme from "../theme.jsx";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { findSpecificApp } from "../components/AppFramework.jsx"
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
Button,
|
||||
Grid,
|
||||
Card,
|
||||
} from "@mui/material";
|
||||
|
||||
// import magic wand icon from material ui icons
|
||||
import {
|
||||
AutoFixHigh as AutoFixHighIcon,
|
||||
ArrowForward as ArrowForwardIcon,
|
||||
} from '@mui/icons-material';
|
||||
//import { useAlert
|
||||
|
||||
const Priority = (props) => {
|
||||
const { globalUrl, userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
let navigate = useNavigate();
|
||||
|
||||
var realigned = false
|
||||
let newdescription = priority.description
|
||||
const descsplit = priority.description.split("&")
|
||||
if (appFramework !== undefined && descsplit.length === 5 && priority.description.includes(":default")) {
|
||||
console.log("descsplit: ", descsplit)
|
||||
if (descsplit[1] === "") {
|
||||
const item = findSpecificApp(appFramework, descsplit[0])
|
||||
console.log("item: ", item)
|
||||
if (item !== null) {
|
||||
descsplit[1] = item.large_image
|
||||
descsplit[0] = descsplit[0].split(":")[0]
|
||||
}
|
||||
|
||||
realigned = true
|
||||
}
|
||||
|
||||
if (descsplit[3] === "") {
|
||||
const item = findSpecificApp(appFramework, descsplit[2])
|
||||
console.log("item: ", item)
|
||||
if (item !== null) {
|
||||
descsplit[3] = item.large_image
|
||||
descsplit[2] = descsplit[2].split(":")[0]
|
||||
}
|
||||
|
||||
realigned = true
|
||||
}
|
||||
|
||||
newdescription = descsplit.join("&")
|
||||
}
|
||||
|
||||
const changeRecommendation = (recommendation, action) => {
|
||||
const data = {
|
||||
action: action,
|
||||
name: recommendation.name,
|
||||
};
|
||||
|
||||
|
||||
fetch(`${globalUrl}/api/v1/recommendations/modify`, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
} else {
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
if (checkLogin !== undefined) {
|
||||
checkLogin()
|
||||
}
|
||||
} else {
|
||||
if (responseJson.success === false && responseJson.reason !== undefined) {
|
||||
toast("Failed change recommendation: ", responseJson.reason)
|
||||
} else {
|
||||
toast("Failed change recommendation");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast("Failed dismissing alert. Please contact support@shuffler.io if this persists.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: 15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: theme.palette.surfaceColor, display: "flex", }}>
|
||||
<div style={{flex: 2, overflow: "hidden",}}>
|
||||
<span style={{display: "flex", }}>
|
||||
{priority.type === "usecase" || priority.type == "apps" ? <AutoFixHighIcon style={{height: 19, width: 19, marginLeft: 3, marginRight: 10, }}/> : null}
|
||||
<Typography variant="body1" >
|
||||
{priority.name}
|
||||
</Typography>
|
||||
</span>
|
||||
{priority.type === "usecase" && priority.description.includes("&") ?
|
||||
<span style={{display: "flex", marginTop: 10, }}>
|
||||
<img src={newdescription.split("&")[1]} alt={priority.name} style={{height: "auto", width: 30, marginRight: realigned ? -10 : 10, borderRadius: theme.palette.borderRadius, marginTop: realigned ? 5 : 0 }} />
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 3, }}>
|
||||
{newdescription.split("&")[0]}
|
||||
</Typography>
|
||||
|
||||
{newdescription.split("&").length > 3 ?
|
||||
<span style={{display: "flex", }}>
|
||||
<ArrowForwardIcon style={{marginLeft: 15, marginRight: 15, }}/>
|
||||
<img src={newdescription.split("&")[3]} alt={priority.name+"2"} style={{height: "auto", width: 30, marginRight: realigned ? -10 : 10, borderRadius: theme.palette.borderRadius, marginTop: realigned ? 5 : 0 }} />
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 3}}>
|
||||
{newdescription.split("&")[2]}
|
||||
</Typography>
|
||||
</span>
|
||||
: null}
|
||||
|
||||
</span>
|
||||
:
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
{priority.description}
|
||||
</Typography>
|
||||
}
|
||||
</div>
|
||||
<div style={{flex: 1, display: "flex", marginLeft: 30, }}>
|
||||
<Button style={{height: 50, borderRadius: 25, marginTop: 8, width: 175, marginRight: 10, color: priority.active === false ? "white" : "black", backgroundColor: priority.active === false ? theme.palette.inputColor : "rgba(255,255,255,0.8)", }} variant="contained" color="secondary" onClick={() => {
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "recommendation",
|
||||
action: `click_${priority.name}`,
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
navigate(priority.url)
|
||||
|
||||
if (setAdminTab !== undefined && setCurTab !== undefined) {
|
||||
if (priority.description.toLowerCase().includes("notification workflow")) {
|
||||
setCurTab(0)
|
||||
setAdminTab(0)
|
||||
}
|
||||
|
||||
if (priority.description.toLowerCase().includes("hybrid shuffle")) {
|
||||
setCurTab(6)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
Explore
|
||||
</Button>
|
||||
{priority.active === true ?
|
||||
<Button style={{borderRadius: 25, width: 100, height: 50, marginTop: 8, }} variant="text" color="secondary" onClick={() => {
|
||||
// dismiss -> get envs
|
||||
changeRecommendation(priority, "dismiss")
|
||||
}}>
|
||||
Dismiss
|
||||
</Button>
|
||||
: null }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Priority;
|
157
shuffle/frontend/src/components/RenderCytoscape.js
Normal file
@ -0,0 +1,157 @@
|
||||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import * as cytoscape from "cytoscape";
|
||||
import CytoscapeComponent from "react-cytoscapejs";
|
||||
import cystyle from "../defaultCytoscapeStyle";
|
||||
|
||||
const surfaceColor = "#27292D";
|
||||
const CytoscapeWrapper = (props) => {
|
||||
const { globalUrl, inworkflow } = props;
|
||||
|
||||
const [elements, setElements] = useState([]);
|
||||
const [workflow, setWorkflow] = useState(inworkflow);
|
||||
const [cy, setCy] = React.useState();
|
||||
const bodyWidth = 200;
|
||||
const bodyHeight = 150;
|
||||
|
||||
const setupGraph = () => {
|
||||
const actions = workflow.actions.map((action) => {
|
||||
const node = {};
|
||||
node.position = action.position;
|
||||
node.data = action;
|
||||
|
||||
node.data._id = action["id"];
|
||||
node.data.type = "ACTION";
|
||||
node.isStartNode = action["id"] === workflow.start;
|
||||
|
||||
var example = "";
|
||||
if (
|
||||
action.example !== undefined &&
|
||||
action.example !== null &&
|
||||
action.example.length > 0
|
||||
) {
|
||||
example = action.example;
|
||||
}
|
||||
|
||||
node.data.example = example;
|
||||
return node;
|
||||
});
|
||||
|
||||
const triggers = workflow.triggers.map((trigger) => {
|
||||
const node = {};
|
||||
node.position = trigger.position;
|
||||
node.data = trigger;
|
||||
|
||||
node.data._id = trigger["id"];
|
||||
node.data.type = "TRIGGER";
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
// FIXME - tmp branch update
|
||||
var insertedNodes = [].concat(actions, triggers);
|
||||
const edges = workflow.branches.map((branch, index) => {
|
||||
//workflow.branches[index].conditions = [{
|
||||
|
||||
const edge = {};
|
||||
var conditions = workflow.branches[index].conditions;
|
||||
if (conditions === undefined || conditions === null) {
|
||||
conditions = [];
|
||||
}
|
||||
|
||||
var label = "";
|
||||
if (conditions.length === 1) {
|
||||
label = conditions.length + " condition";
|
||||
} else if (conditions.length > 1) {
|
||||
label = conditions.length + " conditions";
|
||||
}
|
||||
|
||||
edge.data = {
|
||||
id: branch.id,
|
||||
_id: branch.id,
|
||||
source: branch.source_id,
|
||||
target: branch.destination_id,
|
||||
label: label,
|
||||
conditions: conditions,
|
||||
hasErrors: branch.has_errors,
|
||||
};
|
||||
|
||||
// This is an attempt at prettier edges. The numbers are weird to work with.
|
||||
/*
|
||||
//http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html
|
||||
const sourcenode = actions.find(node => node.data._id === branch.source_id)
|
||||
const destinationnode = actions.find(node => node.data._id === branch.destination_id)
|
||||
if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) {
|
||||
//node.data._id = action["id"]
|
||||
console.log("SOURCE: ", sourcenode.position)
|
||||
console.log("DESTINATIONNODE: ", destinationnode.position)
|
||||
|
||||
var opposite = true
|
||||
if (sourcenode.position.x > destinationnode.position.x) {
|
||||
opposite = false
|
||||
} else {
|
||||
opposite = true
|
||||
}
|
||||
|
||||
edge.style = {
|
||||
'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"],
|
||||
'control-point-weight': ['0.3', '0.7'],
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return edge;
|
||||
});
|
||||
|
||||
setWorkflow(workflow);
|
||||
|
||||
// Verifies if a branch is valid and skips others
|
||||
var newedges = [];
|
||||
for (var key in edges) {
|
||||
var item = edges[key];
|
||||
|
||||
const sourcecheck = insertedNodes.find(
|
||||
(data) => data.data.id === item.data.source
|
||||
);
|
||||
const destcheck = insertedNodes.find(
|
||||
(data) => data.data.id === item.data.target
|
||||
);
|
||||
if (sourcecheck === undefined || destcheck === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newedges.push(item);
|
||||
}
|
||||
|
||||
insertedNodes = insertedNodes.concat(newedges);
|
||||
setElements(insertedNodes);
|
||||
};
|
||||
|
||||
if (elements.length === 0) {
|
||||
setupGraph();
|
||||
}
|
||||
|
||||
return (
|
||||
<CytoscapeComponent
|
||||
elements={elements}
|
||||
minZoom={0.35}
|
||||
maxZoom={2.0}
|
||||
style={{
|
||||
width: bodyWidth - 15,
|
||||
height: bodyHeight - 5,
|
||||
backgroundColor: surfaceColor,
|
||||
}}
|
||||
stylesheet={cystyle}
|
||||
boxSelectionEnabled={true}
|
||||
autounselectify={false}
|
||||
showGrid={true}
|
||||
cy={(incy) => {
|
||||
// FIXME: There's something specific loading when
|
||||
// you do the first hover of a node. Why is this different?
|
||||
//console.log("CY: ", incy)
|
||||
setCy(incy);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CytoscapeWrapper;
|
157
shuffle/frontend/src/components/RenderCytoscape.jsx
Normal file
@ -0,0 +1,157 @@
|
||||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import * as cytoscape from "cytoscape";
|
||||
import CytoscapeComponent from "react-cytoscapejs";
|
||||
import cystyle from "../defaultCytoscapeStyle.jsx";
|
||||
|
||||
const surfaceColor = "#27292D";
|
||||
const CytoscapeWrapper = (props) => {
|
||||
const { globalUrl, inworkflow } = props;
|
||||
|
||||
const [elements, setElements] = useState([]);
|
||||
const [workflow, setWorkflow] = useState(inworkflow);
|
||||
const [cy, setCy] = React.useState();
|
||||
const bodyWidth = 200;
|
||||
const bodyHeight = 150;
|
||||
|
||||
const setupGraph = () => {
|
||||
const actions = workflow.actions.map((action) => {
|
||||
const node = {};
|
||||
node.position = action.position;
|
||||
node.data = action;
|
||||
|
||||
node.data._id = action["id"];
|
||||
node.data.type = "ACTION";
|
||||
node.isStartNode = action["id"] === workflow.start;
|
||||
|
||||
var example = "";
|
||||
if (
|
||||
action.example !== undefined &&
|
||||
action.example !== null &&
|
||||
action.example.length > 0
|
||||
) {
|
||||
example = action.example;
|
||||
}
|
||||
|
||||
node.data.example = example;
|
||||
return node;
|
||||
});
|
||||
|
||||
const triggers = workflow.triggers.map((trigger) => {
|
||||
const node = {};
|
||||
node.position = trigger.position;
|
||||
node.data = trigger;
|
||||
|
||||
node.data._id = trigger["id"];
|
||||
node.data.type = "TRIGGER";
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
// FIXME - tmp branch update
|
||||
var insertedNodes = [].concat(actions, triggers);
|
||||
const edges = workflow.branches.map((branch, index) => {
|
||||
//workflow.branches[index].conditions = [{
|
||||
|
||||
const edge = {};
|
||||
var conditions = workflow.branches[index].conditions;
|
||||
if (conditions === undefined || conditions === null) {
|
||||
conditions = [];
|
||||
}
|
||||
|
||||
var label = "";
|
||||
if (conditions.length === 1) {
|
||||
label = conditions.length + " condition";
|
||||
} else if (conditions.length > 1) {
|
||||
label = conditions.length + " conditions";
|
||||
}
|
||||
|
||||
edge.data = {
|
||||
id: branch.id,
|
||||
_id: branch.id,
|
||||
source: branch.source_id,
|
||||
target: branch.destination_id,
|
||||
label: label,
|
||||
conditions: conditions,
|
||||
hasErrors: branch.has_errors,
|
||||
};
|
||||
|
||||
// This is an attempt at prettier edges. The numbers are weird to work with.
|
||||
/*
|
||||
//http://manual.graphspace.org/projects/graphspace-python/en/latest/demos/edge-types.html
|
||||
const sourcenode = actions.find(node => node.data._id === branch.source_id)
|
||||
const destinationnode = actions.find(node => node.data._id === branch.destination_id)
|
||||
if (sourcenode !== undefined && destinationnode !== undefined && branch.source_id !== branch.destination_id) {
|
||||
//node.data._id = action["id"]
|
||||
console.log("SOURCE: ", sourcenode.position)
|
||||
console.log("DESTINATIONNODE: ", destinationnode.position)
|
||||
|
||||
var opposite = true
|
||||
if (sourcenode.position.x > destinationnode.position.x) {
|
||||
opposite = false
|
||||
} else {
|
||||
opposite = true
|
||||
}
|
||||
|
||||
edge.style = {
|
||||
'control-point-distance': opposite ? ["25%", "-75%"] : ["-10%", "90%"],
|
||||
'control-point-weight': ['0.3', '0.7'],
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return edge;
|
||||
});
|
||||
|
||||
setWorkflow(workflow);
|
||||
|
||||
// Verifies if a branch is valid and skips others
|
||||
var newedges = [];
|
||||
for (var key in edges) {
|
||||
var item = edges[key];
|
||||
|
||||
const sourcecheck = insertedNodes.find(
|
||||
(data) => data.data.id === item.data.source
|
||||
);
|
||||
const destcheck = insertedNodes.find(
|
||||
(data) => data.data.id === item.data.target
|
||||
);
|
||||
if (sourcecheck === undefined || destcheck === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
newedges.push(item);
|
||||
}
|
||||
|
||||
insertedNodes = insertedNodes.concat(newedges);
|
||||
setElements(insertedNodes);
|
||||
};
|
||||
|
||||
if (elements.length === 0) {
|
||||
setupGraph();
|
||||
}
|
||||
|
||||
return (
|
||||
<CytoscapeComponent
|
||||
elements={elements}
|
||||
minZoom={0.35}
|
||||
maxZoom={2.0}
|
||||
style={{
|
||||
width: bodyWidth - 15,
|
||||
height: bodyHeight - 5,
|
||||
backgroundColor: surfaceColor,
|
||||
}}
|
||||
stylesheet={cystyle}
|
||||
boxSelectionEnabled={true}
|
||||
autounselectify={false}
|
||||
showGrid={true}
|
||||
cy={(incy) => {
|
||||
// FIXME: There's something specific loading when
|
||||
// you do the first hover of a node. Why is this different?
|
||||
//console.log("CY: ", incy)
|
||||
setCy(incy);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CytoscapeWrapper;
|
47
shuffle/frontend/src/components/ScrollToTop.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useEffect } from "react";
|
||||
//import { withRouter } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export const removeQuery = (query) => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
if (params[query] !== undefined) {
|
||||
delete params[query]
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
const queryString = Object.keys(params).map(key => key + '=' + params[key]).join('&')
|
||||
const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?' + queryString
|
||||
window.history.pushState({path:newurl},'',newurl)
|
||||
}
|
||||
|
||||
// ensures scrolling happens in the right way on different pages and when changing
|
||||
function ScrollToTop({ getUserNotifications, curpath, setCurpath, history }) {
|
||||
let location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// Custom handler for certain scroll mechanics
|
||||
//
|
||||
//console.log("OLD: ", curpath, "NeW: ", window.location.pathname)
|
||||
if (curpath === window.location.pathname && curpath === "/usecases") {
|
||||
} else {
|
||||
|
||||
window.scroll({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
setCurpath(window.location.pathname);
|
||||
getUserNotifications();
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/36904185/react-router-scroll-to-top-on-every-transition
|
||||
//export default withRouter(ScrollToTop);
|
||||
// https://v5.reactrouter.com/web/api/Hooks/uselocation
|
||||
export default ScrollToTop;
|
660
shuffle/frontend/src/components/Searchfield.jsx
Normal file
@ -0,0 +1,660 @@
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import theme from '../theme.jsx';
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Chip,
|
||||
IconButton,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
List,
|
||||
Card,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
import {
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {Search as SearchIcon, Close as CloseIcon, Folder as FolderIcon, Code as CodeIcon, LibraryBooks as LibraryBooksIcon} from '@mui/icons-material'
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import aa from 'search-insights'
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
//import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
|
||||
// https://www.algolia.com/doc/api-reference/widgets/search-box/react/
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
|
||||
}
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const SearchField = props => {
|
||||
const { serverside, userdata } = props
|
||||
|
||||
let navigate = useNavigate();
|
||||
const borderRadius = 3
|
||||
const node = useRef()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [oldPath, setOldPath] = useState("")
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
if (serverside === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (window !== undefined && window.location !== undefined && window.location.pathname === "/search") {
|
||||
return null
|
||||
}
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
if (window.location.pathname !== oldPath) {
|
||||
setSearchOpen(false)
|
||||
setOldPath(window.location.pathname)
|
||||
}
|
||||
|
||||
//useEffect(() => {
|
||||
// if (searchOpen) {
|
||||
// var tarfield = document.getElementById("shuffle_search_field")
|
||||
// tarfield.focus()
|
||||
// }
|
||||
//}, searchOpen)
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled, } ) => {
|
||||
const keyPressHandler = (e) => {
|
||||
// e.preventDefault();
|
||||
if (e.which === 13) {
|
||||
// alert("You pressed enter!");
|
||||
navigate("/search?q=" + currentRefinement, { state: value, replace: true });
|
||||
}
|
||||
};
|
||||
/*
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" style={{textAlign: "right", zIndex: 5001, cursor: "pointer", width: 100, }} onMouseOver={(event) => {
|
||||
event.preventDefault()
|
||||
}}>
|
||||
<CloseIcon style={{marginRight: 5,}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}} />
|
||||
</InputAdornment>
|
||||
),
|
||||
*/
|
||||
|
||||
return (
|
||||
<form id="search_form" noValidate type="searchbox" action="" role="search" style={{margin: "10px 0px 0px 0px", }} onClick={() => {
|
||||
}}>
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.surfaceColor, borderRadius: borderRadius, minWidth: 403, maxWidth: 403, }}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
margin: 0,
|
||||
fontSize: "0.9em",
|
||||
paddingLeft: 10,
|
||||
},
|
||||
disableUnderline: true,
|
||||
endAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5, color: "#f86a3e",}}/>
|
||||
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
placeholder="Find Public Apps, Workflows, Documentation..."
|
||||
value={currentRefinement}
|
||||
onKeyDown={keyPressHandler}
|
||||
id="shuffle_search_field"
|
||||
onClick={(event) => {
|
||||
if (!searchOpen) {
|
||||
setSearchOpen(true)
|
||||
setTimeout(() => {
|
||||
var tarfield = document.getElementById("shuffle_search_field")
|
||||
//console.log("TARFIELD: ", tarfield)
|
||||
tarfield.focus()
|
||||
}, 100)
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setTimeout(() => {
|
||||
setSearchOpen(false)
|
||||
}, 500)
|
||||
}}
|
||||
onChange={(event) => {
|
||||
//if (event.currentTarget.value.length > 0 && !searchOpen) {
|
||||
// setSearchOpen(true)
|
||||
//}
|
||||
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkflowHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
|
||||
|
||||
var tmp = searchOpen
|
||||
if (!searchOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const positionInfo = document.activeElement.getBoundingClientRect()
|
||||
const outerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
}
|
||||
|
||||
if (hits.length > 4) {
|
||||
hits = hits.slice(0, 4)
|
||||
}
|
||||
|
||||
var type = "workflows"
|
||||
const baseImage = <CodeIcon />
|
||||
|
||||
return (
|
||||
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: 405, height: 408, left: 75, boxShadows: "none",}}>
|
||||
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
|
||||
Workflows
|
||||
</Typography>
|
||||
|
||||
<List style={{backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
<ListItemAvatar onClick={() => console.log(hits)}>
|
||||
<Avatar>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"No workflows found."}
|
||||
secondary={"Try a broader search term"}
|
||||
/>
|
||||
</ListItem>
|
||||
:
|
||||
hits.map((hit, index) => {
|
||||
const innerlistitemStyle = {
|
||||
width: positionInfo.width+35,
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
const name = hit.name === undefined ?
|
||||
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
|
||||
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
|
||||
const secondaryText = hit.description !== undefined && hit.description !== null && hit.description.length > 3 ? hit.description.slice(0, 40)+"..." : ""
|
||||
const appGroup = hit.action_references === undefined || hit.action_references === null ? [] : hit.action_references
|
||||
const avatar = baseImage
|
||||
|
||||
var parsedUrl = isCloud ? `/workflows/${hit.objectID}` : `https://shuffler.io/workflows/${hit.objectID}`
|
||||
|
||||
parsedUrl += `?queryID=${hit.__queryID}`
|
||||
|
||||
// <a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
return (
|
||||
<Link key={hit.objectID} to={parsedUrl} rel="noopener noreferrer" style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
//console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Workflow Clicked',
|
||||
index: 'workflows',
|
||||
objectIDs: [hit.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: hit.__queryID,
|
||||
positions: [hit.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
if (!isCloud) {
|
||||
event.preventDefault()
|
||||
window.open(parsedUrl, '_blank');
|
||||
}
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<div style={{}}>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
/>
|
||||
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left",}}>
|
||||
{appGroup.map((app, index) => {
|
||||
// Putting all this in secondary of ListItemText looked weird.
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/apps/"+app.id)
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={app.name} placement="bottom">
|
||||
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
)})
|
||||
}
|
||||
</List>
|
||||
{/*
|
||||
<span style={{display: "flex", textAlign: "left", float: "left", position: "absolute", left: 15, bottom: 10, }}>
|
||||
<Link to="/search" style={{textDecoration: "none", color: "#f85a3e"}}>
|
||||
<Typography variant="body2" style={{}}>
|
||||
See all workflows
|
||||
</Typography>
|
||||
</Link>
|
||||
</span>
|
||||
*/}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const AppHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
|
||||
|
||||
var tmp = searchOpen
|
||||
if (!searchOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
const positionInfo = document.activeElement.getBoundingClientRect()
|
||||
const outerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
}
|
||||
|
||||
if (hits.length > 4) {
|
||||
hits = hits.slice(0, 4)
|
||||
}
|
||||
|
||||
var type = "app"
|
||||
const baseImage = <LibraryBooksIcon />
|
||||
|
||||
return (
|
||||
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, width: 1155, height: 408, left: -305, boxShadows: "none",}}>
|
||||
<IconButton style={{zIndex: 5000, position: "absolute", right: 14, color: "grey"}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
|
||||
Apps
|
||||
</Typography>
|
||||
|
||||
<List style={{backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
<ListItemAvatar onClick={() => console.log(hits)}>
|
||||
<Avatar>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"No apps found."}
|
||||
secondary={"Try a broader search term"}
|
||||
/>
|
||||
</ListItem>
|
||||
:
|
||||
hits.map((hit, index) => {
|
||||
const innerlistitemStyle = {
|
||||
width: positionInfo.width+35,
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
const name = hit.name === undefined ?
|
||||
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
|
||||
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
|
||||
|
||||
var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
|
||||
const avatar = hit.image_url === undefined ?
|
||||
baseImage
|
||||
:
|
||||
<Avatar
|
||||
src={hit.image_url}
|
||||
variant="rounded"
|
||||
/>
|
||||
|
||||
//console.log(hit)
|
||||
if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) {
|
||||
secondaryText = hit.categories.slice(0,3).map((data, index) => {
|
||||
if (index === 0) {
|
||||
return data
|
||||
}
|
||||
|
||||
return ", "+data
|
||||
|
||||
/*
|
||||
<Chip
|
||||
key={index}
|
||||
style={chipStyle}
|
||||
label={data}
|
||||
onClick={() => {
|
||||
//handleChipClick
|
||||
}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
*/
|
||||
})
|
||||
}
|
||||
|
||||
var parsedUrl = isCloud ? `/apps/${hit.objectID}` : `https://shuffler.io/apps/${hit.objectID}`
|
||||
parsedUrl += `?queryID=${hit.__queryID}`
|
||||
|
||||
return (
|
||||
<Link key={hit.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'App Clicked',
|
||||
index: 'appsearch',
|
||||
objectIDs: [hit.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: hit.__queryID,
|
||||
positions: [hit.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
if (!isCloud) {
|
||||
event.preventDefault()
|
||||
window.open(parsedUrl, '_blank');
|
||||
}
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={secondaryText}
|
||||
/>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
)})
|
||||
}
|
||||
</List>
|
||||
<span style={{display: "flex", textAlign: "left", float: "left", position: "absolute", left: 15, bottom: 10, }}>
|
||||
<Link to="/search" style={{textDecoration: "none", color: "#f85a3e"}}>
|
||||
<Typography variant="body1" style={{}}>
|
||||
See more
|
||||
</Typography>
|
||||
</Link>
|
||||
</span>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const DocHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
|
||||
|
||||
var tmp = searchOpen
|
||||
if (!searchOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const positionInfo = document.activeElement.getBoundingClientRect()
|
||||
const outerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
}
|
||||
|
||||
if (hits.length > 4) {
|
||||
hits = hits.slice(0, 4)
|
||||
}
|
||||
|
||||
const type = "documentation"
|
||||
const baseImage = <LibraryBooksIcon />
|
||||
|
||||
//console.log(type, hits.length, hits)
|
||||
|
||||
return (
|
||||
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: 405, height: 408, left: 470, boxShadows: "none",}}>
|
||||
<IconButton style={{zIndex: 5000, position: "absolute", right: 14, color: "grey"}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
|
||||
Documentation
|
||||
</Typography>
|
||||
{/*
|
||||
<IconButton edge="end" aria-label="delete" style={{position: "absolute", top: 5, right: 15,}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
*/}
|
||||
<List style={{backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
<ListItemAvatar onClick={() => console.log(hits)}>
|
||||
<Avatar>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"No documentation."}
|
||||
secondary={"Try a broader search term"}
|
||||
/>
|
||||
</ListItem>
|
||||
:
|
||||
hits.map((hit, index) => {
|
||||
const innerlistitemStyle = {
|
||||
width: positionInfo.width+35,
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
var name = hit.name === undefined ?
|
||||
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title
|
||||
:
|
||||
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
|
||||
|
||||
if (name.length > 30) {
|
||||
name = name.slice(0, 30)+"..."
|
||||
}
|
||||
const secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
|
||||
const avatar = hit.image_url === undefined ?
|
||||
baseImage
|
||||
:
|
||||
<Avatar
|
||||
src={hit.image_url}
|
||||
variant="rounded"
|
||||
/>
|
||||
|
||||
var parsedUrl = hit.urlpath !== undefined ? hit.urlpath : ""
|
||||
parsedUrl += `?queryID=${hit.__queryID}`
|
||||
if (parsedUrl.includes("/apps/")) {
|
||||
const extraHash = hit.url_hash === undefined ? "" : `#${hit.url_hash}`
|
||||
|
||||
parsedUrl = `/apps/${hit.filename}`
|
||||
parsedUrl += `?tab=docs&queryID=${hit.__queryID}${extraHash}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={hit.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Document Clicked',
|
||||
index: 'documentation',
|
||||
objectIDs: [hit.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: hit.__queryID,
|
||||
positions: [hit.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={secondaryText}
|
||||
/>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
)})
|
||||
}
|
||||
</List>
|
||||
{type === "documentation" ?
|
||||
<span style={{display: "flex", textAlign: "right", position: "absolute", right: 15, bottom: 10,}}>
|
||||
<Typography variant="body2" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
: null}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomAppHits = connectHits(AppHits)
|
||||
const CustomWorkflowHits = connectHits(WorkflowHits)
|
||||
const CustomDocHits = connectHits(DocHits)
|
||||
|
||||
return (
|
||||
<div ref={node} style={{width: "100%", maxWidth: 425, margin: "auto", position: "relative", }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
|
||||
console.log("CLICKED")
|
||||
}}>
|
||||
<Configure clickAnalytics />
|
||||
<CustomSearchBox />
|
||||
<Index indexName="appsearch">
|
||||
<CustomAppHits />
|
||||
</Index>
|
||||
<Index indexName="documentation">
|
||||
<CustomDocHits />
|
||||
</Index>
|
||||
<Index indexName="workflows">
|
||||
<CustomWorkflowHits />
|
||||
</Index>
|
||||
</InstantSearch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchField;
|
252
shuffle/frontend/src/components/SecurityFramework.jsx
Normal file
@ -0,0 +1,252 @@
|
||||
import React, {useState } from 'react';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import AppFramework, { usecases } from "../components/AppFramework.jsx";
|
||||
import {Link} from 'react-router-dom';
|
||||
import ReactGA from 'react-ga4';
|
||||
|
||||
import { Button, LinearProgress, Typography } from '@mui/material';
|
||||
|
||||
export const securityFramework = [
|
||||
{
|
||||
image: <path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" />,
|
||||
text: "Cases",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M6.93767 0C8.71083 0 10.4114 0.704386 11.6652 1.9582C12.919 3.21202 13.6234 4.91255 13.6234 6.68571C13.6234 8.34171 13.0165 9.864 12.0188 11.0366L12.2965 11.3143H13.1091L18.252 16.4571L16.7091 18L11.5662 12.8571V12.0446L11.2885 11.7669C10.116 12.7646 8.59367 13.3714 6.93767 13.3714C5.16451 13.3714 3.46397 12.667 2.21015 11.4132C0.956339 10.1594 0.251953 8.45888 0.251953 6.68571C0.251953 4.91255 0.956339 3.21202 2.21015 1.9582C3.46397 0.704386 5.16451 0 6.93767 0ZM6.93767 2.05714C4.36624 2.05714 2.3091 4.11429 2.3091 6.68571C2.3091 9.25714 4.36624 11.3143 6.93767 11.3143C9.5091 11.3143 11.5662 9.25714 11.5662 6.68571C11.5662 4.11429 9.5091 2.05714 6.93767 2.05714Z" />,
|
||||
text: "SIEM",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M11.223 10.971L3.85195 14.4L7.28095 7.029L14.652 3.6L11.223 10.971ZM9.25195 0C8.07006 0 6.89973 0.232792 5.8078 0.685084C4.71587 1.13738 3.72372 1.80031 2.88799 2.63604C1.20016 4.32387 0.251953 6.61305 0.251953 9C0.251953 11.3869 1.20016 13.6761 2.88799 15.364C3.72372 16.1997 4.71587 16.8626 5.8078 17.3149C6.89973 17.7672 8.07006 18 9.25195 18C11.6389 18 13.9281 17.0518 15.6159 15.364C17.3037 13.6761 18.252 11.3869 18.252 9C18.252 7.8181 18.0192 6.64778 17.5669 5.55585C17.1146 4.46392 16.4516 3.47177 15.6159 2.63604C14.7802 1.80031 13.788 1.13738 12.6961 0.685084C11.6042 0.232792 10.4338 0 9.25195 0ZM9.25195 8.01C8.98939 8.01 8.73758 8.1143 8.55192 8.29996C8.36626 8.48563 8.26195 8.73744 8.26195 9C8.26195 9.26256 8.36626 9.51437 8.55192 9.70004C8.73758 9.8857 8.98939 9.99 9.25195 9.99C9.51452 9.99 9.76633 9.8857 9.95199 9.70004C10.1376 9.51437 10.242 9.26256 10.242 9C10.242 8.73744 10.1376 8.48563 9.95199 8.29996C9.76633 8.1143 9.51452 8.01 9.25195 8.01Z" />,
|
||||
text: "Assets",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M13.3318 2.223C13.2598 2.223 13.1878 2.205 13.1248 2.169C11.3968 1.278 9.90284 0.9 8.11184 0.9C6.32984 0.9 4.63784 1.323 3.09884 2.169C2.88284 2.286 2.61284 2.205 2.48684 1.989C2.36984 1.773 2.45084 1.494 2.66684 1.377C4.34084 0.468 6.17684 0 8.11184 0C10.0288 0 11.7028 0.423 13.5388 1.368C13.7638 1.485 13.8448 1.755 13.7278 1.971C13.6468 2.133 13.4938 2.223 13.3318 2.223ZM0.452843 6.948C0.362843 6.948 0.272843 6.921 0.191843 6.867C-0.015157 6.723 -0.0601571 6.444 0.0838429 6.237C0.974843 4.977 2.10884 3.987 3.45884 3.294C6.28484 1.836 9.90284 1.827 12.7378 3.285C14.0878 3.978 15.2218 4.959 16.1128 6.21C16.2568 6.408 16.2118 6.696 16.0048 6.84C15.7978 6.984 15.5188 6.939 15.3748 6.732C14.5648 5.598 13.5388 4.707 12.3238 4.086C9.74084 2.763 6.43784 2.763 3.86384 4.095C2.63984 4.725 1.61384 5.625 0.803843 6.759C0.731843 6.885 0.596843 6.948 0.452843 6.948ZM6.07784 17.811C5.96084 17.811 5.84384 17.766 5.76284 17.676C4.97984 16.893 4.55684 16.389 3.95384 15.3C3.33284 14.193 3.00884 12.843 3.00884 11.394C3.00884 8.721 5.29484 6.543 8.10284 6.543C10.9108 6.543 13.1968 8.721 13.1968 11.394C13.1968 11.646 12.9988 11.844 12.7468 11.844C12.4948 11.844 12.2968 11.646 12.2968 11.394C12.2968 9.216 10.4158 7.443 8.10284 7.443C5.78984 7.443 3.90884 9.216 3.90884 11.394C3.90884 12.69 4.19684 13.887 4.74584 14.859C5.32184 15.894 5.71784 16.335 6.41084 17.037C6.58184 17.217 6.58184 17.496 6.41084 17.676C6.31184 17.766 6.19484 17.811 6.07784 17.811ZM12.5308 16.146C11.4598 16.146 10.5148 15.876 9.74084 15.345C8.39984 14.436 7.59884 12.96 7.59884 11.394C7.59884 11.142 7.79684 10.944 8.04884 10.944C8.30084 10.944 8.49884 11.142 8.49884 11.394C8.49884 12.663 9.14684 13.86 10.2448 14.598C10.8838 15.03 11.6308 15.237 12.5308 15.237C12.7468 15.237 13.1068 15.21 13.4668 15.147C13.7098 15.102 13.9438 15.264 13.9888 15.516C14.0338 15.759 13.8718 15.993 13.6198 16.038C13.1068 16.137 12.6568 16.146 12.5308 16.146ZM10.7218 18C10.6858 18 10.6408 17.991 10.6048 17.982C9.17384 17.586 8.23784 17.055 7.25684 16.092C5.99684 14.841 5.30384 13.176 5.30384 11.394C5.30384 9.936 6.54584 8.748 8.07584 8.748C9.60584 8.748 10.8478 9.936 10.8478 11.394C10.8478 12.357 11.6848 13.14 12.7198 13.14C13.7548 13.14 14.5918 12.357 14.5918 11.394C14.5918 8.001 11.6668 5.247 8.06684 5.247C5.51084 5.247 3.17084 6.669 2.11784 8.874C1.76684 9.603 1.58684 10.458 1.58684 11.394C1.58684 12.096 1.64984 13.203 2.18984 14.643C2.27984 14.877 2.16284 15.138 1.92884 15.219C1.69484 15.309 1.43384 15.183 1.35284 14.958C0.911843 13.779 0.695843 12.609 0.695843 11.394C0.695843 10.314 0.902843 9.333 1.30784 8.478C2.50484 5.967 5.15984 4.338 8.06684 4.338C12.1618 4.338 15.4918 7.497 15.4918 11.385C15.4918 12.843 14.2498 14.031 12.7198 14.031C11.1898 14.031 9.94784 12.843 9.94784 11.385C9.94784 10.422 9.11084 9.639 8.07584 9.639C7.04084 9.639 6.20384 10.422 6.20384 11.385C6.20384 12.924 6.79784 14.364 7.88684 15.444C8.74184 16.29 9.56084 16.758 10.8298 17.109C11.0728 17.172 11.2078 17.424 11.1448 17.658C11.0998 17.865 10.9108 18 10.7218 18Z" />,
|
||||
text: "IAM",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image: <path d="M16.1091 8.57143H14.8234V5.14286C14.8234 4.19143 14.052 3.42857 13.1091 3.42857H9.68052V2.14286C9.68052 1.57454 9.45476 1.02949 9.0529 0.627628C8.65103 0.225765 8.10599 0 7.53767 0C6.96935 0 6.4243 0.225765 6.02244 0.627628C5.62057 1.02949 5.39481 1.57454 5.39481 2.14286V3.42857H1.96624C1.51158 3.42857 1.07555 3.60918 0.754056 3.93067C0.432565 4.25216 0.251953 4.6882 0.251953 5.14286V8.4H1.53767C2.82338 8.4 3.85195 9.42857 3.85195 10.7143C3.85195 12 2.82338 13.0286 1.53767 13.0286H0.251953V16.2857C0.251953 16.7404 0.432565 17.1764 0.754056 17.4979C1.07555 17.8194 1.51158 18 1.96624 18H5.22338V16.7143C5.22338 15.4286 6.25195 14.4 7.53767 14.4C8.82338 14.4 9.85195 15.4286 9.85195 16.7143V18H13.1091C13.5638 18 13.9998 17.8194 14.3213 17.4979C14.6428 17.1764 14.8234 16.7404 14.8234 16.2857V12.8571H16.1091C16.6774 12.8571 17.2225 12.6314 17.6243 12.2295C18.0262 11.8277 18.252 11.2826 18.252 10.7143C18.252 10.146 18.0262 9.60092 17.6243 9.19906C17.2225 8.79719 16.6774 8.57143 16.1091 8.57143Z" />,
|
||||
text: "Intel",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M9.89516 7.71433H8.60945V5.1429H9.89516V7.71433ZM9.89516 10.2858H8.60945V9.00004H9.89516V10.2858ZM14.3952 2.57147H4.10944C3.76845 2.57147 3.44143 2.70693 3.20031 2.94805C2.95919 3.18917 2.82373 3.51619 2.82373 3.85719V15.4286L5.39516 12.8572H14.3952C14.7362 12.8572 15.0632 12.7217 15.3043 12.4806C15.5454 12.2395 15.6809 11.9125 15.6809 11.5715V3.85719C15.6809 3.14361 15.1023 2.57147 14.3952 2.57147Z" />,
|
||||
text: "Comms",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M0.251953 10.6011H3.8391L9.38052 -4.92572e-08L10.8977 11.5696L15.0377 6.28838L19.3191 10.6011H23.3948V13.1836H18.252L15.2562 10.175L9.1491 18L7.88909 8.41894L5.39481 13.1836H0.251953V10.6011Z" />,
|
||||
text: "Network",
|
||||
description: "Case management"
|
||||
},
|
||||
{
|
||||
image:
|
||||
<path d="M19.1722 8.9957L17.0737 6.60487L17.3661 3.44004L14.2615 2.73483L12.6361 -3.28068e-08L9.71206 1.25561L6.78803 -3.28068e-08L5.16261 2.73483L2.05797 3.43144L2.35038 6.59627L0.251953 8.9957L2.35038 11.3865L2.05797 14.56L5.16261 15.2652L6.78803 18L9.71206 16.7358L12.6361 17.9914L14.2615 15.2566L17.3661 14.5514L17.0737 11.3865L19.1722 8.9957ZM10.5721 13.2957H8.85205V11.5757H10.5721V13.2957ZM10.5721 9.85571H8.85205V4.69565H10.5721V9.85571Z" />,
|
||||
text: "EDR & AV",
|
||||
description: "Case management"
|
||||
},
|
||||
]
|
||||
|
||||
const LandingpageUsecases = (props) => {
|
||||
const { userdata } = props
|
||||
|
||||
const [selectedUsecase, setSelectedUsecase] = useState("Phishing")
|
||||
const usecasekeys = usecases === undefined || usecases === null ? [] : Object.keys(usecases)
|
||||
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"
|
||||
const buttonStyle = {borderRadius: 25, height: 50, width: 260, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18, backgroundImage: buttonBackground}
|
||||
|
||||
const HandleTitle = (props) => {
|
||||
const { usecases, selectedUsecase, setSelecedUsecase } = props
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setProgress((oldProgress) => {
|
||||
if (oldProgress >= 105) {
|
||||
const foundIndex = usecasekeys.findIndex(key => key === selectedUsecase)
|
||||
var newitem = usecasekeys[foundIndex+1]
|
||||
if (newitem === undefined || newitem === 0) {
|
||||
newitem = usecasekeys[1]
|
||||
}
|
||||
|
||||
setSelectedUsecase(newitem)
|
||||
return -18
|
||||
}
|
||||
|
||||
if (oldProgress >= 65) {
|
||||
return oldProgress + 3
|
||||
}
|
||||
|
||||
if (oldProgress >= 80) {
|
||||
return oldProgress + 1
|
||||
}
|
||||
|
||||
return oldProgress + 6
|
||||
})
|
||||
}, 165)
|
||||
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (usecases === null || usecases === undefined || usecases.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const modifier = isMobile ? 17 : 22
|
||||
return (
|
||||
<span style={{margin: "auto", textAlign: isMobile ? "center" : "left", width: isMobile ? 280 : "100%",}}>
|
||||
<b>Handle <br/>
|
||||
<span style={{marginBottom: 10}}>
|
||||
<i id="usecase-text">{selectedUsecase}</i>
|
||||
<LinearProgress variant="determinate" value={progress} style={{marginTop: 0, marginBottom: 0, height: 3, width: isMobile ? "100%" : selectedUsecase.length*modifier, borderRadius: 10, }} />
|
||||
</span>
|
||||
with confidence</b>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const parsedWidth = isMobile ? "100%" : 1100
|
||||
return (
|
||||
<div style={{width: isMobile ? null : parsedWidth, margin: isMobile ? "0px 0px 0px 0px" : "auto", color: "white", textAlign: isMobile ? "center" : "left",}}>
|
||||
<div style={{display: "flex", position: "relative",}}>
|
||||
<div style={{maxWidth: isMobile ? "100%" : 420, paddingTop: isMobile ? 0 : 120, zIndex: 1000, margin: "auto",}}>
|
||||
|
||||
<Typography variant="h1" style={{margin: "auto", width: isMobile ? 280 : "100%", marginTop: isMobile ? 50 : 0}}>
|
||||
<HandleTitle usecases={usecases} selectedUsecase={selectedUsecase} setSelectedUsecase={setSelectedUsecase} />
|
||||
|
||||
{/*<b>Security Automation <i>is Hard</i></b>*/}
|
||||
</Typography>
|
||||
<Typography variant="h6" style={{marginTop: isMobile ? 15 : 0,}}>
|
||||
Connecting your everchanging environment is hard. We get it! That's why we built Shuffle, where you can use and share your security workflows to everyones benefit.
|
||||
{/*Shuffle is an automation platform where you don't need to be an expert to automate. Get access to our large pool of security playbooks, apps and people.*/}
|
||||
</Typography>
|
||||
<div style={{display: "flex", textAlign: "center", itemAlign: "center",}}>
|
||||
{isMobile ? null :
|
||||
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_pricing",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground, marginRight: 10,
|
||||
}}>
|
||||
See Pricing
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
{isMobile ? null :
|
||||
<Link rel="noopener noreferrer" to={"/register?message=You'll need to sign up first. No name, company or credit card required."} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_try_it_out",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
borderRadius: 25, height: 40, width: 175, margin: "15px 0px 15px 0px", fontSize: 14, color: "white", backgroundImage: buttonBackground,
|
||||
}}>
|
||||
Start for free
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{isMobile ? null :
|
||||
<div style={{marginLeft: 200, marginTop: 125, zIndex: 1000}}>
|
||||
<AppFramework
|
||||
userdata={userdata}
|
||||
showOptions={false}
|
||||
selectedOption={selectedUsecase}
|
||||
rolling={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{isMobile ? null :
|
||||
<div style={{position: "absolute", top: 50, right: -200, zIndex: 0, }}>
|
||||
<svg width="351" height="433" viewBox="0 0 351 433" fill="none" xmlns="http://www.w3.org/2000/svg" style={{zIndex: 0, }}>
|
||||
<path d="M167.781 184.839C167.781 235.244 208.625 276.104 259.03 276.104C309.421 276.104 350.28 235.244 350.28 184.839C350.28 134.448 309.421 93.5892 259.03 93.5892C208.625 93.5741 167.781 134.433 167.781 184.839ZM330.387 184.839C330.387 224.263 298.439 256.195 259.03 256.195C219.621 256.195 187.674 224.248 187.674 184.839C187.674 145.43 219.636 113.483 259.03 113.483C298.439 113.483 330.387 145.43 330.387 184.839Z" fill="white" fill-opacity="0.2"/>
|
||||
<path d="M167.781 387.368C167.781 412.578 188.203 433 213.398 433C238.593 433 259.03 412.578 259.03 387.368C259.03 362.157 238.608 341.735 213.398 341.735C188.187 341.735 167.781 362.172 167.781 387.368ZM249.076 387.368C249.076 407.08 233.095 423.046 213.398 423.046C193.686 423.046 177.72 407.065 177.72 387.368C177.72 367.671 193.686 351.69 213.398 351.69C233.095 351.705 249.076 367.671 249.076 387.368Z" fill="white" fill-opacity="0.2"/>
|
||||
<path d="M56.8637 0.738726C25.7052 0.738724 0.44632 25.9976 0.446317 57.1561C0.446314 88.3146 25.7052 113.573 56.8637 113.573C88.0221 113.573 113.281 88.3146 113.281 57.1561C113.281 25.9977 88.0222 0.738729 56.8637 0.738726Z" fill="white" fill-opacity="0.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div style={{display: "flex", width: isMobile ? "100%" : 300, itemAlign: "center", margin: "auto", marginTop: 20, flexDirection: isMobile ? "column" : "row", textAlign: "center",}}>
|
||||
{isMobile ?
|
||||
<Link rel="noopener noreferrer" to={"/pricing"} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant={isMobile ? "contained" : "outlined"}
|
||||
color={isMobile ? "primary" : "secondary"}
|
||||
style={buttonStyle}
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_pricing",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
>
|
||||
See pricing
|
||||
</Button>
|
||||
</Link>
|
||||
: null
|
||||
}
|
||||
{/*isMobile ?
|
||||
<Link rel="noopener noreferrer" to={"/docs/features"} style={{textDecoration: "none"}}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
ReactGA.event({
|
||||
category: "landingpage",
|
||||
action: "click_main_features",
|
||||
label: "",
|
||||
})
|
||||
}}
|
||||
color="secondary"
|
||||
style={buttonStyle}>
|
||||
Features
|
||||
</Button>
|
||||
</Link>
|
||||
: null*/}
|
||||
</div>
|
||||
{isMobile ? null :
|
||||
<div style={{display: "flex", width: parsedWidth, margin: "auto", marginTop: 150}}>
|
||||
{securityFramework.map((data, index) => {
|
||||
return (
|
||||
<div key={index} style={{flex: 1, textAlign: "center",}}>
|
||||
<span style={{margin: "auto", width: 25,}}>
|
||||
<svg width="25" height="25" fill="white" xmlns="http://www.w3.org/2000/svg" >
|
||||
{data.image}
|
||||
</svg>
|
||||
</span>
|
||||
<Typography variant="body2" style={{color: "white", marginRight: 5}}>
|
||||
{data.text}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LandingpageUsecases;
|
1745
shuffle/frontend/src/components/ShuffleCodeEditor.jsx
Normal file
233
shuffle/frontend/src/components/SuggestedWorkflows.jsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactGA from 'react-ga4';
|
||||
import theme from '../theme.jsx';
|
||||
import PaperComponent from "../components/PaperComponent.jsx"
|
||||
import UsecaseSearch, { usecaseTypes } from "../components/UsecaseSearch.jsx"
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
Badge,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Delete as DeleteIcon,
|
||||
AutoFixHigh as AutoFixHighIcon,
|
||||
Done as DoneIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const SuggestedWorkflows = (props) => {
|
||||
const { globalUrl, userdata, usecaseSuggestions, frameworkData, setUsecaseSuggestions, inputSearch, apps, } = props
|
||||
|
||||
const [usecaseSearch, setUsecaseSearch] = React.useState("")
|
||||
const [usecaseSearchType, setUsecaseSearchType] = React.useState("")
|
||||
const [finishedUsecases, setFinishedUsecases] = React.useState([])
|
||||
const [previousUsecase, setPreviousUsecase] = React.useState("")
|
||||
const [closeWindow, setCloseWindow] = React.useState(false)
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (closeWindow === true) {
|
||||
console.log("WINDOW CLOSED")
|
||||
finishedUsecases.push(usecaseSearch)
|
||||
setFinishedUsecases(finishedUsecases)
|
||||
|
||||
setCloseWindow(false)
|
||||
}
|
||||
}, [closeWindow])
|
||||
|
||||
if (usecaseSuggestions === undefined || usecaseSuggestions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (inputSearch !== previousUsecase) {
|
||||
setPreviousUsecase(inputSearch)
|
||||
setFinishedUsecases([])
|
||||
}
|
||||
|
||||
if (finishedUsecases.length === usecaseSuggestions.length) {
|
||||
console.log("Closing finished usecases 2")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
//useEffect(() => {
|
||||
// //if (defaultSearch ===
|
||||
// //setFinishedUsecases(finishedUsecases)
|
||||
// console.log("Finished default usecase?", usecaseSearch)
|
||||
//}, [usecaseSearch])
|
||||
|
||||
const foundZindex = usecaseSearch.length > 0 && usecaseSearchType.length > 0 ? -1 : 12500
|
||||
|
||||
const IndividualUsecase = (props) => {
|
||||
const { usecase, index } = props
|
||||
const [hovering, setHovering] = React.useState(false)
|
||||
|
||||
const usecasename = usecase.name
|
||||
const bordercolor = usecase.color !== undefined ? usecase.color : "rgba(255,255,255,0.3)"
|
||||
|
||||
|
||||
const srcimage = usecase.items[0].app
|
||||
var dstimage = usecase.items[1].app
|
||||
if (usecase.items.length > 2) {
|
||||
dstimage = usecase.items[2].app
|
||||
}
|
||||
|
||||
if (srcimage === undefined || dstimage === undefined) {
|
||||
console.log("Error in src or dst: returning!")
|
||||
return null
|
||||
}
|
||||
|
||||
const finished = finishedUsecases.includes(usecasename)
|
||||
const selectedIcon = finished ? <DoneIcon /> : <AutoFixHighIcon />
|
||||
|
||||
if (finished) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Simple visual of the usecase
|
||||
return (
|
||||
<Tooltip
|
||||
title={`Try usecase "${usecasename}"`}
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<div key={index} style={{cursor: finished ? "auto" : "pointer", marginTop: 10, padding: 10, borderRadius: theme.palette.borderRadius, border: `1px solid ${bordercolor}`, display: "flex", backgroundColor: hovering === true ? theme.palette.inputColor : theme.palette.surfaceColor, }} onMouseOver={() => {
|
||||
setHovering(true)
|
||||
}} onMouseOut={() => {
|
||||
setHovering(false)
|
||||
}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_suggested_workflow",
|
||||
label: usecasename,
|
||||
})
|
||||
}
|
||||
|
||||
console.log("Try usecase ", usecasename)
|
||||
setUsecaseSearchType(usecase.type)
|
||||
setUsecaseSearch(usecasename)
|
||||
|
||||
}}>
|
||||
<div style={{flex: 10}}>
|
||||
<Typography variant="body2">
|
||||
{usecasename}
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginTop: 5, }}>
|
||||
<img alt={srcimage.large_image} src={srcimage.large_image} style={{borderRadius: 20, height: 30, width: 30, marginRight: 15, }}/>
|
||||
<img alt={dstimage.large_image} src={dstimage.large_image} style={{borderRadius: 20, height: 30, width: 30, }}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style={{flex: 1}}>
|
||||
{selectedIcon}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
//<Paper style={{width: 275, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: 50, }}>
|
||||
return (
|
||||
<Paper style={{margin: "auto", position: "relative", backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette.borderRadius, zIndex: foundZindex, border: "1px solid rgba(255,255,255,0.2)", top: 100, left: 85,}}>
|
||||
<Dialog
|
||||
open={usecaseSearch.length > 0 && usecaseSearchType.length > 0}
|
||||
onClose={() => {
|
||||
finishedUsecases.push(usecaseSearch)
|
||||
setFinishedUsecases(finishedUsecases)
|
||||
|
||||
setUsecaseSearch("")
|
||||
setUsecaseSearchType("")
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
minWidth: 450,
|
||||
padding: 50,
|
||||
overflow: "hidden",
|
||||
zIndex: 10050,
|
||||
border: theme.palette.defaultBorder,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 14,
|
||||
right: 18,
|
||||
color: "grey",
|
||||
}}
|
||||
onClick={() => {
|
||||
finishedUsecases.push(usecaseSearch)
|
||||
setFinishedUsecases(finishedUsecases)
|
||||
|
||||
setUsecaseSearch("")
|
||||
setUsecaseSearchType("")
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={usecaseSearchType}
|
||||
usecaseSearch={usecaseSearch}
|
||||
appFramework={frameworkData}
|
||||
userdata={userdata}
|
||||
autotry={true}
|
||||
setCloseWindow={setCloseWindow}
|
||||
setUsecaseSearch={setUsecaseSearch}
|
||||
apps={apps}
|
||||
/>
|
||||
</Dialog>
|
||||
<div style={{minWidth: 250, maxWidth: 250, padding: 15, borderRadius: theme.palette.borderRadius, position: "relative", }}>
|
||||
<Typography variant="body1" style={{textAlign: "center"}}>
|
||||
Suggested Workflows ({finishedUsecases.length}/{usecaseSuggestions.length})
|
||||
</Typography>
|
||||
<IconButton
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
color: "grey",
|
||||
padding: 2,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (setUsecaseSuggestions !== undefined) {
|
||||
setUsecaseSuggestions([])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{height: 18, width: 18, }} />
|
||||
</IconButton>
|
||||
{usecaseSuggestions.map((usecase, index) => {
|
||||
|
||||
return (
|
||||
<IndividualUsecase
|
||||
key={index}
|
||||
usecase={usecase}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
|
||||
})}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuggestedWorkflows;
|