initial commit

This commit is contained in:
braginini
2021-09-03 15:00:54 +02:00
commit 99fa0fb4ac
53 changed files with 39872 additions and 0 deletions

48
.github/workflows/build_and_push.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: build and push
on:
push:
branches:
- main
tags:
- "**"
jobs:
build_n_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: setup-node
uses: actions/setup-node@v2
with:
node-version: '16'
cache: 'npm'
- name: Install dependecies
run: npm install
- name: Build
# skiping fail on warning for now
run: CI=false npm run build
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: wiretrustee/dashboard
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_TOKEN }}
-
name: Docker build and push
uses: docker/build-push-action@v2
with:
context: .
file: docker/Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
src/auth_config.json
.idea
.eslintcache

70
README.md Normal file
View File

@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
Use the [Docker documentation](docker/README.md).
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

7
craco.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
style: {
postcss: {
plugins: [require('tailwindcss'), require('autoprefixer')]
}
}
};

24
docker/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM alpine:3.14
RUN apk add --no-cache bash curl less ca-certificates git tzdata zip gettext \
nginx curl supervisor certbot-nginx && \
rm -rf /var/cache/apk/* && mkdir -p /run/nginx
STOPSIGNAL SIGINT
EXPOSE 80
EXPOSE 443
ENTRYPOINT ["/usr/bin/supervisord","-c","/etc/supervisord.conf"]
WORKDIR /usr/share/nginx/html
# copy configuration files
COPY docker/default.conf /etc/nginx/http.d/default.conf
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/init_cert.sh /usr/local/init_cert.sh
COPY docker/init_react_envs.sh /usr/local/init_react_envs.sh
RUN chmod +x /usr/local/init_cert.sh && rm /etc/crontabs/root
RUN chmod +x /usr/local/init_react_envs.sh
# configure supervisor
COPY docker/supervisord.conf /etc/supervisord.conf
# copy build files
COPY build/ /usr/share/nginx/html/

23
docker/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Wiretrustee Dashboard
Wiretrustee Dashboard is a the Wiretrustee Managemenet server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
## Tags
```latest``` ```vX.X.X``` not available yet.
```main``` builded on every PR being merged to the repository
## How to use this image
HTTP run:
```shell
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
```
Using SSL certificate from Let's Encript®:
```shell
docker run -d --rm -p 80:80 -p 443:443 \
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
-e LETSENCRYPT_EMAIL=hello@mydomain.com \
wiretrustee/dashboard:main
```
> For SSL generation, you need to run this image in a server with proper public IP and a domain name pointing to it.
## Environment variables
* ```NGINX_SSL_PORT``` Changes the port that Nginx listens to. Defaults to ```443```
* ```LETSENCRYPT_DOMAIN``` Enables Certbot`s client execution for the specified domain. Defaults to ```none```
* ```LETSENCRYPT_EMAIL``` Email used in Certbot`s client execution to register the certificate request. Defaults to ```example@local```

16
docker/default.conf Normal file
View File

@@ -0,0 +1,16 @@
# simple server configuration to replace nginx's default
server {
listen 80 default_server;
listen [::]:80 default_server;
root /usr/share/nginx/html;
location / {
try_files $uri /index.html;
}
# You may need this to prevent return 404 recursion.
location = /404.html {
internal;
}
}

22
docker/init_cert.sh Normal file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -ex
LETSENCRYPT_DOMAIN=${LETSENCRYPT_DOMAIN:-"none"}
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL:-"example@local"}
NGINX_SSL_PORT=${NGINX_SSL_PORT:-443}
# If no domain is provided, skip certbot execution and configuration
if [ "${LETSENCRYPT_DOMAIN}-x" == "none-x" ]; then
exit 0
fi
# Request a certificate
# this also updates the nginx config file with new SSL entries
certbot -n --nginx --agree-tos --email ${LETSENCRYPT_EMAIL} -d ${LETSENCRYPT_DOMAIN} --https-port ${NGINX_SSL_PORT}
# Add cron job file
cat <<EOF >/etc/crontabs/certbot
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 */12 * * * root certbot -q renew --nginx --https-port ${NGINX_SSL_PORT}
EOF
# start cron daemon
supervisorctl start cron

37
docker/init_react_envs.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -e
if [[ -z "${AUTH0_DOMAIN}" ]]; then
echo "AUTH0_DOMAIN environment variable must be set"
exit 1
fi
if [[ -z "${AUTH0_CLIENT_ID}" ]]; then
echo "AUTH0_CLIENT_ID environment variable must be set"
exit 1
fi
if [[ -z "${AUTH0_AUDIENCE}" ]]; then
echo "AUTH0_AUDIENCE environment variable must be set"
exit 1
fi
if [[ -z "${WIRETRUSTEE_MGMT_API_ENDPOINT}" ]]; then
echo "WIRETRUSTEE_MGMT_API_ENDPOINT environment variable must be set"
exit 1
fi
AUTH0_DOMAIN=${AUTH0_DOMAIN}
AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
WIRETRUSTEE_MGMT_API_ENDPOINT=${WIRETRUSTEE_MGMT_API_ENDPOINT}
# replace ENVs in the config
ENV_STR="\$\$AUTH0_DOMAIN \$\$AUTH0_CLIENT_ID \$\$AUTH0_AUDIENCE \$\$WIRETRUSTEE_MGMT_API_ENDPOINT"
MAIN_JS=$(find /usr/share/nginx/html/static/js/main.*js)
cp "$MAIN_JS" "$MAIN_JS".copy
envsubst "$ENV_STR" < "$MAIN_JS".copy > "$MAIN_JS"
rm "$MAIN_JS".copy

142
docker/nginx.conf Normal file
View File

@@ -0,0 +1,142 @@
# /etc/nginx/nginx.conf
daemon off;
user nginx;
# Set number of worker processes automatically based on number of CPU cores.
worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Configures default error logger.
error_log /proc/self/fd/2 warn;
# Includes files with directives to load dynamic modules.
include /etc/nginx/modules/*.conf;
# Uncomment to include files with config snippets into the root context.
# NOTE: This will be enabled by default in Alpine 3.15.
#include /etc/nginx/conf.d/*.conf;
events {
# The maximum number of simultaneous connections that can be opened by
# a worker process.
worker_connections 1024;
multi_accept on;
}
http {
# Includes mapping of file name extensions to MIME types of responses
# and defines the default type.
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Name servers used to resolve names of upstream servers into addresses.
# It's also needed when using tcpsocket and udpsocket in Lua modules.
#resolver 1.1.1.1 1.0.0.1 2606:4700:4700::1111 2606:4700:4700::1001;
# Don't tell nginx version to the clients. Default is 'on'.
server_tokens off;
# Specifies the maximum accepted body size of a client request, as
# indicated by the request header Content-Length. If the stated content
# length is greater than this size, then the client receives the HTTP
# error code 413. Set to 0 to disable. Default is '1m'.
client_max_body_size 1m;
# Sendfile copies data between one FD and other from within the kernel,
# which is more efficient than read() + write(). Default is off.
sendfile on;
# Causes nginx to attempt to send its HTTP response head in one packet,
# instead of using partial frames. Default is 'off'.
tcp_nopush on;
# Enables the specified protocols. Default is TLSv1 TLSv1.1 TLSv1.2.
# TIP: If you're not obligated to support ancient clients, remove TLSv1.1.
ssl_protocols TLSv1.2 TLSv1.3;
# Path of the file with Diffie-Hellman parameters for EDH ciphers.
# TIP: Generate with: `openssl dhparam -out /etc/ssl/nginx/dh2048.pem 2048`
#ssl_dhparam /etc/ssl/nginx/dh2048.pem;
# Specifies that our cipher suits should be preferred over client ciphers.
# Default is 'off'.
ssl_prefer_server_ciphers on;
# Enables a shared SSL cache with size that can hold around 8000 sessions.
# Default is 'none'.
ssl_session_cache shared:SSL:2m;
# Specifies a time during which a client may reuse the session parameters.
# Default is '5m'.
ssl_session_timeout 1h;
# Disable TLS session tickets (they are insecure). Default is 'on'.
ssl_session_tickets off;
# Enable gzipping of responses.
gzip on;
# Set the Vary HTTP header as defined in the RFC 2616. Default is 'off'.
gzip_vary on;
# General gzip configurations
gzip_proxied any;
gzip_comp_level 9;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_disable "msie6";
# Compress the following MIME types.
gzip_types
application/atom+xml
application/javascript
application/x-javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/javascript
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# Helper variable for proxying websockets.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Specifies the main log format.
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Sets the path, format, and configuration for a buffered log write.
access_log /proc/self/fd/1 main;
# Includes virtual hosts configs.
include /etc/nginx/http.d/*.conf;
}
# TIP: Uncomment if you use stream module.
#include /etc/nginx/stream.conf;

67
docker/supervisord.conf Normal file
View File

@@ -0,0 +1,67 @@
[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700
username = dummy
password = dummy
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock
username = dummy
password = dummy
[supervisord]
nodaemon=true
user=root
pidfile=/run/supervisord.pid
logfile=/proc/self/fd/1
logfile_maxbytes=0
loglevel=warn
[program:cron]
command=crond -f
user=root
priority=101
numprocs=1
autostart=0
autorestart=true
stdout_logfile=/proc/self/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx
user=root
priority=100
numprocs=1
autostart=1
autorestart=true
stdout_logfile=/proc/self/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_maxbytes=0
[program:init_cert]
command=/usr/local/init_cert.sh
user=root
[program:init_react_envs]
command=/usr/local/init_react_envs.sh
user=root
numprocs=1
autostart=1
autorestart=false
startretries=1
priority=200
stdout_logfile=/proc/self/fd/1
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_maxbytes=0

37112
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "wiretrustee-dashboard",
"version": "0.1.0",
"private": true,
"dependencies": {
"@auth0/auth0-react": "^1.6.0",
"@craco/craco": "^6.0.0",
"@headlessui/react": "^1.4.0",
"@tailwindcss/postcss7-compat": "^2.0.2",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"autoprefixer": "^9",
"highlight.js": "^11.2.0",
"history": "^5.0.1",
"postcss": "^7",
"prop-types": "^15.7.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.1",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"web-vitals": "^0.2.4"
},
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@heroicons/react": "^1.0.4"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

42
public/index.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Wiretrustee</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

15
public/manifest.json Normal file
View File

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

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

86
src/App.js Normal file
View File

@@ -0,0 +1,86 @@
import React, {useEffect, useState} from 'react';
import Navbar from './components/Navbar';
import {Redirect, Route, Switch} from 'react-router-dom';
import Peers from './views/Peers';
import Footer from './components/Footer';
import {useAuth0} from "@auth0/auth0-react";
import Loading from "./components/Loading";
import SetupKeys from "./views/SetupKeys";
import AddPeer from "./views/AddPeer";
import AccessControl from "./views/AccessControl";
import Activity from "./views/Activity";
function App() {
const {
isLoading,
isAuthenticated,
loginWithRedirect,
error
} = useAuth0();
const [isOpen, setIsOpen] = useState(false);
const toggle = () => {
setIsOpen(!isOpen);
};
useEffect(() => {
const hideMenu = () => {
if (window.innerWidth > 768 && isOpen) {
setIsOpen(false);
console.log('i resized');
}
};
window.addEventListener('resize', hideMenu);
return () => {
window.removeEventListener('resize', hideMenu);
};
});
if (error) {
return <div>Oops... {error.message}</div>;
}
if (isLoading) {
return <Loading/>;
}
if (!isAuthenticated) {
loginWithRedirect({})
}
return (
isAuthenticated && (
<>
{/*<div className='h-screen flex justify-center items-center bg-green-400'>*/}
<Navbar toggle={toggle}/>
<div className="min-h-screen bg-white">
<Switch>
<Route
exact
path="/"
render={() => {
return (
<Redirect to="/peers"/>
)
}}
/>
<Route path='/peers' exact component={Peers}/>
<Route path="/add-peer" component={AddPeer}/>
<Route path="/setup-keys" component={SetupKeys}/>
<Route path="/acls" component={AccessControl}/>
<Route path="/activity" component={Activity}/>
</Switch>
</div>
<Footer/>
</>
)
);
}
export default App;

8
src/App.test.js Normal file
View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

55
src/api/ManagementAPI.js Normal file
View File

@@ -0,0 +1,55 @@
import {getConfig} from "../config";
const {apiOrigin} = getConfig();
export const callApi = async (method, headers, body, getAccessTokenSilently, endpoint) => {
const token = await getAccessTokenSilently();
if (!headers) {
headers = {}
}
headers.Authorization = `Bearer ${token}`
const requestOptions = {
method: method,
headers: headers,
body: body
};
const response = await fetch(`${apiOrigin}${endpoint}`, requestOptions);
return await response.json();
};
export const getSetupKeys = async (getAccessTokenSilently) => {
return callApi("GET", {}, null, getAccessTokenSilently, "/api/setup-keys")
}
export const revokeSetupKey = async (getAccessTokenSilently, keyId) => {
return callApi(
"PUT",
{'Content-Type': 'application/json'},
JSON.stringify({Revoked: true}),
getAccessTokenSilently,
"/api/setup-keys/" + keyId)
}
export const renameSetupKey = async (getAccessTokenSilently, keyId, newName) => {
return callApi(
"PUT",
{'Content-Type': 'application/json'},
JSON.stringify({Name: newName}),
getAccessTokenSilently,
"/api/setup-keys/" + keyId)
}
export const getPeers = async (getAccessTokenSilently) => {
return callApi("GET", {}, null, getAccessTokenSilently, "/api/peers")
}
export const deletePeer = async (getAccessTokenSilently, peerId) => {
return callApi(
"DELETE",
{},
null,
getAccessTokenSilently,
"/api/peers/" + peerId)
}

52
src/assets/bars.svg Normal file
View File

@@ -0,0 +1,52 @@
<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill-opacity="0.8">
<rect y="10" width="15" height="120" rx="6" fill="#4D4D4D">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="30" y="10" width="15" height="120" rx="6" fill="#4D4D4D">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="60" width="15" height="140" rx="6" fill="#FF6600">
<animate attributeName="height"
begin="0s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="90" y="10" width="15" height="120" rx="6" fill="#C8BEB7">
<animate attributeName="height"
begin="0.25s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.25s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
<rect x="120" y="10" width="15" height="120" rx="6" fill="#C8BEB7">
<animate attributeName="height"
begin="0.5s" dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="y"
begin="0.5s" dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
repeatCount="indefinite" />
</rect>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
src/assets/logo-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

28
src/components/Content.js Normal file
View File

@@ -0,0 +1,28 @@
import React from 'react';
import ImageOne from '../images/egg.jpg';
import ImageTwo from '../images/egg-2.jpg';
const Content = () => {
return (
<>
<div className='menu-card'>
<img src={ImageOne} alt='egg' className='h-full rounded mb-20 shadow' />
<div className='center-content'>
<h2 className='text-2xl mb-2'>Egg Muffins</h2>
<p className='mb-2'>Cripsy, delicious, and nutritious</p>
<span>$16</span>
</div>
</div>
<div className='menu-card'>
<img src={ImageTwo} alt='egg' className='h-full rounded mb-20 shadow' />
<div className='center-content'>
<h2 className='text-2xl mb-2'>Egg Salad</h2>
<p className='mb-2'>Cripsy, delicious, and nutritious</p>
<span>$18</span>
</div>
</div>
</>
);
};
export default Content;

View File

@@ -0,0 +1,39 @@
import React from 'react';
const CopyButton = ({idPrefix, toCopy}) => {
const copyIconId = idPrefix + "copy"
const copySuccessIconId = idPrefix + "copy-success"
const classHidden = "hidden"
const handleKeyCopy = () => {
navigator.clipboard.writeText(toCopy)
let copyIcon = document.getElementById(copyIconId);
let copySuccessIcon = document.getElementById(copySuccessIconId);
copyIcon.classList.add(classHidden);
copySuccessIcon.classList.remove(classHidden);
setTimeout(function() {
copySuccessIcon.classList.add(classHidden);
copyIcon.classList.remove(classHidden);
}, 1200);
}
return (
<button
onClick={handleKeyCopy}
className="whitespace-nowrap font-medium text-gray-500 hover:text-gray-400">
<svg id={copyIconId} xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"/>
</svg>
<svg id={copySuccessIconId} xmlns="http://www.w3.org/2000/svg" className="hidden h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7"/>
</svg>
</button>
)
}
export default CopyButton;

View File

@@ -0,0 +1,36 @@
import React from 'react';
const CopyButton = ({idPrefix, text}) => {
const copyIconId = idPrefix + "copy"
const copySuccessIconId = idPrefix + "copy-success"
const classHidden = "hidden"
const handleKeyCopy = () => {
navigator.clipboard.writeText(text)
let copyIcon = document.getElementById(copyIconId);
let copySuccessIcon = document.getElementById(copySuccessIconId);
copyIcon.classList.add(classHidden);
copySuccessIcon.classList.remove(classHidden);
setTimeout(function() {
copySuccessIcon.classList.add(classHidden);
copyIcon.classList.remove(classHidden);
}, 1200);
}
return (
<button
onClick={handleKeyCopy}
className="whitespace-nowrap font-medium text-gray-500 hover:text-gray-400">
<div id={copyIconId}>
{text}
</div>
<div id={copySuccessIconId} className="flex flex-row hidden text-green-500">
Copied!
</div>
</button>
)
}
export default CopyButton;

View File

@@ -0,0 +1,103 @@
import {Fragment, useEffect, useRef, useState} from 'react'
import {Dialog, Transition} from '@headlessui/react'
import {ExclamationIcon} from '@heroicons/react/outline'
import PropTypes from "prop-types";
const DeleteDialog = ({show, text, title, confirmCallback}) => {
const [open, setOpen] = useState(show)
const cancelButtonRef = useRef(null)
useEffect(() => {
setOpen(show)
}, [show]);
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef}
onClose={() =>confirmCallback(false)}>
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"/>
</Transition.Child>
{/* This element is to trick the browser into centering the modal contents. */}
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
&#8203;
</span>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
className="inline-block align-bottom bg-white squared-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div
className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 squared-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationIcon className="h-6 w-6 text-red-600" aria-hidden="true"/>
</div>
<div className="font-mono mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<Dialog.Title as="h3" className="text-m leading-6 font-medium text-gray-900">
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm font-mono text-gray-500">
{text}
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
type="button"
className="font-mono w-full inline-flex justify-center squared-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
confirmCallback(true)
}}
>
Confirm
</button>
<button
type="button"
className="font-mono mt-3 w-full inline-flex justify-center squared-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
onClick={() => {
confirmCallback(false)
}}
ref={cancelButtonRef}
>
Cancel
</button>
</div>
</div>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
)
}
DeleteDialog.propTypes = {
show: PropTypes.bool,
confirmCallback: PropTypes.func,
text: PropTypes.string
};
DeleteDialog.defaultProps = {};
export default DeleteDialog;

View File

@@ -0,0 +1,56 @@
import React, {Fragment} from 'react';
import {Menu, Transition} from "@headlessui/react";
import {Link} from "react-router-dom";
import {classNames} from "../utils/common";
const EditButton = ({items, handler}) => {
const handleAction = (action) => {
handler(action)
}
return (
<Menu as="div">
<div>
<Menu.Button
className="whitespace-nowrap font-medium text-gray-500 hover:text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z"/>
</svg>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-100 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute mt-2 squared-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
{items.map((item, idx) => (
<Menu.Item key={item.name}>
{({active}) => (
<Link
to="#"
className={classNames(active ? 'bg-gray-100' : 'font-mono', 'block px-4 py-2 text-sm text-gray-700 font-mono')}
onClick={() => handleAction(item.name)}
>
{item.name}
</Link>
)}
</Menu.Item>
))}
</Menu.Items>
</Transition>
</Menu>
)
}
export default EditButton;

View File

@@ -0,0 +1,28 @@
import {Link} from 'react-router-dom'
export default function EmptyPeersPanel() {
return (
<Link
as="button"
to="/add-peer"
className="relative block w-full border-2 border-gray-300 border-dashed squared-lg p-12 text-center hover:border-gray-400 focus:outline-none"
>
<svg
className="mx-auto h-12 w-12 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
strokeLinecap="square"
strokeLinejoin="square"
strokeWidth={2}
d="M8 14v20c0 4.418 7.163 8 16 8 1.381 0 2.721-.087 4-.252M8 14c0 4.418 7.163 8 16 8s16-3.582 16-8M8 14c0-4.418 7.163-8 16-8s16 3.582 16 8m0 0v14m0-4c0 4.418-7.163 8-16 8S8 28.418 8 24m32 10v6m0 0v6m0-6h6m-6 0h-6"
/>
</svg>
<span className="mt-2 block font-mono font-semibold text-sm font-medium text-gray-900">Let's get started by adding your first peer</span>
</Link>
)
}

13
src/components/Footer.js Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
const Footer = () => {
return (
<div className='flex justify-center items-center h-24 bg-gray-100 text-gray'>
<p className="font-mono">
Copyright © 2021 <a className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600 font-mono" href="https://wiretrustee.com">Wiretrustee Authors</a>
</p>
</div>
);
};
export default Footer;

34
src/components/Hero.js Normal file
View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Link } from 'react-router-dom';
const Hero = () => {
return (
<div className='bg-white h-screen flex flex-col justify-center items-center'>
<h1 className='lg:text-9xl md:text-7xl sm:text-5xl text-3xl font-black mb-14'>
EGGCELLENT
</h1>
<Link
className='py-6 px-10 bg-yellow-500 rounded-full text-3xl hover:bg-yellow-300 transition duration-300 ease-in-out flex items-center animate-bounce'
to='/menu'
>
Order Now{' '}
<svg
className='w-6 h-6 ml-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z'
/>
</svg>
</Link>
</div>
);
};
export default Hero;

View File

@@ -0,0 +1,73 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import hljs from "highlight.js";
import "highlight.js/styles/monokai-sublime.css";
const registeredLanguages = {};
class Highlight extends Component {
constructor(props) {
super(props);
this.state = { loaded: false };
this.codeNode = React.createRef();
}
componentDidMount() {
const { language } = this.props;
if (language && !registeredLanguages[language]) {
try {
const newLanguage = require(`highlight.js/lib/languages/${language}`);
hljs.registerLanguage(language, newLanguage);
registeredLanguages[language] = true;
this.setState({ loaded: true }, this.highlight);
} catch (e) {
console.error(e);
throw Error(`Cannot register the language ${language}`);
}
} else {
this.setState({ loaded: true });
}
}
componentDidUpdate() {
this.highlight();
}
highlight = () => {
this.codeNode &&
this.codeNode.current &&
hljs.highlightElement(this.codeNode.current);
};
render() {
const { language, children } = this.props;
const { loaded } = this.state;
if (!loaded) {
return null;
}
return (
<pre className="rounded">
<code ref={this.codeNode} className={language}>
{children}
</code>
</pre>
);
}
}
Highlight.propTypes = {
children: PropTypes.node.isRequired,
language: PropTypes.string,
};
Highlight.defaultProps = {
language: "javascript",
};
export default Highlight;

13
src/components/Loading.js Normal file
View File

@@ -0,0 +1,13 @@
import React from "react";
import loading from "../assets/bars.svg";
const Loading = () => (
<div>
<div className="flex h-screen items-center justify-center" >
<img src={loading} alt="Loading" width="50" height="50"/>
</div>
</div>
);
export default Loading;

228
src/components/Navbar.js Normal file
View File

@@ -0,0 +1,228 @@
import React, {Fragment} from 'react';
import {Link, NavLink} from 'react-router-dom';
import logo from "../assets/logo.png";
import {Disclosure, Menu, Transition} from '@headlessui/react'
import {MenuIcon, XIcon} from '@heroicons/react/outline'
import {useAuth0} from "@auth0/auth0-react";
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
const Navbar = ({toggle}) => {
const {
user,
isAuthenticated,
logout,
} = useAuth0();
const logoutWithRedirect = () =>
logout({
returnTo: window.location.origin,
});
return (
<Disclosure as="nav" className="bg-gray-100 border-b border-gray-200">
{({open}) => (
<>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-24">
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<Link to="/">
<img
className="block lg:hidden h-10 w-auto"
src={logo}
alt="Workflow"
/>
<img
className="hidden lg:block h-10 w-auto"
src={logo}
alt="Workflow"
/>
</Link>
</div>
<div className="hidden sm:ml-16 sm:flex sm:space-x-8">
{/* Current: "border-indigo-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
{isAuthenticated && (
<NavLink
to="/peers"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-indigo-500 text-gray-600 inline-flex items-center px-1 pt-1 text-m font-medium font-mono"
>
Peers
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/add-peer"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-indigo-500 text-gray-600 inline-flex items-center px-1 pt-1 text-m font-medium font-mono"
>
Add Peer
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/setup-keys"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-indigo-500 text-gray-600 inline-flex items-center px-1 pt-1 text-m font-medium font-mono"
>
Setup Keys
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/acls"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-indigo-500 text-gray-600 inline-flex items-center px-1 pt-1 text-m font-medium font-mono"
>
Access Control
</NavLink>
)}
{isAuthenticated && (
<NavLink
to="/activity"
activeClassName="border-indigo-500 text-gray-900 border-b-2"
className="border-indigo-500 text-gray-600 inline-flex items-center px-1 pt-1 text-m font-medium font-mono"
>
Activity
</NavLink>
)}
</div>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
<Menu as="div" className="ml-3 relative">
<div>
<Menu.Button
className="bg-white rounded-full flex text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span className="sr-only">Open user menu</span>
<img
className="h-12 w-auto rounded-full"
src={user.picture}
alt=""
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="origin-top-right absolute right-0 mt-2 w-48 squared-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({active}) => (
<NavLink
to="#"
id="qsLogoutBtn"
className={classNames(active ? 'bg-gray-100' : 'font-mono', 'block px-4 py-2 text-sm text-gray-700 font-mono')}
onClick={() => logoutWithRedirect()}
>
Sign out
</NavLink>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
</div>
<div className="-mr-2 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">
<span className="sr-only">Open main menu</span>
{open ? (
<XIcon className="block h-6 w-6" aria-hidden="true"/>
) : (
<MenuIcon className="block h-6 w-6" aria-hidden="true"/>
)}
</Disclosure.Button>
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="pt-2 pb-3 space-y-1">
{isAuthenticated && (
<Link
to="/peers"
className="block px-4 py-2 text-base font-medium font-mono text-gray-500 hover:text-gray-800 hover:bg-gray-100"
>
Peers
</Link>
)}
{isAuthenticated && (
<Link
to="/add-peer"
className="block px-4 py-2 text-base font-medium font-mono text-gray-500 hover:text-gray-800 hover:bg-gray-100"
>
Add Peer
</Link>
)}
{isAuthenticated && (
<Link
to="/setup-keys"
className="block px-4 py-2 text-base font-medium font-mono text-gray-500 hover:text-gray-800 hover:bg-gray-100"
>
Setup Keys
</Link>
)}
{isAuthenticated && (
<Link
to="/acls"
className="block px-4 py-2 text-base font-medium font-mono text-gray-500 hover:text-gray-800 hover:bg-gray-100"
>
Access Control
</Link>
)}
{isAuthenticated && (
<Link
to="/activity"
className="block px-4 py-2 text-base font-medium font-mono text-gray-500 hover:text-gray-800 hover:bg-gray-100"
>
Activity
</Link>
)}
</div>
<div className="pt-4 pb-3 border-t border-gray-200">
<div className="flex items-center px-4">
<div className="flex-shrink-0">
<img
className="h-10 w-10 rounded-full"
src={user.picture}
alt=""
/>
</div>
<div className="ml-3">
<div className="text-base font-medium text-gray-800 font-mono">{user.email}</div>
</div>
</div>
<div className="mt-3 space-y-1">
<Link
to="#"
className="block px-4 py-2 text-base font-medium font-mono text-gray-500 hover:text-gray-800 hover:bg-gray-100"
onClick={() => logoutWithRedirect()}
>
Sign out
</Link>
</div>
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
};
export default Navbar;

View File

@@ -0,0 +1,60 @@
import LinuxTab from "./LinuxTab";
import {useState} from "react";
import {classNames} from "../../utils/common";
import WindowsTab from "./WindowsTab";
import MacTab from "./MacTab";
const tabs = [
{name: 'Linux', idx: 1},
{name: 'Windows', idx: 2},
{name: 'MacOS', idx: 3}
]
const AddPeerTabSelector = ({setupKey}) => {
const [openTab, setOpenTab] = useState(1);
return (
<div>
<div className="hidden sm:block">
<div className="border-b border-gray-200">
<nav className="-mb-px flex space-x-8 font-mono" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.name}
onClick={e => {
e.preventDefault();
setOpenTab(tab.idx)
}}
className={classNames(
tab.idx === openTab
? 'border-indigo-500 text-gray-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300',
'whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm'
)}
aria-current={tab.idx === openTab ? 'page' : undefined}
>
{tab.name}
</button>
))}
</nav>
</div>
</div>
<div className="w-full mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
<div className={openTab === 1 ? "block" : "hidden"} id="linux-installation-steps">
<LinuxTab setupKey={setupKey}/>
</div>
<div className={openTab === 2 ? "block" : "hidden"} id="windows-installation-steps">
<WindowsTab setupKey={setupKey}/>
</div>
<div className={openTab === 3 ? "block" : "hidden"} id="macos-installation-steps">
<MacTab setupKey={setupKey}/>
</div>
</div>
</div>
</div>
)
}
export default AddPeerTabSelector;

View File

@@ -0,0 +1,118 @@
import ArrowCircleRightIcon from "@heroicons/react/outline/ArrowCircleRightIcon";
import Highlight from "../Highlight";
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
import WindowsTab from "./WindowsTab";
const LinuxTab = ({setupKey}) => {
const steps = [
{
id: 1,
target: 'Add Wiretrustee\'s repository:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
commands: ["sudo apt update && sudo apt install gnupg2 curl", "curl -fsSL https://wiretrustee.github.io/key.gpg | sudo apt-key add -", "curl -fsSL https://wiretrustee.github.io/dists/linux/lists | sudo tee /etc/apt/sources.list.d/wiretrustee.list"],
copy: true
},
{
id: 2,
target: 'Install Wiretrustee:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["sudo apt-get update", "sudo apt-get install wiretrustee"]
},
{
id: 3,
target: 'Login and run Wiretrustee:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["sudo wiretrustee login --setup-key <PASTE-SETUP-KEY>", 'sudo systemctl start wiretrustee']
},
{
id: 4,
target: 'Get your IP address:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["ip addr show wt0"]
},
{
id: 5,
target: 'Repeat on other machines.',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
copy: false,
content: null,
commands: null
},
]
const formatCommands = (commands, key) => {
return commands.map(c => key != null ? c.replace("<PASTE-SETUP-KEY>", key.Key) : c).join("\n")
}
return (
<ol role="list" className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={"linux-tab-step-" + step.id}
className={classNames(stepIdx !== steps.length - 1 ? 'pb-10' : '', 'relative')}>
<>
{stepIdx !== steps.length - 1 ? (
<div
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-gray-300"
aria-hidden="true"/>
) : null}
<a href={step.href} className="relative flex items-start group">
<span className="h-9 " aria-hidden="true">
<span
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 squared-full group-hover:border-gray-400">
<span className="text-m font-mono text-gray-700">{step.id}</span>
</span>
</span>
<span className="ml-4 min-w-0 ">
<span className="text-m tracking-wide font-mono text-gray-700">{step.target}</span>
<div className="flex flex-col space-y-2 ">
<span
className="text-sm text-gray-500">
{
step.content != null ? (step.content) : (
step.commands && (<Highlight language="bash">
{formatCommands(step.commands, setupKey)}
</Highlight>)
)
}
</span>
{step.copy && (<CopyButton toCopy={formatCommands(step.commands, setupKey)}
idPrefix={"add-peer-code-" + step.id}/>)}
</div>
</span>
</a>
</>
</li>
))}
</ol>
)
}
export default LinuxTab;
LinuxTab.propTypes = {
setupKey: PropTypes.object,
};
LinuxTab.defaultProps = {};

View File

@@ -0,0 +1,131 @@
import ArrowCircleRightIcon from "@heroicons/react/outline/ArrowCircleRightIcon";
import Highlight from "../Highlight";
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
const MacTab = ({setupKey}) => {
const steps = [
{
id: 1,
target: 'Download latest release (Darwin asset):',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: <button className="underline text-indigo-500" onClick={()=> window.open("https://github.com/wiretrustee/wiretrustee/releases", "_blank")}>Wiretrustee GitHub Releases</button>,
//content: <a href="https://github.com/wiretrustee/wiretrustee/releases">Wiretrustee GitHub Releases</a>,
commands: [],
copy: false
},
{
id: 2,
target: 'Decompress and move to a fixed path in your system:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["tar -xvzf wiretrustee_0.1.0-rc-1_darwin_amd64.tar.gz","sudo mv wiretrusee /usr/local/bin/wiretrustee", "sudo chmod +x /usr/local/bin/wiretrustee"]
},
{
id: 3,
target: 'Configure MAC\'s PATH environment variable:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["export PATH=$PATH:/usr/local/bin"]
},
{
id: 4,
target: 'Login and run Wiretrustee:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["sudo wiretrustee login --setup-key <PASTE-SETUP-KEY>", "sudo wiretrustee up &"]
},
{
id: 5,
target: 'Get your IP address:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["sudo ipconfig getifaddr utun100"]
},
{
id: 6,
target: 'Repeat on other machines.',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
copy: false,
content: null,
commands: null
},
]
const formatCommands = (commands, key) => {
return commands.map(c => key != null ? c.replace("<PASTE-SETUP-KEY>", key.Key) : c).join("\n")
}
return (
<ol role="list" className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={"linux-tab-step-" + step.id}
className={classNames(stepIdx !== steps.length - 1 ? 'pb-10' : '', 'relative')}>
<>
{stepIdx !== steps.length - 1 ? (
<div
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-gray-300"
aria-hidden="true"/>
) : null}
<a href={step.href} className="relative flex items-start group">
<span className="h-9 " aria-hidden="true">
<span
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 squared-full group-hover:border-gray-400">
<span className="text-m font-mono text-gray-700">{step.id}</span>
</span>
</span>
<span className="ml-4 min-w-0 ">
<span className="text-m tracking-wide font-mono text-gray-700">{step.target}</span>
<div className="flex flex-col space-y-2 ">
<span
className="text-sm text-gray-500">
{
step.content != null ? (
<div className="font-mono underline mt-4">
{step.content}
</div>
) : (
step.commands && (<Highlight language="bash">
{formatCommands(step.commands, setupKey)}
</Highlight>)
)
}
</span>
{step.copy && (<CopyButton toCopy={formatCommands(step.commands, setupKey)}
idPrefix={"add-peer-code-" + step.id}/>)}
</div>
</span>
</a>
</>
</li>
))}
</ol>
)
}
export default MacTab;
MacTab.propTypes = {
setupKey: PropTypes.object,
};
MacTab.defaultProps = {};

View File

@@ -0,0 +1,103 @@
import {Fragment, useState} from 'react'
import {Listbox, Transition} from '@headlessui/react'
import {CheckIcon, SelectorIcon} from '@heroicons/react/solid'
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
const SetupKeySelect = ({data, onSelected}) => {
const [selected, setSelected] = useState(data.length > 0 ? data[0] : {Name: "...", Id: "none"})
const handleSelected = selectedKey => {
setSelected(selectedKey)
onSelected(selectedKey)
let keyBox = document.getElementById("key-box");
keyBox.classList.remove("hidden")
};
return (
<div>
<span className="text-m tracking-wide font-mono text-gray-700">Select setup key to register peer:</span>
<span className="ml-4 min-w-0">
<div className="flex flex-col space-y-2">
<Listbox value={selected} onChange={handleSelected}>
{({open}) => (
<>
<div className="mt-1 relative">
<Listbox.Button
className="bg-white relative w-full border border-gray-300 squared-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate font-mono">{selected.Name}</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon className="h-5 w-5 text-gray-400" aria-hidden="true"/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
{data.map((item) => (
<Listbox.Option
key={item.Id}
className={({active}) =>
classNames(
active ? 'text-white bg-indigo-600' : 'text-gray-900',
'cursor-default select-none relative py-2 pl-3 pr-9'
)
}
value={item}
>
{({selected, active}) => (
<>
<span className={classNames(selected ? 'font-semibold' : 'font-mono', 'block truncate')}>
{item.Name}
</span>
{selected ? (
<span
className={classNames(
active ? 'text-white' : 'text-indigo-600',
'absolute inset-y-0 right-0 flex items-center pr-4'
)}
>
<CheckIcon className="h-5 w-5" aria-hidden="true"/>
</span>
) : null}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
<div id="key-box" className="hidden rounded-md bg-gray-100 p-4">
<div className="ml-3 flex-1 md:flex md:justify-between">
<p className="text-sm font-mono text-gray-700">{selected.Key}</p>
<p className="mt-4 text-sm md:mt-0 md:ml-6">
<CopyButton toCopy={selected.Key}/>
</p>
</div>
</div>
</div>
</span>
</div>
)
}
SetupKeySelect.propTypes = {
data: PropTypes.array,
onSelected: PropTypes.func,
};
SetupKeySelect.defaultProps = {};
export default SetupKeySelect

View File

@@ -0,0 +1,141 @@
import ArrowCircleRightIcon from "@heroicons/react/outline/ArrowCircleRightIcon";
import Highlight from "../Highlight";
import CopyButton from "../CopyButton";
import {classNames} from "../../utils/common";
import PropTypes from "prop-types";
import SetupKeySelect from "./SetupKeySelect";
const WindowsTab = ({setupKey}) => {
const steps = [
{
id: 1,
target: 'Download latest release (Windows asset):',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: <button className="underline text-indigo-500" onClick={()=> window.open("https://github.com/wiretrustee/wiretrustee/releases", "_blank")}>Wiretrustee GitHub Releases</button>,
//content: <a href="https://github.com/wiretrustee/wiretrustee/releases">Wiretrustee GitHub Releases</a>,
commands: [],
copy: false
},
{
id: 2,
target: 'Open Powershell as Administrator.',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: <div/>,
copy: false,
commands: []
},
{
id: 3,
target: 'Decompress and move to a fixed path in your system:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["mkdir C:\\Wiretrustee", "tar -xvzf wiretrustee_0.1.0-rc-1_windows_amd64.tar.gz -C C:\\Wiretrustee"]
},
{
id: 4,
target: 'Install Wiretrustee service:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["C:\\Wiretrustee\\wiretrustee.exe service install"]
},
{
id: 5,
target: 'Login and run Wiretrustee:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["C:\\Wiretrustee\\wiretrustee.exe login --setup-key <PASTE-SETUP-KEY>", 'C:\\Wiretrustee\\wiretrustee.exe service start']
},
{
id: 6,
target: 'Get your IP address:',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
content: null,
copy: true,
commands: ["netsh interface ip show config name=\"wt0\""]
},
{
id: 7,
target: 'Repeat on other machines.',
icon: ArrowCircleRightIcon,
iconBackground: 'bg-gray-600',
copy: false,
content: null,
commands: null
},
]
const formatCommands = (commands, key) => {
return commands.map(c => key != null ? c.replace("<PASTE-SETUP-KEY>", key.Key) : c).join("\n")
}
return (
<ol role="list" className="overflow-hidden">
{steps.map((step, stepIdx) => (
<li key={"linux-tab-step-" + step.id}
className={classNames(stepIdx !== steps.length - 1 ? 'pb-10' : '', 'relative')}>
<>
{stepIdx !== steps.length - 1 ? (
<div
className="-ml-px absolute mt-0.5 top-4 left-4 w-0.5 h-full bg-gray-300"
aria-hidden="true"/>
) : null}
<a href={step.href} className="relative flex items-start group">
<span className="h-9 " aria-hidden="true">
<span
className="relative z-10 w-8 h-8 flex items-center justify-center bg-white border-2 border-gray-300 squared-full group-hover:border-gray-400">
<span className="text-m font-mono text-gray-700">{step.id}</span>
</span>
</span>
<span className="ml-4 min-w-0 ">
<span className="text-m tracking-wide font-mono text-gray-700">{step.target}</span>
<div className="flex flex-col space-y-2 ">
<span
className="text-sm text-gray-500">
{
step.content != null ? (
<div className="font-mono underline mt-4">
{step.content}
</div>
) : (
step.commands && (<Highlight language="bash">
{formatCommands(step.commands, setupKey)}
</Highlight>)
)
}
</span>
{step.copy && (<CopyButton toCopy={formatCommands(step.commands, setupKey)}
idPrefix={"add-peer-code-" + step.id}/>)}
</div>
</span>
</a>
</>
</li>
))}
</ol>
)
}
export default WindowsTab;
WindowsTab.propTypes = {
setupKey: PropTypes.object,
};
WindowsTab.defaultProps = {};

21
src/config.js Normal file
View File

@@ -0,0 +1,21 @@
import configJson from "./config.json";
export function getConfig() {
// Configure the audience here. By default, it will take whatever is in the config
// (specified by the `audience` key) unless it's the default value of "YOUR_API_IDENTIFIER" (which
// is what you get sometimes by using the Auth0 sample download tool from the quickstart page, if you
// don't have an API).
// If this resolves to `null`, the API page changes to show some helpful info about what to do
// with the audience.
const audience =
configJson.audience && configJson.audience !== "YOUR_API_IDENTIFIER"
? configJson.audience
: null;
return {
domain: configJson.domain,
clientId: configJson.clientId,
apiOrigin: configJson.apiOrigin,
...(audience ? { audience } : null),
};
}

6
src/config.json Normal file
View File

@@ -0,0 +1,6 @@
{
"domain": "$AUTH0_DOMAIN",
"clientId": "$AUTH0_CLIENT_ID",
"audience": "$AUTH0_AUDIENCE",
"apiOrigin": "$WIRETRUSTEE_MGMT_API_ENDPOINT"
}

11
src/index.css Normal file
View File

@@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.menu-card {
@apply flex flex-col justify-center items-center bg-white h-screen font-mono py-40;
}
.center-content {
@apply flex flex-col justify-center items-center;
}

43
src/index.js Normal file
View File

@@ -0,0 +1,43 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';
import history from "./utils/history";
import { getConfig } from "./config";
import {Auth0Provider} from "@auth0/auth0-react";
const onRedirectCallback = (appState) => {
history.push(
appState && appState.returnTo ? appState.returnTo : window.location.pathname
);
};
const config = getConfig();
const providerConfig = {
domain: config.domain,
clientId: config.clientId,
...(config.audience ? { audience: config.audience } : null),
redirectUri: window.location.origin,
useRefreshTokens: true,
onRedirectCallback,
};
ReactDOM.render(
<Auth0Provider {...providerConfig}>
<React.StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.StrictMode>
</Auth0Provider>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

13
src/reportWebVitals.js Normal file
View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

74
src/utils/common.js Normal file
View File

@@ -0,0 +1,74 @@
export const formatDate = date => {
return new Date(date).toLocaleDateString("en-GB", { weekday: 'short', year: '2-digit', month: 'short', day: 'numeric' });
}
export const classNames = (...classes) => {
return classes.filter(Boolean).join(' ')
}
const MONTH_NAMES = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
function getFormattedDate(date, preformattedDate = false, hideYear = false) {
const day = date.getDate();
const month = MONTH_NAMES[date.getMonth()];
const year = date.getFullYear();
let minutes = date.getMinutes();
if (minutes < 10) {
// Adding leading zero to minutes
minutes = `0${ minutes }`;
}
if (preformattedDate) {
// Today
// Yesterday
return `${ preformattedDate }`;
}
if (hideYear) {
// 10. January
return `${ day }. ${ month }`;
}
// 10. January 2017.
return `${ day }. ${ month } ${ year }`;
}
export const timeAgo = (dateParam) => {
if (!dateParam) {
return null;
}
const date = typeof dateParam === 'object' ? dateParam : new Date(dateParam);
const DAY_IN_MS = 86400000; // 24 * 60 * 60 * 1000
const today = new Date();
const yesterday = new Date(today - DAY_IN_MS);
const seconds = Math.round((today - date) / 1000);
const minutes = Math.round(seconds / 60);
const isToday = today.toDateString() === date.toDateString();
const isYesterday = yesterday.toDateString() === date.toDateString();
const isThisYear = today.getFullYear() === date.getFullYear();
if (seconds < 5) {
return 'just now';
} else if (seconds < 60) {
return `${ seconds } seconds ago`;
} else if (seconds < 90) {
return 'about a minute ago';
} else if (minutes < 60) {
return `${ minutes } minutes ago`;
} else if (isToday) {
return getFormattedDate(date, 'Today'); // Today at 10:20
} else if (isYesterday) {
return getFormattedDate(date, 'Yesterday'); // Yesterday at 10:20
} else if (isThisYear) {
return getFormattedDate(date, false, true); // 10. January at 10:20
}
return getFormattedDate(date); // 10. January 2017. at 10:20
}

2
src/utils/history.js Normal file
View File

@@ -0,0 +1,2 @@
import { createBrowserHistory } from "history";
export default createBrowserHistory();

View File

@@ -0,0 +1,59 @@
import React, {useState} from "react";
import {withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
export const AccessControlComponent = () => {
const [error] = useState(null)
return (
<>
<div className="py-10">
<header>
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-mono leading-tight text-gray-900 font-bold">Access Control</h1>
</div>
</header>
<main>
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{error != null && (
<span>{error.toString()}</span>
)}
<h1 className="text-m font-mono leading-tight text-gray-900 font-bold">
Create and control access groups
</h1>
<br/>
<p className="text-sm font-mono">
Here you will be able to specify what peers or groups of peers are able to connect to
each other.
For example, you might have 3 departments in your organization - IT, HR, Finance.
In most cases Finance and HR departments wouldn't need to access machines of the IT
department.
In such scenario you could create 3 separate tags (groups) and label peers accordingly
so that only
peers
from the same group can access each other.
You could also specify what groups can connect to each other and do fine grained control
even on a
peer level.
</p>
<br/>
<p className="text-sm font-mono">Stay tuned.</p>
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(AccessControlComponent,
{
onRedirecting: () => <Loading/>,
}
);

47
src/views/Activity.js Normal file
View File

@@ -0,0 +1,47 @@
import React, {useState} from "react";
import {withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
export const ActivityComponent = () => {
const [error] = useState(null)
return (
<>
<div className="py-10">
<header>
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-mono leading-tight text-gray-900 font-bold">Access Control</h1>
</div>
</header>
<main>
<div className="max-w-5xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{error != null && (
<span>{error.toString()}</span>
)}
<h1 className="text-m font-mono leading-tight text-gray-900 font-bold">
Monitor system activity.
</h1>
<br/>
<p className="text-sm font-mono">
Here you will be able to see activity of peers. E.g. events like Peer A has connected to Peer B
</p>
<br/>
<p className="text-sm font-mono">Stay tuned.</p>
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(ActivityComponent,
{
onRedirecting: () => <Loading/>,
}
);

69
src/views/AddPeer.js Normal file
View File

@@ -0,0 +1,69 @@
import React, {useEffect, useState} from "react";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {getSetupKeys} from "../api/ManagementAPI";
import AddPeerTabSelector from "../components/addpeer/AddPeerTabSelector";
import SetupKeySelect from "../components/addpeer/SetupKeySelect";
export const AddPeerComponent = () => {
const [setupKeys, setSetupKeys] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [selectedKey, setSelectedKey] = useState(null)
const {
getAccessTokenSilently,
} = useAuth0();
const handleError = error => {
console.error('Error to fetch data:', error);
setLoading(false)
setError(error);
};
useEffect(() => {
getSetupKeys(getAccessTokenSilently)
.then(responseData => setSetupKeys(responseData))
.then(() => setLoading(false))
.catch(error => handleError(error))
}, [getAccessTokenSilently])
return (
<>
<div className="py-10">
<header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl leading-tight text-gray-900 font-mono font-bold">Add Peer</h1>
</div>
</header>
<main>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
{setupKeys && (<nav aria-label="Progress">
<div className="flex max-w-lg flex-col space-y-2">
<SetupKeySelect data={setupKeys.filter(k => k.Valid)} onSelected={setSelectedKey}/>
</div>
<AddPeerTabSelector setupKey={selectedKey}/>
</nav>)}
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(AddPeerComponent,
{
onRedirecting: () => <Loading/>,
}
);

200
src/views/Peers.js Normal file
View File

@@ -0,0 +1,200 @@
import React, {useEffect, useState} from "react";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {deletePeer, getPeers} from "../api/ManagementAPI";
import {timeAgo} from "../utils/common";
import EditButton from "../components/EditButton";
import CopyText from "../components/CopyText";
import DeleteModal from "../components/DeleteDialog";
import EmptyPeersPanel from "../components/EmptyPeers";
export const Peers = () => {
const [peers, setPeers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [deleteDialogText, setDeleteDialogText] = useState("")
const [deleteDialogTitle, setDeleteDialogTitle] = useState("")
const [peerToDelete, setPeerToDelete] = useState(null)
const {
getAccessTokenSilently,
} = useAuth0();
const handleError = error => {
console.error('Error to fetch data:', error);
setLoading(false)
setError(error);
};
//called when user clicks on table row menu item
const handleRowMenuClick = (action, peer) => {
if (action === 'Delete') {
setPeerToDelete(peer)
setDeleteDialogText("Are you sure you want to delete peer from your account?")
setDeleteDialogTitle("Delete peer \"" + peer.Name + "\"")
setShowDeleteDialog(true)
}
};
const refresh = () => {
getPeers(getAccessTokenSilently)
.then(responseData => responseData.sort((a, b) => (a.Name > b.Name) ? 1 : -1))
.then(sorted => setPeers(sorted))
.then(() => setLoading(false))
.catch(error => handleError(error))
}
// after user confirms (or not) deletion of the peer
const handleDeleteConfirmation = (confirmed) => {
setShowDeleteDialog(false)
if (confirmed) {
deletePeer(getAccessTokenSilently, peerToDelete.IP)
.then(() => setPeerToDelete(null))
.then(() => refresh())
.catch(error => {
setPeerToDelete(null)
console.log(error)
})
} else {
setPeerToDelete(null)
}
}
useEffect(() => {
refresh()
}, [getAccessTokenSilently])
return (
<>
<div className="py-10">
<header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-mono leading-tight text-gray-900 font-bold">Peers</h1>
</div>
</header>
<main>
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
<main>
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
{peers.length === 0 ?
(<EmptyPeersPanel/>) : (
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
<DeleteModal show={showDeleteDialog}
confirmCallback={handleDeleteConfirmation}
text={deleteDialogText} title={deleteDialogTitle}/>
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div
className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div
className="shadow border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-100">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
IP
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Last Seen
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
OS
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{peers.map((peer, idx) => (
<tr key={peer.IP}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-semibold font-mono text-gray-900">{peer.Name}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-mono text-gray-900">
<CopyText text={peer.IP.toUpperCase()}
idPrefix={"peers-ip-" + peer.IP}/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{peer.Connected && (
<span
className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-green-100 text-green-800">
Connected
</span>
)}
{!peer.Connected && (
<span
className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-red-100 text-red-800">
Disconnected
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">
{peer.Connected ? ("just now") : timeAgo(peer.LastSeen)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{peer.OS}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-m font-medium">
<EditButton items={[{name: "Delete"}]}
handler={action => handleRowMenuClick(action, peer)}/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</main>
</div>
</div>
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(Peers,
{
onRedirecting: () => <Loading/>,
}
);

199
src/views/SetupKeys.js Normal file
View File

@@ -0,0 +1,199 @@
import React, {useEffect, useState} from "react";
import {useAuth0, withAuthenticationRequired} from "@auth0/auth0-react";
import Loading from "../components/Loading";
import {formatDate, timeAgo} from "../utils/common";
import {getSetupKeys, revokeSetupKey} from "../api/ManagementAPI";
import EditButton from "../components/EditButton";
import CopyText from "../components/CopyText";
import DeleteModal from "../components/DeleteDialog";
export const SetupKeysComponent = () => {
const [setupKeys, setSetupKeys] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [deleteDialogText, setDeleteDialogText] = useState("")
const [deleteDialogTitle, setDeleteDialogTitle] = useState("")
const [keyToRevoke, setKeyToRevoke] = useState(null)
const {
getAccessTokenSilently,
} = useAuth0();
const handleError = error => {
console.error('Error to fetch data:', error);
setLoading(false)
setError(error);
};
//called when user clicks on table row menu item
const handleRowMenuClick = (action, key) => {
if (action === 'Revoke') {
setKeyToRevoke(key)
setDeleteDialogText("Are you sure you want to revoke setup key?")
setDeleteDialogTitle("Revoke key \"" + key.Name + "\"")
setShowDeleteDialog(true)
}
};
// after user confirms (or not) revoking the key
const handleRevokeConfirmation = (confirmed) => {
setShowDeleteDialog(false)
if (confirmed && !keyToRevoke.Revoked) {
revokeSetupKey(getAccessTokenSilently, keyToRevoke.Id)
.then(() => setKeyToRevoke(null))
.then(() => refresh())
.catch(error => {
setKeyToRevoke(null)
console.log(error)
})
} else {
setKeyToRevoke(null)
}
}
const refresh = () => {
getSetupKeys(getAccessTokenSilently)
.then(responseData => responseData.sort((a, b) => (a.Name > b.Name) ? 1 : -1))
.then(sorted => setSetupKeys(sorted))
.then(() => setLoading(false))
.catch(error => handleError(error))
}
useEffect(() => {
refresh()
}, [getAccessTokenSilently])
return (
<>
<div className="py-10">
<header>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-mono leading-tight text-gray-900 font-bold">Setup Keys</h1>
</div>
</header>
<main>
{loading && (<Loading/>)}
{error != null && (
<span>{error.toString()}</span>
)}
{setupKeys && (
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="px-4 py-8 sm:px-0">
<DeleteModal show={showDeleteDialog}
confirmCallback={handleRevokeConfirmation}
text={deleteDialogText} title={deleteDialogTitle}/>
<div className="flex flex-col">
<div className="-my-2 sm:-mx-6 lg:-mx-8">
<div
className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
<div
className="shadow border-b border-gray-200 sm:rounded-lg">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-100">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Name
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
State
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Type
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Key
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Last Used
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Used Times
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Expires
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only">Edit</span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{setupKeys.map((setupKey, idx) => (
<tr key={setupKey.Id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium font-semibold font-mono text-gray-900">{setupKey.Name}</td>
<td className="px-6 py-4 whitespace-nowrap">
{setupKey.Valid && (
<span
className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-green-100 text-green-800">
valid
</span>
)}
{!setupKey.Valid && (
<span
className="px-2 inline-flex text-sm leading-5 font-mono squared-full bg-red-100 text-red-800">
{setupKey.State}
</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{setupKey.Type.toLowerCase()}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">
<CopyText text={setupKey.Key.toUpperCase()}
idPrefix={"setup-keys" + setupKey.Id}/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{setupKey.UsedTimes === 0 ? "unused" : timeAgo(setupKey.LastUsed)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{setupKey.UsedTimes}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono text-gray-700">{formatDate(setupKey.Expires)}</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-m font-medium">
<EditButton items={[{name: "Revoke"}]}
handler={action => handleRowMenuClick(action, setupKey)}/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
)}
</main>
</div>
</>
);
}
;
export default withAuthenticationRequired(SetupKeysComponent,
{
onRedirecting: () => <Loading/>,
}
);

11
tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = {
purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {}
},
variants: {
extend: {}
},
plugins: []
};