Compare commits
492 Commits
@nhost/rea
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc1730fce | ||
|
|
c1cd1e813c | ||
|
|
e08a074474 | ||
|
|
95932fa3f2 | ||
|
|
99402b77d1 | ||
|
|
f6fb2cd8e6 | ||
|
|
5c2cf59b41 | ||
|
|
a6d31dc260 | ||
|
|
bd73557a47 | ||
|
|
c95bab70c2 | ||
|
|
52d4b5de45 | ||
|
|
6b4ab50f74 | ||
|
|
ceba605d0b | ||
|
|
9249a85ee5 | ||
|
|
f4d70f88e9 | ||
|
|
0d09b80b12 | ||
|
|
195adfb04a | ||
|
|
b09930c8a4 | ||
|
|
687951281e | ||
|
|
c0f05acd9b | ||
|
|
d8c2d369aa | ||
|
|
a4e4926aeb | ||
|
|
4a9471cc16 | ||
|
|
567e370bdc | ||
|
|
a91f2db0e2 | ||
|
|
aea99ad2c8 | ||
|
|
594488e435 | ||
|
|
bb83b0f81a | ||
|
|
0384d7c7c4 | ||
|
|
7e356a9604 | ||
|
|
013e55a307 | ||
|
|
2a71257cde | ||
|
|
583a4401d0 | ||
|
|
914e91a0b0 | ||
|
|
98698213e2 | ||
|
|
756daa97cd | ||
|
|
ab5a2b119c | ||
|
|
ffdcce1463 | ||
|
|
a3dcb6106e | ||
|
|
208d224763 | ||
|
|
564d000bfc | ||
|
|
266fda07ab | ||
|
|
6174e1ddcc | ||
|
|
af2e3eae37 | ||
|
|
d2b4c126f3 | ||
|
|
7f2e182c47 | ||
|
|
ae40bd54d4 | ||
|
|
b5cc47078a | ||
|
|
7f251111e2 | ||
|
|
c03dacc3a3 | ||
|
|
8b9e1a0ce8 | ||
|
|
cf9cfec330 | ||
|
|
1c94f56c59 | ||
|
|
f06d5deba3 | ||
|
|
8ff58d7f23 | ||
|
|
8dd1c7415b | ||
|
|
ebd2749e38 | ||
|
|
80b604adda | ||
|
|
9d73050792 | ||
|
|
2a86b8876c | ||
|
|
91a1a41f5d | ||
|
|
22e9c27c81 | ||
|
|
2d2beb53d2 | ||
|
|
b403b0d6a0 | ||
|
|
4dbac55cb4 | ||
|
|
c6e31ac741 | ||
|
|
0d3e8b3992 | ||
|
|
b2afd14d61 | ||
|
|
f28f28b6ee | ||
|
|
834b959271 | ||
|
|
4dbc9ccc87 | ||
|
|
2764a1c4b6 | ||
|
|
1666ca2ec5 | ||
|
|
346791d4d5 | ||
|
|
94bdafe22f | ||
|
|
33782e9d41 | ||
|
|
ea02e1e104 | ||
|
|
98bf6e3792 | ||
|
|
d9dcafd643 | ||
|
|
4f3d97b5ad | ||
|
|
d1801ceae9 | ||
|
|
b6f9fe6304 | ||
|
|
15b652d7e0 | ||
|
|
cdc0047cb7 | ||
|
|
7c0e71e8be | ||
|
|
175d6d8cbc | ||
|
|
2ae277409a | ||
|
|
ada10170b7 | ||
|
|
0ed77cbe8b | ||
|
|
1abc68992f | ||
|
|
795962e3c2 | ||
|
|
ba998eb632 | ||
|
|
d3fc1bbeb9 | ||
|
|
7b9c2016d0 | ||
|
|
ebf4070be6 | ||
|
|
59e3f6abc6 | ||
|
|
0c9a03a7ff | ||
|
|
42cbe27914 | ||
|
|
d7e7b0e51b | ||
|
|
036181dd75 | ||
|
|
2828a9fe01 | ||
|
|
88bc71dffc | ||
|
|
17699870a6 | ||
|
|
5c0304ab73 | ||
|
|
637265e3d9 | ||
|
|
229604d8e1 | ||
|
|
1165a33079 | ||
|
|
82c0dd9d87 | ||
|
|
a9bfab1778 | ||
|
|
60b7a664d2 | ||
|
|
4f708d04d6 | ||
|
|
a02af56056 | ||
|
|
575136dcfb | ||
|
|
2bc6346cbc | ||
|
|
91cd494a3d | ||
|
|
d57e0a5287 | ||
|
|
dd5e7093f0 | ||
|
|
3e9bb84f07 | ||
|
|
6665b58ec8 | ||
|
|
456e893cd6 | ||
|
|
fd76170ca6 | ||
|
|
682aef2d94 | ||
|
|
cb53f71d4a | ||
|
|
4fc0b40cb4 | ||
|
|
9da2d01c55 | ||
|
|
0d5e7850f8 | ||
|
|
8ed965c669 | ||
|
|
cb284b40b1 | ||
|
|
c59f622feb | ||
|
|
9a30edd038 | ||
|
|
b8b4e36175 | ||
|
|
6e47ef68d5 | ||
|
|
2697414637 | ||
|
|
e753b2faed | ||
|
|
6f62ec7d2a | ||
|
|
d365ef1953 | ||
|
|
99ee9fd10d | ||
|
|
9608a327c9 | ||
|
|
492b83ef58 | ||
|
|
09f53ae43f | ||
|
|
7724ac7e06 | ||
|
|
02343fe171 | ||
|
|
98dd8d039c | ||
|
|
456e057497 | ||
|
|
93e9b58a58 | ||
|
|
019a7c2335 | ||
|
|
a0901914ac | ||
|
|
64882a8e16 | ||
|
|
1ed572fe39 | ||
|
|
e06271a8ae | ||
|
|
bca239ebb2 | ||
|
|
e33caa046d | ||
|
|
cbbd331341 | ||
|
|
c41bfaffdd | ||
|
|
f2596b0b14 | ||
|
|
4d0c3111d1 | ||
|
|
7ff9644ac7 | ||
|
|
87fdaa7144 | ||
|
|
bc23d051ba | ||
|
|
132a4f4be9 | ||
|
|
c330dc1c00 | ||
|
|
4bebd9842b | ||
|
|
541f2d250c | ||
|
|
6b31e31430 | ||
|
|
9ba2208dd7 | ||
|
|
538541fc79 | ||
|
|
cb0aab48b3 | ||
|
|
dcc760b6bb | ||
|
|
d2227be0d0 | ||
|
|
93e807edc4 | ||
|
|
14738d4b58 | ||
|
|
6ac4cfa80c | ||
|
|
295c6c9110 | ||
|
|
02911e4a54 | ||
|
|
2be825de08 | ||
|
|
65afad1b91 | ||
|
|
dbea13a1d2 | ||
|
|
5e2a419700 | ||
|
|
9e6660450c | ||
|
|
2db9be03ff | ||
|
|
1b8f505050 | ||
|
|
6ab6bccded | ||
|
|
b12bc1c27f | ||
|
|
04d1641cf7 | ||
|
|
e91c09aa9c | ||
|
|
7c04aad870 | ||
|
|
520044c5ab | ||
|
|
22fed668d6 | ||
|
|
ae14fb1a2f | ||
|
|
f4ca0aab42 | ||
|
|
d8a0d196af | ||
|
|
cd53718631 | ||
|
|
1fcd3f1851 | ||
|
|
67d370feaa | ||
|
|
0173ec356b | ||
|
|
2fb43d417a | ||
|
|
966b8158c2 | ||
|
|
800fac6093 | ||
|
|
20eb04cd46 | ||
|
|
5d31a88556 | ||
|
|
06019ae7ba | ||
|
|
523af2901f | ||
|
|
1e0a2c0c43 | ||
|
|
9b60f9ca48 | ||
|
|
90bc800c92 | ||
|
|
023060cee6 | ||
|
|
0bb3b127d0 | ||
|
|
cd914d30ed | ||
|
|
71dddccb9c | ||
|
|
2a846040ef | ||
|
|
8b580d05c4 | ||
|
|
24d70e8ea8 | ||
|
|
8212432983 | ||
|
|
7df00a3291 | ||
|
|
0186aa5c98 | ||
|
|
67e5cc839c | ||
|
|
25c5bd1ccd | ||
|
|
e5fa48d818 | ||
|
|
f34da3a715 | ||
|
|
fab330ce55 | ||
|
|
3a76080e40 | ||
|
|
db57572f38 | ||
|
|
b030eae999 | ||
|
|
b10f4fa631 | ||
|
|
4a6e8c9945 | ||
|
|
3a4d087e6f | ||
|
|
c408b38f28 | ||
|
|
b2336b2b33 | ||
|
|
fcf68d5c32 | ||
|
|
9e71677cd2 | ||
|
|
ce48ce0860 | ||
|
|
9b93cf95ec | ||
|
|
34d85e54d6 | ||
|
|
6ae9d6dd01 | ||
|
|
1e8d8afe70 | ||
|
|
80478e39e0 | ||
|
|
ee7338cc12 | ||
|
|
3a39970d28 | ||
|
|
b0607673ed | ||
|
|
d9f27c6c65 | ||
|
|
ea0b35fe84 | ||
|
|
4fbd22f232 | ||
|
|
c92142845f | ||
|
|
c93f818315 | ||
|
|
d20ed86219 | ||
|
|
e04390307c | ||
|
|
5c931f14ae | ||
|
|
a7ab1243a4 | ||
|
|
5638b5e770 | ||
|
|
c5ce895c97 | ||
|
|
2be99c0e55 | ||
|
|
f6299b537d | ||
|
|
11d06cce4b | ||
|
|
e9e0ad6d3c | ||
|
|
3ad2d5ced8 | ||
|
|
42bfa7c16d | ||
|
|
0dea974a2e | ||
|
|
8165b8f13e | ||
|
|
1c8864c992 | ||
|
|
dd6921676f | ||
|
|
a1193da4fb | ||
|
|
ff83a29dcc | ||
|
|
4771bff65c | ||
|
|
6106265d87 | ||
|
|
3c2455b7e1 | ||
|
|
7bc8de103b | ||
|
|
e49ba4bd02 | ||
|
|
44ac1a0095 | ||
|
|
04ce686e7c | ||
|
|
ca01224083 | ||
|
|
d489232cbc | ||
|
|
9efc0522c3 | ||
|
|
4c86ed787e | ||
|
|
9a3c05f69b | ||
|
|
2b03f80d59 | ||
|
|
baa3f0531d | ||
|
|
f9bbc78241 | ||
|
|
7f62c8b7c2 | ||
|
|
de2bcb6320 | ||
|
|
64a6d934e7 | ||
|
|
f455022e6d | ||
|
|
baac8a3605 | ||
|
|
be20cd5cdb | ||
|
|
1de4137369 | ||
|
|
18fae1a891 | ||
|
|
03c58613b8 | ||
|
|
fb23dd6b39 | ||
|
|
c12bd632fc | ||
|
|
a71eea09a9 | ||
|
|
c9f88326b2 | ||
|
|
d1f7842169 | ||
|
|
4566c2a202 | ||
|
|
c678918e73 | ||
|
|
ac8efcbdd5 | ||
|
|
9bc346e8d4 | ||
|
|
efed987d31 | ||
|
|
7e113bfb1f | ||
|
|
bdab7da7d3 | ||
|
|
c2d9993968 | ||
|
|
508ba62207 | ||
|
|
a3318de06e | ||
|
|
f1d358d77c | ||
|
|
d558ef9ecf | ||
|
|
26d577d7ae | ||
|
|
75c7ba7f12 | ||
|
|
d62dfe19a2 | ||
|
|
0dbef188b1 | ||
|
|
fa9f7ca052 | ||
|
|
8857314e22 | ||
|
|
85f1c4a98e | ||
|
|
efa6b5755d | ||
|
|
44f13f6240 | ||
|
|
2b19416787 | ||
|
|
e01cb2ed49 | ||
|
|
388eef041f | ||
|
|
4e5d43f300 | ||
|
|
622c48a94b | ||
|
|
e1a87a05b1 | ||
|
|
db342f453e | ||
|
|
2148317282 | ||
|
|
54386a3b56 | ||
|
|
ff40b99f84 | ||
|
|
33f8f1d78a | ||
|
|
c50fe47ab4 | ||
|
|
0580f832c8 | ||
|
|
7d1eb099c0 | ||
|
|
e15322296b | ||
|
|
91a2bf905b | ||
|
|
0f9393fe27 | ||
|
|
aebb822549 | ||
|
|
1e2be6fadf | ||
|
|
aafbf5173d | ||
|
|
01e13e2f8c | ||
|
|
4364647501 | ||
|
|
ef117c284e | ||
|
|
3f919c0a80 | ||
|
|
49e447e7b7 | ||
|
|
66b4f3d0be | ||
|
|
aa7fdafe8b | ||
|
|
5f9c6c8346 | ||
|
|
7d6de3b289 | ||
|
|
57e41f77a9 | ||
|
|
f5c2a0ef4f | ||
|
|
d52bc8cca5 | ||
|
|
04a3e4c965 | ||
|
|
853c0c5775 | ||
|
|
2e6923dc73 | ||
|
|
7d6d70d0c7 | ||
|
|
57db5b83d4 | ||
|
|
7a2100cc17 | ||
|
|
5d55f3fa60 | ||
|
|
8b0c44a93c | ||
|
|
e0cc7cce0a | ||
|
|
6e7d5e0dd4 | ||
|
|
54c143ebf6 | ||
|
|
8b9fa0b150 | ||
|
|
c3bb79e1dd | ||
|
|
128d21e4ec | ||
|
|
40e503c356 | ||
|
|
d007e0ade8 | ||
|
|
fa32513ba7 | ||
|
|
8893d9e010 | ||
|
|
81d2fd865c | ||
|
|
fe3c462099 | ||
|
|
f8b082cb02 | ||
|
|
0c748e6ee6 | ||
|
|
e2c4ca85b3 | ||
|
|
0165b998c2 | ||
|
|
5d970cc229 | ||
|
|
7167170663 | ||
|
|
0f77de2dd0 | ||
|
|
6ae91e48d1 | ||
|
|
69db1594cc | ||
|
|
158cf0da49 | ||
|
|
7992fc3baa | ||
|
|
85d9596956 | ||
|
|
16d383516e | ||
|
|
2ca193ccf3 | ||
|
|
ab8e12003d | ||
|
|
29cdf6b125 | ||
|
|
41cc3dc5d0 | ||
|
|
6b67c9996a | ||
|
|
23274dee41 | ||
|
|
a5b55c2667 | ||
|
|
1263676eb3 | ||
|
|
b1b647ad96 | ||
|
|
21bbaf5e95 | ||
|
|
eef9c91403 | ||
|
|
1742cb444d | ||
|
|
c4f374d7f3 | ||
|
|
369ec13070 | ||
|
|
101129eef2 | ||
|
|
228fda0364 | ||
|
|
74085c67a2 | ||
|
|
a273725419 | ||
|
|
c5240f8d74 | ||
|
|
4490068257 | ||
|
|
3601de3f85 | ||
|
|
ac9404610b | ||
|
|
63570db57c | ||
|
|
538ed78f5a | ||
|
|
b1a31ecb00 | ||
|
|
3d151c448c | ||
|
|
bac8ace434 | ||
|
|
fdd417ed25 | ||
|
|
3cab18713a | ||
|
|
a402fc17de | ||
|
|
fb94dae43a | ||
|
|
4416ceb9cf | ||
|
|
f694846eae | ||
|
|
4762ebf61e | ||
|
|
73e28b5831 | ||
|
|
2a7dc5060f | ||
|
|
9b8ede40a9 | ||
|
|
f005c20d99 | ||
|
|
4adfd613b6 | ||
|
|
b6da82c8e3 | ||
|
|
816456edc4 | ||
|
|
deaf0e86d4 | ||
|
|
23f8206f18 | ||
|
|
9dde4d7988 | ||
|
|
26385b9cf9 | ||
|
|
6d318206ef | ||
|
|
4d727b78a1 | ||
|
|
de0a125e98 | ||
|
|
ea1ad29031 | ||
|
|
3da40e5712 | ||
|
|
b9087a4add | ||
|
|
1b7a6d0252 | ||
|
|
1417d3e794 | ||
|
|
e187923858 | ||
|
|
8a60ed4074 | ||
|
|
d7d11a44a7 | ||
|
|
062e4691cd | ||
|
|
a95d49fa2c | ||
|
|
d14fc96899 | ||
|
|
93db718254 | ||
|
|
c367bd58b9 | ||
|
|
0bfed4d9e1 | ||
|
|
1f3aecd379 | ||
|
|
42306ea3bb | ||
|
|
1b12a175f6 | ||
|
|
32060aaea0 | ||
|
|
f94cace5f2 | ||
|
|
5de965d9a5 | ||
|
|
e10b3adc11 | ||
|
|
457db76b06 | ||
|
|
1e952a026e | ||
|
|
2f4c040789 | ||
|
|
74648752b4 | ||
|
|
09d218a3fe | ||
|
|
2e8938dbb0 | ||
|
|
ec60d03536 | ||
|
|
2f3767552f | ||
|
|
bc401c0dd2 | ||
|
|
2145243b19 | ||
|
|
ca012d790c | ||
|
|
aeda14ef53 | ||
|
|
3fa5e2005a | ||
|
|
beadd84adb | ||
|
|
f8f55d2b99 | ||
|
|
03a98d4f3a | ||
|
|
8ed8e04ab6 | ||
|
|
587efd4551 | ||
|
|
a48dd5bf74 | ||
|
|
ef53df5cb3 | ||
|
|
322ab50138 | ||
|
|
435efd2bc5 | ||
|
|
5501a5937e | ||
|
|
80a6808a82 | ||
|
|
987bd70312 | ||
|
|
feb22e62c1 | ||
|
|
e7d4c77a6d | ||
|
|
628e32464d | ||
|
|
dc82043254 | ||
|
|
997e9d58a8 | ||
|
|
8a3f1706fe | ||
|
|
12cbe4d534 | ||
|
|
9f21931201 | ||
|
|
09351e1910 | ||
|
|
1c2ea5a407 | ||
|
|
7055ffc37a | ||
|
|
c68ce6d480 | ||
|
|
98a149c8bf | ||
|
|
ceb558975e | ||
|
|
7a87321a7e | ||
|
|
9349766c0a | ||
|
|
31655191a3 | ||
|
|
e3b91efa84 | ||
|
|
cfe736776a | ||
|
|
481bf237cc | ||
|
|
33ce9bf1b9 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
12
.github/actions/install-dependencies/action.yaml
vendored
12
.github/actions/install-dependencies/action.yaml
vendored
@@ -5,6 +5,10 @@ inputs:
|
||||
description: 'Turborepo token'
|
||||
TURBO_TEAM:
|
||||
description: 'Turborepo team'
|
||||
BUILD:
|
||||
description: 'Build packages'
|
||||
default: 'default'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
@@ -35,6 +39,14 @@ runs:
|
||||
# * They are reused through the Turborepo cache
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm build:all
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
- shell: bash
|
||||
name: Build everything in the monorepo
|
||||
if: ${{ inputs.BUILD == 'default' }}
|
||||
run: pnpm build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
|
||||
108
.github/actions/nhost-cli/README.md
vendored
Normal file
108
.github/actions/nhost-cli/README.md
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Nhost CLI GitHub Action
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
```
|
||||
|
||||
### Install the CLI and start the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
```
|
||||
|
||||
### Set another working directory
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: examples/react-apollo
|
||||
start: true
|
||||
```
|
||||
|
||||
### Don't wait for the app to be ready
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
wait: false
|
||||
```
|
||||
|
||||
### Stop the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
- name: Do something
|
||||
cmd: echo "do something"
|
||||
- name: Stop
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
stop: true
|
||||
```
|
||||
|
||||
### Install a given value of the CLI
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
```
|
||||
|
||||
### Inject values into nhost/config.yaml
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.16.1
|
||||
```
|
||||
75
.github/actions/nhost-cli/action.yaml
vendored
Normal file
75
.github/actions/nhost-cli/action.yaml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Nhost CLI
|
||||
description: 'Action to install the Nhost CLI and to run an application'
|
||||
inputs:
|
||||
start:
|
||||
description: "Start the application. If false, the application won't be started"
|
||||
default: 'false'
|
||||
wait:
|
||||
description: 'If starting the application, wait until it is ready'
|
||||
default: 'true'
|
||||
stop:
|
||||
description: 'Stop the application'
|
||||
default: 'false'
|
||||
path:
|
||||
description: 'Path to the application'
|
||||
default: '.'
|
||||
version:
|
||||
description: 'Version of the Nhost CLI'
|
||||
default: 'latest'
|
||||
config:
|
||||
description: 'Values to be injected into nhost/config.yaml'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Check if Nhost CLI is already installed
|
||||
id: check-nhost-cli
|
||||
shell: bash
|
||||
# TODO check if the version is the same
|
||||
run: |
|
||||
if [ -z "$(which nhost)" ]
|
||||
then
|
||||
echo "installed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "installed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Install Nhost CLI
|
||||
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
|
||||
shell: bash
|
||||
run: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
- name: Set custom configuration
|
||||
if: ${{ inputs.config }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: config="${{ inputs.config }}" yq -i '. *= env(config)' nhost/config.yaml
|
||||
- name: Start the application
|
||||
if: ${{ inputs.start == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost dev --no-browser &
|
||||
- name: Wait for the app to be ready
|
||||
id: wait
|
||||
if: ${{ inputs.start == 'true' && inputs.wait == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -sSf --connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry 300 \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time 300 \
|
||||
--retry-connrefused \
|
||||
'http://localhost:9695' > /dev/null
|
||||
- name: Log on failure
|
||||
if: steps.wait.outcome == 'failure'
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: |
|
||||
nhost logs
|
||||
exit 1
|
||||
- name: Stop the application
|
||||
if: ${{ inputs.stop == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost down
|
||||
22
.github/labeler.yml
vendored
Normal file
22
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
dashboard:
|
||||
- dashboard/**/*
|
||||
|
||||
documentation:
|
||||
- any:
|
||||
- docs/**/*
|
||||
- '!docs/docs/reference/docgen/**/*'
|
||||
|
||||
examples:
|
||||
- examples/**/*
|
||||
|
||||
sdk:
|
||||
- packages/**/*
|
||||
|
||||
react:
|
||||
- '{packages,examples}/*react*/**/*'
|
||||
|
||||
nextjs:
|
||||
- '{packages,examples}/*next*/**/*'
|
||||
|
||||
vue:
|
||||
- '{packages,examples}/*vue*/**/*'
|
||||
16
.github/renovate.json
vendored
Normal file
16
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"docker-compose": {
|
||||
"enabled": true
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"pnpm",
|
||||
"node"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
]
|
||||
}
|
||||
66
.github/workflows/changesets.yaml
vendored
66
.github/workflows/changesets.yaml
vendored
@@ -13,11 +13,12 @@ on:
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TEAM: nhost
|
||||
DASHBOARD_PACKAGE: '@nhost/dashboard'
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||
@@ -30,8 +31,8 @@ jobs:
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Create PR or Publish release
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
@@ -60,7 +61,8 @@ jobs:
|
||||
uses: ./.github/workflows/dashboard.yaml
|
||||
secrets: inherit
|
||||
|
||||
publish:
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
@@ -120,3 +122,59 @@ jobs:
|
||||
- name: Remove tag on failure
|
||||
if: failure()
|
||||
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
bump-cli:
|
||||
name: Bump Dashboard version in the Nhost CLI
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- version
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: nhost/cli
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
fetch-depth: 0
|
||||
- name: Bump version in source code
|
||||
run: |
|
||||
IMAGE=$(echo ${{ env.DASHBOARD_PACKAGE }} | sed 's/@\(.\+\)\/\(.\+\)/\1\\\/\2/g')
|
||||
VERSION="${{ needs.version.outputs.dashboardVersion }}"
|
||||
EXPRESSION='s/"'$IMAGE':[0-9]\+\.[0-9]\+\.[0-9]\+"/"'$IMAGE':'$VERSION'"/g'
|
||||
find ./ -type f -exec sed -i -e $EXPRESSION {} \;
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
branch: bump-dashboard-version
|
||||
delete-branch: true
|
||||
title: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'
|
||||
body: |
|
||||
This PR bumps the Nhost Dashboard Docker image to version ${{ needs.version.outputs.dashboardVersion }}.
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
name: Packages
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'dashboard/**'
|
||||
- 'docs/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
@@ -13,15 +11,16 @@ on:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'dashboard/**'
|
||||
- 'docs/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build @nhost packages
|
||||
@@ -36,21 +35,34 @@ jobs:
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v34
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
# * If the lockfile has changed, we don't filter anything in order to run all the e2e tests.
|
||||
- name: filter packages
|
||||
id: filter-packages
|
||||
if: steps.changed-lockfile.outputs.any_changed != 'true' && github.event_name == 'pull_request'
|
||||
run: echo "filter=${{ format('--filter=...[origin/{0}]', github.base_ref) }}" >> $GITHUB_OUTPUT
|
||||
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
||||
# * In a PR, only include packages that have been modified, and their dependencies
|
||||
- name: List examples with an e2e script
|
||||
id: set-matrix
|
||||
run: |
|
||||
FILTER_MODIFIED="${{ github.event_name == 'pull_request' && format('--filter=...[origin/{0}]', github.base_ref) || '' }}"
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' $FILTER_MODIFIED \
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' ${{ steps.filter-packages.outputs.filter }} \
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp)
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
e2e:
|
||||
name: 'e2e: ${{ matrix.package.name }}'
|
||||
name: 'e2e (${{ matrix.package.path }})'
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.matrix != '[]' && needs.build.outputs.matrix != '' }}
|
||||
strategy:
|
||||
@@ -70,7 +82,7 @@ jobs:
|
||||
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
||||
- name: Install Nhost CLI
|
||||
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
|
||||
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
|
||||
uses: ./.github/actions/nhost-cli
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e test
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
@@ -89,6 +101,7 @@ jobs:
|
||||
path: |
|
||||
${{format('{0}/cypress/screenshots/**', matrix.package.path)}}
|
||||
${{format('{0}/cypress/videos/**', matrix.package.path)}}
|
||||
|
||||
unit:
|
||||
name: Unit tests
|
||||
needs: build
|
||||
@@ -103,7 +116,7 @@ jobs:
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Run unit tests
|
||||
run: pnpm run test
|
||||
run: pnpm run test:all
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
@@ -113,6 +126,7 @@ jobs:
|
||||
run: |
|
||||
echo '### Code coverage' >> $GITHUB_STEP_SUMMARY
|
||||
echo 'Visit [codecov](https://app.codecov.io/gh/nhost/nhost/) to see the code coverage reports' >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: build
|
||||
@@ -127,4 +141,4 @@ jobs:
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
run: pnpm run lint:all
|
||||
2
.github/workflows/contributors.yaml
vendored
2
.github/workflows/contributors.yaml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
name: A job to automate contrib in readme
|
||||
steps:
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.4
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
23
.github/workflows/dashboard.yaml
vendored
23
.github/workflows/dashboard.yaml
vendored
@@ -2,24 +2,17 @@ name: 'Dashboard'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'dashboard/**'
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TURBO_TEAM: nhost
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
@@ -33,10 +26,6 @@ jobs:
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
@@ -50,10 +39,6 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: http://localhost:1337
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
|
||||
15
.github/workflows/labeler.yaml
vendored
Normal file
15
.github/workflows/labeler.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
name: 'Pull Request Labeler'
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: '${{ secrets.GH_PAT }}'
|
||||
sync-labels: ''
|
||||
68
.github/workflows/renovate.yaml
vendored
Normal file
68
.github/workflows/renovate.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
name: Renovate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [closed]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
|
||||
jobs:
|
||||
renovate-changeset:
|
||||
name: Add changeset
|
||||
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'renovate/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
BUILD: 'none'
|
||||
- name: Determine bumps
|
||||
id: bumps
|
||||
run: |
|
||||
LAST_NON_PR_SHA=$(git log --no-merges main origin/${{ github.head_ref }} --format=format:%h -- | head -2 | tail -1)
|
||||
echo "result<<EOF" >> $GITHUB_OUTPUT
|
||||
pnpm recursive list --depth -1 --parseable \
|
||||
--filter='!nhost-root' \
|
||||
--filter=[$LAST_NON_PR_SHA] \
|
||||
| xargs -I@ jq ".name" @/package.json \
|
||||
| sort \
|
||||
| uniq -u \
|
||||
| awk '$0=$0": patch"' \
|
||||
>> $GITHUB_OUTPUT
|
||||
echo 'EOF' >> $GITHUB_OUTPUT
|
||||
- name: Install dictionary
|
||||
if: steps.bumps.outputs.result != ''
|
||||
run: sudo apt-get install wbritish
|
||||
- name: Generate changeset file name
|
||||
id: file_name
|
||||
if: steps.bumps.outputs.result != ''
|
||||
run: |
|
||||
FILE_NAME=$(shuf -n 3 /usr/share/dict/words | tr '\n' '-' | sed 's/-$//' | sed 's/'"'"'s//g' | tr '[:upper:]' '[:lower:]')
|
||||
echo "result=./.changeset/${FILE_NAME}.md" >> $GITHUB_OUTPUT
|
||||
- name: Create changeset file
|
||||
if: steps.bumps.outputs.result != ''
|
||||
run: |
|
||||
cat <<EOF > ${{ steps.file_name.outputs.result }}
|
||||
---
|
||||
${{ steps.bumps.outputs.result }}
|
||||
---
|
||||
|
||||
${{ github.event.pull_request.title }}
|
||||
EOF
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
if: steps.bumps.outputs.result != ''
|
||||
with:
|
||||
commit_message: ${{ github.event.pull_request.title }}
|
||||
branch: main
|
||||
104
.github/workflows/test-nhost-cli-action.yaml
vendored
Normal file
104
.github/workflows/test-nhost-cli-action.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Test Nhost CLI action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- '.github/actions/nhost-cli/**'
|
||||
- '!.github/actions/nhost-cli/**/*.md'
|
||||
|
||||
jobs:
|
||||
install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
- name: should succeed running the nhost command
|
||||
run: nhost
|
||||
|
||||
start:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
- name: should be running
|
||||
run: curl -sSf 'http://localhost:9695' > /dev/null
|
||||
|
||||
stop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI, start and stop the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
stop: true
|
||||
- name: should have no live docker container
|
||||
run: |
|
||||
if [ -z "docker ps -q" ]; then
|
||||
echo "Some docker containers are still running"
|
||||
docker ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
wait: false
|
||||
- name: should not be ready
|
||||
run: curl -sSf -o /dev/null 'http://localhost:9695' > /dev/null && exit 1 || true
|
||||
- name: should eventually be ready
|
||||
run: |
|
||||
curl -sSf --connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry 300 \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time 300 \
|
||||
--retry-connrefused \
|
||||
'http://localhost:9695' > /dev/null
|
||||
|
||||
config:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and run the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.15.0
|
||||
- name: should find the injected hasura-auth version
|
||||
run: |
|
||||
VERSION=$(curl -sSf 'http://localhost:1337/v1/auth/version')
|
||||
EXPECTED_VERSION='{"version":"v0.15.0"}'
|
||||
if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Expected version $EXPECTED_VERSION but got $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
- name: should find the correct version
|
||||
run: nhost version | head -n 1 | grep v0.8.10 || exit 1
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,6 +48,10 @@ todo.md
|
||||
.netlify
|
||||
.monorepo-example
|
||||
|
||||
# Local Vercel folder
|
||||
.vercel
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# TypeDoc output
|
||||
|
||||
@@ -14,4 +14,5 @@ package.json
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
*.d.ts
|
||||
.next
|
||||
.next
|
||||
**/pnpm-lock.yaml
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": ["./dashboard"]
|
||||
}
|
||||
|
||||
@@ -99,11 +99,6 @@ You can take a look at the changeset documentation: [How to add a changeset](htt
|
||||
|
||||
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
|
||||
|
||||
The document generation script that is run in the pre-commit hook requires to be built first. You may need to run the following command before the commit:
|
||||
|
||||
```sh
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
<!-- ## Good practices
|
||||
- lint
|
||||
|
||||
94
README.md
94
README.md
@@ -179,14 +179,21 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Grégory D'Angelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/guicurcio">
|
||||
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
|
||||
<br />
|
||||
<sub><b>Guido Curcio</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subatuba21">
|
||||
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
|
||||
@@ -221,15 +228,15 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Christopher Möller</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/GavanWilhite">
|
||||
<img src="https://avatars.githubusercontent.com/u/2085119?v=4" width="100;" alt="GavanWilhite"/>
|
||||
<br />
|
||||
<sub><b>Gavan Wilhite</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FuzzyReason">
|
||||
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
|
||||
@@ -237,13 +244,6 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Vadim Smirnov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/macmac49">
|
||||
<img src="https://avatars.githubusercontent.com/u/831190?v=4" width="100;" alt="macmac49"/>
|
||||
@@ -258,21 +258,28 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Subhendu Kundu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/badgifter">
|
||||
<img src="https://avatars.githubusercontent.com/u/50094885?v=4" width="100;" alt="badgifter"/>
|
||||
<br />
|
||||
<sub><b>Bad Gifter</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/heygambo">
|
||||
<img src="https://avatars.githubusercontent.com/u/449438?v=4" width="100;" alt="heygambo"/>
|
||||
<br />
|
||||
<sub><b>Christian Gambardella</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dbarrosop">
|
||||
<img src="https://avatars.githubusercontent.com/u/6246622?v=4" width="100;" alt="dbarrosop"/>
|
||||
<br />
|
||||
<sub><b>David Barroso</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/hajek-raven">
|
||||
<img src="https://avatars.githubusercontent.com/u/7288737?v=4" width="100;" alt="hajek-raven"/>
|
||||
@@ -294,13 +301,21 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Jerry Jäppinen</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/wollerman">
|
||||
<img src="https://avatars.githubusercontent.com/u/1610241?v=4" width="100;" alt="wollerman"/>
|
||||
<br />
|
||||
<sub><b>Matt Wollerman</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mdp18">
|
||||
<img src="https://avatars.githubusercontent.com/u/11698527?v=4" width="100;" alt="mdp18"/>
|
||||
<br />
|
||||
<sub><b>Max</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mustafa-hanif">
|
||||
<img src="https://avatars.githubusercontent.com/u/30019262?v=4" width="100;" alt="mustafa-hanif"/>
|
||||
@@ -314,8 +329,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Nicolas Bourdin</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/piromsurang">
|
||||
<img src="https://avatars.githubusercontent.com/u/17776837?v=4" width="100;" alt="piromsurang"/>
|
||||
@@ -343,7 +357,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Muttenzer</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alexander-mart">
|
||||
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
|
||||
@@ -357,8 +372,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Amir Ahmic</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/akd-io">
|
||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||
@@ -386,7 +400,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Chris Wetherell</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
@@ -400,8 +415,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Dago</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dminkovsky">
|
||||
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
||||
@@ -429,7 +443,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Helio Alves</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nkhdo">
|
||||
<img src="https://avatars.githubusercontent.com/u/26102306?v=4" width="100;" alt="nkhdo"/>
|
||||
@@ -437,14 +452,20 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Hoang Do</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/iangabrielsanchez">
|
||||
<img src="https://avatars.githubusercontent.com/u/9511946?v=4" width="100;" alt="iangabrielsanchez"/>
|
||||
<br />
|
||||
<sub><b>Ian Gabriel Sanchez</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/eltociear">
|
||||
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
|
||||
<br />
|
||||
<sub><b>Ikko Ashimine</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jladuval">
|
||||
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
||||
@@ -465,7 +486,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Lucas Bois</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MarcelloTheArcane">
|
||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||
@@ -486,8 +508,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Nirmalya Ghosh</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/quentin-decre">
|
||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||
@@ -495,13 +516,21 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Quentin Decré</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elephant3">
|
||||
<img src="https://avatars.githubusercontent.com/u/48279149?v=4" width="100;" alt="elephant3"/>
|
||||
<br />
|
||||
<sub><b>Siarhei Lipchyk</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/altschuler">
|
||||
<img src="https://avatars.githubusercontent.com/u/956928?v=4" width="100;" alt="altschuler"/>
|
||||
<br />
|
||||
<sub><b>Simon Altschuler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/atapas">
|
||||
<img src="https://avatars.githubusercontent.com/u/3633137?v=4" width="100;" alt="atapas"/>
|
||||
@@ -529,8 +558,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Zach Burnaby</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/komninoschat">
|
||||
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
||||
|
||||
@@ -19,9 +19,11 @@ module.exports = {
|
||||
'*.spec.ts',
|
||||
'*.spec.tsx',
|
||||
'tests/**/*.ts',
|
||||
'tests/**/*.d.ts'
|
||||
'tests/**/*.d.ts',
|
||||
'e2e/**/*.ts',
|
||||
'e2e/**/*.d.ts'
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'simple-import-sort', 'cypress'],
|
||||
plugins: ['@typescript-eslint', 'cypress'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
@@ -30,31 +32,6 @@ module.exports = {
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
// Node.js builtins. You could also generate this regex if you use a `.js` config.
|
||||
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
|
||||
[
|
||||
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
|
||||
],
|
||||
// Packages
|
||||
['^\\w'],
|
||||
// Internal packages.
|
||||
['^(@|config/)(/*|$)'],
|
||||
// Side effect imports.
|
||||
['^\\u0000'],
|
||||
// Parent imports. Put `..` last.
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
// Other relative imports. Put same-folder imports and `.` last.
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
// Style imports.
|
||||
['^.+\\.s?css$']
|
||||
]
|
||||
}
|
||||
],
|
||||
'import/no-anonymous-default-export': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
module.exports = {
|
||||
'(packages|integrations)/(docgen|hasura-auth-js|hasura-storage-js|nextjs|nhost-js|react|core|vue)/src/**/*.{js,ts,jsx,tsx}':
|
||||
['pnpm docgen', 'git add docs'],
|
||||
'(nhost-cloud.yaml|**/nhost/config.yaml)': () => [
|
||||
'pnpm sync-versions',
|
||||
"git add ':(glob)**/nhost/config.yaml'"
|
||||
|
||||
@@ -28,22 +28,47 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types", "**/*/dist", "**/*/build", "**/*/.next", "**/*/umd"
|
||||
"./node_modules/@types",
|
||||
"**/*/dist",
|
||||
"**/*/build",
|
||||
"**/*/.next",
|
||||
"**/*/umd"
|
||||
],
|
||||
"paths": {
|
||||
"@nhost/apollo": ["../packages/apollo/src/index.ts"],
|
||||
"@nhost/core": ["../packages/core/src/index.ts"],
|
||||
"@nhost/docgen": ["../packages/docgen/src/index.ts"],
|
||||
"@nhost/hasura-auth-js": ["../packages/hasura-auth-js/src/index.ts"],
|
||||
"@nhost/hasura-storage-js": ["../packages/hasura-storage-js/src/index.ts"],
|
||||
"@nhost/nextjs": ["../packages/nextjs/src/index.ts"],
|
||||
"@nhost/nhost-js": ["../packages/nhost-js/src/index.ts"],
|
||||
"@nhost/react": ["../packages/react/src/index.ts"],
|
||||
"@nhost/react-apollo": ["../packages/react-apollo/src/index.ts"],
|
||||
"@nhost/react-auth": ["../packages/react-auth/src/index.ts"],
|
||||
"@nhost/vue": ["../packages/vue/src/index.ts"]
|
||||
"@nhost/apollo": [
|
||||
"../packages/apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-storage-js": [
|
||||
"../packages/hasura-storage-js/src/index.ts"
|
||||
],
|
||||
"@nhost/nextjs": [
|
||||
"../packages/nextjs/src/index.ts"
|
||||
],
|
||||
"@nhost/nhost-js": [
|
||||
"../packages/nhost-js/src/index.ts"
|
||||
],
|
||||
"@nhost/react": [
|
||||
"../packages/react/src/index.ts"
|
||||
],
|
||||
"@nhost/react-apollo": [
|
||||
"../packages/react-apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/react-auth": [
|
||||
"../packages/react-auth/src/index.ts"
|
||||
],
|
||||
"@nhost/vue": [
|
||||
"../packages/vue/src/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['next', 'airbnb', 'airbnb-typescript', 'airbnb/hooks', 'prettier'],
|
||||
|
||||
@@ -1,5 +1,129 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.7.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [57db5b83]
|
||||
- @nhost/nextjs@1.11.0
|
||||
- @nhost/nhost-js@1.7.0
|
||||
- @nhost/react@0.17.0
|
||||
- @nhost/react-apollo@4.11.0
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a6d31dc2: fix(dashboard): don't break the UI when project is not loaded yet
|
||||
|
||||
## 0.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7f251111: Use `NhostProvider` instead of `NhostReactProvider` and `NhostNextProvider`
|
||||
|
||||
`NhostReactProvider` and `NhostNextProvider` are now deprecated
|
||||
|
||||
- f4d70f88: fix(dashboard): do not break when region is nullish
|
||||
- 4a9471cc: Windows Live Provider displayed link updated to match backend url
|
||||
- 594488e4: fix(dashboard): do not show error when submitting Apple provider settings
|
||||
- Updated dependencies [7f251111]
|
||||
- @nhost/nextjs@1.10.0
|
||||
- @nhost/react@0.16.0
|
||||
- @nhost/react-apollo@4.10.0
|
||||
|
||||
## 0.7.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 80b604ad: fix(dashboard): use correct Hasura slug
|
||||
|
||||
## 0.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2d2beb53: fix(dashboard): prevent error on GraphQL page
|
||||
- ac8efcbd: chore(dashboard): deprecate old DNS name
|
||||
|
||||
## 0.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 132a4f4b: chore(dashboard): remove unused dependencies
|
||||
- 132a4f4b: chore(deps): synchronize @types/react-dom and @types/react versions
|
||||
- db57572f: fix(dashboard): correct section paddings when no env vars
|
||||
- Updated dependencies [132a4f4b]
|
||||
- @nhost/react@0.15.2
|
||||
- @nhost/react-apollo@4.9.2
|
||||
- @nhost/nextjs@1.9.3
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 34d85e54: chore(deps): update dependency critters to ^0.0.16
|
||||
- 9b93cf95: chore(deps): update dependency @netlify/functions to ^0.11.0
|
||||
- e0439030: chore(deps): update dependency @types/react-dom to v18.0.9
|
||||
- Updated dependencies [82124329]
|
||||
- @nhost/nextjs@1.9.2
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a1193da4: fix(dashboard): remove character limit from env var inputs
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 44f13f62: chore(dashboard): cleanup unused files
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e01cb2ed: chore(dashboard): change settings sidebar menu item density
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- db342f45: chore(dashboard): refactor Roles and Permissions settings sections
|
||||
- 8b9fa0b1: feat(dashboard): add Environment Variables page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [66b4f3d0]
|
||||
- Updated dependencies [2e6923dc]
|
||||
- Updated dependencies [ef117c28]
|
||||
- Updated dependencies [aebb8225]
|
||||
- @nhost/core@0.9.4
|
||||
- @nhost/nhost-js@1.6.2
|
||||
- @nhost/nextjs@1.9.1
|
||||
- @nhost/react@0.15.1
|
||||
- @nhost/react-apollo@4.9.1
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- eef9c914: feat(dashboard): add Roles and Permissions page
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a48dd5bf: feat(dashboard): make backend port configurable
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5de965d9: fix(dashboard): alphabetic ordering of providers
|
||||
- b9087a4a: fix(dashboard): console -> dashboard terminology
|
||||
- ca012d79: docs(workos): WorkOS Docs
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
FROM node:16-alpine AS pruner
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
@@ -17,10 +16,13 @@ RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
||||
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_URL http://localhost:9693
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_URL http://localhost:9695
|
||||
ENV NEXT_PUBLIC_ENV dev
|
||||
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
||||
|
||||
# placeholders for ports, will be replaced on runtime by entrypoint script
|
||||
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_PORT __NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_PORT __NEXT_PUBLIC_NHOST_HASURA_PORT__
|
||||
ENV NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT __NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__
|
||||
|
||||
RUN yarn global add pnpm@7.17.0
|
||||
COPY .gitignore .gitignore
|
||||
@@ -40,11 +42,14 @@ RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
COPY --from=builder /app/dashboard/next.config.js .
|
||||
COPY --from=builder /app/dashboard/package.json .
|
||||
COPY --from=builder /app/dashboard/public ./dashboard/public
|
||||
COPY --chown=nextjs:nodejs dashboard/docker-entrypoint.sh .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/next.config.js .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/package.json .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/public ./dashboard/public
|
||||
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/standalone/app ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/static ./dashboard/.next/static
|
||||
|
||||
CMD node dashboard/server.js
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "dashboard/server.js"]
|
||||
|
||||
@@ -30,31 +30,27 @@ First, you need to run the following command to start your backend locally:
|
||||
cd <your_nhost_project> && nhost dev
|
||||
```
|
||||
|
||||
Two environment variables are required to connect the Nhost Dashboard to your local backend:
|
||||
You can connect the Nhost Dashboard to your locally running backend by setting the following environment variables in `.env.development.local`:
|
||||
|
||||
- `NEXT_PUBLIC_NHOST_PLATFORM` should be set to `false`, because otherwise the Nhost Dashboard will try to connect to the Nhost platform.
|
||||
- `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` should be set to `http://localhost:9693` unless Hasura is configured to run on a different port. This is the URL of Hasura's migrations endpoint.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```bash
|
||||
NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
```
|
||||
|
||||
### Full list of environment variables
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. |
|
||||
| `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` | URL of Hasura's migrations endpoint. Used only if local development is enabled. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_URL` | URL of the Hasura Console. Used only when `NEXT_PUBLIC_ENV` is `dev`. |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||
| Name | Description |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. Setting this to `true` turns off local development. |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_MIGRATIONS_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `9693` |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_HASURA_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled and `NEXT_PUBLIC_ENV` is `dev`. Default: `9695` |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `1337` |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||
|
||||
## ESLint Rules
|
||||
|
||||
@@ -67,6 +63,7 @@ NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
| `import/extensions` | JS / TS files should be imported without file extensions. |
|
||||
| `react/jsx-filename-extension` | JSX should only appear in `.jsx` and `.tsx` files. |
|
||||
| `react/jsx-no-bind` | Further investigation must be made on the performance impact of functions directly passed as props to components. |
|
||||
| `import/order` | Until we have a better auto-formatter, we disable this rule. |
|
||||
| `import/no-extraneous-dependencies` | `devDependencies` should be excluded from the list of disallowed imports. |
|
||||
| `curly` | By default it only enforces curly braces for multi-line blocks, but it should be enforced for single-line blocks as well. |
|
||||
| `no-restricted-exports` | `export { default } from './module'` is used heavily in `@/ui/v2` which is a restricted export by default. |
|
||||
|
||||
15
dashboard/docker-entrypoint.sh
Executable file
15
dashboard/docker-entrypoint.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# read ports from env variables or use defaults
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_PORT="${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT:=9693}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_PORT="${NEXT_PUBLIC_NHOST_HASURA_PORT:=9695}"
|
||||
NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT="${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT:=1337}"
|
||||
|
||||
# replace placeholders
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__/${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT}/g" {} +
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_HASURA_PORT__/${NEXT_PUBLIC_NHOST_HASURA_PORT}/g" {} +
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__/${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT}/g" {} +
|
||||
|
||||
exec "$@"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.4.2",
|
||||
"version": "0.7.10",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -8,7 +8,7 @@
|
||||
"build": "next build --no-lint",
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 6",
|
||||
"lint": "next lint --max-warnings 3",
|
||||
"test": "vitest",
|
||||
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
|
||||
"nhost:dev": "nhost dev -d",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@graphiql/react": "^0.14.0",
|
||||
"@graphiql/react": "^0.15.0",
|
||||
"@graphiql/toolkit": "^0.8.0",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
@@ -34,12 +34,13 @@
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/system": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^5.0.8",
|
||||
"@nhost/core": "^0.9.3",
|
||||
"@nhost/nextjs": "^1.9.0",
|
||||
"@nhost/nhost-js": "^1.6.1",
|
||||
"@nhost/react": "^0.15.0",
|
||||
"@nhost/react-apollo": "^4.9.0",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/nhost-js": "workspace:*",
|
||||
"@nhost/react": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@segment/snippet": "^4.15.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
@@ -51,21 +52,20 @@
|
||||
"cross-fetch": "^3.1.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.1.0",
|
||||
"graphiql": "^2.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^4.3.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "^12.3.1",
|
||||
"next-seo": "^5.14.1",
|
||||
"node-pg-format": "^1.3.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettysize": "^2.0.0",
|
||||
"randomstring": "^1.2.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-is": "17.0.2",
|
||||
@@ -75,7 +75,6 @@
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.31.2",
|
||||
"slugify": "^1.6.5",
|
||||
"smartlook-client": "^6.0.0",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^1.8.0",
|
||||
"utility-types": "^3.10.0",
|
||||
@@ -90,7 +89,6 @@
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@netlify/functions": "^0.7.2",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
@@ -101,18 +99,15 @@
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.8",
|
||||
"@types/react-dom": "18.0.9",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/validator": "^13.7.10",
|
||||
@@ -123,10 +118,7 @@
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"concurrently": "^6.3.0",
|
||||
"critters": "^0.0.10",
|
||||
"csstype": "^3.0.10",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
@@ -136,20 +128,17 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^4.18.2",
|
||||
"express-validator": "^6.14.2",
|
||||
"jsdom": "^20.0.3",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.49.0",
|
||||
"postcss": "^8.4.19",
|
||||
"postmark": "^2.7.8",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"tailwindcss": "^3.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.4",
|
||||
|
||||
44
dashboard/public/assets/BR.svg
Normal file
44
dashboard/public/assets/BR.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
1
dashboard/public/assets/twilio.svg
Normal file
1
dashboard/public/assets/twilio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="21" height="21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.08 0c5.578 0 10.08 4.507 10.08 10.09 0 5.584-4.502 10.09-10.08 10.09A10.072 10.072 0 0 1 0 10.09C0 4.507 4.503 0 10.08 0Zm0 2.69a7.375 7.375 0 0 0-7.392 7.4c0 4.104 3.293 7.4 7.392 7.4 4.1 0 7.392-3.296 7.392-7.4 0-4.103-3.293-7.4-7.392-7.4Zm-2.486 7.804c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.083-2.086c0-1.143.94-2.085 2.083-2.085Zm4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.084-2.086c0-1.143.941-2.085 2.084-2.085Zm0-4.978c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 10.483 7.6c0-1.143.941-2.085 2.084-2.085Zm-4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 5.51 7.6c0-1.143.94-2.085 2.083-2.085Z" fill="#F22F46"/></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
@@ -1,107 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { inputErrorMessages } from '@/utils/getErrorMessage';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUpdateApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export function ChangeApplicationName({ close }: any) {
|
||||
const [updateAppName, { client }] = useUpdateApplicationMutation({});
|
||||
const { workspaceContext } = useWorkspaceContext();
|
||||
const [name, setName] = useState(workspaceContext.appName);
|
||||
const [applicationError, setApplicationError] = useState<any>('');
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const slug = slugifyString(name);
|
||||
if (slug.length < 4 || slug.length > 32) {
|
||||
setApplicationError('Slug should be within 4 and 32 characters.');
|
||||
return;
|
||||
}
|
||||
if (!inputErrorMessages(name, setName, setApplicationError, 'project')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAppName({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
name,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast('Project name changed');
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
`Error trying to delete project: ${currentApplication.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
await updateOwnCache(client);
|
||||
await router.push(`/${currentWorkspace.slug}/${slug}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h3" component="h2">
|
||||
Change Project Name
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-4 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
label="New Project Name"
|
||||
id="projectName"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setApplicationError('');
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus
|
||||
helperText={`https://app.nhost.io/${
|
||||
currentWorkspace.slug
|
||||
}/${slugifyString(name)}`}
|
||||
/>
|
||||
|
||||
{applicationError && (
|
||||
<Alert severity="error">{applicationError}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-flow-row gap-2">
|
||||
<Button type="submit" disabled={applicationError}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={close}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeApplicationName;
|
||||
@@ -1,166 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type { EnvironmentVariableFragment } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
refetchGetEnvironmentVariablesWhereQuery,
|
||||
useDeleteEnvironmentVariableMutation,
|
||||
useUpdateEnvironmentVariableMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type EnvModalProps = {
|
||||
show: boolean;
|
||||
close: VoidFunction;
|
||||
envVar: EnvironmentVariableFragment;
|
||||
};
|
||||
|
||||
export default function EditEnvVarModal({
|
||||
show,
|
||||
close,
|
||||
envVar,
|
||||
}: EnvModalProps) {
|
||||
const { workspaceContext } = useWorkspaceContext();
|
||||
const { appId } = workspaceContext;
|
||||
|
||||
const [updateEnvVar, { loading: updateLoading }] =
|
||||
useUpdateEnvironmentVariableMutation({
|
||||
refetchQueries: [
|
||||
refetchGetEnvironmentVariablesWhereQuery({
|
||||
where: {
|
||||
appId: {
|
||||
_eq: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [deleteEnvVar, { loading: deleteLoading }] =
|
||||
useDeleteEnvironmentVariableMutation({
|
||||
refetchQueries: [
|
||||
refetchGetEnvironmentVariablesWhereQuery({
|
||||
where: {
|
||||
appId: {
|
||||
_eq: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [prodValue, setProdValue] = useState(envVar.prodValue || '');
|
||||
const [devValue, setDevValue] = useState(envVar.devValue || '');
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await updateEnvVar({
|
||||
variables: {
|
||||
id: envVar.id,
|
||||
environmentVariable: {
|
||||
prodValue,
|
||||
devValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
close();
|
||||
triggerToast('Error updating environment variable');
|
||||
return;
|
||||
}
|
||||
triggerToast(`Environment variable ${envVar.name} updated successfully`);
|
||||
close();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteEnvVar({
|
||||
variables: {
|
||||
id: envVar.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
close();
|
||||
triggerToast('Error deleting environment variable');
|
||||
return;
|
||||
}
|
||||
triggerToast(`Environment variable ${envVar.name} removed successfully`);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal showModal={show} close={close}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{envVar.name}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
The default value will be available in all environments, unless
|
||||
you override it. All values are encrypted.
|
||||
</Text>
|
||||
|
||||
<div className="my-2 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
autoFocus
|
||||
disabled
|
||||
autoComplete="off"
|
||||
defaultValue={envVar.name}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={prodValue}
|
||||
onChange={(event) => setProdValue(event.target.value)}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={devValue}
|
||||
onChange={(event) => setDevValue(event.target.value)}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={updateLoading}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
loading={deleteLoading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button onClick={close} variant="outlined" color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
|
||||
import TwilioIcon from '@/components/icons/TwilioIcon';
|
||||
import {
|
||||
useGetSmsSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Button, Input } from '@/ui';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function EditSMSSettingsForm({
|
||||
close,
|
||||
isAlreadyEnabled,
|
||||
}: {
|
||||
close: () => void;
|
||||
isAlreadyEnabled: boolean;
|
||||
}) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useFormContext<EditSMSSettingsFormData>();
|
||||
const { control } = useFormContext<EditSMSSettingsFormData>();
|
||||
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
let toastId: string;
|
||||
|
||||
const client = useApolloClient();
|
||||
const isNotCompleted =
|
||||
!watch('accountSID') ||
|
||||
!watch('authToken') ||
|
||||
!watch('messagingServiceSID');
|
||||
|
||||
const handleEditSMSSettings = async (data: EditSMSSettingsFormData) => {
|
||||
try {
|
||||
toastId = showLoadingToast('Updating SMS settings...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authSmsTwilioAccountSid: data.accountSID,
|
||||
authSmsTwilioAuthToken: data.authToken,
|
||||
authSmsTwilioMessagingServiceId: data.messagingServiceSID,
|
||||
authSmsPasswordlessEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getSMSSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast('SMS settings updated successfully.');
|
||||
close();
|
||||
} catch (error) {
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(handleEditSMSSettings)}
|
||||
className="flex w-full flex-col pb-1"
|
||||
autoComplete="off"
|
||||
>
|
||||
{errors &&
|
||||
Object.entries(errors).map(([type, error]) => (
|
||||
<Alert key={type} className="mb-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-row place-content-between border-t border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Account SID
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="accountSID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message:
|
||||
'The Account SID must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="accountSID"
|
||||
placeholder="Account SID"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Auth Token
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="authToken"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message:
|
||||
'The Auth Token must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="authToken"
|
||||
placeholder="Auth Token"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_/.]+$/gi.test(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Messaging Service SID
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="messagingServiceSID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[+a-zA-Z0-9-_/.]+$/,
|
||||
message:
|
||||
'The Messaging Service SID must either be a valid phone number or contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="messagingServiceSID"
|
||||
required
|
||||
placeholder="Messaging Service SID"
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[+a-zA-Z0-9-_/.]+$/gi.test(value)) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="text-grayscaleDark mt-2 border text-sm+ font-normal"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || isNotCompleted}
|
||||
>
|
||||
{isAlreadyEnabled ? 'Update SMS Settings' : 'Enable SMS'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditSMSSettingsModal({
|
||||
close,
|
||||
isAlreadyEnabled,
|
||||
}: {
|
||||
close: () => void;
|
||||
isAlreadyEnabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-modal px-6 py-4 text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto mt-2.5">
|
||||
<TwilioIcon className=" text-greyscaleDark" />
|
||||
</div>
|
||||
<Text
|
||||
variant="subHeading"
|
||||
color="greyscaleDark"
|
||||
size="large"
|
||||
className="mt-3 text-center"
|
||||
>
|
||||
Set up Twilio SMS Service
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="mt-0.5 mb-6 text-center font-normal"
|
||||
>
|
||||
SMS messages are sent through Twilio. Create an account and a
|
||||
messaging service at https://console.twilio.com.
|
||||
</Text>
|
||||
<div>
|
||||
<ErrorBoundary fallbackRender={ErrorBoundaryFallback}>
|
||||
<EditSMSSettingsForm
|
||||
close={close}
|
||||
isAlreadyEnabled={isAlreadyEnabled}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EditSMSSettingsProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface EditSMSSettingsFormData {
|
||||
accountSID: string;
|
||||
authToken: string;
|
||||
messagingServiceSID: string;
|
||||
}
|
||||
|
||||
export function EditSMSSettings({ close }: EditSMSSettingsProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<EditSMSSettingsFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
accountSID: '',
|
||||
authToken: '',
|
||||
messagingServiceSID: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSmsSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue('accountSID', data.app.authSmsTwilioAccountSid);
|
||||
form.setValue('authToken', data.app.authSmsTwilioAuthToken);
|
||||
form.setValue(
|
||||
'messagingServiceSID',
|
||||
data.app.authSmsTwilioMessagingServiceId,
|
||||
);
|
||||
}, [data, form]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<EditSMSSettingsModal
|
||||
close={close}
|
||||
isAlreadyEnabled={data.app.authSmsPasswordlessEnabled}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import {
|
||||
useGetSmsSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Text, Toggle } from '@/ui';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function EnableSMSSignIn({ openSMSSettingsModal }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
|
||||
const { data, loading, error } = useGetSmsSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [enableSMSLoginMethod, setEnableSMSLoginMethod] = useState(false);
|
||||
const client = useApolloClient();
|
||||
let toastId: string;
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEnableSMSLoginMethod(data.app.authSmsPasswordlessEnabled);
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const handleDisable = async () => {
|
||||
try {
|
||||
toastId = showLoadingToast('Disabling SMS login...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authSmsPasswordlessEnabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
setEnableSMSLoginMethod(false);
|
||||
await client.refetchQueries({ include: ['getSMSSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast('Passwordless SMS disabled.');
|
||||
} catch (updateError) {
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
|
||||
throw updateError;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-20">
|
||||
<div className="mx-auto font-display">
|
||||
<div className="flex flex-col place-content-between">
|
||||
<div className="">
|
||||
<div className="flex flex-row place-content-between">
|
||||
<div className="relative flex flex-row">
|
||||
<Image
|
||||
src="/assets/SMS.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Phone Number (SMS)"
|
||||
/>
|
||||
<Text
|
||||
variant="body"
|
||||
size="large"
|
||||
className="ml-2 font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
Phone Number (SMS)
|
||||
</Text>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'ml-2 align-bottom text-sm- font-medium text-blue transition-opacity duration-300',
|
||||
!enableSMSLoginMethod && 'invisible opacity-0',
|
||||
enableSMSLoginMethod && 'opacity-100',
|
||||
)}
|
||||
onClick={() => openSMSSettingsModal()}
|
||||
>
|
||||
Edit SMS settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Toggle
|
||||
checked={enableSMSLoginMethod}
|
||||
onChange={async () => {
|
||||
if (enableSMSLoginMethod) {
|
||||
await handleDisable();
|
||||
} else {
|
||||
openSMSSettingsModal();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-row self-center align-middle">
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className="self-center"
|
||||
>
|
||||
Sign in users with Phone Number (SMS).
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EnableSMSSignIn;
|
||||
@@ -5,7 +5,8 @@ import Button from '@/ui/v2/Button';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { generateAppServiceUrl } from '@/utils/helpers';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HasuraDataProps {
|
||||
@@ -24,8 +25,12 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? process.env.NEXT_PUBLIC_NHOST_HASURA_URL || 'http://localhost:9695'
|
||||
: generateRemoteAppUrl(currentApplication.subdomain);
|
||||
? LOCAL_HASURA_URL
|
||||
: `${generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
'hasura',
|
||||
)}/console`;
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
@@ -59,7 +64,7 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Link
|
||||
href={`${hasuraUrl}/console`}
|
||||
href={hasuraUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="grid grid-flow-col items-center justify-center gap-1 rounded-[4px] bg-btn p-2 text-sm+ font-medium text-white hover:ring-2 motion-safe:transition-all"
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppInjectedVariablesQuery,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
export interface EditCustomUserJWTTokenData {
|
||||
customUserJWTToken: string;
|
||||
}
|
||||
|
||||
export type JWTSecretModalState = 'SHOW' | 'EDIT';
|
||||
|
||||
export interface JWTSecretModalProps {
|
||||
close: () => void;
|
||||
data?: any;
|
||||
jwtSecret: string;
|
||||
initialModalState?: JWTSecretModalState;
|
||||
}
|
||||
|
||||
export function EditJWTSecretModal({ close }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { submitState, setSubmitState } = useSubmitState();
|
||||
|
||||
const [updateApplication] = useUpdateApplicationMutation({
|
||||
refetchQueries: [
|
||||
refetchGetAppInjectedVariablesQuery({ id: currentApplication.id }),
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<EditCustomUserJWTTokenData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
customUserJWTToken: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleEditJWTSecret = async (data: EditCustomUserJWTTokenData) => {
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
try {
|
||||
JSON.parse(data.customUserJWTToken);
|
||||
} catch (error) {
|
||||
setSubmitState({
|
||||
error: new Error('The custom JWT token should be valid json.'),
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
hasuraGraphqlJwtSecret: data.customUserJWTToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
triggerToast(
|
||||
`Successfully added custom JWT token to ${currentApplication.name}.`,
|
||||
);
|
||||
close();
|
||||
} catch (error) {
|
||||
triggerToast(
|
||||
`Error adding custom JWT token to ${currentApplication.name}`,
|
||||
);
|
||||
setSubmitState({ error, loading: false, fieldsWithError: [] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="w-modal px-6 py-4"
|
||||
onSubmit={handleSubmit(handleEditJWTSecret)}
|
||||
>
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row text-left">
|
||||
<Text variant="h3" component="h2">
|
||||
Add Custom JWT Secret
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
You can add your custom JWT key here. Hasura will use this key to
|
||||
validate the identity of your users.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{submitState.error && (
|
||||
<Alert severity="error">{submitState.error.message}</Alert>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="customUserJWTToken"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Paste your custom JWT token here..."
|
||||
componentsProps={{
|
||||
inputRoot: {
|
||||
className: 'font-mono bg-header',
|
||||
},
|
||||
}}
|
||||
aria-label="Custom JWT token"
|
||||
type="text"
|
||||
value={field.value}
|
||||
onBlur={() =>
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
})
|
||||
}
|
||||
multiline
|
||||
rows={6}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
|
||||
return (
|
||||
<div className="w-modal px-6 py-4">
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row text-left">
|
||||
<Text variant="h3" component="h2">
|
||||
Auth JWT Secret
|
||||
</Text>
|
||||
<Text variant="subtitle2">
|
||||
This is the key used for generating JWTs. It's HMAC-SHA-based
|
||||
and the same as configured in Hasura.
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
defaultValue={JWTKey}
|
||||
multiline
|
||||
disabled
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
rows={6}
|
||||
componentsProps={{
|
||||
inputRoot: { className: 'font-mono' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto max-w-sm text-center">
|
||||
<Text variant="subtitle2">
|
||||
Already using a third party auth service? <br />
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 ml-0.5 text-xs font-medium text-blue"
|
||||
onClick={() => {
|
||||
editJWTSecret();
|
||||
}}
|
||||
>
|
||||
Add your custom JWT token
|
||||
</button>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function JWTSecretModal({
|
||||
close,
|
||||
data,
|
||||
jwtSecret,
|
||||
initialModalState,
|
||||
}: any) {
|
||||
const [jwtSecretModalState, setJwtSecretModalState] =
|
||||
useState<JWTSecretModalState>(initialModalState || 'SHOW');
|
||||
|
||||
const editJWTSecret = () => {
|
||||
setJwtSecretModalState('EDIT');
|
||||
};
|
||||
|
||||
if (jwtSecretModalState === 'EDIT') {
|
||||
return <EditJWTSecretModal close={close} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowJWTTokenModal
|
||||
JWTKey={jwtSecret || data}
|
||||
editJWTSecret={editJWTSecret}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function SelectedWorkspaceOnNewApp({ current }: any) {
|
||||
const user = nhost.auth.getUser();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row space-x-2 self-center">
|
||||
{current.name === 'Default Workspace' ? (
|
||||
<Avatar
|
||||
className="h-5 w-5 self-center rounded-full"
|
||||
name={user?.displayName}
|
||||
avatarUrl={user?.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-5 w-5 overflow-hidden rounded-md">
|
||||
<Image src="/logos/new.svg" alt="Nhost Logo" width={20} height={20} />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small" color="greyscaleDark" className="font-normal">
|
||||
{current.name}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default SelectedWorkspaceOnNewApp;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './AppDeployments';
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { SelectorOption } from '@/ui/Selector';
|
||||
import Selector from '@/ui/Selector';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Toggle } from '@/ui/Toggle';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface PermissionSettingsProps {
|
||||
text: string;
|
||||
desc?: string;
|
||||
toggle?: boolean;
|
||||
onChange?: any;
|
||||
checked?: boolean;
|
||||
options?: any;
|
||||
value?: SelectorOption;
|
||||
} // @TODO: Fix alt attribute on images.
|
||||
// @FIX: Double border
|
||||
|
||||
export function PermissionSetting({
|
||||
text,
|
||||
desc,
|
||||
toggle,
|
||||
checked = false,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
}: PermissionSettingsProps) {
|
||||
return (
|
||||
<div className="flex flex-row place-content-between py-2">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col space-y-1 self-center px-0.5',
|
||||
!desc && 'py-3.5',
|
||||
desc && 'py-2',
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
className="font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text
|
||||
variant="body"
|
||||
size="tiny"
|
||||
className="font-normal"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{toggle ? (
|
||||
<div className="flex flex-row">
|
||||
<Toggle checked={checked} onChange={onChange} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row self-center">
|
||||
<Selector
|
||||
width="w-28"
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { Provider as ProviderType } from '@/types/providers';
|
||||
import Status, { StatusEnum } from '@/ui/Status';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface ProviderProps {
|
||||
provider: ProviderType;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export function Provider({ provider, enabled }: ProviderProps) {
|
||||
const { name, logo } = provider;
|
||||
|
||||
const {
|
||||
query: { workspaceSlug, appSlug },
|
||||
} = useRouter();
|
||||
|
||||
const nameLowerCase = name.toLowerCase();
|
||||
return (
|
||||
<Link
|
||||
href={`/${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
href={`${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
|
||||
className="flex cursor-pointer flex-row place-content-between border-t py-2.5"
|
||||
>
|
||||
<div className="grid grid-flow-col items-center gap-2">
|
||||
<div className="h-6 w-6">
|
||||
<Image
|
||||
src={logo}
|
||||
alt={`Logo of ${name}`}
|
||||
width={24}
|
||||
height={24}
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
<Text className="font-medium" color="greyscaleDark" size="normal">
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{enabled ? (
|
||||
<Status status={StatusEnum.Live}>Enabled</Status>
|
||||
) : (
|
||||
<Status status={StatusEnum.Closed}>Disabled</Status>
|
||||
)}
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default Provider;
|
||||
@@ -1,183 +0,0 @@
|
||||
import { CreateUserRoleModal } from '@/components/applications/users/roles/CreateRoleModal';
|
||||
import { EditUserRoleModal } from '@/components/applications/users/roles/EditUserRoleModal';
|
||||
import Lock from '@/components/icons/Lock';
|
||||
import type { GetRolesQuery } from '@/generated/graphql';
|
||||
import { Modal } from '@/ui';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import type { Dispatch, MouseEvent, MouseEventHandler } from 'react';
|
||||
import { useReducer } from 'react';
|
||||
|
||||
function RolesTableHead() {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-64 py-3 text-left font-medium text-base">
|
||||
<Text className="text-xs font-bold text-greyscaleDark">Role</Text>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserRoleProps {
|
||||
role: string;
|
||||
isSystemRole: boolean;
|
||||
onClick?: MouseEventHandler<HTMLTableRowElement>;
|
||||
}
|
||||
|
||||
function UserRole({ role, isSystemRole, onClick }: UserRoleProps) {
|
||||
return (
|
||||
<tr
|
||||
className={clsx(isSystemRole ? 'cursor-not-allowed' : 'cursor-pointer')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<td className="py-2">
|
||||
<Text
|
||||
size="normal"
|
||||
className={clsx(
|
||||
isSystemRole ? 'text-greyscaleGrey' : 'text-greyscaleDark',
|
||||
'pl-1 font-medium',
|
||||
)}
|
||||
>
|
||||
{role}
|
||||
</Text>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{isSystemRole ? (
|
||||
<div className="inline-flex pr-1">
|
||||
<Text
|
||||
size="tiny"
|
||||
className=" font-mono text-xs font-medium uppercase tracking-wide text-greyscaleGrey"
|
||||
>
|
||||
System Role
|
||||
</Text>
|
||||
<Lock className="ml-1 h-5 w-5 text-greyscaleGrey" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex self-center py-2 pr-1.5">
|
||||
<ChevronRightIcon className="h-4.5 w-4.5 text-greyscaleDark" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export type UserRoleDetails = {
|
||||
name: string;
|
||||
isSystemRole: boolean;
|
||||
};
|
||||
|
||||
export const getUserRoles = (data): UserRoleDetails[] => {
|
||||
const authUserDefaultAllowedRoles =
|
||||
data.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
return authUserDefaultAllowedRoles.map((role: string) => ({
|
||||
name: role,
|
||||
isSystemRole: ['user', 'me'].includes(role),
|
||||
}));
|
||||
};
|
||||
|
||||
type ModalState = {
|
||||
visible: boolean;
|
||||
type: 'create' | 'edit';
|
||||
payload: UserRoleDetails;
|
||||
};
|
||||
|
||||
type ModalAction = {
|
||||
type: 'OPEN_CREATE_MODAL' | 'OPEN_EDIT_MODAL' | 'CLOSE_MODAL';
|
||||
payload?: UserRoleDetails;
|
||||
};
|
||||
|
||||
function modalStateReducer(state: ModalState, action: ModalAction): ModalState {
|
||||
switch (action.type) {
|
||||
case 'OPEN_CREATE_MODAL':
|
||||
return { ...state, visible: true, type: 'create', payload: null };
|
||||
case 'OPEN_EDIT_MODAL':
|
||||
return { ...state, visible: true, type: 'edit', payload: action.payload };
|
||||
case 'CLOSE_MODAL':
|
||||
return { ...state, visible: false };
|
||||
default:
|
||||
throw new Error(`Action type ${action.type} is not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
function AddNewUserRole({ dispatch }: { dispatch: Dispatch<ModalAction> }) {
|
||||
return (
|
||||
<tr className="cursor-pointer border-y-1 border-solid border-gray-300">
|
||||
<td className="p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'OPEN_CREATE_MODAL' })}
|
||||
>
|
||||
<Text className="text-sm+ font-medium text-blue">
|
||||
Create New Role
|
||||
</Text>
|
||||
</button>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function RolesTableBody({ data }: { data: GetRolesQuery }) {
|
||||
const userRoles = getUserRoles(data);
|
||||
const [
|
||||
{ visible: modalVisible, type: modalType, payload: modalPayload },
|
||||
dispatch,
|
||||
] = useReducer(modalStateReducer, {
|
||||
visible: false,
|
||||
type: null,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
function handleRoleEdit(event: MouseEvent<HTMLTableRowElement>, role: any) {
|
||||
dispatch({ type: 'OPEN_EDIT_MODAL', payload: role });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={modalVisible}
|
||||
close={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
>
|
||||
{modalType === 'create' ? (
|
||||
<CreateUserRoleModal
|
||||
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
/>
|
||||
) : (
|
||||
<EditUserRoleModal
|
||||
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<tbody className="divide-y-1 border-t-1 border-b-1 border-solid border-gray-300 ">
|
||||
{userRoles.map((role) => (
|
||||
<UserRole
|
||||
key={role.name}
|
||||
role={role.name}
|
||||
isSystemRole={role.isSystemRole}
|
||||
onClick={
|
||||
role.isSystemRole
|
||||
? undefined
|
||||
: (event) => handleRoleEdit(event, role)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<AddNewUserRole dispatch={dispatch} />
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RolesTable({ data }: { data: GetRolesQuery }) {
|
||||
return (
|
||||
<table className="w-full table-fixed overflow-x-auto">
|
||||
<RolesTableHead />
|
||||
<RolesTableBody data={data} />
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { TextProps } from '@/ui/Text';
|
||||
import { Text } from '@/ui/Text';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface SettingsSectionProps {
|
||||
/**
|
||||
* Title of this section.
|
||||
*/
|
||||
title: ReactNode;
|
||||
/**
|
||||
* Props to be passed to the title component.
|
||||
*/
|
||||
titleProps?: TextProps;
|
||||
/**
|
||||
* Props to be passed to the wrapper component.
|
||||
*/
|
||||
wrapperProps?: TextProps;
|
||||
/**
|
||||
* Description of this section.
|
||||
*/
|
||||
desc?: ReactNode;
|
||||
/**
|
||||
* Props to be passed to the description component.
|
||||
*/
|
||||
descriptionProps?: TextProps;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
children,
|
||||
title,
|
||||
titleProps,
|
||||
descriptionProps,
|
||||
desc,
|
||||
wrapperProps,
|
||||
}: PropsWithChildren<SettingsSectionProps>) {
|
||||
const { className: titleClassName, ...restTitleProps } = titleProps || {};
|
||||
const { className: wrapperClassName } = wrapperProps || {};
|
||||
const { className: descriptionClassName, ...restDescriptionProps } =
|
||||
descriptionProps || {};
|
||||
|
||||
return (
|
||||
<div className={twMerge('mt-10', wrapperClassName)}>
|
||||
<div className="mx-auto font-display">
|
||||
<div className="flex flex-col place-content-between">
|
||||
<div>
|
||||
<Text
|
||||
size="large"
|
||||
variant="heading"
|
||||
className={twMerge('mb-1.5 font-medium', titleClassName)}
|
||||
color="greyscaleDark"
|
||||
{...restTitleProps}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className={twMerge('mb-3 font-normal', descriptionClassName)}
|
||||
{...restDescriptionProps}
|
||||
>
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import useCustomClaims from '@/hooks/useCustomClaims';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type {
|
||||
CreatePermissionVariableBaseFormData,
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
} from './CreatePermissionVariableModalBase';
|
||||
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
|
||||
|
||||
export type CreatePermissionVariableFormData =
|
||||
CreatePermissionVariableBaseFormData;
|
||||
|
||||
export type CreatePermissionVariableModalProps = Pick<
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
'onClose'
|
||||
>;
|
||||
|
||||
export default function CreatePermissionVariableModal({
|
||||
onClose,
|
||||
}: CreatePermissionVariableModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const form = useForm<CreatePermissionVariableFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceContext: { appId },
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const { data: customClaims } = useCustomClaims({ appId });
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
|
||||
});
|
||||
|
||||
async function handleSubmit(permissionVariable: CustomClaim) {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (
|
||||
customClaims.some(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Permission variable with this field name already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: [...customClaims, permissionVariable]
|
||||
.filter((claim) => !claim.system)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast('Permission variable created');
|
||||
|
||||
if (!onClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CreatePermissionVariableModalBase
|
||||
title="Create Permission Variable"
|
||||
type="create"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={onClose}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { ChangeEvent, MouseEventHandler, ReactNode } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface CreatePermissionVariableBaseFormData {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CreateModalBaseProps<T> {
|
||||
/**
|
||||
* Title of this modal.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Type of this modal.
|
||||
*/
|
||||
type?: 'create' | 'edit';
|
||||
/**
|
||||
* Callback to be called when the modal is closed.
|
||||
*/
|
||||
onClose?: VoidFunction;
|
||||
/**
|
||||
* Callback to be called when remove button is clicked.
|
||||
*/
|
||||
onRemove?: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Callback to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: SubmitHandler<T>;
|
||||
/**
|
||||
* Error to be displayed.
|
||||
*/
|
||||
errorComponent?: ReactNode;
|
||||
}
|
||||
|
||||
export type CreatePermissionVariableModalBaseProps =
|
||||
CreateModalBaseProps<CreatePermissionVariableBaseFormData>;
|
||||
|
||||
export default function CreatePermissionVariableModalBase({
|
||||
title,
|
||||
type,
|
||||
onClose,
|
||||
onRemove,
|
||||
onSubmit,
|
||||
errorComponent,
|
||||
}: CreatePermissionVariableModalBaseProps) {
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
register,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useFormContext<CreatePermissionVariableBaseFormData>();
|
||||
|
||||
const keyHandlers = register('key', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z-]+$/i,
|
||||
message: 'Must contain only letters and hyphens',
|
||||
},
|
||||
});
|
||||
|
||||
const valueHandlers = register('value', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9._[\]]+$/i,
|
||||
message: 'Must contain only letters, dots, brackets, and underscores',
|
||||
},
|
||||
});
|
||||
|
||||
const isComplete = !!watch('key') && !!watch('value');
|
||||
|
||||
return (
|
||||
<div className="w-modal p-6 text-left">
|
||||
<div className="grid w-full grid-flow-col items-center justify-between">
|
||||
<Text variant="h3" component="h2">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{type === 'edit' && onRemove && (
|
||||
<Button variant="borderless" color="error" onClick={onRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text className="mt-2 text-sm+ text-greyscaleDark">
|
||||
Enter the field name and the path you want to use in this permission
|
||||
variable.
|
||||
</Text>
|
||||
|
||||
{errorComponent}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="my-4 grid grid-flow-row divide-y-1 divide-solid divide-gray-200 border-y border-gray-200">
|
||||
<Input
|
||||
{...keyHandlers}
|
||||
value={watch('key')}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
keyHandlers.onChange(event);
|
||||
}}
|
||||
id="key"
|
||||
variant="inline"
|
||||
inlineInputProportion="66%"
|
||||
label="Field name"
|
||||
fullWidth
|
||||
startAdornment={
|
||||
<Text className="min-w-[73px] text-sm+ text-greyscaleGrey">
|
||||
X-Hasura-
|
||||
</Text>
|
||||
}
|
||||
componentsProps={{
|
||||
inputWrapper: { className: 'my-1' },
|
||||
input: {
|
||||
className: 'border-transparent focus-within:border-solid pl-2',
|
||||
},
|
||||
inputRoot: { className: '!pl-[1px]' },
|
||||
}}
|
||||
autoFocus
|
||||
error={!!errors?.key?.message}
|
||||
helperText={errors?.key?.message}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...valueHandlers}
|
||||
value={watch('value')}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
valueHandlers.onChange(event);
|
||||
}}
|
||||
id="value"
|
||||
variant="inline"
|
||||
inlineInputProportion="66%"
|
||||
label="Path"
|
||||
fullWidth
|
||||
startAdornment={
|
||||
<Text className="text-sm+ text-greyscaleGrey">user.</Text>
|
||||
}
|
||||
componentsProps={{
|
||||
inputWrapper: { className: 'my-1' },
|
||||
input: {
|
||||
className: 'border-transparent focus-within:border-solid pl-2',
|
||||
},
|
||||
inputRoot: { className: '!pl-[1px]' },
|
||||
}}
|
||||
error={!!errors?.value?.message}
|
||||
helperText={errors?.value?.message}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || !isComplete}
|
||||
>
|
||||
{type === 'create' ? 'Create Permission Variable' : 'Save Changes'}
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import useCustomClaims from '@/hooks/useCustomClaims';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type {
|
||||
CreatePermissionVariableBaseFormData,
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
} from './CreatePermissionVariableModalBase';
|
||||
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
|
||||
|
||||
export type EditPermissionVariableFormData =
|
||||
CreatePermissionVariableBaseFormData;
|
||||
|
||||
export type EditPermissionVariableModalProps = Pick<
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
'onClose'
|
||||
> & {
|
||||
/**
|
||||
* The permission variable to edit.
|
||||
*/
|
||||
payload: CustomClaim;
|
||||
};
|
||||
|
||||
export default function EditPermissionVariableModal({
|
||||
payload: originalCustomClaim,
|
||||
...props
|
||||
}: EditPermissionVariableModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
|
||||
const form = useForm<EditPermissionVariableFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
key: originalCustomClaim.key || '',
|
||||
value: originalCustomClaim.value || '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceContext: { appId },
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const { data: customClaims } = useCustomClaims({ appId });
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
|
||||
});
|
||||
|
||||
async function handleSubmit(permissionVariable: CustomClaim) {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (
|
||||
originalCustomClaim.key.toLowerCase() !==
|
||||
permissionVariable.key.toLowerCase() &&
|
||||
customClaims.some(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Permission variable with this field name already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
// we need to preserve the original position of the permission variable
|
||||
const currentIndex = customClaims.findIndex(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === originalCustomClaim.key.toLowerCase(),
|
||||
);
|
||||
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: customClaims
|
||||
.slice(0, currentIndex)
|
||||
.concat(permissionVariable)
|
||||
.concat(customClaims.slice(currentIndex + 1))
|
||||
.filter((claim) => !claim.system)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`Permission variable updated`);
|
||||
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove() {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: customClaims
|
||||
.filter(
|
||||
(claim) =>
|
||||
claim.key !== originalCustomClaim.key && !claim.system,
|
||||
)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setShowRemoveModal(false);
|
||||
|
||||
triggerToast('Permission variable removed');
|
||||
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRemoveModal}
|
||||
close={() => setShowRemoveModal(false)}
|
||||
>
|
||||
<div className="grid w-96 grid-flow-row gap-2 p-6 text-left text-greyscaleDark">
|
||||
<Text variant="h3" component="h2">
|
||||
Remove {originalCustomClaim.key}?
|
||||
</Text>
|
||||
|
||||
<Text>You will not be able to use it in permissions anymore.</Text>
|
||||
|
||||
<Text>
|
||||
If you have permission checks currently using this property, they
|
||||
will never resolve to true.
|
||||
</Text>
|
||||
|
||||
<div className="mt-2 grid grid-flow-row gap-2">
|
||||
<Button color="error" onClick={handleRemove} className="w-full">
|
||||
Remove Permission Variable
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setShowRemoveModal(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CreatePermissionVariableModalBase
|
||||
title="Edit Permission Variable"
|
||||
type="edit"
|
||||
onSubmit={handleSubmit}
|
||||
onRemove={() => setShowRemoveModal(true)}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Loading from '@/ui/Loading';
|
||||
import {
|
||||
refetchGetRolesQuery,
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import type {
|
||||
CreateUserRoleBaseFormData,
|
||||
CreateUserRoleModalBaseProps,
|
||||
} from './CreateUserRoleModalBase';
|
||||
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
|
||||
|
||||
export type CreateUserRoleFormData = CreateUserRoleBaseFormData;
|
||||
|
||||
export type CreateUserRoleModalProps = Pick<
|
||||
CreateUserRoleModalBaseProps,
|
||||
'onClose'
|
||||
>;
|
||||
|
||||
export function CreateUserRoleModal({ onClose }: CreateUserRoleModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<CreateUserRoleBaseFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentRolesData,
|
||||
loading,
|
||||
error: getRolesError,
|
||||
} = useGetRolesQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (getRolesError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2.5xl">
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(data) {
|
||||
setError(undefined);
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = `${currentRolesData.app.authUserDefaultAllowedRoles},${data.roleName}`;
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!onClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CreateUserRoleModalBase
|
||||
title="Create New Role"
|
||||
type="create"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={onClose}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { CreateModalBaseProps } from '@/components/applications/users/permissions/modal/CreatePermissionVariableModalBase';
|
||||
import { Input } from '@/ui';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface CreateUserRoleBaseFormData {
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export type CreateUserRoleModalBaseProps =
|
||||
CreateModalBaseProps<CreateUserRoleBaseFormData>;
|
||||
export type CreateUserRoleModal = Pick<CreateUserRoleModalBaseProps, 'onClose'>;
|
||||
|
||||
export function CreateUserRoleModalBase({
|
||||
title,
|
||||
type,
|
||||
onRemove,
|
||||
onSubmit,
|
||||
errorComponent,
|
||||
}: CreateUserRoleModalBaseProps) {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useFormContext<CreateUserRoleBaseFormData>();
|
||||
|
||||
return (
|
||||
<div className="w-modal- p-6 text-left">
|
||||
<div className="mx-auto items-center justify-between">
|
||||
<Text
|
||||
variant="heading"
|
||||
className="text-center text-lg font-medium text-greyscaleDark"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{errorComponent}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="mt-3 mb-3 divide-y border-t border-b py-1">
|
||||
<div className="flex flex-row place-content-between py-2">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
New Role Name
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="roleName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message: 'Must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="roleName"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{type === 'create' ? 'Create New User Role' : 'Save Changes'}
|
||||
</Button>
|
||||
{type === 'edit' && onRemove && (
|
||||
<Button variant="menu" border onClick={onRemove}>
|
||||
<Text className="text-sm+ font-medium text-red">Remove Role</Text>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import type { GetRolesQuery } from '@/generated/graphql';
|
||||
import {
|
||||
refetchGetRolesQuery,
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { Button } from '@/ui/Button';
|
||||
import Loading from '@/ui/Loading';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import type {
|
||||
CreateUserRoleBaseFormData,
|
||||
CreateUserRoleModalBaseProps,
|
||||
} from './CreateUserRoleModalBase';
|
||||
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
|
||||
|
||||
export type EditUserRoleFormData = CreateUserRoleBaseFormData;
|
||||
|
||||
export type EditUserRoleModalProps = Pick<
|
||||
CreateUserRoleModalBaseProps,
|
||||
'onClose'
|
||||
> & {
|
||||
/**
|
||||
* The permission variable to edit.
|
||||
*/
|
||||
payload: any;
|
||||
};
|
||||
|
||||
export function EditUserRoleModal({
|
||||
payload: originalRole,
|
||||
...props
|
||||
}: EditUserRoleModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const form = useForm<EditUserRoleFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
roleName: originalRole.name || '',
|
||||
},
|
||||
});
|
||||
|
||||
const [updateApp, { loading: loadingUpdateAppMutation }] =
|
||||
useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentRolesData,
|
||||
loading,
|
||||
error: getRolesError,
|
||||
} = useGetRolesQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (getRolesError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2.5xl">
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(data: EditUserRoleFormData) {
|
||||
setError(undefined);
|
||||
|
||||
const currentUserRoles =
|
||||
currentRolesData.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
const roleBeingEdited = currentUserRoles.find(
|
||||
(role) => role === originalRole.name,
|
||||
);
|
||||
|
||||
const indexofRoleBeingEdited = currentUserRoles.indexOf(roleBeingEdited);
|
||||
const newRoleName = data.roleName;
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = currentUserRoles.slice();
|
||||
|
||||
if (data.roleName !== originalRole.name) {
|
||||
newAuthUserDefaultAllowedRoles[indexofRoleBeingEdited] = newRoleName;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles:
|
||||
newAuthUserDefaultAllowedRoles.join(','),
|
||||
},
|
||||
},
|
||||
});
|
||||
triggerToast(`Role "${data.roleName}" updated successfully`);
|
||||
props.onClose();
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(data: GetRolesQuery) {
|
||||
setError(undefined);
|
||||
|
||||
// Get the current roles of this application.
|
||||
const currentUserRoles = data.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
// Remove the role from the current roles.
|
||||
const filteredCurrentUserRoles = currentUserRoles.filter(
|
||||
(role) => role !== originalRole.name,
|
||||
);
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = filteredCurrentUserRoles.join(',');
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
props.onClose();
|
||||
triggerToast(`Role "${originalRole.name}" removed successfully`);
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRemoveModal}
|
||||
close={() => setShowRemoveModal(false)}
|
||||
>
|
||||
<div className="px-6 pt-5 text-center text-greyscaleDark">
|
||||
<Text variant="heading" className="mb-2 text-lg font-medium">
|
||||
Remove Role "{originalRole.name}"?
|
||||
</Text>
|
||||
|
||||
<div className="my-4">
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => handleRemove(currentRolesData)}
|
||||
className="w-full"
|
||||
loading={loadingUpdateAppMutation}
|
||||
>
|
||||
Remove Role
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowRemoveModal(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CreateUserRoleModalBase
|
||||
title="Edit Role"
|
||||
type="edit"
|
||||
onSubmit={handleSubmit}
|
||||
onRemove={() => setShowRemoveModal(true)}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useState } from 'react';
|
||||
|
||||
type EnvModalProps = {
|
||||
onSubmit: (props: {
|
||||
name: string;
|
||||
prodValue: string;
|
||||
devValue: string;
|
||||
}) => Promise<void>;
|
||||
name?: string;
|
||||
prodValue?: string;
|
||||
devValue?: string;
|
||||
close: VoidFunction;
|
||||
};
|
||||
|
||||
interface AddEnvVarModalVariablesError {
|
||||
hasError: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const DISABLED_START_ENV_VARIABLES = [
|
||||
'NHOST_',
|
||||
'HASURA_',
|
||||
'AUTH_',
|
||||
'STORAGE_',
|
||||
'POSTGRES_',
|
||||
];
|
||||
|
||||
const DISABLED_ENV_VARIABLES = [
|
||||
'PATH',
|
||||
'NODE_PATH',
|
||||
'PYTHONPATH',
|
||||
'GEM_PATH',
|
||||
'HOSTNAME',
|
||||
'TERM',
|
||||
'NODE_VERSION',
|
||||
'YARN_VERSION',
|
||||
'NODE_ENV',
|
||||
'HOME',
|
||||
];
|
||||
|
||||
export default function AddEnvVarModal({
|
||||
name: externalName,
|
||||
prodValue: externalProdValue,
|
||||
devValue: externalDevValue,
|
||||
close,
|
||||
onSubmit,
|
||||
}: EnvModalProps) {
|
||||
const [name, setName] = useState(externalName || '');
|
||||
const [prodValue, setProdValue] = useState(externalProdValue || '');
|
||||
const [devValue, setDevValue] = useState(externalDevValue || '');
|
||||
const [error, setError] = useState<AddEnvVarModalVariablesError>({
|
||||
hasError: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const noError: AddEnvVarModalVariablesError = {
|
||||
hasError: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
setError({ hasError: false, message: '' });
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
DISABLED_START_ENV_VARIABLES.some((envVar) =>
|
||||
name.toUpperCase().startsWith(envVar),
|
||||
)
|
||||
) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The environment variable name cannot start with a value that is reserved for an internal environment variable.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
DISABLED_ENV_VARIABLES.some((envVar) => envVar === name.toUpperCase())
|
||||
) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The environment variable name cannot be a value that is reserved for internal use.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// only allow alphabet characters and underscores
|
||||
const onlyLettersWithNumbersStartsWithLetter = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
|
||||
if (!onlyLettersWithNumbersStartsWithLetter.test(name)) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The name contains invalid characters. Only letters, digits, and underscores are allowed. Furthermore, the name should start with a letter.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
setError({ hasError: true, message: 'Variable name is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prodValue) {
|
||||
setError({ hasError: true, message: 'Production value is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devValue) {
|
||||
setError({ hasError: true, message: 'Development value is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit({
|
||||
name,
|
||||
prodValue,
|
||||
devValue,
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{name || 'EXAMPLE_NAME'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
The default value will be available in all environments, unless you
|
||||
override it. All values are encrypted.
|
||||
</Text>
|
||||
|
||||
<div className="my-2 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
fullWidth
|
||||
placeholder="EXAMPLE_NAME"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setName(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={prodValue}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setProdValue(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={devValue}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setDevValue(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error.hasError && (
|
||||
<Alert severity="warning" className="mb-2">
|
||||
<Text className="font-medium">Warning</Text>
|
||||
<Text>{error.message}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit">Add</Button>
|
||||
|
||||
<Button onClick={close} variant="outlined" color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,13 @@ export type DialogType =
|
||||
| 'CREATE_TABLE'
|
||||
| 'EDIT_TABLE'
|
||||
| 'CREATE_FOREIGN_KEY'
|
||||
| 'EDIT_FOREIGN_KEY';
|
||||
| 'EDIT_FOREIGN_KEY'
|
||||
| 'CREATE_ROLE'
|
||||
| 'EDIT_ROLE'
|
||||
| 'CREATE_PERMISSION_VARIABLE'
|
||||
| 'EDIT_PERMISSION_VARIABLE'
|
||||
| 'CREATE_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_ENVIRONMENT_VARIABLE';
|
||||
|
||||
export interface DialogConfig<TPayload = unknown> {
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import { CreateForeignKeyForm } from '@/components/data-browser/CreateForeignKeyForm';
|
||||
import { EditForeignKeyForm } from '@/components/data-browser/EditForeignKeyForm';
|
||||
import CreateForeignKeyForm from '@/components/data-browser/CreateForeignKeyForm';
|
||||
import EditForeignKeyForm from '@/components/data-browser/EditForeignKeyForm';
|
||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
||||
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
|
||||
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import AlertDialog from '@/ui/v2/AlertDialog';
|
||||
import { BaseDialog } from '@/ui/v2/Dialog';
|
||||
import Drawer from '@/ui/v2/Drawer';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
|
||||
import type {
|
||||
BaseSyntheticEvent,
|
||||
DetailedHTMLProps,
|
||||
HTMLProps,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import DialogContext from './DialogContext';
|
||||
import {
|
||||
@@ -16,13 +28,21 @@ import {
|
||||
drawerReducer,
|
||||
} from './dialogReducers';
|
||||
|
||||
function LoadingComponent() {
|
||||
function LoadingComponent({
|
||||
className,
|
||||
...props
|
||||
}: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> = {}) {
|
||||
return (
|
||||
<div className="grid items-center justify-center px-6 py-4">
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'grid items-center justify-center px-6 py-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ActivityIndicator
|
||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
||||
label="Loading form..."
|
||||
delay={500}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -30,27 +50,27 @@ function LoadingComponent() {
|
||||
|
||||
const CreateRecordForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateRecordForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateColumnForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/EditColumnForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateTableForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateTableForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditTableForm = dynamic(
|
||||
() => import('@/components/data-browser/EditTableForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
@@ -209,6 +229,25 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
[closeDialog, closeDrawer, onDirtyStateChange, openDialog, openDrawer],
|
||||
);
|
||||
|
||||
const sharedDialogProps = {
|
||||
...dialogPayload,
|
||||
onSubmit: async (values: any) => {
|
||||
await dialogPayload?.onSubmit?.(values);
|
||||
|
||||
closeDialog();
|
||||
},
|
||||
onCancel: closeDialogWithDirtyGuard,
|
||||
};
|
||||
|
||||
const sharedDrawerProps = {
|
||||
onSubmit: async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
},
|
||||
onCancel: closeDrawerWithDirtyGuard,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={contextValue}>
|
||||
<AlertDialog
|
||||
@@ -249,33 +288,47 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
open={dialogOpen}
|
||||
onClose={closeDialogWithDirtyGuard}
|
||||
TransitionProps={{ onExited: clearDialogContent, unmountOnExit: false }}
|
||||
PaperProps={{ className: 'max-w-md w-full' }}
|
||||
PaperProps={{
|
||||
...dialogProps?.PaperProps,
|
||||
className: twMerge(
|
||||
'max-w-md w-full',
|
||||
dialogProps?.PaperProps?.className,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<RetryableErrorBoundary
|
||||
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
||||
>
|
||||
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
|
||||
<CreateForeignKeyForm
|
||||
{...dialogPayload}
|
||||
onSubmit={async (values) => {
|
||||
await dialogPayload?.onSubmit(values);
|
||||
|
||||
closeDialog();
|
||||
}}
|
||||
onCancel={closeDialogWithDirtyGuard}
|
||||
/>
|
||||
<CreateForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_FOREIGN_KEY' && (
|
||||
<EditForeignKeyForm
|
||||
{...dialogPayload}
|
||||
onSubmit={async (values) => {
|
||||
await dialogPayload?.onSubmit(values);
|
||||
<EditForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
closeDialog();
|
||||
}}
|
||||
onCancel={closeDialogWithDirtyGuard}
|
||||
/>
|
||||
{activeDialogType === 'CREATE_ROLE' && (
|
||||
<CreateRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_ROLE' && (
|
||||
<EditRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
|
||||
<CreatePermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_PERMISSION_VARIABLE' && (
|
||||
<EditPermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_ENVIRONMENT_VARIABLE' && (
|
||||
<CreateEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
|
||||
<EditEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</BaseDialog>
|
||||
@@ -292,61 +345,34 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<RetryableErrorBoundary>
|
||||
{activeDrawerType === 'CREATE_RECORD' && (
|
||||
<CreateRecordForm
|
||||
{...sharedDrawerProps}
|
||||
columns={drawerPayload?.columns}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_COLUMN' && (
|
||||
<CreateColumnForm
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
<CreateColumnForm {...sharedDrawerProps} />
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_COLUMN' && (
|
||||
<EditColumnForm
|
||||
{...sharedDrawerProps}
|
||||
column={drawerPayload?.column}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_TABLE' && (
|
||||
<CreateTableForm
|
||||
{...sharedDrawerProps}
|
||||
schema={drawerPayload?.schema}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_TABLE' && (
|
||||
<EditTableForm
|
||||
{...sharedDrawerProps}
|
||||
table={drawerPayload?.table}
|
||||
schema={drawerPayload?.schema}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface CreateForeignKeyFormProps
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
||||
}
|
||||
|
||||
export function CreateForeignKeyForm({
|
||||
export default function CreateForeignKeyForm({
|
||||
onSubmit,
|
||||
selectedColumn,
|
||||
...props
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './CreateForeignKeyForm';
|
||||
export { CreateForeignKeyForm as default } from './CreateForeignKeyForm';
|
||||
export { default } from './CreateForeignKeyForm';
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface EditForeignKeyFormProps
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
||||
}
|
||||
|
||||
export function EditForeignKeyForm({
|
||||
export default function EditForeignKeyForm({
|
||||
foreignKeyRelation,
|
||||
selectedColumn,
|
||||
onSubmit,
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './EditForeignKeyForm';
|
||||
export { EditForeignKeyForm as default } from './EditForeignKeyForm';
|
||||
export { default } from './EditForeignKeyForm';
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface ErrorComponentProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
function ErrorComponent({ message }: ErrorComponentProps) {
|
||||
return (
|
||||
<div className="my-4 rounded-md bg-warning px-4 py-2 text-dark">
|
||||
<Text className="font-medium text-textOrange">Error</Text>
|
||||
<Text className="pt-2 font-medium text-dimBlack" size="normal">
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ErrorComponent;
|
||||
@@ -1,22 +0,0 @@
|
||||
function Copy({ stroke = '#21324B', ...props }: any) {
|
||||
return (
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M10.5 10.5h3v-8h-8v3"
|
||||
stroke={stroke}
|
||||
strokeWidth={1.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.5 5.5h-8v8h8v-8z"
|
||||
stroke={stroke}
|
||||
strokeWidth={1.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Copy;
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function Eye(props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Eye;
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function EyeOff(
|
||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default EyeOff;
|
||||
@@ -1,14 +0,0 @@
|
||||
export default function Lock(
|
||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 1.75a1.5 1.5 0 0 0-1.5 1.5v1.5h3v-1.5A1.5 1.5 0 0 0 8 1.75Zm-3 1.5v1.5H3c-.69 0-1.25.56-1.25 1.25v7c0 .69.56 1.25 1.25 1.25h10c.69 0 1.25-.56 1.25-1.25V6c0-.69-.56-1.25-1.25-1.25h-2v-1.5a3 3 0 0 0-6 0Zm-1.75 3h9.5v6.5h-9.5v-6.5ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
fill="#21324B"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function Plus(props: any) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 4.75C9.787 4.75 4.75 9.787 4.75 16S9.787 27.25 16 27.25 27.25 22.213 27.25 16 22.213 4.75 16 4.75zM3.25 16C3.25 8.958 8.958 3.25 16 3.25S28.75 8.958 28.75 16 23.042 28.75 16 28.75 3.25 23.042 3.25 16zm12 .75H10v-1.5h5.25V10h1.5v5.25H22v1.5h-5.25V22h-1.5v-5.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Plus;
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function TwilioIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={108}
|
||||
height={32}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.864 0c8.844 0 15.984 7.147 15.984 16s-7.14 16-15.984 16C8.019 32 .88 24.853.88 16S8.02 0 16.864 0Zm0 4.267c-6.5 0-11.722 5.226-11.722 11.733a11.694 11.694 0 0 0 11.722 11.733c6.5 0 11.721-5.226 11.721-11.733A11.694 11.694 0 0 0 16.864 4.267Zm27.173 2.026c.213-.106.426.107.426.214v4.586h8.525c.106 0 .32.214.32.32l.639 2.667.639 2.667.107.32.106-.32 1.599-5.334c0-.213.213-.32.32-.32h4.262c.106 0 .32.214.32.32l1.704 5.654.107-.32 1.385-5.334c0-.213.213-.32.32-.32h10.869c.106 0 .213.214.213.32V25.6c0 .213-.213.32-.32.32h-5.434c-.213 0-.32-.213-.32-.32V12.16l-4.05 13.44c0 .187-.162.292-.275.315l-.044.005H60.98c-.107 0-.32-.213-.32-.32l-.853-2.88-.959-3.093L56.93 25.6c0 .213-.213.32-.32.32h-4.475c-.106 0-.32-.213-.32-.32l-4.049-13.44v3.413c0 .214-.213.32-.32.32h-3.09v3.627c0 1.067.533 1.493 1.492 1.493.426 0 .96-.106 1.492-.32.106-.106.32 0 .32.214v4.266c-.96.534-2.345.854-3.836.854-3.517 0-5.435-1.6-5.435-5.12v-5.014h-1.385c-.213 0-.32-.213-.32-.32V11.52c0-.213.213-.32.32-.32h1.385V8.32c0-.213.106-.32.32-.32l5.328-1.707Zm54.984 4.48c4.689 0 8.099 3.52 8.099 7.574 0 4.053-3.41 7.573-8.205 7.573-4.689 0-8.099-3.52-8.099-7.573 0-4.054 3.41-7.574 8.205-7.574Zm-16.197-4.48c.213 0 .32.107.32.214v18.986c0 .214-.213.32-.32.32H77.39c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.434Zm7.14 4.8c.213 0 .32.214.32.32v13.974c0 .213-.214.32-.32.32h-5.435c-.213 0-.32-.214-.32-.32V11.413c0-.213.214-.32.32-.32h5.435ZM12.92 16.64a3.322 3.322 0 0 1 3.303 3.307 3.322 3.322 0 0 1-3.303 3.306 3.322 3.322 0 0 1-3.303-3.306 3.322 3.322 0 0 1 3.303-3.307Zm7.886 0a3.322 3.322 0 0 1 3.303 3.307 3.322 3.322 0 0 1-3.303 3.306 3.322 3.322 0 0 1-3.304-3.306 3.322 3.322 0 0 1 3.304-3.307Zm78.214-.747c-1.385 0-2.344 1.174-2.344 2.56 0 1.387 1.066 2.56 2.344 2.56 1.386 0 2.345-1.173 2.345-2.56 0-1.493-1.066-2.666-2.345-2.56ZM20.807 8.747a3.322 3.322 0 0 1 3.303 3.306 3.322 3.322 0 0 1-3.303 3.307 3.322 3.322 0 0 1-3.304-3.307 3.322 3.322 0 0 1 3.304-3.306Zm-7.886 0a3.322 3.322 0 0 1 3.303 3.306 3.322 3.322 0 0 1-3.303 3.307 3.322 3.322 0 0 1-3.303-3.307 3.322 3.322 0 0 1 3.303-3.306Zm62.87-2.454c.107 0 .213.107.32.214V9.92c0 .213-.213.32-.32.32h-5.647c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.647Zm14.28 0c.213 0 .319.107.319.214V9.92c0 .213-.213.32-.32.32h-5.647c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.647Z"
|
||||
fill="#F22F46"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default TwilioIcon;
|
||||
@@ -4,19 +4,19 @@ import { twMerge } from 'tailwind-merge';
|
||||
export interface ContainerProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
|
||||
/**
|
||||
* Class name passed to the wrapper element.
|
||||
* Class name passed to the root element.
|
||||
*/
|
||||
wrapperClassName?: string;
|
||||
rootClassName?: string;
|
||||
}
|
||||
|
||||
export default function Container({
|
||||
children,
|
||||
className,
|
||||
wrapperClassName,
|
||||
rootClassName,
|
||||
...props
|
||||
}: ContainerProps) {
|
||||
return (
|
||||
<div className={twMerge('mx-auto w-full bg-white', wrapperClassName)}>
|
||||
<div className={twMerge('mx-auto w-full bg-white', rootClassName)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'mx-auto max-w-7xl bg-white px-5 pt-6 pb-20',
|
||||
|
||||
@@ -5,6 +5,9 @@ import Image from 'next/image';
|
||||
|
||||
export default function OverviewProjectInfo() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { region, subdomain } = currentApplication || {};
|
||||
const isRegionAvailable =
|
||||
region?.awsName && region?.countryCode && region?.city;
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row content-start gap-6">
|
||||
@@ -16,28 +19,28 @@ export default function OverviewProjectInfo() {
|
||||
<div className="grid grid-flow-row gap-3">
|
||||
<InfoCard
|
||||
title="Region"
|
||||
value={currentApplication.region?.awsName}
|
||||
value={region?.awsName}
|
||||
customValue={
|
||||
currentApplication.region && (
|
||||
region.countryCode &&
|
||||
region.city && (
|
||||
<div className="grid grid-flow-col items-center gap-1 self-center">
|
||||
<Image
|
||||
src={`/assets/${currentApplication.region.countryCode}.svg`}
|
||||
alt={`Logo of ${currentApplication.region.countryCode}`}
|
||||
src={`/assets/${region.countryCode}.svg`}
|
||||
alt={`Logo of ${region.countryCode}`}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
|
||||
<Text className="text-sm font-medium text-greyscaleDark">
|
||||
{currentApplication.region.city} (
|
||||
{currentApplication.region.awsName})
|
||||
{region.city} ({region.awsName})
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
disableCopy={!currentApplication.region}
|
||||
disableCopy={!isRegionAvailable}
|
||||
/>
|
||||
|
||||
<InfoCard title="Subdomain" value={currentApplication.subdomain} />
|
||||
<InfoCard title="Subdomain" value={subdomain} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import Input from '@/ui/v2/Input';
|
||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword/generateRandomDatabasePassword';
|
||||
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword';
|
||||
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
|
||||
@@ -8,7 +8,6 @@ import Switch from '@/ui/v2/Switch';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import Image from 'next/image';
|
||||
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
|
||||
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface SettingsContainerProps
|
||||
@@ -31,7 +30,7 @@ export interface SettingsContainerProps
|
||||
/**
|
||||
* The description for the section.
|
||||
*/
|
||||
description: string | ReactNode;
|
||||
description?: string | ReactNode;
|
||||
/**
|
||||
* Link to the documentation.
|
||||
*
|
||||
@@ -40,6 +39,8 @@ export interface SettingsContainerProps
|
||||
docsLink?: string;
|
||||
/**
|
||||
* Props for the primary action.
|
||||
*
|
||||
* @deprecated Use `slotProps.submitButton` instead.
|
||||
*/
|
||||
primaryActionButtonProps?: ButtonProps;
|
||||
/**
|
||||
@@ -75,9 +76,26 @@ export interface SettingsContainerProps
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Props to be passed to the Switch component.
|
||||
* Props to be passed to different slots inside the component.
|
||||
*/
|
||||
switchProps?: SwitchProps;
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props to be passed to the root element.
|
||||
*/
|
||||
root?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
|
||||
/**
|
||||
* Props to be passed to the `<Switch />` component.
|
||||
*/
|
||||
switch?: SwitchProps;
|
||||
/**
|
||||
* Props to be passed to the footer element.
|
||||
*/
|
||||
submitButton?: ButtonProps;
|
||||
/**
|
||||
* Props to be passed to the footer element.
|
||||
*/
|
||||
footer?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
|
||||
};
|
||||
}
|
||||
|
||||
export default function SettingsContainer({
|
||||
@@ -94,14 +112,15 @@ export default function SettingsContainer({
|
||||
switchId,
|
||||
showSwitch = false,
|
||||
rootClassName,
|
||||
switchProps,
|
||||
docsTitle,
|
||||
slotProps: { root, switch: switchSlot, submitButton, footer } = {},
|
||||
}: SettingsContainerProps) {
|
||||
return (
|
||||
<div
|
||||
{...root}
|
||||
className={twMerge(
|
||||
'grid grid-flow-row gap-4 rounded-lg border-1 border-gray-200 bg-white py-4',
|
||||
rootClassName,
|
||||
root?.className || rootClassName,
|
||||
)}
|
||||
>
|
||||
<div className="grid grid-flow-col place-content-between gap-3 px-4">
|
||||
@@ -131,14 +150,14 @@ export default function SettingsContainer({
|
||||
checked={enabled}
|
||||
onChange={(e) => onEnabledChange(e.target.checked)}
|
||||
className="self-center"
|
||||
{...switchProps}
|
||||
{...switchSlot}
|
||||
/>
|
||||
)}
|
||||
{switchId && showSwitch && (
|
||||
<ControlledSwitch
|
||||
className="self-center"
|
||||
name={switchId}
|
||||
{...switchProps}
|
||||
{...switchSlot}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -148,9 +167,11 @@ export default function SettingsContainer({
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...footer}
|
||||
className={twMerge(
|
||||
'grid grid-flow-col items-center gap-x-2 border-t border-gray-200 px-4 pt-3.5',
|
||||
docsLink ? 'place-content-between' : 'justify-end',
|
||||
footer?.className,
|
||||
)}
|
||||
>
|
||||
{docsLink && (
|
||||
@@ -173,11 +194,17 @@ export default function SettingsContainer({
|
||||
|
||||
<Button
|
||||
variant={
|
||||
primaryActionButtonProps?.disabled ? 'outlined' : 'contained'
|
||||
(submitButton || primaryActionButtonProps)?.disabled
|
||||
? 'outlined'
|
||||
: 'contained'
|
||||
}
|
||||
color={
|
||||
(submitButton || primaryActionButtonProps)?.disabled
|
||||
? 'secondary'
|
||||
: 'primary'
|
||||
}
|
||||
color={primaryActionButtonProps?.disabled ? 'secondary' : 'primary'}
|
||||
type="submit"
|
||||
{...primaryActionButtonProps}
|
||||
{...(submitButton || primaryActionButtonProps)}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
|
||||
import ProjectLayout from '@/components/layout/ProjectLayout';
|
||||
import type { SettingsSidebarProps } from '@/components/settings/SettingsSidebar';
|
||||
@@ -33,8 +34,8 @@ export default function SettingsLayout({
|
||||
{...sidebarProps}
|
||||
/>
|
||||
|
||||
<div className="flex w-full flex-auto flex-col overflow-x-hidden">
|
||||
{children}
|
||||
<div className="flex w-full flex-auto flex-col overflow-x-hidden bg-[#fafafa]">
|
||||
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
|
||||
</div>
|
||||
</ProjectLayout>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ function SettingsNavLink({
|
||||
return (
|
||||
<ListItem.Root>
|
||||
<ListItem.Button
|
||||
dense
|
||||
href={finalUrl}
|
||||
component={NavLink}
|
||||
selected={active}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface BaseEnvironmentVariableFormValues {
|
||||
/**
|
||||
* Identifier of the environment variable.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The name of the role.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Development environment variable value.
|
||||
*/
|
||||
devValue: string;
|
||||
/**
|
||||
* Production environment variable value.
|
||||
*/
|
||||
prodValue: string;
|
||||
}
|
||||
|
||||
export interface BaseEnvironmentVariableFormProps {
|
||||
/**
|
||||
* Determines the mode of the form.
|
||||
*
|
||||
* @default 'edit'
|
||||
*/
|
||||
mode?: 'edit' | 'create';
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: (values: BaseEnvironmentVariableFormValues) => void;
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
onCancel?: VoidFunction;
|
||||
/**
|
||||
* Submit button text.
|
||||
*
|
||||
* @default 'Save'
|
||||
*/
|
||||
submitButtonText?: string;
|
||||
}
|
||||
|
||||
export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
||||
name: Yup.string()
|
||||
.required('This field is required.')
|
||||
.test(
|
||||
'isEnvVarPermitted',
|
||||
'This is a reserved name.',
|
||||
(value) =>
|
||||
![
|
||||
'PATH',
|
||||
'NODE_PATH',
|
||||
'PYTHONPATH',
|
||||
'GEM_PATH',
|
||||
'HOSTNAME',
|
||||
'TERM',
|
||||
'NODE_VERSION',
|
||||
'YARN_VERSION',
|
||||
'NODE_ENV',
|
||||
'HOME',
|
||||
].includes(value),
|
||||
)
|
||||
.test(
|
||||
'isEnvVarPrefixPermitted',
|
||||
`The name can't start with NHOST_, HASURA_, AUTH_, STORAGE_ or POSTGRES_.`,
|
||||
(value) =>
|
||||
['NHOST_', 'HASURA_', 'AUTH_', 'STORAGE_', 'POSTGRES_'].every(
|
||||
(prefix) => !value.startsWith(prefix),
|
||||
),
|
||||
)
|
||||
.test('isEnvVarValid', `The name must start with a letter.`, (value) =>
|
||||
/^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/i.test(value),
|
||||
),
|
||||
devValue: Yup.string().required('This field is required.'),
|
||||
prodValue: Yup.string().required('This field is required.'),
|
||||
});
|
||||
|
||||
export default function BaseEnvironmentVariableForm({
|
||||
mode = 'edit',
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
}: BaseEnvironmentVariableFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const form = useFormContext<BaseEnvironmentVariableFormValues>();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, dirtyFields, isSubmitting },
|
||||
} = form;
|
||||
|
||||
// react-hook-form's isDirty gets true even if an input field is focused, then
|
||||
// immediately unfocused - we can't rely on that information
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
||||
<Text variant="subtitle1" component="span">
|
||||
Environment Variables are made available to all your services. All
|
||||
values are encrypted.
|
||||
</Text>
|
||||
|
||||
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
||||
<Input
|
||||
{...register('name', {
|
||||
onChange: (event) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/g.test(event.target.value)
|
||||
) {
|
||||
// we need to prevent invalid characters from being entered
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.target.value = event.target.value.replace(
|
||||
/[^a-zA-Z0-9_]/g,
|
||||
'',
|
||||
);
|
||||
}
|
||||
},
|
||||
})}
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="EXAMPLE_NAME"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.name}
|
||||
helperText={errors?.name?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus={mode === 'create'}
|
||||
disabled={mode === 'edit'}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('prodValue')}
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
placeholder="Enter value"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.prodValue}
|
||||
helperText={errors?.prodValue?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus={mode === 'edit'}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('devValue')}
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
placeholder="Enter value"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.devValue}
|
||||
helperText={errors?.devValue?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './BaseEnvironmentVariableForm';
|
||||
export { default } from './BaseEnvironmentVariableForm';
|
||||
@@ -0,0 +1,117 @@
|
||||
import type {
|
||||
BaseEnvironmentVariableFormProps,
|
||||
BaseEnvironmentVariableFormValues,
|
||||
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
||||
import BaseEnvironmentVariableForm, {
|
||||
baseEnvironmentVariableFormValidationSchema,
|
||||
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetEnvironmentVariablesQuery,
|
||||
useInsertEnvironmentVariablesMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface CreateEnvironmentVariableFormProps
|
||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function CreateEnvironmentVariableForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: CreateEnvironmentVariableFormProps) {
|
||||
const form = useForm<BaseEnvironmentVariableFormValues>({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
devValue: '',
|
||||
prodValue: '',
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
||||
});
|
||||
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
},
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const [insertEnvironmentVariables] = useInsertEnvironmentVariablesMutation({
|
||||
refetchQueries: ['getEnvironmentVariables'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading environment variables..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { setError } = form;
|
||||
|
||||
async function handleSubmit({
|
||||
name,
|
||||
prodValue,
|
||||
devValue,
|
||||
}: BaseEnvironmentVariableFormValues) {
|
||||
if (
|
||||
data?.environmentVariables?.some(
|
||||
(environmentVariable) => environmentVariable.name === name,
|
||||
)
|
||||
) {
|
||||
setError('name', {
|
||||
message: 'This environment variable already exists.',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const insertEnvironmentVariablePromise = insertEnvironmentVariables({
|
||||
variables: {
|
||||
environmentVariables: [
|
||||
{ appId: currentApplication.id, name, prodValue, devValue },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
insertEnvironmentVariablePromise,
|
||||
{
|
||||
loading: 'Creating environment variable...',
|
||||
success: 'Environment variable has been created successfully.',
|
||||
error: 'An error occurred while creating the environment variable.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
onSubmit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BaseEnvironmentVariableForm
|
||||
mode="create"
|
||||
submitButtonText="Create"
|
||||
onSubmit={handleSubmit}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CreateEnvironmentVariableForm';
|
||||
export { default } from './CreateEnvironmentVariableForm';
|
||||
@@ -0,0 +1,124 @@
|
||||
import type {
|
||||
BaseEnvironmentVariableFormProps,
|
||||
BaseEnvironmentVariableFormValues,
|
||||
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
||||
import BaseEnvironmentVariableForm, {
|
||||
baseEnvironmentVariableFormValidationSchema,
|
||||
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { EnvironmentVariable } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetEnvironmentVariablesQuery,
|
||||
useUpdateEnvironmentVariableMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface EditEnvironmentVariableFormProps
|
||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> {
|
||||
/**
|
||||
* The environment variable to edit.
|
||||
*/
|
||||
originalEnvironmentVariable: EnvironmentVariable;
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function EditEnvironmentVariableForm({
|
||||
originalEnvironmentVariable,
|
||||
onSubmit,
|
||||
...props
|
||||
}: EditEnvironmentVariableFormProps) {
|
||||
const form = useForm<BaseEnvironmentVariableFormValues>({
|
||||
defaultValues: {
|
||||
id: originalEnvironmentVariable.id || '',
|
||||
name: originalEnvironmentVariable.name || '',
|
||||
devValue: originalEnvironmentVariable.devValue || '',
|
||||
prodValue: originalEnvironmentVariable.prodValue || '',
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
||||
});
|
||||
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
},
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const [updateEnvironmentVariable] = useUpdateEnvironmentVariableMutation({
|
||||
refetchQueries: ['getEnvironmentVariables'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading environment variables..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { setError } = form;
|
||||
|
||||
async function handleSubmit({
|
||||
id,
|
||||
name,
|
||||
prodValue,
|
||||
devValue,
|
||||
}: BaseEnvironmentVariableFormValues) {
|
||||
if (
|
||||
data?.environmentVariables?.some(
|
||||
(environmentVariable) =>
|
||||
environmentVariable.name === name &&
|
||||
environmentVariable.name !== originalEnvironmentVariable.name,
|
||||
)
|
||||
) {
|
||||
setError('name', {
|
||||
message: 'This environment variable already exists.',
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updateEnvironmentVariablePromise = updateEnvironmentVariable({
|
||||
variables: {
|
||||
id,
|
||||
environmentVariable: {
|
||||
prodValue,
|
||||
devValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateEnvironmentVariablePromise,
|
||||
{
|
||||
loading: 'Updating environment variable...',
|
||||
success: 'Environment variable has been updated successfully.',
|
||||
error: 'An error occurred while updating the environment variable.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
onSubmit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BaseEnvironmentVariableForm onSubmit={handleSubmit} {...props} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './EditEnvironmentVariableForm';
|
||||
export { default } from './EditEnvironmentVariableForm';
|
||||
@@ -0,0 +1,244 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { EnvironmentVariable } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import DotsVerticalIcon from '@/ui/v2/icons/DotsVerticalIcon';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useDeleteEnvironmentVariableMutation,
|
||||
useGetEnvironmentVariablesQuery,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
||||
import { Fragment } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface PermissionVariableSettingsFormValues {
|
||||
/**
|
||||
* Permission variables.
|
||||
*/
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
}
|
||||
|
||||
export default function EnvironmentVariableSettings() {
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [deleteEnvironmentVariable] = useDeleteEnvironmentVariableMutation({
|
||||
refetchQueries: ['getEnvironmentVariables'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading environment variables..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleDeleteVariable({ id }: EnvironmentVariable) {
|
||||
const deleteEnvironmentVariablePromise = deleteEnvironmentVariable({
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
deleteEnvironmentVariablePromise,
|
||||
{
|
||||
loading: 'Deleting environment variable...',
|
||||
success: 'Environment variable has been deleted successfully.',
|
||||
error: 'An error occurred while deleting the environment variable.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
}
|
||||
|
||||
function handleOpenCreator() {
|
||||
openDialog('CREATE_ENVIRONMENT_VARIABLE', {
|
||||
title: 'Create Environment Variable',
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'gap-2 max-w-sm' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleOpenEditor(originalVariable: EnvironmentVariable) {
|
||||
openDialog('EDIT_ENVIRONMENT_VARIABLE', {
|
||||
title: 'Edit Environment Variables',
|
||||
payload: { originalEnvironmentVariable: originalVariable },
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'gap-2 max-w-sm' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirmDelete(originalVariable: EnvironmentVariable) {
|
||||
openAlertDialog({
|
||||
title: 'Delete Environment Variable',
|
||||
payload: (
|
||||
<Text>
|
||||
Are you sure you want to delete the "
|
||||
<strong>{originalVariable.name}</strong>" environment variable?
|
||||
This cannot be undone.
|
||||
</Text>
|
||||
),
|
||||
props: {
|
||||
primaryButtonColor: 'error',
|
||||
primaryButtonText: 'Delete',
|
||||
onPrimaryAction: () => handleDeleteVariable(originalVariable),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const availableEnvironmentVariables =
|
||||
[...data.environmentVariables].sort(
|
||||
(a, b) =>
|
||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Project Environment Variables"
|
||||
description="Environment Variables are key-value pairs configured outside your source code. They are used to store environment-specific values such as API keys."
|
||||
docsLink="https://docs.nhost.io/platform/environment-variables"
|
||||
docsTitle="Environment Variables"
|
||||
rootClassName="gap-0"
|
||||
className={twMerge(
|
||||
'px-0 my-2',
|
||||
availableEnvironmentVariables.length === 0 && 'gap-2',
|
||||
)}
|
||||
slotProps={{ submitButton: { className: 'hidden' } }}
|
||||
>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-2 border-b-1 border-gray-200 px-4 py-3">
|
||||
<Text className="font-medium">Variable Name</Text>
|
||||
<Text className="font-medium lg:col-span-2">Updated</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
{availableEnvironmentVariables.length > 0 && (
|
||||
<List>
|
||||
{availableEnvironmentVariables.map((environmentVariable, index) => {
|
||||
const timestamp = formatDistanceToNowStrict(
|
||||
parseISO(environmentVariable.updatedAt),
|
||||
{ addSuffix: true, roundingMethod: 'floor' },
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={environmentVariable.id}>
|
||||
<ListItem.Root
|
||||
className="px-4 grid grid-cols-2 lg:grid-cols-3 gap-2"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<IconButton variant="borderless" color="secondary">
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
handleOpenEditor(environmentVariable)
|
||||
}
|
||||
>
|
||||
<Text className="font-medium">Edit</Text>
|
||||
</Dropdown.Item>
|
||||
|
||||
<Divider component="li" />
|
||||
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
handleConfirmDelete(environmentVariable)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
className="font-medium"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text className="truncate">
|
||||
{environmentVariable.name}
|
||||
</ListItem.Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle1"
|
||||
className="lg:col-span-2 truncate"
|
||||
>
|
||||
{timestamp === '0 seconds ago' ||
|
||||
timestamp === 'in 0 seconds'
|
||||
? 'Now'
|
||||
: timestamp}
|
||||
</Text>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availableEnvironmentVariables.length - 1
|
||||
? '!mt-4'
|
||||
: '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="justify-self-start mx-4"
|
||||
variant="borderless"
|
||||
startIcon={<PlusIcon />}
|
||||
onClick={handleOpenCreator}
|
||||
>
|
||||
Create Environment Variable
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './EnvironmentVariableSettings';
|
||||
export { default } from './EnvironmentVariableSettings';
|
||||
@@ -0,0 +1,214 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||
import { useAppClient } from '@/hooks/useAppClient';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import EyeIcon from '@/ui/v2/icons/EyeIcon';
|
||||
import EyeOffIcon from '@/ui/v2/icons/EyeOffIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { generateAppServiceUrl, generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { useGetAppInjectedVariablesQuery } from '@/utils/__generated__/graphql';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
export default function SystemEnvironmentVariableSettings() {
|
||||
const [showAdminSecret, setShowAdminSecret] = useState(false);
|
||||
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||
|
||||
const { openAlertDialog } = useDialog();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { data, loading, error } = useGetAppInjectedVariablesQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
});
|
||||
|
||||
const appClient = useAppClient({ start: false });
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading system environment variables..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
function showJwtSecret() {
|
||||
openAlertDialog({
|
||||
title: 'Auth JWT Secret',
|
||||
payload: (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Text variant="subtitle2">
|
||||
This is the key used for generating JWTs. It's HMAC-SHA-based
|
||||
and the same as configured in Hasura.
|
||||
</Text>
|
||||
|
||||
<Input
|
||||
defaultValue={data?.app?.hasuraGraphqlJwtSecret}
|
||||
disabled
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={5}
|
||||
hideEmptyHelperText
|
||||
inputProps={{ className: 'font-mono' }}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
props: {
|
||||
hidePrimaryAction: true,
|
||||
secondaryButtonText: 'Close',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const systemEnvironmentVariables = [
|
||||
{
|
||||
key: 'NHOST_BACKEND_URL',
|
||||
value: generateRemoteAppUrl(currentApplication.subdomain),
|
||||
},
|
||||
{ key: 'NHOST_SUBDOMAIN', value: currentApplication.subdomain },
|
||||
{ key: 'NHOST_REGION', value: currentApplication.region.awsName },
|
||||
{
|
||||
key: 'NHOST_HASURA_URL',
|
||||
value:
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? LOCAL_HASURA_URL
|
||||
: `${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
currentApplication.region.awsName,
|
||||
'hasura',
|
||||
)}/console`,
|
||||
},
|
||||
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
|
||||
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
||||
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="System Environment Variables"
|
||||
description="Environment Variables are key-value pairs configured outside your source code. They are used to store environment-specific values such as API keys."
|
||||
docsLink="https://docs.nhost.io/platform/environment-variables#system-environment-variables"
|
||||
rootClassName="gap-0"
|
||||
className="px-0 mt-2 mb-2.5"
|
||||
slotProps={{ submitButton: { className: 'invisible' } }}
|
||||
>
|
||||
<div className="grid grid-cols-3 border-b-1 gap-2 border-gray-200 px-4 py-3">
|
||||
<Text className="font-medium">Variable Name</Text>
|
||||
<Text className="font-medium lg:col-span-2">Value</Text>
|
||||
</div>
|
||||
|
||||
<List>
|
||||
<ListItem.Root className="px-4 grid grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<ListItem.Text>NHOST_ADMIN_SECRET</ListItem.Text>
|
||||
|
||||
<div className="grid grid-flow-col lg:col-span-2 gap-2 items-center justify-start">
|
||||
<Text className="text-greyscaleGreyDark truncate">
|
||||
{showAdminSecret ? (
|
||||
<InlineCode className="!text-sm font-medium max-h-[initial] h-[initial]">
|
||||
{currentApplication?.hasuraGraphqlAdminSecret}
|
||||
</InlineCode>
|
||||
) : (
|
||||
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
aria-label={
|
||||
showAdminSecret ? 'Hide Admin Secret' : 'Show Admin Secret'
|
||||
}
|
||||
onClick={() => setShowAdminSecret((show) => !show)}
|
||||
>
|
||||
{showAdminSecret ? (
|
||||
<EyeOffIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider component="li" className="!my-4" />
|
||||
|
||||
<ListItem.Root className="px-4 grid grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<ListItem.Text>NHOST_WEBHOOK_SECRET</ListItem.Text>
|
||||
|
||||
<div className="grid grid-flow-col gap-2 lg:col-span-2 items-center justify-start">
|
||||
<Text className="text-greyscaleGreyDark truncate">
|
||||
{showWebhookSecret ? (
|
||||
<InlineCode className="!text-sm font-medium max-h-[initial] h-[initial]">
|
||||
{data?.app?.webhookSecret}
|
||||
</InlineCode>
|
||||
) : (
|
||||
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
aria-label={
|
||||
showWebhookSecret
|
||||
? 'Hide Webhook Secret'
|
||||
: 'Show Webhook Secret'
|
||||
}
|
||||
onClick={() => setShowWebhookSecret((show) => !show)}
|
||||
>
|
||||
{showWebhookSecret ? (
|
||||
<EyeOffIcon className="w-5 h-5" />
|
||||
) : (
|
||||
<EyeIcon className="w-5 h-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider component="li" className="!my-4" />
|
||||
|
||||
{systemEnvironmentVariables.map((environmentVariable, index) => (
|
||||
<Fragment key={environmentVariable.key}>
|
||||
<ListItem.Root className="px-4 grid grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<ListItem.Text>{environmentVariable.key}</ListItem.Text>
|
||||
|
||||
<Text className="truncate lg:col-span-2">
|
||||
{environmentVariable.value}
|
||||
</Text>
|
||||
</ListItem.Root>
|
||||
|
||||
{index !== systemEnvironmentVariables.length - 1 && (
|
||||
<Divider className="!my-4" />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
<Divider component="li" className="!mt-4 !mb-2.5" />
|
||||
|
||||
<ListItem.Root className="px-4 grid grid-cols-2 lg:grid-cols-3 justify-start">
|
||||
<ListItem.Text>NHOST_JWT_SECRET</ListItem.Text>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={showJwtSecret}
|
||||
size="small"
|
||||
className="justify-self-start"
|
||||
>
|
||||
Show JWT Secret
|
||||
</Button>
|
||||
</ListItem.Root>
|
||||
</List>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './SystemEnvironmentVariableSettings';
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface BasePermissionVariableFormValues {
|
||||
/**
|
||||
* Permission variable key.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Permission variable value.
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BasePermissionVariableFormProps {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: (values: BasePermissionVariableFormValues) => void;
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
onCancel?: VoidFunction;
|
||||
/**
|
||||
* Submit button text.
|
||||
*
|
||||
* @default 'Save'
|
||||
*/
|
||||
submitButtonText?: string;
|
||||
}
|
||||
|
||||
export const basePermissionVariableValidationSchema = Yup.object({
|
||||
key: Yup.string().required('This field is required.'),
|
||||
value: Yup.string().required('This field is required.'),
|
||||
});
|
||||
|
||||
export default function BasePermissionVariableForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
}: BasePermissionVariableFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const form = useFormContext<BasePermissionVariableFormValues>();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { dirtyFields, errors, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2 px-6 pb-6">
|
||||
<Text variant="subtitle1" component="span">
|
||||
Enter the field name and the path you want to use in this permission
|
||||
variable.
|
||||
</Text>
|
||||
|
||||
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
||||
<Input
|
||||
{...register('key', {
|
||||
onChange: (event) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// we need to prevent invalid characters from being entered
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.target.value = event.target.value.replace(
|
||||
/[^a-zA-Z-]/gi,
|
||||
'',
|
||||
);
|
||||
}
|
||||
},
|
||||
})}
|
||||
id="key"
|
||||
label="Field Name"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.key}
|
||||
helperText={errors?.key?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
slotProps={{ input: { className: '!pl-px' } }}
|
||||
startAdornment={
|
||||
<Text className="shrink-0 pl-2 text-greyscaleGrey">X-Hasura-</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('value', {
|
||||
onChange: (event) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-_.[\]]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// we need to prevent invalid characters from being entered
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
event.target.value = event.target.value.replace(
|
||||
/[^a-zA-Z-.[\]]/gi,
|
||||
'',
|
||||
);
|
||||
}
|
||||
},
|
||||
})}
|
||||
id="value"
|
||||
label="Path"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.value}
|
||||
helperText={errors?.value?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
slotProps={{ input: { className: '!pl-px' } }}
|
||||
startAdornment={
|
||||
<Text className="shrink-0 pl-2 text-greyscaleGrey">user.</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './BasePermissionVariableForm';
|
||||
export { default } from './BasePermissionVariableForm';
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
BasePermissionVariableFormProps,
|
||||
BasePermissionVariableFormValues,
|
||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||
import BasePermissionVariableForm, {
|
||||
basePermissionVariableValidationSchema,
|
||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
||||
import getPermissionVariablesObject from '@/utils/settings/getPermissionVariablesObject';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface CreatePermissionVariableFormProps
|
||||
extends Pick<BasePermissionVariableFormProps, 'onCancel'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function CreatePermissionVariableForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: CreatePermissionVariableFormProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { data, error, loading } = useGetAppCustomClaimsQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const form = useForm<BasePermissionVariableFormValues>({
|
||||
defaultValues: {
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(basePermissionVariableValidationSchema),
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: ['getAppCustomClaims'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator delay={1000} label="Loading permission variables..." />
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { setError } = form;
|
||||
const availablePermissionVariables = getPermissionVariablesArray(
|
||||
data?.app?.authJwtCustomClaims,
|
||||
);
|
||||
|
||||
async function handleSubmit({
|
||||
key,
|
||||
value,
|
||||
}: BasePermissionVariableFormValues) {
|
||||
if (
|
||||
availablePermissionVariables.some(
|
||||
(permissionVariable) => permissionVariable.key === key,
|
||||
)
|
||||
) {
|
||||
setError('key', { message: 'This key is already in use.' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const permissionVariablesObject = getPermissionVariablesObject(
|
||||
availablePermissionVariables.filter(
|
||||
(permissionVariable) => !permissionVariable.isSystemClaim,
|
||||
),
|
||||
);
|
||||
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authJwtCustomClaims: {
|
||||
...permissionVariablesObject,
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Creating permission variable...',
|
||||
success: 'Permission variable has been created successfully.',
|
||||
error:
|
||||
'An error occurred while trying to create the permission variable.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
await onSubmit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BasePermissionVariableForm onSubmit={handleSubmit} {...props} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CreatePermissionVariableForm';
|
||||
export { default } from './CreatePermissionVariableForm';
|
||||
@@ -0,0 +1,142 @@
|
||||
import type {
|
||||
BasePermissionVariableFormProps,
|
||||
BasePermissionVariableFormValues,
|
||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||
import BasePermissionVariableForm, {
|
||||
basePermissionVariableValidationSchema,
|
||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import getPermissionVariables from '@/utils/settings/getPermissionVariablesArray';
|
||||
import getPermissionVariablesObject from '@/utils/settings/getPermissionVariablesObject';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface EditPermissionVariableFormProps
|
||||
extends Pick<BasePermissionVariableFormProps, 'onCancel'> {
|
||||
/**
|
||||
* The permission variable to be edited.
|
||||
*/
|
||||
originalVariable: CustomClaim;
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function EditPermissionVariableForm({
|
||||
originalVariable,
|
||||
onSubmit,
|
||||
...props
|
||||
}: EditPermissionVariableFormProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const { data, error, loading } = useGetAppCustomClaimsQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const form = useForm<BasePermissionVariableFormValues>({
|
||||
defaultValues: {
|
||||
key: originalVariable.key || '',
|
||||
value: originalVariable.value || '',
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(basePermissionVariableValidationSchema),
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: ['getAppCustomClaims'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator delay={1000} label="Loading permission variables..." />
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { setError } = form;
|
||||
const availablePermissionVariables = getPermissionVariables(
|
||||
data?.app?.authJwtCustomClaims,
|
||||
);
|
||||
|
||||
async function handleSubmit({
|
||||
key,
|
||||
value,
|
||||
}: BasePermissionVariableFormValues) {
|
||||
if (
|
||||
availablePermissionVariables.some(
|
||||
(permissionVariable) =>
|
||||
permissionVariable.key === key &&
|
||||
permissionVariable.key !== originalVariable.key,
|
||||
)
|
||||
) {
|
||||
setError('key', { message: 'This key is already in use.' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const originalPermissionVariableIndex =
|
||||
availablePermissionVariables.findIndex(
|
||||
(permissionVariable) => permissionVariable.key === originalVariable.key,
|
||||
);
|
||||
|
||||
const updatedPermissionVariables = availablePermissionVariables.map(
|
||||
(permissionVariable, index) => {
|
||||
if (index === originalPermissionVariableIndex) {
|
||||
return { key, value };
|
||||
}
|
||||
|
||||
return permissionVariable;
|
||||
},
|
||||
);
|
||||
|
||||
const permissionVariablesObject = getPermissionVariablesObject(
|
||||
updatedPermissionVariables.filter(
|
||||
(permissionVariable) => !permissionVariable.isSystemClaim,
|
||||
),
|
||||
);
|
||||
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authJwtCustomClaims: {
|
||||
...permissionVariablesObject,
|
||||
[key]: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Updating permission variable...',
|
||||
success: 'Permission variable has been updated successfully.',
|
||||
error:
|
||||
'An error occurred while trying to update the permission variable.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
await onSubmit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BasePermissionVariableForm onSubmit={handleSubmit} {...props} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './EditPermissionVariableForm';
|
||||
export { default } from './EditPermissionVariableForm';
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import DotsVerticalIcon from '@/ui/v2/icons/DotsVerticalIcon';
|
||||
import LockIcon from '@/ui/v2/icons/LockIcon';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import Tooltip from '@/ui/v2/Tooltip';
|
||||
import getPermissionVariables from '@/utils/settings/getPermissionVariablesArray';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { Fragment } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface PermissionVariableSettingsFormValues {
|
||||
/**
|
||||
* Permission variables.
|
||||
*/
|
||||
authJwtCustomClaims: CustomClaim[];
|
||||
}
|
||||
|
||||
export default function PermissionVariableSettings() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading, error } = useGetAppCustomClaimsQuery({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: ['getAppCustomClaims'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator delay={1000} label="Loading permission variables..." />
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleDeleteVariable({ key }: CustomClaim) {
|
||||
const filteredCustomClaims = Object.keys(
|
||||
data?.app?.authJwtCustomClaims,
|
||||
).filter((customClaimKey) => customClaimKey !== key);
|
||||
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authJwtCustomClaims: filteredCustomClaims.reduce(
|
||||
(customClaims, currentKey) => ({
|
||||
...customClaims,
|
||||
[currentKey]: data?.app?.authJwtCustomClaims[currentKey],
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Deleting permission variable...',
|
||||
success: 'Permission variable has been deleted successfully.',
|
||||
error: 'An error occurred while trying to delete permission variable.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
}
|
||||
|
||||
function handleOpenCreator() {
|
||||
openDialog('CREATE_PERMISSION_VARIABLE', {
|
||||
title: 'Create Permission Variable',
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleOpenEditor(originalVariable: CustomClaim) {
|
||||
openDialog('EDIT_PERMISSION_VARIABLE', {
|
||||
title: 'Edit Permission Variable',
|
||||
payload: { originalVariable },
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirmDelete(originalVariable: CustomClaim) {
|
||||
openAlertDialog({
|
||||
title: 'Delete Permission Variable',
|
||||
payload: (
|
||||
<Text>
|
||||
Are you sure you want to delete the "
|
||||
<strong>X-Hasura-{originalVariable.key}</strong>" permission
|
||||
variable? This cannot be undone.
|
||||
</Text>
|
||||
),
|
||||
props: {
|
||||
onPrimaryAction: () => handleDeleteVariable(originalVariable),
|
||||
primaryButtonColor: 'error',
|
||||
primaryButtonText: 'Delete',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const availablePermissionVariables = getPermissionVariables(
|
||||
data?.app?.authJwtCustomClaims,
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Permission Variables"
|
||||
description="Permission variables are used to define permission rules in the GraphQL API."
|
||||
docsLink="https://docs.nhost.io/graphql/permissions"
|
||||
rootClassName="gap-0"
|
||||
className="px-0 my-2"
|
||||
slotProps={{ submitButton: { className: 'invisible' } }}
|
||||
>
|
||||
<div className="grid grid-cols-2 border-b-1 border-gray-200 px-4 py-3">
|
||||
<Text className="font-medium">Field name</Text>
|
||||
<Text className="font-medium">Path</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<List>
|
||||
{availablePermissionVariables.map((customClaim, index) => (
|
||||
<Fragment key={customClaim.key}>
|
||||
<ListItem.Root
|
||||
className="px-4 grid grid-cols-2"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Tooltip
|
||||
title={
|
||||
customClaim.isSystemClaim
|
||||
? "You can't edit system permission variables"
|
||||
: ''
|
||||
}
|
||||
placement="right"
|
||||
disableHoverListener={!customClaim.isSystemClaim}
|
||||
hasDisabledChildren={customClaim.isSystemClaim}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<Dropdown.Trigger asChild hideChevron>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
disabled={customClaim.isSystemClaim}
|
||||
>
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
</Tooltip>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleOpenEditor(customClaim)}
|
||||
>
|
||||
<Text className="font-medium">Edit</Text>
|
||||
</Dropdown.Item>
|
||||
|
||||
<Divider component="li" />
|
||||
|
||||
<Dropdown.Item
|
||||
onClick={() => handleConfirmDelete(customClaim)}
|
||||
>
|
||||
<Text
|
||||
className="font-medium"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text
|
||||
primary={
|
||||
<>
|
||||
X-Hasura-{customClaim.key}{' '}
|
||||
{customClaim.isSystemClaim && (
|
||||
<LockIcon className="w-4 h-4" />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Text className="font-medium">user.{customClaim.value}</Text>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availablePermissionVariables.length - 1
|
||||
? '!mt-4'
|
||||
: '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Button
|
||||
className="justify-self-start mx-4"
|
||||
variant="borderless"
|
||||
startIcon={<PlusIcon />}
|
||||
onClick={handleOpenCreator}
|
||||
>
|
||||
Create Permission Variable
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './PermissionVariableSettings';
|
||||
export { default } from './PermissionVariableSettings';
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface BaseRoleFormValues {
|
||||
/**
|
||||
* The name of the role.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BaseRoleFormProps {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: (values: BaseRoleFormValues) => void;
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
onCancel?: VoidFunction;
|
||||
/**
|
||||
* Submit button text.
|
||||
*
|
||||
* @default 'Save'
|
||||
*/
|
||||
submitButtonText?: string;
|
||||
}
|
||||
|
||||
export const baseRoleFormValidationSchema = Yup.object({
|
||||
name: Yup.string().required('This field is required.'),
|
||||
});
|
||||
|
||||
export default function BaseRoleForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
}: BaseRoleFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const form = useFormContext<BaseRoleFormValues>();
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, dirtyFields, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2 px-6 pb-6">
|
||||
<Text variant="subtitle1" component="span">
|
||||
Enter the name for the role below.
|
||||
</Text>
|
||||
|
||||
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
||||
<Input
|
||||
{...register('name')}
|
||||
inputProps={{ maxLength: 100 }}
|
||||
id="name"
|
||||
label="Name"
|
||||
placeholder="Enter value"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.name}
|
||||
helperText={errors?.name?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './BaseRoleForm';
|
||||
export { default } from './BaseRoleForm';
|
||||
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
BaseRoleFormProps,
|
||||
BaseRoleFormValues,
|
||||
} from '@/components/settings/roles/BaseRoleForm';
|
||||
import BaseRoleForm, {
|
||||
baseRoleFormValidationSchema,
|
||||
} from '@/components/settings/roles/BaseRoleForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface CreateRoleFormProps
|
||||
extends Pick<BaseRoleFormProps, 'onCancel'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function CreateRoleForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: CreateRoleFormProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { data, loading, error } = useGetRolesQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const form = useForm<BaseRoleFormValues>({
|
||||
defaultValues: {},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseRoleFormValidationSchema),
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['getRoles'] });
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={1000} label="Loading roles..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { setError } = form;
|
||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
||||
|
||||
async function handleSubmit({ name }: BaseRoleFormValues) {
|
||||
if (availableRoles.some((role) => role.name === name)) {
|
||||
setError('name', { message: 'This role already exists.' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: `${data?.app?.authUserDefaultAllowedRoles},${name}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Creating role...',
|
||||
success: 'Role has been created successfully.',
|
||||
error: 'An error occurred while trying to create the role.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
await onSubmit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BaseRoleForm
|
||||
submitButtonText="Create"
|
||||
onSubmit={handleSubmit}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CreateRoleForm';
|
||||
export { default } from './CreateRoleForm';
|
||||
@@ -0,0 +1,125 @@
|
||||
import type {
|
||||
BaseRoleFormProps,
|
||||
BaseRoleFormValues,
|
||||
} from '@/components/settings/roles/BaseRoleForm';
|
||||
import BaseRoleForm, {
|
||||
baseRoleFormValidationSchema,
|
||||
} from '@/components/settings/roles/BaseRoleForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { Role } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface EditRoleFormProps extends Pick<BaseRoleFormProps, 'onCancel'> {
|
||||
/**
|
||||
* The role to be edited.
|
||||
*/
|
||||
originalRole: Role;
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function EditRoleForm({
|
||||
originalRole,
|
||||
onSubmit,
|
||||
...props
|
||||
}: EditRoleFormProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { data, loading, error } = useGetRolesQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const form = useForm<BaseRoleFormValues>({
|
||||
defaultValues: {
|
||||
name: originalRole.name || '',
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseRoleFormValidationSchema),
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: ['getRoles'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={1000} label="Loading roles..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { setError } = form;
|
||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
||||
|
||||
async function handleSubmit({ name }: BaseRoleFormValues) {
|
||||
if (
|
||||
availableRoles.some(
|
||||
(role) => role.name === name && role.name !== originalRole.name,
|
||||
)
|
||||
) {
|
||||
setError('name', { message: 'This role already exists.' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultAllowedRolesList =
|
||||
data?.app?.authUserDefaultAllowedRoles.split(',') || [];
|
||||
|
||||
const originalRoleIndex = defaultAllowedRolesList.findIndex(
|
||||
(role) => role.trim() === originalRole.name,
|
||||
);
|
||||
|
||||
const updatedDefaultAllowedRoles = defaultAllowedRolesList
|
||||
.map((role, index) => {
|
||||
if (index === originalRoleIndex) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return role;
|
||||
})
|
||||
.join(',');
|
||||
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authUserDefaultRole:
|
||||
data?.app?.authUserDefaultRole === originalRole.name
|
||||
? name
|
||||
: data?.app?.authUserDefaultRole,
|
||||
authUserDefaultAllowedRoles: updatedDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Updating role...',
|
||||
success: 'Role has been updated successfully.',
|
||||
error: 'An error occurred while trying to update the role.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
await onSubmit?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<BaseRoleForm onSubmit={handleSubmit} {...props} />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './EditRoleForm';
|
||||
export { default } from './EditRoleForm';
|
||||
@@ -0,0 +1,270 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { Role } from '@/types/application';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Chip from '@/ui/v2/Chip';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import DotsVerticalIcon from '@/ui/v2/icons/DotsVerticalIcon';
|
||||
import LockIcon from '@/ui/v2/icons/LockIcon';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { Fragment } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface RoleSettingsFormValues {
|
||||
/**
|
||||
* Default role.
|
||||
*/
|
||||
authUserDefaultRole: string;
|
||||
/**
|
||||
* Allowed roles for the project.
|
||||
*/
|
||||
authUserDefaultAllowedRoles: Role[];
|
||||
}
|
||||
|
||||
export default function RoleSettings() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading, error } = useGetRolesQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: ['getRoles'],
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={1000} label="Loading user roles..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSetAsDefault({ name }: Role) {
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authUserDefaultRole: name,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Updating default role...',
|
||||
success: 'Default role has been updated successfully.',
|
||||
error: 'An error occurred while trying to update the default role.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDeleteRole({ name }: Role) {
|
||||
const filteredRoles = data?.app?.authUserDefaultAllowedRoles
|
||||
.split(',')
|
||||
.filter((role) => role !== name)
|
||||
.join(',');
|
||||
|
||||
const updateAppPromise = updateApp({
|
||||
variables: {
|
||||
id: currentApplication?.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: filteredRoles,
|
||||
authUserDefaultRole:
|
||||
name === data?.app?.authUserDefaultRole
|
||||
? 'user'
|
||||
: data?.app?.authUserDefaultRole,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
updateAppPromise,
|
||||
{
|
||||
loading: 'Deleting role...',
|
||||
success: 'Role has been deleted successfully.',
|
||||
error: 'An error occurred while trying to delete the role.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
}
|
||||
|
||||
function handleOpenCreator() {
|
||||
openDialog('CREATE_ROLE', {
|
||||
title: 'Create Role',
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleOpenEditor(originalRole: Role) {
|
||||
openDialog('EDIT_ROLE', {
|
||||
title: 'Edit Role',
|
||||
payload: { originalRole },
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleConfirmDelete(originalRole: Role) {
|
||||
openAlertDialog({
|
||||
title: 'Delete Role',
|
||||
payload: (
|
||||
<Text>
|
||||
Are you sure you want to delete the "
|
||||
<strong>{originalRole.name}</strong>" role? This cannot be
|
||||
undone.
|
||||
</Text>
|
||||
),
|
||||
props: {
|
||||
onPrimaryAction: () => handleDeleteRole(originalRole),
|
||||
primaryButtonColor: 'error',
|
||||
primaryButtonText: 'Delete',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Roles"
|
||||
description="Roles are used to control access to your application."
|
||||
docsLink="https://docs.nhost.io/authentication/users#roles"
|
||||
rootClassName="gap-0"
|
||||
className="px-0 my-2"
|
||||
slotProps={{ submitButton: { className: 'invisible' } }}
|
||||
>
|
||||
<div className="border-b-1 border-gray-200 px-4 py-3">
|
||||
<Text className="font-medium">Name</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<List>
|
||||
{availableRoles.map((role, index) => (
|
||||
<Fragment key={role.name}>
|
||||
<ListItem.Root
|
||||
className="px-4"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<IconButton variant="borderless" color="secondary">
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Dropdown.Item onClick={() => handleSetAsDefault(role)}>
|
||||
<Text className="font-medium">Set as Default</Text>
|
||||
</Dropdown.Item>
|
||||
|
||||
<Divider component="li" />
|
||||
|
||||
<Dropdown.Item
|
||||
disabled={role.isSystemRole}
|
||||
onClick={() => handleOpenEditor(role)}
|
||||
>
|
||||
<Text className="font-medium">Edit</Text>
|
||||
</Dropdown.Item>
|
||||
|
||||
<Divider component="li" />
|
||||
|
||||
<Dropdown.Item
|
||||
disabled={role.isSystemRole}
|
||||
onClick={() => handleConfirmDelete(role)}
|
||||
>
|
||||
<Text
|
||||
className="font-medium"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text
|
||||
primaryTypographyProps={{
|
||||
className:
|
||||
'inline-grid grid-flow-col gap-1 items-center h-6 font-medium',
|
||||
}}
|
||||
primary={
|
||||
<>
|
||||
{role.name}
|
||||
|
||||
{role.isSystemRole && <LockIcon className="w-4 h-4" />}
|
||||
|
||||
{data?.app?.authUserDefaultRole === role.name && (
|
||||
<Chip
|
||||
component="span"
|
||||
color="info"
|
||||
size="small"
|
||||
label="Default"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availableRoles.length - 1 ? '!mt-4' : '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Button
|
||||
className="justify-self-start mx-4"
|
||||
variant="borderless"
|
||||
startIcon={<PlusIcon />}
|
||||
onClick={handleOpenCreator}
|
||||
>
|
||||
Create Role
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './RoleSettings';
|
||||
export { default } from './RoleSettings';
|
||||
@@ -79,7 +79,7 @@ export default function AnonymousSignInSettings() {
|
||||
<Form onSubmit={handlePasswordProtectionSettingsChange}>
|
||||
<SettingsContainer
|
||||
title="Anonymous Users"
|
||||
description="Allow users to sign-in anonymously."
|
||||
description="Allow users to sign in anonymously."
|
||||
primaryActionButtonProps={{
|
||||
disabled:
|
||||
form.formState.isSubmitting ||
|
||||
|
||||
@@ -11,7 +11,7 @@ import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { generateAppServiceUrl } from '@/utils/helpers';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -23,7 +23,6 @@ export interface AppleProviderFormValues {
|
||||
authAppleKeyId: string;
|
||||
authAppleClientId: string;
|
||||
authApplePrivateKey: string;
|
||||
authAppleScope: string;
|
||||
}
|
||||
|
||||
export default function AppleProviderSettings() {
|
||||
@@ -38,7 +37,6 @@ export default function AppleProviderSettings() {
|
||||
authAppleKeyId,
|
||||
authAppleClientId,
|
||||
authApplePrivateKey,
|
||||
authAppleScope,
|
||||
},
|
||||
},
|
||||
loading,
|
||||
@@ -57,7 +55,6 @@ export default function AppleProviderSettings() {
|
||||
authAppleKeyId,
|
||||
authAppleClientId,
|
||||
authApplePrivateKey,
|
||||
authAppleScope,
|
||||
authAppleEnabled,
|
||||
},
|
||||
});
|
||||
@@ -83,9 +80,7 @@ export default function AppleProviderSettings() {
|
||||
const updateAppMutation = updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
...values,
|
||||
},
|
||||
app: values,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -96,7 +91,7 @@ export default function AppleProviderSettings() {
|
||||
success: `Apple settings have been updated successfully.`,
|
||||
error: `An error occurred while trying to update the project's Apple settings.`,
|
||||
},
|
||||
{ ...toastStyleProps },
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
@@ -107,10 +102,12 @@ export default function AppleProviderSettings() {
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<SettingsContainer
|
||||
title="Apple"
|
||||
description="Allows users to sign in with Apple."
|
||||
primaryActionButtonProps={{
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
description="Allow users to sign in with Apple."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-apple"
|
||||
docsTitle="how to sign in users with Apple"
|
||||
@@ -134,9 +131,9 @@ export default function AppleProviderSettings() {
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
<Input
|
||||
{...register('authAppleScope')}
|
||||
name="authAppleScope"
|
||||
id="authAppleScope"
|
||||
{...register('authAppleClientId')}
|
||||
name="authAppleClientId"
|
||||
id="authAppleClientId"
|
||||
label="Service ID"
|
||||
placeholder="Apple Service ID"
|
||||
className="col-span-1"
|
||||
@@ -168,9 +165,11 @@ export default function AppleProviderSettings() {
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
defaultValue={`${generateRemoteAppUrl(
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
)}/v1/auth/signin/provider/apple/callback`}
|
||||
currentApplication.region.awsName,
|
||||
'auth',
|
||||
)}/v1/signin/provider/apple/callback`}
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
@@ -185,9 +184,11 @@ export default function AppleProviderSettings() {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateRemoteAppUrl(
|
||||
`${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
)}/v1/auth/signin/provider/apple/callback`,
|
||||
currentApplication.region.awsName,
|
||||
'auth',
|
||||
)}/v1/signin/provider/apple/callback`,
|
||||
'Redirect URL',
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -13,7 +13,7 @@ import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { generateAppServiceUrl } from '@/utils/helpers';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -88,7 +88,7 @@ export default function DiscordProviderSettings() {
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<SettingsContainer
|
||||
title="Discord"
|
||||
description="Allows users to sign in with Discord."
|
||||
description="Allow users to sign in with Discord."
|
||||
primaryActionButtonProps={{
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
@@ -112,9 +112,11 @@ export default function DiscordProviderSettings() {
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateRemoteAppUrl(
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
)}/v1/auth/signin/provider/discord/callback`}
|
||||
currentApplication.region.awsName,
|
||||
'auth',
|
||||
)}/v1/signin/provider/discord/callback`}
|
||||
disabled
|
||||
endAdornment={
|
||||
<InputAdornment position="end" className="absolute right-2">
|
||||
@@ -125,9 +127,11 @@ export default function DiscordProviderSettings() {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateRemoteAppUrl(
|
||||
`${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
)}/v1/auth/signin/provider/discord/callback`,
|
||||
currentApplication.region.awsName,
|
||||
'auth',
|
||||
)}/v1/signin/provider/discord/callback`,
|
||||
'Redirect URL',
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface EmailAndPasswordFormValues {
|
||||
authPasswordHibpEnabled: boolean;
|
||||
}
|
||||
|
||||
export default function EmailSettings() {
|
||||
export default function EmailAndPasswordSettings() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [GetAppLoginDataDocument],
|
||||
@@ -61,7 +61,7 @@ export default function EmailSettings() {
|
||||
|
||||
const { formState } = form;
|
||||
|
||||
const handleEmailSettingsChange = async (
|
||||
const handleEmailAndPasswordSettingsChange = async (
|
||||
values: EmailAndPasswordFormValues,
|
||||
) => {
|
||||
const updateAppMutation = updateApp({
|
||||
@@ -90,29 +90,31 @@ export default function EmailSettings() {
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleEmailSettingsChange}>
|
||||
<Form onSubmit={handleEmailAndPasswordSettingsChange}>
|
||||
<SettingsContainer
|
||||
title="Email and Password"
|
||||
description="Sign in users using email and password."
|
||||
description="Allow users to sign in with email and password."
|
||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-email-and-password"
|
||||
docsTitle="how to sign in users with email and password"
|
||||
className="grid grid-flow-row"
|
||||
showSwitch
|
||||
enabled
|
||||
switchProps={{ disabled: true }}
|
||||
primaryActionButtonProps={{
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
slotProps={{
|
||||
switch: {
|
||||
disabled: true,
|
||||
},
|
||||
submitButton: {
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ControlledCheckbox
|
||||
name="authEmailSigninEmailVerifiedRequired"
|
||||
id="authEmailSigninEmailVerifiedRequired"
|
||||
label={
|
||||
<span className="inline-grid grid-flow-row gap-y-[2px] text-[15px]">
|
||||
<span className="text-[15px] font-medium">
|
||||
Require Verified Emails
|
||||
</span>
|
||||
<span className="inline-grid grid-flow-row gap-y-0.5 text-sm+">
|
||||
<span className="font-medium">Require Verified Emails</span>
|
||||
<span className="font-normal text-greyscaleMedium">
|
||||
Users must verify their email to be able to sign in.
|
||||
</span>
|
||||
@@ -124,11 +126,9 @@ export default function EmailSettings() {
|
||||
name="authPasswordHibpEnabled"
|
||||
id="authPasswordHibpEnabled"
|
||||
label={
|
||||
<span className="inline-grid grid-flow-row gap-y-[2px] text-[15px]">
|
||||
<span className="text-[15px] font-medium">
|
||||
Password Protection
|
||||
</span>
|
||||
<span className="text-[12px] font-normal text-greyscaleMedium">
|
||||
<span className="inline-grid grid-flow-row gap-y-0.5 text-sm+">
|
||||
<span className="font-medium">Password Protection</span>
|
||||
<span className="font-normal text-greyscaleMedium">
|
||||
Passwords must pass haveibeenpwned.com during sign-up.
|
||||
</span>
|
||||
</span>
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './EmailAndPasswordSettings';
|
||||
export { default } from './EmailAndPasswordSettings';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user