wip add frames
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
.git
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
node_modules
|
||||
build
|
||||
README.md
|
|
@ -1,2 +1,4 @@
|
|||
REACT_APP_API_URL=http://stackspin_proxy:8081/api/v1
|
||||
REACT_APP_HYDRA_PUBLIC_URL=https://sso.init.stackspin.net
|
||||
STACK_NAME=dashboard
|
||||
DOCKER_CONTEXT=
|
||||
DOMAIN=
|
||||
LETS_ENCRYPT_ENV=production
|
||||
|
|
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
export NODE_OPTIONS=--openssl-legacy-provider
|
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
|||
FROM node:14-alpine AS BUILDER
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock /app/
|
||||
RUN yarn install
|
||||
COPY . /app
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx:latest
|
||||
COPY deployment/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=builder /app/build /usr/share/nginx/html
|
25
Makefile
Normal file
|
@ -0,0 +1,25 @@
|
|||
include .env
|
||||
export
|
||||
|
||||
build:
|
||||
docker build -t dashboard .
|
||||
docker tag dashboard yksflip/dashboard:latest
|
||||
|
||||
run:
|
||||
docker run --rm -it -p 80:80 dashboard
|
||||
|
||||
push:
|
||||
docker push yksflip/dashboard:latest
|
||||
|
||||
rm:
|
||||
docker stack rm ${STACK_NAME}
|
||||
|
||||
deploy: rm
|
||||
DOMAIN=${DOMAIN} docker stack deploy --resolve-image always --compose-file compose.yml ${STACK_NAME}
|
||||
|
||||
update:
|
||||
docker pull yksflip/dashboard:latest
|
||||
docker service update dashboard_app --image yksflip/dashboard:latest --force
|
||||
|
||||
exec:
|
||||
docker exec -it $$(docker ps --format "{{ .Names }}" | grep dashboard) bash
|
29
compose.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
version: "3.8"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: yksflip/dashboard:latest
|
||||
build: .
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 10
|
||||
start_period: 1m
|
||||
networks:
|
||||
- proxy
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80"
|
||||
- "traefik.http.routers.${STACK_NAME}.rule=Host(`${DOMAIN}`${EXTRA_DOMAINS})"
|
||||
- "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure"
|
||||
- "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}"
|
||||
- "traefik.http.middlewares.${STACK_NAME}.headers.customFrameOptionsValue=sameorigin"
|
||||
- "coop-cloud.${STACK_NAME}.app.version=1.0.0+v0.1"
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
external: true
|
|
@ -1,3 +0,0 @@
|
|||
FROM nginx:latest
|
||||
COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||
COPY . /usr/share/nginx/html
|
|
@ -26,10 +26,11 @@
|
|||
"react-helmet": "^6.1.0",
|
||||
"react-hook-form": "^7.22.0",
|
||||
"react-hot-toast": "^2.0.0",
|
||||
"react-iframe": "^1.8.0",
|
||||
"react-markdown": "^7.0.1",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-router": "6.2.1",
|
||||
"react-router-dom": "6.2.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-simple-code-editor": "^0.11.0",
|
||||
"react-table": "^7.7.0",
|
||||
|
@ -89,6 +90,7 @@
|
|||
"postcss": "^7",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^2.3.2",
|
||||
"react-inject-env": "^2.1.0",
|
||||
"sass": "^1.36.0"
|
||||
},
|
||||
"pre-commit": "lint-staged",
|
||||
|
|
BIN
public/assets/lit_logos/apple-touch-icon_lit_transp.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/lit_logos/favicon_lit_transp.ico
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
public/assets/lit_logos/lit_transp.png
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
public/assets/lit_logos/lit_transp_16x16.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
public/assets/lit_logos/lit_transp_192x192.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/lit_logos/lit_transp_256x256.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/lit_logos/lit_transp_32x32.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
public/assets/lit_logos/lit_transp_512x512.png
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
public/assets/lit_logos/lit_transp_title.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
public/assets/lit_logos/lit_transp_title_52.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/assets/lit_logos/lit_transp_title_96.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
public/assets/lit_logos/lit_transp_title_sub.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
public/assets/lit_logos/lit_transp_title_sub_192.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/assets/lit_logos/lit_transp_title_sub_96.png
Normal file
After Width: | Height: | Size: 5 KiB |
|
@ -1,7 +0,0 @@
|
|||
<svg width="25" height="44" viewBox="0 0 25 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 35.1171C0 39.0948 4.18188 41.6853 7.74332 39.9138L22.8687 32.39C24.0424 31.8062 24.7844 30.6083 24.7844 29.2975V20.9534L1.64288 32.4649C0.636359 32.9656 0 33.9929 0 35.1171Z" fill="#2D535A"/>
|
||||
<path d="M0 22.8091C0 27.6308 5.06914 30.7709 9.38621 28.6235L24.7844 20.9641C24.7844 16.1423 19.7151 13.0021 15.398 15.1496L0 22.8091Z" fill="#54C6CC"/>
|
||||
<path d="M2.2161 21.7068C0.858395 22.3821 -0.103566 23.8187 0.458285 25.2271C1.79955 28.5895 5.84578 30.3846 9.38621 28.6235L24.7844 20.9641C24.7844 16.1423 19.7151 13.0021 15.398 15.1496L2.2161 21.7068Z" fill="#2D535A"/>
|
||||
<path d="M2.2161 21.7068C0.858395 22.3821 -0.103566 23.8187 0.458285 25.2271C1.79955 28.5895 5.84578 30.3846 9.38621 28.6235L22.5683 22.0664C23.926 21.3911 24.888 19.9545 24.3261 18.546C22.9848 15.1836 18.9385 13.3884 15.398 15.1496L2.2161 21.7068Z" fill="#1E8290"/>
|
||||
<path d="M0 22.8121L23.3077 11.2182C24.2124 10.7682 24.7844 9.8448 24.7844 8.83432C24.7844 4.77111 20.5126 2.12495 16.8747 3.93462L2.25625 11.2064C0.873945 11.894 0 13.3048 0 14.8487V22.8121Z" fill="#54C6CC"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,9 +0,0 @@
|
|||
<svg width="151" height="44" viewBox="0 0 151 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="151" height="44" fill="transparent"/>
|
||||
<path d="M40.8246 11.7373C37.2206 11.7373 34.89 13.5633 34.89 16.6388C34.89 19.7142 36.8842 20.8915 40.4162 21.6604L40.9688 21.7805C43.1312 22.2611 44.2604 22.7416 44.2604 24.1351C44.2604 25.4806 43.1792 26.4417 41.0168 26.4417C38.8544 26.4417 37.5329 25.4326 37.5329 23.4143V23.1741H34.4094V23.4143C34.4094 27.0664 37.1245 29.2288 41.0168 29.2288C44.9092 29.2288 47.3839 27.1145 47.3839 24.039C47.3839 20.9636 45.1014 19.7142 41.5214 18.9454L40.9688 18.8252C38.9025 18.3687 38.0135 17.7921 38.0135 16.5427C38.0135 15.2933 38.9025 14.5244 40.8246 14.5244C42.7468 14.5244 43.9241 15.2933 43.9241 17.2154V17.5998H47.0476V17.2154C47.0476 13.5633 44.4286 11.7373 40.8246 11.7373ZM47.5746 19.4259H50.554V26.2015C50.554 27.8353 51.6112 28.8925 53.1969 28.8925H56.5607V26.4417H54.2541C53.8216 26.4417 53.5814 26.2015 53.5814 25.7209V19.4259H56.849V16.9752H53.5814V13.275H50.554V16.9752H47.5746V19.4259ZM69.9725 28.8925V16.9752H66.9932V18.5849H66.7529C66.0321 17.5518 64.8788 16.6388 62.8125 16.6388C59.9774 16.6388 57.4305 19.0415 57.4305 22.9338C57.4305 26.8261 59.9774 29.2288 62.8125 29.2288C64.8788 29.2288 66.0321 28.3158 66.7529 27.2827H66.9932V28.8925H69.9725ZM63.7256 26.6339C61.8515 26.6339 60.4579 25.2884 60.4579 22.9338C60.4579 20.5792 61.8515 19.2337 63.7256 19.2337C65.5996 19.2337 66.9932 20.5792 66.9932 22.9338C66.9932 25.2884 65.5996 26.6339 63.7256 26.6339ZM71.4505 22.9338C71.4505 26.6339 74.1896 29.2288 77.6495 29.2288C80.9892 29.2288 82.9354 27.2827 83.6081 24.6637L80.6768 23.967C80.4126 25.5047 79.5236 26.5859 77.6975 26.5859C75.8715 26.5859 74.4779 25.2404 74.4779 22.9338C74.4779 20.6272 75.8715 19.2817 77.6975 19.2817C79.5236 19.2817 80.4126 20.435 80.5807 21.8766L83.512 21.2519C82.9834 18.5849 80.9652 16.6388 77.6495 16.6388C74.1896 16.6388 71.4505 19.2337 71.4505 22.9338ZM97.4406 16.9752H92.9716L88.0221 21.348V12.0737H84.9947V28.8925H88.0221V25.0241L89.68 23.6066L93.6204 28.8925H97.3445L91.8424 21.7565L97.4406 16.9752ZM98.2594 20.3149C98.2594 22.6695 100.23 23.5345 102.728 24.015L103.353 24.1351C104.843 24.4235 105.516 24.7839 105.516 25.5527C105.516 26.3216 104.843 26.9223 103.449 26.9223C102.056 26.9223 100.926 26.3456 100.614 24.6157L97.8269 25.3365C98.2354 27.8353 100.326 29.2288 103.449 29.2288C106.477 29.2288 108.447 27.8112 108.447 25.3125C108.447 22.8137 106.429 22.1409 103.738 21.6123L103.113 21.4922C101.863 21.2519 101.191 20.9156 101.191 20.1227C101.191 19.4019 101.815 18.9454 102.969 18.9454C104.122 18.9454 104.939 19.4259 105.227 20.7233L107.966 19.8824C107.39 17.9603 105.636 16.6388 102.969 16.6388C100.134 16.6388 98.2594 17.9603 98.2594 20.3149ZM109.985 33.6978H113.012V27.3547H113.252C113.925 28.3158 115.078 29.2288 117.145 29.2288C119.98 29.2288 122.527 26.8261 122.527 22.9338C122.527 19.0415 119.98 16.6388 117.145 16.6388C115.078 16.6388 113.925 17.5518 113.204 18.5849H112.964V16.9752H109.985V33.6978ZM116.231 26.6339C114.357 26.6339 112.964 25.2884 112.964 22.9338C112.964 20.5792 114.357 19.2337 116.231 19.2337C118.106 19.2337 119.499 20.5792 119.499 22.9338C119.499 25.2884 118.106 26.6339 116.231 26.6339ZM123.38 13.5153C123.38 14.7407 124.317 15.5816 125.518 15.5816C126.72 15.5816 127.657 14.7407 127.657 13.5153C127.657 12.2899 126.72 11.449 125.518 11.449C124.317 11.449 123.38 12.2899 123.38 13.5153ZM127.032 16.9752H124.005V28.8925H127.032V16.9752ZM129.249 16.9752V28.8925H132.277V22.7416C132.277 20.5311 133.358 19.2337 135.208 19.2337C136.842 19.2337 137.755 20.1227 137.755 21.9247V28.8925H140.782V21.7805C140.782 18.8252 138.932 16.7829 136.145 16.7829C133.958 16.7829 132.949 17.744 132.469 18.7531H132.228V16.9752H129.249Z" fill="#2D535A"/>
|
||||
<path d="M0 35.1171C0 39.0948 4.18188 41.6853 7.74332 39.9138L22.8687 32.39C24.0424 31.8062 24.7844 30.6083 24.7844 29.2975V20.9534L1.64288 32.4649C0.636359 32.9656 0 33.9929 0 35.1171Z" fill="#2D535A"/>
|
||||
<path d="M0 22.8091C0 27.6308 5.06914 30.7709 9.38621 28.6235L24.7844 20.9641C24.7844 16.1423 19.7151 13.0021 15.398 15.1496L0 22.8091Z" fill="#54C6CC"/>
|
||||
<path d="M2.2161 21.7068C0.858395 22.3821 -0.103566 23.8187 0.458285 25.2271C1.79955 28.5895 5.84578 30.3846 9.38621 28.6235L24.7844 20.9641C24.7844 16.1423 19.7151 13.0021 15.398 15.1496L2.2161 21.7068Z" fill="#2D535A"/>
|
||||
<path d="M2.2161 21.7068C0.858395 22.3821 -0.103566 23.8187 0.458285 25.2271C1.79955 28.5895 5.84578 30.3846 9.38621 28.6235L22.5683 22.0664C23.926 21.3911 24.888 19.9545 24.3261 18.546C22.9848 15.1836 18.9385 13.3884 15.398 15.1496L2.2161 21.7068Z" fill="#1E8290"/>
|
||||
<path d="M0 22.8121L23.3077 11.2182C24.2124 10.7682 24.7844 9.8448 24.7844 8.83432C24.7844 4.77111 20.5126 2.12495 16.8747 3.93462L2.25625 11.2064C0.873945 11.894 0 13.3048 0 14.8487V22.8121Z" fill="#54C6CC"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 4.7 KiB |
BIN
public/assets/vikunja.png
Normal file
After Width: | Height: | Size: 52 KiB |
|
@ -1,21 +1,19 @@
|
|||
<!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"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/assets/lit_logos/favicon_lit_transp.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" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/assets/lit_logos/lit_transp_192x192.png" />
|
||||
<!--
|
||||
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" />
|
||||
<!--
|
||||
<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.
|
||||
|
@ -24,12 +22,14 @@
|
|||
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>React App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
<script src='/env.js'></script>
|
||||
<title>Dashboard</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.
|
||||
|
||||
|
@ -39,5 +39,6 @@
|
|||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -3,17 +3,17 @@
|
|||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"src": "assets/lit_logos/favicon_lit_transp.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"src": "assets/lit_logos/lit_transp_192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"src": "assets/lit_logos/lit_transp_512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
# Nextcloud
|
||||
|
||||
![Nextcloud](/assets/nextcloud.svg 'Nextcloud')
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum
|
||||
augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris.
|
||||
Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In
|
||||
consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum
|
||||
ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis
|
||||
accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor
|
||||
consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu
|
||||
ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet.
|
||||
Sed fringilla vel justo nec pellentesque.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
|
@ -1,37 +0,0 @@
|
|||
# Wekan
|
||||
|
||||
![Wekan](/assets/wekan.svg 'Wekan')
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum
|
||||
augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris.
|
||||
Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In
|
||||
consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum
|
||||
ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis
|
||||
accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor
|
||||
consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu
|
||||
ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet.
|
||||
Sed fringilla vel justo nec pellentesque.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
|
@ -1,37 +0,0 @@
|
|||
# Wordpress
|
||||
|
||||
![Wordpress](/assets/wordpress.svg 'Wordpress')
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum
|
||||
augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris.
|
||||
Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In
|
||||
consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum
|
||||
ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis
|
||||
accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor
|
||||
consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu
|
||||
ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet.
|
||||
Sed fringilla vel justo nec pellentesque.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
|
@ -1,37 +0,0 @@
|
|||
# Zulip
|
||||
|
||||
![Zulip](/assets/zulip.svg 'Zulip')
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam vel felis rutrum, congue orci non, dictum
|
||||
augue. In hac habitasse platea dictumst. Donec enim neque, vehicula vel consequat non, facilisis sed mauris.
|
||||
Quisque a ligula sed velit gravida tristique. Mauris id nisi convallis, porttitor ante sed, blandit odio. In
|
||||
consequat faucibus dolor, id aliquam quam. Fusce a faucibus tellus. Ut vitae ligula a ex consectetur rutrum
|
||||
ultricies ac velit. Nullam in efficitur velit, efficitur euismod nulla. Mauris feugiat posuere libero, quis
|
||||
accumsan ipsum mollis quis. Quisque at sapien lacus. Etiam aliquet, enim non pulvinar rhoncus, enim dolor
|
||||
consectetur risus, a pharetra eros risus sed velit. Phasellus tristique feugiat ipsum, eget rhoncus arcu
|
||||
ultrices nec. Nam et quam et sem tempor semper dictum nec ipsum. Aenean lobortis mauris non fringilla laoreet.
|
||||
Sed fringilla vel justo nec pellentesque.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
||||
|
||||
Quisque ac sem ipsum. Mauris interdum non risus sed gravida. Integer sit amet metus pharetra, tristique odio
|
||||
a, rhoncus augue. Nam vitae neque in mi rutrum aliquam. Suspendisse vulputate efficitur venenatis. Proin
|
||||
lobortis eros et velit commodo, sed sollicitudin sem maximus. Mauris ut tellus ipsum. Donec facilisis sed
|
||||
ipsum vitae volutpat. Pellentesque viverra ex vel mi blandit, vel eleifend libero tincidunt. Vestibulum nec
|
||||
felis congue, ultrices eros sit amet, maximus lacus.
|
||||
|
||||
Duis faucibus, tellus a commodo accumsan, felis mauris tincidunt ante, vel semper magna felis eget erat. Nam
|
||||
vel odio non diam auctor pretium nec nec dui. Duis non dui ornare sem aliquet malesuada vitae sed odio. Etiam
|
||||
porttitor ligula orci, in tristique ligula laoreet non. Nulla pulvinar mattis nisi volutpat hendrerit. Nunc
|
||||
massa velit, feugiat vitae posuere sed, volutpat tristique ligula. Fusce a vulputate orci. Ut cursus mattis
|
||||
malesuada.
|
72
src/App.tsx
|
@ -1,54 +1,62 @@
|
|||
import React from 'react';
|
||||
import React, { createContext } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Routes, Route, Navigate, Outlet } from 'react-router-dom';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from 'src/services/auth';
|
||||
import { Dashboard, Users, Login } from './modules';
|
||||
import { fetchApps } from './services/apps/api';
|
||||
import { Layout } from './components';
|
||||
import { LoginCallback } from './modules/login/LoginCallback';
|
||||
import { Dashboard } from './modules';
|
||||
import { AppIframe } from './modules/dashboard/AppIframe';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function App() {
|
||||
const { authToken, currentUser, isAdmin } = useAuth();
|
||||
|
||||
const redirectToLogin = !authToken || !currentUser?.app_roles;
|
||||
|
||||
const ProtectedRoute = () => {
|
||||
return isAdmin ? <Outlet /> : <Navigate to="/dashboard" />;
|
||||
};
|
||||
|
||||
const host = window.location.hostname;
|
||||
const splitedDomain = host.split('.');
|
||||
splitedDomain.shift();
|
||||
const rootDomain = splitedDomain.join('.');
|
||||
// @ts-ignore
|
||||
const DASHBOARD_APPS = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Dateiablage',
|
||||
assetSrc: '/assets/nextcloud.svg',
|
||||
internalUrl: `files`,
|
||||
externalUrl: `https://cloud`,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Projektboard',
|
||||
assetSrc: '/assets/vikunja.png',
|
||||
internalUrl: `projects`,
|
||||
externalUrl: `https://vikunja`,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Stackspin</title>
|
||||
<title>Dashboard</title>
|
||||
<meta name="description" content="Stackspin" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon_lit_transp.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/assets/lit_logos/lit_transp_32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/lit_logos/lit_transp_16x16" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</Helmet>
|
||||
|
||||
<div className="app bg-gray-50 min-h-screen flex flex-col">
|
||||
{redirectToLogin ? (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/login-callback" element={<LoginCallback />} />
|
||||
<Route path="*" element={<Navigate to="/login" />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
{
|
||||
// @ts-ignore
|
||||
DASHBOARD_APPS.map((app) => (
|
||||
<Route key={app.name} path={app.internalUrl} element={<AppIframe app={app} />} />
|
||||
))
|
||||
}
|
||||
<Route path="*" element={<Navigate to="/dashboard" />} />
|
||||
</Routes>
|
||||
) : (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/users" element={<ProtectedRoute />}>
|
||||
<Route path="/users" element={<Users />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/dashboard" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
)}
|
||||
</Layout>
|
||||
|
||||
{/* Place to load notifications */}
|
||||
<div
|
||||
|
|
|
@ -1,63 +1,33 @@
|
|||
import React, { Fragment, useMemo, useState } from 'react';
|
||||
import { Disclosure, Menu, Transition } from '@headlessui/react';
|
||||
import React from 'react';
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
import Gravatar from 'react-gravatar';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { UserModal } from '../UserModal';
|
||||
|
||||
const HYDRA_LOGOUT_URL = `${process.env.REACT_APP_HYDRA_PUBLIC_URL}/oauth2/sessions/logout`;
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', to: '/dashboard', requiresAdmin: false },
|
||||
{ name: 'Users', to: '/users', requiresAdmin: true },
|
||||
];
|
||||
|
||||
function classNames(...classes: any[]) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function filterNavigationByDashboardRole(isAdmin: boolean) {
|
||||
if (isAdmin) {
|
||||
return navigation;
|
||||
}
|
||||
|
||||
return navigation.filter((item) => !item.requiresAdmin);
|
||||
}
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { fetchApps } from '../../services/apps/api';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface HeaderProps {}
|
||||
|
||||
const Header: React.FC<HeaderProps> = () => {
|
||||
const [currentUserModal, setCurrentUserModal] = useState(false);
|
||||
const [currentUserId, setCurrentUserId] = useState(null);
|
||||
const { logOut, currentUser, isAdmin } = useAuth();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const currentUserModalOpen = (id: any) => {
|
||||
setCurrentUserId(id);
|
||||
setCurrentUserModal(true);
|
||||
};
|
||||
|
||||
const currentUserModalClose = () => {
|
||||
setCurrentUserModal(false);
|
||||
setCurrentUserId(null);
|
||||
};
|
||||
|
||||
const navigationItems = filterNavigationByDashboardRole(isAdmin);
|
||||
|
||||
const signOutUrl = useMemo(() => {
|
||||
const { hostname } = window.location;
|
||||
// If we are developing locally, we need to use the init cluster's public URL
|
||||
if (hostname === 'localhost') {
|
||||
return HYDRA_LOGOUT_URL;
|
||||
}
|
||||
return `https://${hostname.replace(/^dashboard/, 'sso')}/oauth2/sessions/logout`;
|
||||
}, []);
|
||||
const rootDomain = 'foobar';
|
||||
// @ts-ignore
|
||||
const DASHBOARD_APPS = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Dateiablage',
|
||||
assetSrc: '/assets/nextcloud.svg',
|
||||
internalUrl: `files`,
|
||||
externalUrl: `https://cloud`,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Projektboard',
|
||||
assetSrc: '/assets/vikunja.png',
|
||||
internalUrl: `projects`,
|
||||
externalUrl: `https://vikunja`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -65,10 +35,10 @@ const Header: React.FC<HeaderProps> = () => {
|
|||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
|
||||
<div className="relative flex justify-between h-16">
|
||||
<div className="relative flex justify-between h-10">
|
||||
<div className="absolute inset-y-0 left-0 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-primary-500">
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
|
@ -79,110 +49,60 @@ const Header: React.FC<HeaderProps> = () => {
|
|||
</div>
|
||||
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
|
||||
<Link to="/" className="flex-shrink-0 flex items-center">
|
||||
<img className="block lg:hidden" src="/assets/logo-small.svg" alt="Stackspin" />
|
||||
<img className="hidden lg:block" src="/assets/logo.svg" alt="Stackspin" />
|
||||
<img className="block lg:hidden" src="/assets/lit_logos/lit_transp_title_52.png" alt="Local-IT" />
|
||||
<img className="hidden lg:block" src="/assets/lit_logos/lit_transp_title_52.png" alt="Local-IT" />
|
||||
</Link>
|
||||
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
|
||||
{/* Current: "border-primary-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||
{navigationItems.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
className={clsx(
|
||||
'border-primary-50 text-gray-900 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium',
|
||||
{
|
||||
'border-primary-500 text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
||||
pathname.includes(item.to),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
{
|
||||
// @ts-ignore
|
||||
DASHBOARD_APPS.map((app) => (
|
||||
<Link
|
||||
key={app.name}
|
||||
to={app.internalUrl}
|
||||
className={clsx(
|
||||
'border-primary-50 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium litbutton',
|
||||
{
|
||||
'border-primary-500 litbutton-active hover:border-gray-300 inline-flex items-center px-1 pt-1 text-sm font-medium':
|
||||
pathname.includes(app.internalUrl),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{app.name}
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
|
||||
{/* Profile dropdown */}
|
||||
<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-primary">
|
||||
<span className="sr-only">Open user menu</span>
|
||||
<span className="inline-flex items-center justify-center h-8 w-8 rounded-full bg-gray-500 overflow-hidden">
|
||||
<Gravatar email={currentUser?.email || undefined} size={32} rating="pg" protocol="https://" />
|
||||
</span>
|
||||
</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 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => currentUserModalOpen(currentUser?.id)}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Configure profile
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
onClick={() => logOut()}
|
||||
href={signOutUrl}
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 cursor-pointer' : '',
|
||||
'block px-4 py-2 text-sm text-gray-700 cursor-pointer',
|
||||
)}
|
||||
>
|
||||
Sign out
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="sm:hidden">
|
||||
<div className="pt-2 pb-4 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.to}
|
||||
className={clsx(
|
||||
'border-transparent text-gray-500 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium',
|
||||
{
|
||||
'bg-primary-50 border-primary-400 text-primary-700 block pl-3 pr-4 py-2': pathname.includes(
|
||||
item.to,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
{
|
||||
// @ts-ignore
|
||||
DASHBOARD_APPS.map((app) => (
|
||||
<Link
|
||||
key={app.name}
|
||||
to={app.internalUrl}
|
||||
className={clsx(
|
||||
'border-transparent litbutton block pl-3 pr-4 py-2 border-l-4 litbutton text-base font-medium',
|
||||
{
|
||||
'litbutton-active border-primary-400 block pl-3 pr-4 py-2': pathname.includes(
|
||||
app.internalUrl,
|
||||
),
|
||||
},
|
||||
)}
|
||||
>
|
||||
{app.name}
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
||||
{currentUserModal && (
|
||||
<UserModal open={currentUserModal} onClose={currentUserModalClose} userId={currentUserId} setUserId={_.noop} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,5 +4,4 @@ export { Table } from './Table';
|
|||
export { Banner } from './Banner';
|
||||
export { Tabs } from './Tabs';
|
||||
export { Modal, ConfirmationModal, StepsModal } from './Modal';
|
||||
export { UserModal } from './UserModal';
|
||||
export { ProgressSteps } from './ProgressSteps';
|
||||
|
|
1
src/env.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
export const env = { ...process.env, ...window.env };
|
1
src/index.css
vendored
|
@ -1,6 +1,7 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "./lit_navigation_style.css";
|
||||
|
||||
div[tabindex] {
|
||||
flex: 1;
|
||||
|
|
57
src/lit_navigation_style.css
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
.litbutton{
|
||||
color: #755d86;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.litbutton-card{
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.litbutton-card:before,
|
||||
.litbutton:before {
|
||||
content: "[";
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
-webkit-transform: translateX(20px);
|
||||
-moz-transform: translateX(20px);
|
||||
transform: translateX(20px);
|
||||
-webkit-transition: -webkit-transform 0.3s, opacity 0.2s;
|
||||
-moz-transition: -moz-transform 0.3s, opacity 0.2s;
|
||||
transition: transform 0.3s, opacity 0.2s;
|
||||
}
|
||||
.litbutton:before{
|
||||
margin-right: 10px;
|
||||
}
|
||||
.litbutton-card:after,
|
||||
.litbutton:after {
|
||||
content: "]";
|
||||
display: inline-block;
|
||||
opacity: 0;
|
||||
-webkit-transition: -webkit-transform 0.3s, opacity 0.2s;
|
||||
-moz-transition: -moz-transform 0.3s, opacity 0.2s;
|
||||
transition: transform 0.3s, opacity 0.2s;
|
||||
-webkit-transform: translateX(-20px);
|
||||
-moz-transform: translateX(-20px);
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
.litbutton:after{
|
||||
margin-left: 10px;
|
||||
|
||||
}
|
||||
.litbutton-card:hover,
|
||||
.litbutton:hover{
|
||||
color: #3a97a3;
|
||||
}
|
||||
.litbutton-card:active,
|
||||
.litbutton-active{
|
||||
color: #3a97a3;
|
||||
}
|
||||
.litbutton-card:hover:before,
|
||||
.litbutton-card:hover:after,
|
||||
.litbutton:hover:before,
|
||||
.litbutton:hover:after {
|
||||
color: #3a97a3;
|
||||
opacity: 1;
|
||||
-webkit-transform: translateX(0px);
|
||||
-moz-transform: translateX(0px);
|
||||
transform: translateX(0px);
|
||||
}
|
|
@ -1,96 +0,0 @@
|
|||
import React from 'react';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import { XCircleIcon } from '@heroicons/react/outline';
|
||||
import { Tabs, Banner } from 'src/components';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { AdvancedTab, GeneralTab } from './components';
|
||||
|
||||
const pages = [
|
||||
{ name: 'Apps', to: '/apps', current: true },
|
||||
{ name: 'Nextcloud', to: '', current: false },
|
||||
];
|
||||
|
||||
const tabs = [
|
||||
{ name: 'General', component: <GeneralTab /> },
|
||||
{ name: 'Advanced Configuration', component: <AdvancedTab /> },
|
||||
];
|
||||
|
||||
export const AppSingle: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 flex-grow">
|
||||
<nav className="flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center space-x-4">
|
||||
<li>
|
||||
<div className="flex items-center">
|
||||
<Link to="/dashboard" className="text-sm font-medium text-gray-500 hover:text-gray-700">
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
{pages.map((page) => (
|
||||
<li key={page.name}>
|
||||
<div className="flex items-center">
|
||||
<ChevronRightIcon className="flex-shrink-0 h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<Link
|
||||
to={page.to}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
|
||||
aria-current={page.current ? 'page' : undefined}
|
||||
>
|
||||
{page.name}
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<Banner title="Managing single app instances coming soon." titleSm="Comming soon!" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden opacity-40 cursor-default pointer-events-none select-none">
|
||||
<div className="bg-white overflow-hidden shadow rounded-sm mb-5">
|
||||
<div className="px-4 py-5 sm:p-6 flex justify-between items-center">
|
||||
<div className="mr-4 flex items-center">
|
||||
<img
|
||||
className="h-24 w-24 rounded-md overflow-hidden mr-4"
|
||||
src="./../assets/nextcloud.svg"
|
||||
alt="Nextcloud"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="text-2xl leading-8 font-bold">Nextcloud</h2>
|
||||
<div className="text-sm leading-5 font-medium text-gray-500">Installed on August 25, 2020</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
className="mb-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-yellow-900 bg-yellow-300 hover:bg-yellow-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
|
||||
>
|
||||
<XCircleIcon className="-ml-0.5 mr-2 h-4 w-4 text-yellow-900" aria-hidden="true" />
|
||||
Disable App
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 justify-center"
|
||||
>
|
||||
View Documentation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow rounded-sm">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<Tabs tabs={tabs} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,222 +0,0 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useState, useCallback, useMemo } from 'react';
|
||||
import { ChevronRightIcon, SearchIcon, PlusIcon } from '@heroicons/react/solid';
|
||||
import { CogIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { ConfirmationModal } from 'src/components/Modal';
|
||||
import { Table, Banner } from 'src/components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const pages = [{ name: 'Apps', href: '#', current: true }];
|
||||
|
||||
export const Apps: React.FC = () => {
|
||||
const [selectedRowsIds, setSelectedRowsIds] = useState({});
|
||||
const [deleteModal, setDeleteModal] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const deleteModalOpen = () => setDeleteModal(true);
|
||||
const deleteModalClose = () => setDeleteModal(false);
|
||||
|
||||
const handleSearch = useCallback((event: any) => {
|
||||
setSearch(event.target.value);
|
||||
}, []);
|
||||
|
||||
const data: any[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Nextcloud',
|
||||
status: 'Active for everyone',
|
||||
assetSrc: './assets/nextcloud.svg',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Wekan',
|
||||
status: 'Active for everyone',
|
||||
assetSrc: './assets/wekan.svg',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Rocketchat',
|
||||
status: 'Active for everyone',
|
||||
assetSrc: './assets/rocketchat.svg',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Wordpress',
|
||||
status: 'Active for everyone',
|
||||
assetSrc: './assets/wordpress.svg',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const filterSearch = useMemo(() => {
|
||||
return data.filter((item: any) => item.name?.toLowerCase().includes(search.toLowerCase()));
|
||||
}, [search]);
|
||||
|
||||
const columns: any = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
Cell: (e: any) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-10 w-10">
|
||||
<img className="h-10 w-10 rounded-md overflow-hidden" src={e.cell.row.original.assetSrc} alt="" />
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<div className="text-sm font-medium text-gray-900">{e.cell.row.original.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
accessor: 'status',
|
||||
Cell: (e: any) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 h-4 w-4 rounded-full bg-green-600" />
|
||||
<div className="ml-2 text-sm text-green-600">{e.cell.row.original.status}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: ' ',
|
||||
Cell: () => {
|
||||
return (
|
||||
<div className="text-right opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => {}}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const selectedRows = useCallback((rows: Record<string, boolean>) => {
|
||||
setSelectedRowsIds(rows);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 flex-grow">
|
||||
<nav className="flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center space-x-4">
|
||||
<li>
|
||||
<div className="flex items-center">
|
||||
<Link to="/dashboard" className="text-sm font-medium text-gray-500 hover:text-gray-700">
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
{pages.map((page) => (
|
||||
<li key={page.name}>
|
||||
<div className="flex items-center">
|
||||
<ChevronRightIcon className="flex-shrink-0 h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<a
|
||||
href={page.href}
|
||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
|
||||
aria-current={page.current ? 'page' : undefined}
|
||||
>
|
||||
{page.name}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 overflow-hidden">
|
||||
<Banner
|
||||
title="Your app instances management will be here soon, in the meantime, feel free to explore."
|
||||
titleSm="Comming soon!"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto py-4 sm:px-6 lg:px-8 flex-grow">
|
||||
<div className="pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||
<h1 className="text-3xl leading-6 font-bold text-gray-900">Apps</h1>
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
disabled
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
|
||||
>
|
||||
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Add new app
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between w-100 my-3 items-center">
|
||||
<div className="flex items-center">
|
||||
<div className="inline-block">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 sr-only">
|
||||
Search candidates
|
||||
</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<div className="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
className="focus:ring-primary-dark focus:border-primary-dark block w-full rounded-md pl-10 sm:text-sm border-gray-200"
|
||||
placeholder="Search Apps"
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
|
||||
<button
|
||||
onClick={deleteModalOpen}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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 data={filterSearch} columns={columns} getSelectedRowIds={selectedRows} selectable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
open={deleteModal}
|
||||
onClose={deleteModalClose}
|
||||
title="Delete service"
|
||||
body="Are you sure you want to delete this service? All of your data will be permanently removed. This action cannot be undone."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,164 +0,0 @@
|
|||
import React, { Fragment } from 'react';
|
||||
import Editor from 'react-simple-code-editor';
|
||||
import { Menu, Transition } from '@headlessui/react';
|
||||
import { ChevronDownIcon } from '@heroicons/react/solid';
|
||||
import { highlight, languages } from 'prismjs';
|
||||
import 'prismjs/components/prism-clike';
|
||||
import 'prismjs/components/prism-yaml';
|
||||
import 'prismjs/themes/prism.css';
|
||||
import { initialEditorYaml } from '../../consts';
|
||||
import { Secrets } from './components';
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export const AdvancedTab = () => {
|
||||
const [code, setCode] = React.useState(initialEditorYaml);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between mb-5">
|
||||
<h1 className="text-2xl leading-6 font-medium text-gray-900">Configuration</h1>
|
||||
</div>
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-8">
|
||||
<div>
|
||||
<div>
|
||||
<div className="px-4 h-16 sm:px-6 bg-gray-200 flex justify-between items-center rounded-t-lg">
|
||||
<span className="text-gray-600 text-lg leading-6 font-medium">Edit Configuration</span>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex justify-center w-full rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-primary-500">
|
||||
Versions
|
||||
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
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="z-10 origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href="#"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||
'block px-4 py-2 text-sm',
|
||||
)}
|
||||
>
|
||||
Save Version
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href="#"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||
'block px-4 py-2 text-sm',
|
||||
)}
|
||||
>
|
||||
View Version History
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<a
|
||||
href="#"
|
||||
className={classNames(
|
||||
active ? 'bg-gray-100 text-gray-900' : 'text-gray-700',
|
||||
'block px-4 py-2 text-sm',
|
||||
)}
|
||||
>
|
||||
Restore Current Version
|
||||
</a>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</Menu>
|
||||
</div>
|
||||
<div className="px-4 py-5 sm:p-6 border border-t-0 border-gray-200">
|
||||
<Editor
|
||||
value={code}
|
||||
onValueChange={(value) => setCode(value)}
|
||||
highlight={(value) => highlight(value, languages.js, 'yaml')}
|
||||
preClassName="font-mono whitespace-normal font-light"
|
||||
textareaClassName="font-mono overflow-auto font-light"
|
||||
className="font-mono text-sm font-light"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="bg-gray-100 overflow-hidden rounded-lg">
|
||||
<div className="px-4 h-16 sm:px-6 bg-gray-200 flex items-center">
|
||||
<span className="text-gray-600 text-lg leading-6 font-medium">Current Configuration</span>
|
||||
</div>
|
||||
<div className="px-4 py-5 sm:p-6 overflow-x-auto">
|
||||
<pre className="font-mono text-sm font-light">
|
||||
{`luck: except
|
||||
natural: still
|
||||
near: though
|
||||
search:
|
||||
- feature
|
||||
- - 1980732354.689713
|
||||
- hour
|
||||
- butter:
|
||||
ordinary: 995901949.8974948
|
||||
teeth: true
|
||||
whole:
|
||||
- -952367353
|
||||
- - talk: -1773961379
|
||||
temperature: false
|
||||
oxygen: true
|
||||
laugh:
|
||||
flag:
|
||||
in: 2144751662
|
||||
hospital: -1544066384.1973226
|
||||
law: congress
|
||||
great: stomach`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end mt-10">
|
||||
<button
|
||||
type="button"
|
||||
className="mr-3 inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mr-3 inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-primary-700 bg-primary-100 hover:bg-primary-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
Save changes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Secrets />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,99 +0,0 @@
|
|||
import React, { Fragment, useRef } from 'react';
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
|
||||
type ChangeSecretModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSave?: () => void;
|
||||
};
|
||||
|
||||
export const ChangeSecretModal = ({ open, onClose, onSave = () => {} }: ChangeSecretModalProps) => {
|
||||
const cancelButtonRef = useRef(null);
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
auto-reopen="true"
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
initialFocus={cancelButtonRef}
|
||||
onClose={onClose}
|
||||
>
|
||||
<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">
|
||||
​
|
||||
</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 rounded-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="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-gray-900">
|
||||
Change secret
|
||||
</Dialog.Title>
|
||||
<div className="my-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
<span className="font-bold text-red-500">CAUTION:</span> changing this might cause the app to
|
||||
break.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<label htmlFor="appSecret" className="block text-sm font-medium text-gray-700">
|
||||
App Secret
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
type="text"
|
||||
name="appSecret"
|
||||
id="appSecret"
|
||||
className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
placeholder="Enter New App Secret"
|
||||
/>
|
||||
</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="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={onSave}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-200 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-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={onClose}
|
||||
ref={cancelButtonRef}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
|
@ -1,136 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { ChevronDownIcon, ChevronRightIcon, ClipboardIcon, KeyIcon } from '@heroicons/react/outline';
|
||||
import { showToast } from 'src/common/util/show-toast';
|
||||
import { Table } from 'src/components';
|
||||
import { ChangeSecretModal } from './ChangeSecretModal';
|
||||
|
||||
export const Secrets = () => {
|
||||
const [secretModal, setSecretModal] = useState(false);
|
||||
const [openDangerZone, setOpenDangerZone] = useState(false);
|
||||
|
||||
const secretModalOpen = () => setSecretModal(true);
|
||||
const secretModalClose = () => setSecretModal(false);
|
||||
|
||||
const secretsModalSave = () => {
|
||||
showToast('The secret has been saved successfully.');
|
||||
setSecretModal(false);
|
||||
};
|
||||
|
||||
const toggleDangerZone = () => setOpenDangerZone((prevState) => !prevState);
|
||||
|
||||
const columns: any = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
Header: 'Secret',
|
||||
accessor: 'secret',
|
||||
Cell: (e: any) => {
|
||||
const [reveal, setReveal] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
{!reveal ? (
|
||||
<>
|
||||
<div className="w-48 bg-gray-200 h-4 rounded-full mr-4" />
|
||||
<button
|
||||
onClick={() => setReveal(true)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-2.5 py-1.5 text-xs font-medium rounded text-gray-700 hover:bg-gray-200 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
Reveal secret
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div>{e.cell.row.original.secret}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: ' ',
|
||||
Cell: () => {
|
||||
return (
|
||||
<div className="flex items-center justify-end opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => showToast('The secret has been copied to your clipboard.')}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 mr-3"
|
||||
>
|
||||
<ClipboardIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Copy Secret
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={secretModalOpen}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-red-400 shadow-sm text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
<KeyIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Change secret
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const data: any[] = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'nextcloud_mariadb_password',
|
||||
secret: 'xEtC2dvUsBVzoTA19fOyDF4IwpPSWW62I92F59s69EO9vp56',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'nextcloud_mariadb_root_password',
|
||||
secret: `OB.IHD:HPY{x<)>@+B'p(LUU@k-Wv=w(!@)?7Zq%@nE7GY]!`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'nextcloud_password (admin login)',
|
||||
secret: `#B&>4A;O#XEF]_dE*zxF26>8fF:akgGL8gi#yALK{ZY[P$eE`,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="pb-5 border-b border-gray-200 sm:flex sm:items-center sm:justify-between mt-24 mb-5 cursor-pointer"
|
||||
onClick={toggleDangerZone}
|
||||
>
|
||||
<h1 className="text-2xl leading-6 font-medium text-gray-900">Danger zone</h1>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-900 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
{openDangerZone ? (
|
||||
<ChevronDownIcon className="flex-shrink-0 h-5 w-5 text-gray-600" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronRightIcon className="flex-shrink-0 h-5 w-5 text-gray-600" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{openDangerZone && (
|
||||
<div className="border-2 border-red-500 rounded-md overflow-hidden">
|
||||
<div className="border border-gray-200">
|
||||
<Table data={data} columns={columns} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChangeSecretModal open={secretModal} onClose={secretModalClose} onSave={secretsModalSave} />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export { Secrets } from './Secrets';
|
||||
export { ChangeSecretModal } from './ChangeSecretModal';
|
|
@ -1 +0,0 @@
|
|||
export { AdvancedTab } from './AdvancedTab';
|
|
@ -1,161 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { RadioGroup } from '@headlessui/react';
|
||||
|
||||
function classNames(...classes: any) {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
const settings = [
|
||||
{ name: 'Public access', description: 'This project would be available to anyone who has the link' },
|
||||
{ name: 'Private to Project Members', description: 'Only members of this project would be able to access' },
|
||||
{ name: 'Private to you', description: 'You are the only one able to access this project' },
|
||||
];
|
||||
|
||||
export const GeneralTab = () => {
|
||||
const [selected, setSelected] = useState(settings[0]);
|
||||
|
||||
return (
|
||||
<div className="py-4">
|
||||
<div>
|
||||
<div className="md:grid md:grid-cols-3 md:gap-6">
|
||||
<div className="md:col-span-1">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Privacy</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Change your app privacy</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 md:mt-0 md:col-span-2">
|
||||
<div className="mt-1">
|
||||
<RadioGroup value={selected} onChange={setSelected}>
|
||||
<RadioGroup.Label className="sr-only">Privacy setting</RadioGroup.Label>
|
||||
<div className="bg-white rounded-md -space-y-px">
|
||||
{settings.map((setting, settingIdx) => (
|
||||
<RadioGroup.Option
|
||||
key={setting.name}
|
||||
value={setting}
|
||||
className={({ checked }) =>
|
||||
classNames(
|
||||
settingIdx === 0 ? 'rounded-tl-md rounded-tr-md' : '',
|
||||
settingIdx === settings.length - 1 ? 'rounded-bl-md rounded-br-md' : '',
|
||||
checked ? 'bg-primary-50 border-primary-400 z-10' : 'border-gray-200',
|
||||
'relative border p-4 flex cursor-pointer focus:outline-none',
|
||||
)
|
||||
}
|
||||
>
|
||||
{({ active, checked }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
checked ? 'bg-primary-600 border-transparent' : 'bg-white border-gray-300',
|
||||
active ? 'ring-2 ring-offset-2 ring-primary-100' : '',
|
||||
'h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center',
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||
</span>
|
||||
<div className="ml-3 flex flex-col">
|
||||
<RadioGroup.Label
|
||||
as="span"
|
||||
className={classNames(
|
||||
checked ? 'text-primary-900' : 'text-gray-900',
|
||||
'block text-sm font-medium',
|
||||
)}
|
||||
>
|
||||
{setting.name}
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className={classNames(checked ? 'text-primary-900' : 'text-gray-500', 'block text-sm')}
|
||||
>
|
||||
{setting.description}
|
||||
</RadioGroup.Description>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10">
|
||||
<div className="md:grid md:grid-cols-3 md:gap-6">
|
||||
<div className="md:col-span-1">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Notifications</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Change you notifications settings</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 md:mt-0 md:col-span-2">
|
||||
<fieldset className="space-y-5 -mt-4">
|
||||
<legend className="sr-only">Notifications</legend>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="comments"
|
||||
aria-describedby="comments-description"
|
||||
name="comments"
|
||||
type="checkbox"
|
||||
className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="comments" className="font-medium text-gray-700">
|
||||
Comments
|
||||
</label>
|
||||
<p id="comments-description" className="text-gray-500">
|
||||
Get notified when someones posts a comment on a posting.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="candidates"
|
||||
aria-describedby="candidates-description"
|
||||
name="candidates"
|
||||
type="checkbox"
|
||||
className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="candidates" className="font-medium text-gray-700">
|
||||
Candidates
|
||||
</label>
|
||||
<p id="candidates-description" className="text-gray-500">
|
||||
Get notified when a candidate applies for a job.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="offers"
|
||||
aria-describedby="offers-description"
|
||||
name="offers"
|
||||
type="checkbox"
|
||||
className="focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="offers" className="font-medium text-gray-700">
|
||||
Offers
|
||||
</label>
|
||||
<p id="offers-description" className="text-gray-500">
|
||||
Get notified when a candidate accepts or rejects an offer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { GeneralTab } from './GeneralTab';
|
|
@ -1,2 +0,0 @@
|
|||
export { GeneralTab } from './GeneralTab';
|
||||
export { AdvancedTab } from './AdvancedTab';
|
|
@ -1,23 +0,0 @@
|
|||
export const initialEditorYaml = () => {
|
||||
return `luck: except
|
||||
natural: still
|
||||
near: though
|
||||
search:
|
||||
- feature
|
||||
- - 1980732354.689713
|
||||
- hour
|
||||
- butter:
|
||||
ordinary: 995901949.8974948
|
||||
teeth: true
|
||||
whole:
|
||||
- -952367353
|
||||
- - talk: -1773961379
|
||||
temperature: false
|
||||
oxygen: true
|
||||
laugh:
|
||||
flag:
|
||||
in: 2144751662
|
||||
hospital: -1544066384.1973226
|
||||
law: congress
|
||||
great: stomach`;
|
||||
};
|
|
@ -1,2 +0,0 @@
|
|||
export { Apps } from './Apps';
|
||||
export { AppSingle } from './AppSingle';
|
21
src/modules/dashboard/AppIframe.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
import Iframe from 'react-iframe';
|
||||
|
||||
export const AppIframe: React.FC<any> = ({ app }: { app: any }) => {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div style={{ minHeight: '95vh' }}>
|
||||
<Iframe
|
||||
height="100%"
|
||||
width="100%"
|
||||
position="absolute"
|
||||
frameBorder={0}
|
||||
overflow="hidden"
|
||||
scrolling="no"
|
||||
title={app.name}
|
||||
url={app.externalUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,13 +1,14 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { DASHBOARD_APPS, DASHBOARD_QUICK_ACCESS } from './consts';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useApps } from 'src/services/apps';
|
||||
import { DashboardCard } from './components';
|
||||
|
||||
export const Dashboard: React.FC = () => {
|
||||
const host = window.location.hostname;
|
||||
const splitedDomain = host.split('.');
|
||||
splitedDomain.shift();
|
||||
const rootDomain = splitedDomain.join('.');
|
||||
const { apps, loadApps, appTableLoading } = useApps();
|
||||
const foo = 42;
|
||||
|
||||
useEffect(() => {
|
||||
loadApps();
|
||||
}, [foo]);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
|
@ -16,40 +17,11 @@ export const Dashboard: React.FC = () => {
|
|||
<h1 className="text-3xl leading-6 font-bold text-gray-900">Dashboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 md:gap-4 lg:grid-cols-4 mb-10">
|
||||
{DASHBOARD_APPS(rootDomain!).map((app) => (
|
||||
<DashboardCard app={app} key={app.name} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto py-4 sm:px-6 lg:px-8 h-full flex-grow">
|
||||
<div className="pb-4 border-b border-gray-200 sm:flex sm:items-center">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Utilities</h3>
|
||||
</div>
|
||||
|
||||
<dl className="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{DASHBOARD_QUICK_ACCESS(rootDomain!).map((item) => (
|
||||
<a
|
||||
href={item.url}
|
||||
key={item.name}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={clsx('bg-white rounded-lg overflow-hidden sm:p-2 flex items-center group', {
|
||||
'opacity-40 cursor-default': !item.active,
|
||||
})}
|
||||
>
|
||||
<div className="w-16 h-16 flex items-center justify-center bg-primary-100 group-hover:bg-primary-200 transition-colors rounded-lg mr-4">
|
||||
<item.icon className="h-6 w-6 text-primary-900" aria-hidden="true" />
|
||||
</div>
|
||||
<div>
|
||||
<dt className="truncate text-sm leading-5 font-medium">{item.name}</dt>
|
||||
<dd className="mt-1 text-gray-500 text-sm leading-5 font-normal">{item.description}</dd>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</dl>
|
||||
{!appTableLoading &&
|
||||
// @ts-ignore
|
||||
apps.map((app) => <DashboardCard app={app} key={app.name} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,53 +1,34 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal } from 'src/components';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const DashboardCard: React.FC<any> = ({ app }: { app: any }) => {
|
||||
const [readMoreVisible, setReadMoreVisible] = useState(false);
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const onReadMoreCloseClick = () => setReadMoreVisible(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(app.markdownSrc)
|
||||
.then((res) => res.text())
|
||||
.then((md) => {
|
||||
return setContent(md);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [app.markdownSrc]);
|
||||
const url = `/${app.internalUrl}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0" key={app.name}>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="mr-4 flex items-center">
|
||||
<img
|
||||
className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0"
|
||||
src={app.assetSrc}
|
||||
alt="Nextcloud"
|
||||
/>
|
||||
<Link
|
||||
to={url}
|
||||
// className="mx-1 inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
<div
|
||||
className="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-100 mb-4 md:mb-0"
|
||||
key={app.name}
|
||||
>
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="mr-4 flex items-center">
|
||||
<img
|
||||
className="h-16 w-16 rounded-md overflow-hidden mr-4 flex-shrink-0"
|
||||
src={`/assets/${app.assetSrc}`}
|
||||
alt={app.name}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl leading-8 font-bold">{app.name}</h2>
|
||||
<div>
|
||||
<h2 className="text-xl litbutton-card leading-8 font-bold">{app.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2.5 py-2.5 sm:px-4 flex justify-end">
|
||||
<a
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center px-2.5 py-1.5 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
Launch App
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal open={readMoreVisible} onClose={onReadMoreCloseClick} title={app.name}>
|
||||
<ReactMarkdown className="prose">{content}</ReactMarkdown>
|
||||
</Modal>
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import { ChartBarIcon, InformationCircleIcon } from '@heroicons/react/outline';
|
||||
|
||||
export const DASHBOARD_APPS = (rootDomain: string) => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Nextcloud',
|
||||
assetSrc: '/assets/nextcloud.svg',
|
||||
markdownSrc: '/markdown/nextcloud.md',
|
||||
url: `https://files.${rootDomain}`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Wekan',
|
||||
assetSrc: '/assets/wekan.svg',
|
||||
markdownSrc: '/markdown/wekan.md',
|
||||
url: `https://wekan.${rootDomain}`,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Zulip',
|
||||
assetSrc: '/assets/zulip.svg',
|
||||
markdownSrc: '/markdown/zulip.md',
|
||||
url: `https://zulip.${rootDomain}`,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Wordpress',
|
||||
assetSrc: '/assets/wordpress.svg',
|
||||
markdownSrc: '/markdown/wordpress.md',
|
||||
url: `https://www.${rootDomain}`,
|
||||
},
|
||||
];
|
||||
|
||||
export const DASHBOARD_QUICK_ACCESS = (rootDomain: string) => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Monitoring →',
|
||||
url: `https://grafana.${rootDomain}`,
|
||||
description: 'Monitor your system with Grafana',
|
||||
icon: ChartBarIcon,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Support →',
|
||||
url: 'https://docs.stackspin.net',
|
||||
description: 'Access documentation and forum',
|
||||
icon: InformationCircleIcon,
|
||||
active: true,
|
||||
},
|
||||
];
|
|
@ -1,4 +1 @@
|
|||
export { Login } from './login';
|
||||
export { Dashboard } from './dashboard';
|
||||
export { Apps, AppSingle } from './apps';
|
||||
export { Users } from './users';
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LockClosedIcon } from '@heroicons/react/solid';
|
||||
|
||||
import { performApiCall } from 'src/services/api';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function Login() {
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/login',
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (data.authorizationUrl) {
|
||||
window.location.href = data.authorizationUrl;
|
||||
}
|
||||
} catch (e: any) {
|
||||
showToast('Something went wrong', ToastType.Error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex justify-center">
|
||||
<img className="lg:block" src="assets/logo.svg" alt="Stackspin" />
|
||||
<h2 className="mt-6 text-center text-xl font-bold text-gray-900 sr-only">Sign in</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
type="button"
|
||||
className={clsx(
|
||||
'group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-dark hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500',
|
||||
)}
|
||||
>
|
||||
<span className="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<LockClosedIcon className="h-5 w-5 text-white group-hover:text-primary-light" aria-hidden="true" />
|
||||
</span>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function LoginCallback() {
|
||||
const currentURL = window.location.href;
|
||||
const indexOfQuestionMark = currentURL.indexOf('?');
|
||||
const params = currentURL.slice(indexOfQuestionMark);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { logIn } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
async function logInUser() {
|
||||
if (params.length > 2) {
|
||||
const res = await logIn(params);
|
||||
|
||||
// @ts-ignore
|
||||
if (!res.ok) {
|
||||
navigate('/login');
|
||||
showToast('Something went wrong, please try logging in again.', ToastType.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logInUser();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<div className="flex justify-center items-center border border-transparent text-base font-medium rounded-md text-white transition ease-in-out duration-150">
|
||||
<svg
|
||||
className="animate-spin h-10 w-10 text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle className="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path
|
||||
className="opacity-100"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-lg text-primary-600 mt-2">Logging You in, just a moment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { Login } from './Login';
|
|
@ -1,186 +0,0 @@
|
|||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import React, { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { SearchIcon, PlusIcon, ViewGridAddIcon } from '@heroicons/react/solid';
|
||||
import { CogIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { useUsers } from 'src/services/users';
|
||||
import { Table } from 'src/components';
|
||||
import { debounce } from 'lodash';
|
||||
import { useAuth } from 'src/services/auth';
|
||||
|
||||
import { UserModal } from '../../components/UserModal';
|
||||
import { MultipleUsersModal } from './components';
|
||||
|
||||
export const Users: React.FC = () => {
|
||||
const [selectedRowsIds, setSelectedRowsIds] = useState({});
|
||||
const [configureModal, setConfigureModal] = useState(false);
|
||||
const [multipleUsersModal, setMultipleUsersModal] = useState(false);
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [search, setSearch] = useState('');
|
||||
const { users, loadUsers, userTableLoading } = useUsers();
|
||||
const { isAdmin } = useAuth();
|
||||
|
||||
const handleSearch = (event: any) => {
|
||||
setSearch(event.target.value);
|
||||
};
|
||||
|
||||
const debouncedSearch = useCallback(debounce(handleSearch, 250), []);
|
||||
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
|
||||
return () => {
|
||||
debouncedSearch.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const filterSearch = useMemo(() => {
|
||||
return users.filter((item: any) => item.email?.toLowerCase().includes(search.toLowerCase()));
|
||||
}, [search, users]);
|
||||
|
||||
const configureModalOpen = (id: any) => {
|
||||
setUserId(id);
|
||||
setConfigureModal(true);
|
||||
};
|
||||
|
||||
const configureModalClose = () => setConfigureModal(false);
|
||||
|
||||
const multipleUsersModalClose = () => setMultipleUsersModal(false);
|
||||
|
||||
const columns: any = React.useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: 'Email',
|
||||
accessor: 'email',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: 'Status',
|
||||
accessor: 'status',
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
Header: ' ',
|
||||
Cell: (props: any) => {
|
||||
const { row } = props;
|
||||
|
||||
if (isAdmin) {
|
||||
return (
|
||||
<div className="text-right lg:opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={() => configureModalOpen(row.original.id)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-gray-200 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
>
|
||||
<CogIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Configure
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
width: 'auto',
|
||||
},
|
||||
],
|
||||
[isAdmin],
|
||||
);
|
||||
|
||||
const selectedRows = useCallback((rows: Record<string, boolean>) => {
|
||||
setSelectedRowsIds(rows);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="max-w-7xl mx-auto py-4 px-3 sm:px-6 lg:px-8 h-full flex-grow">
|
||||
<div className="pb-5 mt-6 border-b border-gray-200 sm:flex sm:items-center sm:justify-between">
|
||||
<h1 className="text-3xl leading-6 font-bold text-gray-900">Users</h1>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="mt-3 sm:mt-0 sm:ml-4">
|
||||
<button
|
||||
onClick={() => configureModalOpen(null)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800 mx-5 "
|
||||
>
|
||||
<PlusIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Add new user
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMultipleUsersModal(true)}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-700 hover:bg-primary-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-800"
|
||||
>
|
||||
<ViewGridAddIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Add new users
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between w-100 my-3 items-center mb-5 ">
|
||||
<div className="flex items-center">
|
||||
<div className="inline-block">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-gray-700 sr-only">
|
||||
Search candidates
|
||||
</label>
|
||||
<div className="mt-1 flex rounded-md shadow-sm">
|
||||
<div className="relative flex items-stretch flex-grow focus-within:z-10">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
id="email"
|
||||
className="focus:ring-primary-500 focus:border-primary-500 block w-full rounded-md pl-10 sm:text-sm border-gray-200"
|
||||
placeholder="Search Users"
|
||||
onChange={debouncedSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRowsIds && Object.keys(selectedRowsIds).length !== 0 && (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => {}}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 text-sm font-medium rounded-md text-red-700 bg-red-50 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
||||
>
|
||||
<TrashIcon className="-ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="-my-2 overflow-x-auto 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 overflow-hidden">
|
||||
<Table
|
||||
data={filterSearch as any}
|
||||
columns={columns}
|
||||
getSelectedRowIds={selectedRows}
|
||||
loading={userTableLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{configureModal && (
|
||||
<UserModal open={configureModal} onClose={configureModalClose} userId={userId} setUserId={setUserId} />
|
||||
)}
|
||||
{multipleUsersModal && <MultipleUsersModal open={multipleUsersModal} onClose={multipleUsersModalClose} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,248 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
|
||||
import { Banner, StepsModal, ProgressSteps } from 'src/components';
|
||||
import { Select, TextArea } from 'src/components/Form';
|
||||
import { MultipleUsersData, UserRole, useUsers } from 'src/services/users';
|
||||
import { allAppAccessList } from 'src/components/UserModal/consts';
|
||||
import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types';
|
||||
import { initialMultipleUsersForm, MultipleUsersModalProps } from './types';
|
||||
|
||||
export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => {
|
||||
const [steps, setSteps] = useState<ProgressStepInfo[]>([]);
|
||||
const [isAdminRoleSelected, setAdminRoleSelected] = useState(false);
|
||||
const { createUsers, userModalLoading } = useUsers();
|
||||
|
||||
const { control, handleSubmit } = useForm<MultipleUsersData>({
|
||||
defaultValues: initialMultipleUsersForm,
|
||||
});
|
||||
|
||||
const { fields, update } = useFieldArray({
|
||||
control,
|
||||
name: 'appRoles',
|
||||
});
|
||||
|
||||
const dashboardRole = useWatch({
|
||||
control,
|
||||
name: 'appRoles.0.role',
|
||||
});
|
||||
|
||||
const csvDataWatch = useWatch({
|
||||
control,
|
||||
name: 'csvUserData',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
|
||||
setAdminRoleSelected(isAdminDashboardRoleSelected);
|
||||
if (isAdminDashboardRoleSelected) {
|
||||
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin }));
|
||||
} else {
|
||||
fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dashboardRole]);
|
||||
|
||||
const renderUsersCsvDataInput = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">CSV data</h3>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<TextArea
|
||||
control={control}
|
||||
name="csvUserData"
|
||||
placeholder={`Please paste users in CSV format: email, name\nuser1@example.com,User One\nuser2@example.com,User Two`}
|
||||
rows={15}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderAppAccess = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
|
||||
</div>
|
||||
|
||||
{isAdminRoleSelected && (
|
||||
<div className="sm:col-span-6">
|
||||
<Banner title="Admin users automatically have admin-level access to all apps." titleSm="Admin user" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<div className="flow-root mt-6">
|
||||
<ul className="-my-5 divide-y divide-gray-200">
|
||||
{fields
|
||||
.filter((field) => field.name === 'dashboard')
|
||||
.map((item, index) => (
|
||||
<li className="py-4" key={item.name}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-shrink-0 flex-1 flex items-center">
|
||||
<img
|
||||
className="h-10 w-10 rounded-md overflow-hidden"
|
||||
src={_.find(allAppAccessList, ['name', item.name!])?.image}
|
||||
alt={item.name ?? 'Image'}
|
||||
/>
|
||||
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
|
||||
{_.find(allAppAccessList, ['name', item.name!])?.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Select
|
||||
key={item.id}
|
||||
control={control}
|
||||
name={`appRoles.${index}.role`}
|
||||
options={[
|
||||
{ value: UserRole.User, name: 'User' },
|
||||
{ value: UserRole.Admin, name: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{!isAdminRoleSelected &&
|
||||
fields.map((item, index) => {
|
||||
if (item.name === 'dashboard') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="py-4" key={item.name}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex-shrink-0 flex-1 flex items-center">
|
||||
<img
|
||||
className="h-10 w-10 rounded-md overflow-hidden"
|
||||
src={_.find(allAppAccessList, ['name', item.name!])?.image}
|
||||
alt={item.name ?? 'Image'}
|
||||
/>
|
||||
<h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
|
||||
{_.find(allAppAccessList, ['name', item.name!])?.label}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Select
|
||||
key={item.id}
|
||||
control={control}
|
||||
name={`appRoles.${index}.role`}
|
||||
disabled={isAdminRoleSelected}
|
||||
options={[
|
||||
{ value: UserRole.NoAccess, name: 'No Access' },
|
||||
{ value: UserRole.User, name: 'User' },
|
||||
{ value: UserRole.Admin, name: 'Admin' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSteps([
|
||||
{
|
||||
id: 'Step 1',
|
||||
name: 'Enter CSV user data',
|
||||
status: ProgressStepStatus.Current,
|
||||
},
|
||||
{
|
||||
id: 'Step 2',
|
||||
name: 'Define app access roles',
|
||||
status: ProgressStepStatus.Upcoming,
|
||||
},
|
||||
]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await handleSubmit((data) => createUsers(data))();
|
||||
} catch (e: any) {
|
||||
// Continue
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getActiveStepIndex = () => _.findIndex(steps, { status: ProgressStepStatus.Current });
|
||||
|
||||
const updateStepsStatus = (nextIndex: number) => {
|
||||
const updatedSteps = [...steps];
|
||||
_.forEach(updatedSteps, (step, index) => {
|
||||
if (index < nextIndex) {
|
||||
step.status = ProgressStepStatus.Complete;
|
||||
} else if (index === nextIndex) {
|
||||
step.status = ProgressStepStatus.Current;
|
||||
} else {
|
||||
step.status = ProgressStepStatus.Upcoming;
|
||||
}
|
||||
});
|
||||
setSteps(updatedSteps);
|
||||
};
|
||||
|
||||
const handleStepClick = (stepId: string) => {
|
||||
const activeStepIndex = _.findIndex(steps, { id: stepId });
|
||||
updateStepsStatus(activeStepIndex);
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
const nextIndex = getActiveStepIndex() + 1;
|
||||
updateStepsStatus(nextIndex);
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
const nextIndex = getActiveStepIndex() - 1;
|
||||
updateStepsStatus(nextIndex);
|
||||
};
|
||||
|
||||
const activeStepIndex = getActiveStepIndex();
|
||||
const showSave = !_.some(steps, { status: ProgressStepStatus.Upcoming });
|
||||
const showPrevious = _.some(steps, { status: ProgressStepStatus.Complete });
|
||||
|
||||
return (
|
||||
<StepsModal
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
onSave={handleSave}
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
showPreviousButton={showPrevious}
|
||||
isLoading={userModalLoading}
|
||||
useCancelButton
|
||||
showSaveButton={showSave}
|
||||
saveButtonDisabled={_.isEmpty(csvDataWatch)}
|
||||
>
|
||||
<div className="bg-white px-4">
|
||||
<div className="space-y-10 divide-y divide-gray-200">
|
||||
<div>
|
||||
<div>
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">Add new users</h3>
|
||||
</div>
|
||||
<div className="sm:px-6 pt-6">
|
||||
<ProgressSteps steps={steps} onStepClick={handleStepClick}>
|
||||
{activeStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()}
|
||||
</ProgressSteps>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StepsModal>
|
||||
);
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { MultipleUsersModal } from './MultipleUsersModal';
|
|
@ -1,10 +0,0 @@
|
|||
import { initialAppRoles } from 'src/components/UserModal/consts';
|
||||
|
||||
export type MultipleUsersModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const initialMultipleUsersForm = {
|
||||
appRoles: initialAppRoles,
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export { MultipleUsersModal } from './MultipleUsersModal';
|
|
@ -1 +0,0 @@
|
|||
export { Users } from './Users';
|
|
@ -6,6 +6,9 @@ import storage from 'redux-persist/lib/storage';
|
|||
import { reducer as authReducer } from 'src/services/auth';
|
||||
|
||||
import usersReducer from 'src/services/users/redux/reducers';
|
||||
|
||||
import appsReducer from 'src/services/apps/redux/reducers';
|
||||
|
||||
import { State } from './types';
|
||||
|
||||
const persistConfig = {
|
||||
|
@ -17,6 +20,7 @@ const persistConfig = {
|
|||
const appReducer = combineReducers<State>({
|
||||
auth: authReducer,
|
||||
users: usersReducer,
|
||||
apps: appsReducer,
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, appReducer);
|
||||
|
|
|
@ -2,10 +2,12 @@ import { Store } from 'redux';
|
|||
|
||||
import { AuthState } from 'src/services/auth/redux';
|
||||
import { UsersState } from 'src/services/users/redux';
|
||||
import { AppsState } from 'src/services/apps/redux';
|
||||
|
||||
export interface AppStore extends Store, State {}
|
||||
|
||||
export interface State {
|
||||
auth: AuthState;
|
||||
users: UsersState;
|
||||
apps: AppsState;
|
||||
}
|
||||
|
|
27
src/services/apps/api.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { env } from '../../env';
|
||||
import { createApiCall, performApiCall } from '../api';
|
||||
import { transformApp } from './transformations';
|
||||
import { App } from './types';
|
||||
|
||||
export const fetchApps = async (): Promise<App> => {
|
||||
const apiUrl = env.REACT_APP_API_URL;
|
||||
|
||||
const res = await performApiCall({ hostname: apiUrl, path: '/apps', method: 'GET' });
|
||||
return transformApp(res);
|
||||
// return new Promise(() => [
|
||||
// {
|
||||
// id: '1',
|
||||
// name: 'Dateiablage',
|
||||
// assetSrc: '/assets/nextcloud.svg',
|
||||
// internalUrl: `files`,
|
||||
// externalUrl: `https://cloud`,
|
||||
// },
|
||||
// {
|
||||
// id: '2',
|
||||
// name: 'Projektboard',
|
||||
// assetSrc: '/assets/vikunja.png',
|
||||
// internalUrl: `projects`,
|
||||
// externalUrl: `https://vikunja`,
|
||||
// },
|
||||
// ]);
|
||||
};
|
1
src/services/apps/hooks/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { useApps } from './use-apps';
|
22
src/services/apps/hooks/use-apps.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { fetchApps } from '../redux/actions';
|
||||
import { getApps } from '../redux';
|
||||
import { getAppById, getAppslLoading } from '../redux/selectors';
|
||||
|
||||
export function useApps() {
|
||||
const dispatch = useDispatch();
|
||||
const apps = useSelector(getApps);
|
||||
const app = useSelector(getAppById);
|
||||
const appTableLoading = useSelector(getAppslLoading);
|
||||
|
||||
function loadApps() {
|
||||
return dispatch(fetchApps());
|
||||
}
|
||||
|
||||
return {
|
||||
apps,
|
||||
app,
|
||||
loadApps,
|
||||
appTableLoading,
|
||||
};
|
||||
}
|
7
src/services/apps/index.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export * from './types';
|
||||
|
||||
export { reducer } from './redux';
|
||||
|
||||
export { useApps } from './hooks';
|
||||
|
||||
export { fetchApps } from './api';
|
72
src/services/apps/redux/actions.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import _ from 'lodash';
|
||||
import { Dispatch } from 'redux';
|
||||
import { showToast, ToastType } from 'src/common/util/show-toast';
|
||||
import { State } from 'src/redux/types';
|
||||
import { performApiCall } from 'src/services/api';
|
||||
import { AuthActionTypes } from 'src/services/auth';
|
||||
import { transformApp } from '../transformations';
|
||||
|
||||
export enum AppActionTypes {
|
||||
FETCH_APPS = 'apps/fetch_apps',
|
||||
FETCH_APP = 'apps/fetch_app',
|
||||
UPDATE_APP = 'apps/update_app',
|
||||
CREATE_APP = 'apps/create_app',
|
||||
DELETE_APP = 'apps/delete_app',
|
||||
SET_APP_MODAL_LOADING = 'apps/app_modal_loading',
|
||||
SET_APPS_LOADING = 'apps/apps_loading',
|
||||
CREATE_BATCH_APPS = 'apps/create_batch_apps',
|
||||
}
|
||||
|
||||
export const setAppsLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: AppActionTypes.SET_APPS_LOADING,
|
||||
payload: isLoading,
|
||||
});
|
||||
};
|
||||
|
||||
export const setAppModalLoading = (isLoading: boolean) => (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: AppActionTypes.SET_APP_MODAL_LOADING,
|
||||
payload: isLoading,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchApps = () => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setAppsLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: '/apps',
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: AppActionTypes.FETCH_APPS,
|
||||
payload: data.map(transformApp),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setAppsLoading(false));
|
||||
};
|
||||
|
||||
export const fetchAPPById = (id: string) => async (dispatch: Dispatch<any>) => {
|
||||
dispatch(setAppModalLoading(true));
|
||||
|
||||
try {
|
||||
const { data } = await performApiCall({
|
||||
path: `/apps/${id}`,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: AppActionTypes.FETCH_APP,
|
||||
payload: transformApp(data),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
dispatch(setAppModalLoading(false));
|
||||
};
|
4
src/services/apps/redux/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export * from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getApps } from './selectors';
|
||||
export * from './types';
|
46
src/services/apps/redux/reducers.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { AppActionTypes } from './actions';
|
||||
|
||||
const initialAppsState: any = {
|
||||
apps: [],
|
||||
app: {},
|
||||
appModalLoading: false,
|
||||
appsLoading: false,
|
||||
};
|
||||
|
||||
const appsReducer = (state: any = initialAppsState, action: any) => {
|
||||
switch (action.type) {
|
||||
case AppActionTypes.FETCH_APPS:
|
||||
return {
|
||||
...state,
|
||||
apps: action.payload,
|
||||
};
|
||||
case AppActionTypes.SET_APP_MODAL_LOADING:
|
||||
return {
|
||||
...state,
|
||||
appModalLoading: action.payload,
|
||||
};
|
||||
case AppActionTypes.SET_APPS_LOADING:
|
||||
return {
|
||||
...state,
|
||||
appsLoading: action.payload,
|
||||
};
|
||||
case AppActionTypes.FETCH_APP:
|
||||
case AppActionTypes.UPDATE_APP:
|
||||
case AppActionTypes.CREATE_APP:
|
||||
case AppActionTypes.CREATE_BATCH_APPS:
|
||||
return {
|
||||
...state,
|
||||
isModalVisible: false,
|
||||
app: action.payload,
|
||||
};
|
||||
case AppActionTypes.DELETE_APP:
|
||||
return {
|
||||
...state,
|
||||
app: {},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default appsReducer;
|
5
src/services/apps/redux/selectors.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { State } from 'src/redux';
|
||||
|
||||
export const getApps = (state: State) => state.apps.apps;
|
||||
export const getAppById = (state: State) => state.apps.app;
|
||||
export const getAppslLoading = (state: State) => state.apps.appsLoading;
|
25
src/services/apps/redux/types.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { ApiStatus } from 'src/services/api/redux';
|
||||
|
||||
import { App } from '../types';
|
||||
|
||||
export interface CurrentUserState extends App {
|
||||
_status: ApiStatus;
|
||||
}
|
||||
|
||||
export interface AppsState {
|
||||
currentApp: CurrentUserState;
|
||||
apps: App[];
|
||||
app: App;
|
||||
appsLoading: boolean;
|
||||
}
|
||||
|
||||
// export interface CurrentUserUpdateAPI {
|
||||
// id: number;
|
||||
// phoneNumber?: string;
|
||||
// email?: string;
|
||||
// language?: string;
|
||||
// country?: string;
|
||||
// firstName?: string;
|
||||
// lastName?: string;
|
||||
// password?: string;
|
||||
// }
|
12
src/services/apps/transformations.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import _ from 'lodash';
|
||||
import { App } from './types';
|
||||
|
||||
export const transformApp = (response: any): App => {
|
||||
return {
|
||||
id: response.id ?? '',
|
||||
name: response.name ?? '',
|
||||
assetSrc: response.icon ?? '',
|
||||
internalUrl: response.internalUrl ?? '',
|
||||
externalUrl: response.externalUrl ?? '',
|
||||
};
|
||||
};
|
7
src/services/apps/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export interface App {
|
||||
id: string;
|
||||
name: string;
|
||||
assetSrc: string;
|
||||
internalUrl: string;
|
||||
externalUrl: string;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export * from './actions';
|
||||
export { default as reducer } from './reducers';
|
||||
export { getUsers } from './selectors';
|
||||
export * from './selectors';
|
||||
export * from './types';
|
||||
|
|
8
tailwind.config.js
vendored
|
@ -19,6 +19,14 @@ module.exports = {
|
|||
DEFAULT: '#54C6CC',
|
||||
dark: '#1E8290',
|
||||
},
|
||||
logo: {
|
||||
bluegray: '#3f607d',
|
||||
yellow: '#f5bd1c',
|
||||
lightviolet: '#a587bf',
|
||||
darkviolet: '#755d86',
|
||||
azure: '#3a97a3',
|
||||
lightazure: ' #b4e0e4',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|