Compare commits
322 Commits
@nhost/rea
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce3ece1ad7 | ||
|
|
c81002622c | ||
|
|
35fa6bb043 | ||
|
|
b8f11a13d7 | ||
|
|
1d1555593f | ||
|
|
99fcc36250 | ||
|
|
7e4a756cfe | ||
|
|
5bf61583e0 | ||
|
|
7eac17a1cb | ||
|
|
a41aeeb9ef | ||
|
|
e33df513ff | ||
|
|
323fd5cbe3 | ||
|
|
ffb3c426d3 | ||
|
|
889ee6589e | ||
|
|
5223ee9353 | ||
|
|
c8c5ace7cc | ||
|
|
c6a4c28579 | ||
|
|
850a049ca2 | ||
|
|
eff3f0aefd | ||
|
|
2b1338f716 | ||
|
|
2b58c60747 | ||
|
|
1b2e3fbd1d | ||
|
|
6f4fdcf73f | ||
|
|
cb529dc60c | ||
|
|
68a449dbfc | ||
|
|
7d0c6d083a | ||
|
|
1353477da1 | ||
|
|
549c7cb7eb | ||
|
|
e131c12d5d | ||
|
|
8bb097c9a7 | ||
|
|
ea31e64a71 | ||
|
|
369b931689 | ||
|
|
e1ec5c1be2 | ||
|
|
9822a160d4 | ||
|
|
7c67a2c437 | ||
|
|
8e8884f4e1 | ||
|
|
9923be41ce | ||
|
|
9c22a616a7 | ||
|
|
6bc67e95a5 | ||
|
|
0f6074c16f | ||
|
|
c96d7ccdf2 | ||
|
|
fde7ac7c1c | ||
|
|
24ef6071cc | ||
|
|
bb993b6b03 | ||
|
|
89ca34be9a | ||
|
|
b66d095c95 | ||
|
|
0bad9ff4fa | ||
|
|
9a761f4fec | ||
|
|
bd6b55868a | ||
|
|
afb3fe490e | ||
|
|
eaebd2b028 | ||
|
|
f03ecd91a9 | ||
|
|
96f17c39b1 | ||
|
|
cb7c8c6398 | ||
|
|
4bf40995b5 | ||
|
|
ab5f704280 | ||
|
|
f65e4de955 | ||
|
|
7e0e4d05aa | ||
|
|
decb0b057c | ||
|
|
0954a44f84 | ||
|
|
700cbd9e47 | ||
|
|
3238543b08 | ||
|
|
fc79b890df | ||
|
|
211eb42af5 | ||
|
|
a7398451e3 | ||
|
|
4b4f0d0150 | ||
|
|
f37e2a23e2 | ||
|
|
3f8d68ffab | ||
|
|
f7e706724c | ||
|
|
2832d7299f | ||
|
|
44c5b386c3 | ||
|
|
1a4a061284 | ||
|
|
5a91c477f0 | ||
|
|
66f73d06a8 | ||
|
|
35d52aab87 | ||
|
|
ddd41aae99 | ||
|
|
78555c7e85 | ||
|
|
01ded8ffff | ||
|
|
3c7cf92edf | ||
|
|
bb4301fd34 | ||
|
|
832210d8ad | ||
|
|
a09dad060c | ||
|
|
76b63debf0 | ||
|
|
c8c8948755 | ||
|
|
17e9e5899e | ||
|
|
bd22c48131 | ||
|
|
095d6e918c | ||
|
|
89a239ff3a | ||
|
|
0131886011 | ||
|
|
340c014fe8 | ||
|
|
bc9c8b9456 | ||
|
|
c22b2621ba | ||
|
|
726746c4d3 | ||
|
|
c431570783 | ||
|
|
445d8ef449 | ||
|
|
0f4ea18e42 | ||
|
|
dae7c5d517 | ||
|
|
f673adea00 | ||
|
|
1c6f1e3b33 | ||
|
|
6593e8d3eb | ||
|
|
d1365ea516 | ||
|
|
72dbba7881 | ||
|
|
a3f3991d5a | ||
|
|
c71fe2cf72 | ||
|
|
24c5ed3ea4 | ||
|
|
2d9145f918 | ||
|
|
9a0ab5b887 | ||
|
|
1ddf704c5b | ||
|
|
6f4ee845c6 | ||
|
|
0368663dea | ||
|
|
76ce7d7b6e | ||
|
|
538bfbcb3e | ||
|
|
07b35d1e5f | ||
|
|
2200a0ed07 | ||
|
|
9219838127 | ||
|
|
df23d97126 | ||
|
|
43b68a79eb | ||
|
|
104f149369 | ||
|
|
01228583a0 | ||
|
|
93309dd851 | ||
|
|
2cc18dcb51 | ||
|
|
3b48a62790 | ||
|
|
8897dec056 | ||
|
|
324dda8309 | ||
|
|
ac845c6d92 | ||
|
|
95f62bed07 | ||
|
|
f4af81020b | ||
|
|
0e4d8ff118 | ||
|
|
6999562b59 | ||
|
|
baec5bada7 | ||
|
|
4e56cfc628 | ||
|
|
d167121093 | ||
|
|
822e251b11 | ||
|
|
328c6bb486 | ||
|
|
bef8198cbf | ||
|
|
179313d8a2 | ||
|
|
54bc91923f | ||
|
|
c3ce004f46 | ||
|
|
77b12feb95 | ||
|
|
32d4670bbb | ||
|
|
1dc09942d2 | ||
|
|
3343a36358 | ||
|
|
b755e9086c | ||
|
|
48866d0ee1 | ||
|
|
7d577a68b7 | ||
|
|
5b3b76bd41 | ||
|
|
7f7e7ea7d4 | ||
|
|
aaaf2dc9c5 | ||
|
|
fa9c1ea28c | ||
|
|
87eda76e2b | ||
|
|
8a596f2a9e | ||
|
|
d6d2381598 | ||
|
|
284ef7e7f2 | ||
|
|
6d5c202da9 | ||
|
|
962563d6a0 | ||
|
|
8bf58ba26b | ||
|
|
0c175e7a11 | ||
|
|
70f2fbcfc2 | ||
|
|
d2c4ad3260 | ||
|
|
a9ca2c2946 | ||
|
|
d854dd74b1 | ||
|
|
6f0ac5706c | ||
|
|
9342937440 | ||
|
|
e89cd4e262 | ||
|
|
a05438352b | ||
|
|
78437959bb | ||
|
|
e1a7433adb | ||
|
|
b45aa420d9 | ||
|
|
1d76de3f60 | ||
|
|
e23cf74975 | ||
|
|
a3d01c4fad | ||
|
|
4cdcef9ef5 | ||
|
|
df894ef7e2 | ||
|
|
f7dd6a9fc6 | ||
|
|
2949ff0f62 | ||
|
|
9e37ca4cbc | ||
|
|
af57ccce0f | ||
|
|
5f44aefcc6 | ||
|
|
1527b0a455 | ||
|
|
375e53a3f0 | ||
|
|
96e3ca5a32 | ||
|
|
0e570df9c5 | ||
|
|
1f4c67283e | ||
|
|
fc1c4861a3 | ||
|
|
74feaf6add | ||
|
|
8cd97206cc | ||
|
|
02197639f2 | ||
|
|
38b594aef9 | ||
|
|
f3a8886cd0 | ||
|
|
96f9278c8f | ||
|
|
8d76cf8d40 | ||
|
|
3e1fb974e4 | ||
|
|
9fe2ecd317 | ||
|
|
ada5309b49 | ||
|
|
08698f8246 | ||
|
|
f74871d872 | ||
|
|
0b56e31408 | ||
|
|
c4e3e3f91f | ||
|
|
483fd6c7f4 | ||
|
|
ac37d7bcae | ||
|
|
9adf91ba87 | ||
|
|
d11f6eebb0 | ||
|
|
8a678fbc87 | ||
|
|
6411ec3ec3 | ||
|
|
5187fe76aa | ||
|
|
859f457e4a | ||
|
|
dc2b5b4429 | ||
|
|
b7645e7892 | ||
|
|
3f26056688 | ||
|
|
b1338246aa | ||
|
|
d04ccd600e | ||
|
|
d483ad5602 | ||
|
|
bcf3e6bc2c | ||
|
|
575ff4e9b5 | ||
|
|
2010638540 | ||
|
|
0346495a79 | ||
|
|
2babb0b6f3 | ||
|
|
1f293d0f0c | ||
|
|
af4c886437 | ||
|
|
c182b3ca4b | ||
|
|
d5344ed31f | ||
|
|
6a7801be93 | ||
|
|
7bc5bb857c | ||
|
|
c957039d75 | ||
|
|
96c4032424 | ||
|
|
ec70126b56 | ||
|
|
86b9f9040c | ||
|
|
222f03725b | ||
|
|
10b786e5c6 | ||
|
|
aa8ae88d12 | ||
|
|
0f2c86b41a | ||
|
|
a4c76892dd | ||
|
|
00d278b2cc | ||
|
|
cb6b5faeb9 | ||
|
|
7c4c847b91 | ||
|
|
908887d8c5 | ||
|
|
a2d67bc2db | ||
|
|
1a6cd78254 | ||
|
|
6500629c4b | ||
|
|
add3c2c10e | ||
|
|
dd29b06260 | ||
|
|
490cb25a0f | ||
|
|
0df0dd741e | ||
|
|
2172946879 | ||
|
|
40e50f0e75 | ||
|
|
65cf0888b5 | ||
|
|
21833019ca | ||
|
|
b3171ba3e9 | ||
|
|
6f01f19d02 | ||
|
|
ce92b01eac | ||
|
|
e24a177434 | ||
|
|
56a52b6d48 | ||
|
|
92bfa8c723 | ||
|
|
2a52aaa4a6 | ||
|
|
8280a3e9d8 | ||
|
|
523f60bf68 | ||
|
|
19b11d4084 | ||
|
|
805bae1507 | ||
|
|
f6c014c06f | ||
|
|
c5794f4596 | ||
|
|
fc28817380 | ||
|
|
80bbd3a165 | ||
|
|
7a10617a72 | ||
|
|
f0b6dca1a5 | ||
|
|
5db20adc34 | ||
|
|
12dc41a517 | ||
|
|
768fd56891 | ||
|
|
8a508cb1cc | ||
|
|
34f6a8eef4 | ||
|
|
c9d2d31a9b | ||
|
|
68fb23a361 | ||
|
|
476139e528 | ||
|
|
6a850818a0 | ||
|
|
3970dbba0d | ||
|
|
8ee2166f0d | ||
|
|
e13500a185 | ||
|
|
411f574a51 | ||
|
|
7fc91b992e | ||
|
|
b840012be0 | ||
|
|
645c51a9dc | ||
|
|
0ce6f05539 | ||
|
|
8b1188af53 | ||
|
|
12b01f8dee | ||
|
|
60f4faf409 | ||
|
|
528dff3f1b | ||
|
|
d429fb4a3e | ||
|
|
816c916709 | ||
|
|
b7a2b8b537 | ||
|
|
261d8cf434 | ||
|
|
41f49bde76 | ||
|
|
65f685bdb2 | ||
|
|
f52a7f4aac | ||
|
|
e71b9903d9 | ||
|
|
325fd08aef | ||
|
|
3888704464 | ||
|
|
38e8a10a29 | ||
|
|
d8545eae12 | ||
|
|
3d5bfd87d2 | ||
|
|
e66c5626bd | ||
|
|
a227c6561e | ||
|
|
e885c159df | ||
|
|
09fcb74bef | ||
|
|
a089197197 | ||
|
|
34f843875b | ||
|
|
ca278a8c39 | ||
|
|
75603786e0 | ||
|
|
4e4e699b94 | ||
|
|
da31fa9fba | ||
|
|
95e2afaf47 | ||
|
|
958a56dde9 | ||
|
|
74cb15930e | ||
|
|
aa37a98424 | ||
|
|
11cbdda3a5 | ||
|
|
6d1f4adf10 | ||
|
|
ddbc50c15e | ||
|
|
b2cbf570a3 | ||
|
|
22b8e65031 | ||
|
|
63c94d2036 | ||
|
|
010df48c1e | ||
|
|
fdc11db93d | ||
|
|
cb4749f168 | ||
|
|
46a8fcf471 |
18
.github/CODEOWNERS
vendored
18
.github/CODEOWNERS
vendored
@@ -1,14 +1,14 @@
|
|||||||
# Documentation
|
# Documentation
|
||||||
# https://help.github.com/en/articles/about-code-owners
|
# https://help.github.com/en/articles/about-code-owners
|
||||||
|
|
||||||
/packages @plmercereau @szilarddoro
|
/packages @szilarddoro
|
||||||
/packages/docgen @szilarddoro
|
/packages/docgen @szilarddoro
|
||||||
/integrations/stripe-graphql-js @elitan
|
/integrations/stripe-graphql-js @elitan
|
||||||
/.github @plmercereau
|
/.github @szilarddoro
|
||||||
/dashboard/ @szilarddoro @guicurcio
|
/dashboard/ @szilarddoro
|
||||||
/docs/ @guicurcio @elitan
|
/docs/ @elitan
|
||||||
/config/ @plmercereau @szilarddoro
|
/config/ @szilarddoro
|
||||||
/examples/ @plmercereau
|
/examples/ @szilarddoro
|
||||||
/examples/codegen-react-apollo @elitan @plmercereau
|
/examples/codegen-react-apollo @elitan @szilarddoro
|
||||||
/examples/codegen-react-query @elitan @plmercereau
|
/examples/codegen-react-query @elitan @szilarddoro
|
||||||
/examples/react-apollo-crm @elitan @plmercereau
|
/examples/react-apollo-crm @elitan @szilarddoro
|
||||||
|
|||||||
@@ -40,14 +40,14 @@ runs:
|
|||||||
- shell: bash
|
- shell: bash
|
||||||
name: Build packages
|
name: Build packages
|
||||||
if: ${{ inputs.BUILD == 'all' }}
|
if: ${{ inputs.BUILD == 'all' }}
|
||||||
run: pnpm build:all
|
run: pnpm run build:all
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||||
- shell: bash
|
- shell: bash
|
||||||
name: Build everything in the monorepo
|
name: Build everything in the monorepo
|
||||||
if: ${{ inputs.BUILD == 'default' }}
|
if: ${{ inputs.BUILD == 'default' }}
|
||||||
run: pnpm build
|
run: pnpm run build
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||||
|
|||||||
2
.github/workflows/changesets.yaml
vendored
2
.github/workflows/changesets.yaml
vendored
@@ -98,7 +98,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push to Docker Hub
|
- name: Build and push to Docker Hub
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v4
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,6 @@ on:
|
|||||||
- '**.md'
|
- '**.md'
|
||||||
- 'LICENSE'
|
- 'LICENSE'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'assets/**'
|
- 'assets/**'
|
||||||
@@ -56,7 +55,7 @@ jobs:
|
|||||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||||
| awk "!/null/" \
|
| awk "!/null/" \
|
||||||
| jq -c --slurp)
|
| jq -c --slurp 'map(select(length > 0))')
|
||||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||||
outputs:
|
outputs:
|
||||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@
|
|||||||
"@nhost/docgen": [
|
"@nhost/docgen": [
|
||||||
"../packages/docgen/src/index.ts"
|
"../packages/docgen/src/index.ts"
|
||||||
],
|
],
|
||||||
|
"@nhost/graphql-js": [
|
||||||
|
"../packages/graphql-js/src/index.ts"
|
||||||
|
],
|
||||||
"@nhost/hasura-auth-js": [
|
"@nhost/hasura-auth-js": [
|
||||||
"../packages/hasura-auth-js/src/index.ts"
|
"../packages/hasura-auth-js/src/index.ts"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import replace from '@rollup/plugin-replace'
|
import replace from '@rollup/plugin-replace'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import dts from 'vite-plugin-dts'
|
import dts from 'vite-plugin-dts'
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
@@ -21,7 +20,9 @@ export default defineConfig({
|
|||||||
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
||||||
entryRoot: 'src',
|
entryRoot: 'src',
|
||||||
// Was defaulting to true until version 1.7
|
// Was defaulting to true until version 1.7
|
||||||
skipDiagnostics: true
|
skipDiagnostics: true,
|
||||||
|
// Was defaulting to true until version 2.0
|
||||||
|
copyDtsFiles: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
test: {
|
test: {
|
||||||
@@ -61,7 +62,6 @@ export default defineConfig({
|
|||||||
'@apollo/client/utilities': '@apollo/client/utilities',
|
'@apollo/client/utilities': '@apollo/client/utilities',
|
||||||
'graphql-ws': 'graphql-ws',
|
'graphql-ws': 'graphql-ws',
|
||||||
xstate: 'xstate',
|
xstate: 'xstate',
|
||||||
axios: 'axios',
|
|
||||||
'js-cookie': 'Cookies',
|
'js-cookie': 'Cookies',
|
||||||
react: 'React',
|
react: 'React',
|
||||||
'react-dom': 'ReactDOM',
|
'react-dom': 'ReactDOM',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ module.exports = {
|
|||||||
'error',
|
'error',
|
||||||
{ ignoreTypeReferences: true },
|
{ ignoreTypeReferences: true },
|
||||||
],
|
],
|
||||||
|
'no-console': ['warn', { allow: ['error'] }],
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
'@typescript-eslint/no-shadow': 'error',
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
|||||||
@@ -1,5 +1,125 @@
|
|||||||
# @nhost/dashboard
|
# @nhost/dashboard
|
||||||
|
|
||||||
|
## 0.12.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/react-apollo@5.0.9
|
||||||
|
- @nhost/nextjs@1.13.14
|
||||||
|
|
||||||
|
## 0.12.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2b1338f7: chore(dashboard): bump `turbo` to 1.8.3
|
||||||
|
- 5223ee93: fix(dashboard): show correct deployment status on the main page
|
||||||
|
- 850a049c: chore(deps): update docker/build-push-action action to v4
|
||||||
|
- Updated dependencies [850a049c]
|
||||||
|
- @nhost/nextjs@1.13.13
|
||||||
|
- @nhost/react-apollo@5.0.8
|
||||||
|
|
||||||
|
## 0.12.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 4bf40995: chore(deps): bump `typescript` to `4.9.5`
|
||||||
|
- 8bb097c9: chore(deps): bump `vitest`
|
||||||
|
- 35d52aab: chore(deps): replace `cross-fetch` with `isomorphic-unfetch`
|
||||||
|
- Updated dependencies [4bf40995]
|
||||||
|
- Updated dependencies [8bb097c9]
|
||||||
|
- Updated dependencies [35d52aab]
|
||||||
|
- @nhost/react-apollo@5.0.7
|
||||||
|
- @nhost/nextjs@1.13.12
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- c96d7ccd: fix(dashboard): fix docker builds
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- d1671210: feat(dashboard): use mimir to manage project configuration
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- f65e4de9: chore(deps): bump @graphql-codegen monorepo to v3
|
||||||
|
|
||||||
|
## 0.11.20
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 4b4f0d01: chore(dashboard): improve dialog management
|
||||||
|
|
||||||
|
## 0.11.19
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/react-apollo@5.0.6
|
||||||
|
- @nhost/nextjs@1.13.11
|
||||||
|
|
||||||
|
## 0.11.18
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 01318860: fix(nhost-js): use correct URL for functions requests
|
||||||
|
- Updated dependencies [01318860]
|
||||||
|
- @nhost/react-apollo@5.0.5
|
||||||
|
- @nhost/nextjs@1.13.10
|
||||||
|
|
||||||
|
## 0.11.17
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- f673adea: fix(dashboard): set correct Content-Type for user creation
|
||||||
|
- 445d8ef4: chore(deps): bump `@nhost/react-apollo` to 5.0.4
|
||||||
|
- 445d8ef4: chore(deps): bump `@nhost/nextjs` to 1.13.9
|
||||||
|
- 0368663d: fix(dashboard): allow permission editing for auth and storage schemas
|
||||||
|
- Updated dependencies [445d8ef4]
|
||||||
|
- Updated dependencies [445d8ef4]
|
||||||
|
- @nhost/react-apollo@5.0.4
|
||||||
|
- @nhost/nextjs@1.13.9
|
||||||
|
|
||||||
|
## 0.11.16
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- b755e908: fix(dashboard): use correct date for last seen
|
||||||
|
- 2d9145f9: chore(deps): revert GraphQL client
|
||||||
|
- 1ddf704c: fix(dashboard): don't show false positive message for failed user creation
|
||||||
|
- @nhost/react-apollo@5.0.3
|
||||||
|
- @nhost/nextjs@1.13.8
|
||||||
|
|
||||||
|
## 0.11.15
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/react-apollo@5.0.2
|
||||||
|
- @nhost/nextjs@1.13.7
|
||||||
|
|
||||||
|
## 0.11.14
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2cc18dcb: fix(dashboard): prevent permission editor dropdown from being always open
|
||||||
|
|
||||||
|
## 0.11.13
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 3343a363: chore(dashboard): bump `@testing-library/react` to v14 and `@testing-library/dom` to v9
|
||||||
|
- @nhost/react-apollo@5.0.1
|
||||||
|
- @nhost/nextjs@1.13.6
|
||||||
|
|
||||||
|
## 0.11.12
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 87eda76e: chore(dashboard): bump `@types/react` to v18.0.28 and `@types/react-dom` to v18.0.11
|
||||||
|
- 6f0ac570: feat(dashboard): show dashboard version in account menu
|
||||||
|
|
||||||
## 0.11.11
|
## 0.11.11
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
|
|||||||
RUN apk update
|
RUN apk update
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN yarn global add turbo@1.6.3
|
RUN yarn global add turbo@1.8.3
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ FROM node:16-alpine AS builder
|
|||||||
ARG TURBO_TOKEN
|
ARG TURBO_TOKEN
|
||||||
ARG TURBO_TEAM
|
ARG TURBO_TEAM
|
||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat python3 make g++
|
||||||
RUN apk update
|
RUN apk update
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ pnpm storybook
|
|||||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. 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_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. |
|
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||||
|
| `NEXT_PUBLIC_MAINTENANCE_ACTIVE` | Determines whether or not maintenance mode is active. |
|
||||||
|
| `NEXT_PUBLIC_MAINTENANCE_END_DATE` | Date when maintenance mode will end. |
|
||||||
|
| `NEXT_PUBLIC_MAINTENANCE_UNLOCK_SECRET` | Secret that can be used to bypass maintenance mode. |
|
||||||
|
|
||||||
## ESLint Rules
|
## ESLint Rules
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ const path = require('path');
|
|||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
|
const { version } = require('./package.json');
|
||||||
|
|
||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
@@ -10,6 +11,9 @@ module.exports = withBundleAnalyzer({
|
|||||||
experimental: {
|
experimental: {
|
||||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||||
},
|
},
|
||||||
|
publicRuntimeConfig: {
|
||||||
|
version,
|
||||||
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
dirs: ['src'],
|
dirs: ['src'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/dashboard",
|
"name": "@nhost/dashboard",
|
||||||
"version": "0.11.11",
|
"version": "0.12.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -44,10 +44,8 @@
|
|||||||
"@tanstack/react-table": "^8.5.30",
|
"@tanstack/react-table": "^8.5.30",
|
||||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||||
"analytics-node": "^6.2.0",
|
"analytics-node": "^6.2.0",
|
||||||
"axios": "^0.27.2",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cross-fetch": "^3.1.5",
|
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"generate-password": "^1.7.0",
|
"generate-password": "^1.7.0",
|
||||||
"graphiql": "^2.2.0",
|
"graphiql": "^2.2.0",
|
||||||
@@ -65,7 +63,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-hook-form": "^7.39.5",
|
"react-hook-form": "^7.42.1",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-is": "18.2.0",
|
"react-is": "18.2.0",
|
||||||
"react-loading-skeleton": "^2.2.0",
|
"react-loading-skeleton": "^2.2.0",
|
||||||
@@ -83,10 +81,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.2",
|
"@babel/core": "^7.20.2",
|
||||||
"@graphql-codegen/cli": "^2.8.0",
|
"@graphql-codegen/cli": "^3.0.0",
|
||||||
"@graphql-codegen/typescript": "^2.7.1",
|
"@graphql-codegen/typescript": "^3.0.0",
|
||||||
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
||||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||||
"@next/bundle-analyzer": "^12.3.1",
|
"@next/bundle-analyzer": "^12.3.1",
|
||||||
"@storybook/addon-actions": "^6.5.14",
|
"@storybook/addon-actions": "^6.5.14",
|
||||||
@@ -98,26 +96,27 @@
|
|||||||
"@storybook/manager-webpack5": "^6.5.14",
|
"@storybook/manager-webpack5": "^6.5.14",
|
||||||
"@storybook/react": "^6.5.14",
|
"@storybook/react": "^6.5.14",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"@storybook/testing-library": "^0.0.13",
|
||||||
"@testing-library/dom": "^8.19.0",
|
"@testing-library/dom": "^9.0.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/node": "^16.11.7",
|
"@types/node": "^16.11.7",
|
||||||
"@types/pluralize": "^0.0.29",
|
"@types/pluralize": "^0.0.29",
|
||||||
"@types/react": "18.0.25",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-table": "^7.7.12",
|
"@types/react-table": "^7.7.12",
|
||||||
"@types/testing-library__jest-dom": "^5.14.5",
|
"@types/testing-library__jest-dom": "^5.14.5",
|
||||||
"@types/validator": "^13.7.10",
|
"@types/validator": "^13.7.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"@typescript-eslint/parser": "^5.43.0",
|
"@typescript-eslint/parser": "^5.43.0",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"@vitest/coverage-c8": "^0.27.0",
|
"@vitest/coverage-c8": "^0.29.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"babel-loader": "^8.3.0",
|
"babel-loader": "^8.3.0",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"csstype": "^3.0.10",
|
"csstype": "^3.0.10",
|
||||||
|
"encoding": "^0.1.13",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
@@ -131,6 +130,7 @@
|
|||||||
"lint-staged": ">=13",
|
"lint-staged": ">=13",
|
||||||
"msw": "^1.0.1",
|
"msw": "^1.0.1",
|
||||||
"msw-storybook-addon": "^1.6.3",
|
"msw-storybook-addon": "^1.6.3",
|
||||||
|
"node-fetch": "^3.3.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-organize-imports": "^3.2.0",
|
"prettier-plugin-organize-imports": "^3.2.0",
|
||||||
@@ -141,10 +141,9 @@
|
|||||||
"tailwindcss": "^3.1.2",
|
"tailwindcss": "^3.1.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
"typescript": "^4.8.4",
|
|
||||||
"vite": "^4.0.2",
|
"vite": "^4.0.2",
|
||||||
"vite-tsconfig-paths": "^4.0.3",
|
"vite-tsconfig-paths": "^4.0.3",
|
||||||
"vitest": "^0.27.0",
|
"vitest": "^0.29.0",
|
||||||
"webpack": "^5.75.0"
|
"webpack": "^5.75.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 210 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 176 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 168 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 140 KiB |
@@ -1,6 +0,0 @@
|
|||||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="200" height="200" fill="white" fill-opacity="0.15"/>
|
|
||||||
<rect width="200" height="200" fill="#263245" fill-opacity="0.08"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71 84C71 67.9837 83.9837 55 100 55C116.016 55 129 67.9837 129 84C129 100.016 116.016 113 100 113C83.9837 113 71 100.016 71 84ZM100 49C80.67 49 65 64.67 65 84C65 97.6014 72.7585 109.391 84.0914 115.184C79.3584 116.509 74.7892 118.425 70.496 120.903C61.5257 126.08 54.0757 133.527 48.8946 142.495C48.0657 143.929 48.5568 145.764 49.9914 146.593C51.4261 147.422 53.261 146.931 54.0898 145.496C58.7443 137.44 65.4368 130.75 73.4952 126.099C81.5536 121.448 90.694 119 99.9982 119C109.302 119 118.443 121.449 126.501 126.1C134.559 130.751 141.252 137.441 145.906 145.497C146.735 146.932 148.57 147.423 150.004 146.594C151.439 145.765 151.93 143.93 151.101 142.496C145.92 133.527 138.471 126.081 129.5 120.903C125.208 118.426 120.639 116.509 115.907 115.185C127.241 109.392 135 97.6021 135 84C135 64.67 119.33 49 100 49Z" fill="white" fill-opacity="0.15"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M71 84C71 67.9837 83.9837 55 100 55C116.016 55 129 67.9837 129 84C129 100.016 116.016 113 100 113C83.9837 113 71 100.016 71 84ZM100 49C80.67 49 65 64.67 65 84C65 97.6014 72.7585 109.391 84.0914 115.184C79.3584 116.509 74.7892 118.425 70.496 120.903C61.5257 126.08 54.0757 133.527 48.8946 142.495C48.0657 143.929 48.5568 145.764 49.9914 146.593C51.4261 147.422 53.261 146.931 54.0898 145.496C58.7443 137.44 65.4368 130.75 73.4952 126.099C81.5536 121.448 90.694 119 99.9982 119C109.302 119 118.443 121.449 126.501 126.1C134.559 130.751 141.252 137.441 145.906 145.497C146.735 146.932 148.57 147.423 150.004 146.594C151.439 145.765 151.93 143.93 151.101 142.496C145.92 133.527 138.471 126.081 129.5 120.903C125.208 118.426 120.639 116.509 115.907 115.185C127.241 109.392 135 97.6021 135 84C135 64.67 119.33 49 100 49Z" fill="#263245" fill-opacity="0.25"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,4 +1,7 @@
|
|||||||
import { useDeleteApplicationMutation } from '@/generated/graphql';
|
import {
|
||||||
|
GetOneUserDocument,
|
||||||
|
useDeleteApplicationMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||||
@@ -12,7 +15,9 @@ import { useRouter } from 'next/router';
|
|||||||
|
|
||||||
export default function ApplicationInfo() {
|
export default function ApplicationInfo() {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
const [deleteApplication, { client }] = useDeleteApplicationMutation({
|
||||||
|
refetchQueries: [GetOneUserDocument],
|
||||||
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleClickRemove() {
|
async function handleClickRemove() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import MaintenanceAlert from '@/components/common/MaintenanceAlert';
|
||||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||||
import Container from '@/components/layout/Container';
|
import Container from '@/components/layout/Container';
|
||||||
import { features } from '@/components/overview/features';
|
import { features } from '@/components/overview/features';
|
||||||
@@ -52,6 +53,7 @@ export default function ApplicationLive() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<MaintenanceAlert />
|
||||||
<OverviewTopBar />
|
<OverviewTopBar />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ interface HasuraDataProps {
|
|||||||
export function HasuraData({ close }: HasuraDataProps) {
|
export function HasuraData({ close }: HasuraDataProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const projectAdminSecret = currentApplication?.config?.hasura.adminSecret;
|
||||||
|
|
||||||
if (
|
if (!currentApplication?.subdomain || !projectAdminSecret) {
|
||||||
!currentApplication?.subdomain ||
|
|
||||||
!currentApplication?.hasuraGraphqlAdminSecret
|
|
||||||
) {
|
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,18 +69,11 @@ export function HasuraData({ close }: HasuraDataProps) {
|
|||||||
|
|
||||||
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
||||||
<Text className="font-medium" variant="subtitle2">
|
<Text className="font-medium" variant="subtitle2">
|
||||||
{Array(currentApplication.hasuraGraphqlAdminSecret.length)
|
{Array(projectAdminSecret.length).fill('•').join('')}
|
||||||
.fill('•')
|
|
||||||
.join('')}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() => copy(projectAdminSecret, 'Hasura admin secret')}
|
||||||
copy(
|
|
||||||
currentApplication.hasuraGraphqlAdminSecret,
|
|
||||||
'Hasura admin secret',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="min-w-0 p-1"
|
className="min-w-0 p-1"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import DeploymentStatusMessage from '@/components/deployments/DeploymentStatusMessage';
|
||||||
import { FindOldApps } from '@/components/home';
|
import { FindOldApps } from '@/components/home';
|
||||||
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
|
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
|
||||||
import type { Application, ApplicationState } from '@/types/application';
|
import type { ApplicationState } from '@/types/application';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import { Avatar } from '@/ui/Avatar';
|
|
||||||
import StateBadge from '@/ui/StateBadge';
|
import StateBadge from '@/ui/StateBadge';
|
||||||
import type { DeploymentStatus } from '@/ui/StatusCircle';
|
import type { DeploymentStatus } from '@/ui/StatusCircle';
|
||||||
import { StatusCircle } from '@/ui/StatusCircle';
|
import { StatusCircle } from '@/ui/StatusCircle';
|
||||||
@@ -10,59 +10,11 @@ import Divider from '@/ui/v2/Divider';
|
|||||||
import Link from '@/ui/v2/Link';
|
import Link from '@/ui/v2/Link';
|
||||||
import List from '@/ui/v2/List';
|
import List from '@/ui/v2/List';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
|
||||||
import { getApplicationStatusString } from '@/utils/helpers';
|
import { getApplicationStatusString } from '@/utils/helpers';
|
||||||
import { formatDistance } from 'date-fns';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import NavLink from 'next/link';
|
import NavLink from 'next/link';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
function ApplicationCreatedAt({ createdAt }: any) {
|
|
||||||
return (
|
|
||||||
<Text component="span" className="text-sm">
|
|
||||||
created{' '}
|
|
||||||
{formatDistance(new Date(createdAt), new Date(), {
|
|
||||||
addSuffix: true,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LastSuccessfulDeployment({ deployment }: any) {
|
|
||||||
return (
|
|
||||||
<span className="flex flex-row">
|
|
||||||
<Avatar
|
|
||||||
component="span"
|
|
||||||
name={deployment.commitUserName}
|
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
|
||||||
className="mr-1 h-4 w-4 self-center"
|
|
||||||
/>
|
|
||||||
<Text component="span" className="self-center text-sm">
|
|
||||||
{deployment.commitUserName} deployed{' '}
|
|
||||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
|
||||||
addSuffix: true,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CurrentDeployment({ deployment }: any) {
|
|
||||||
return (
|
|
||||||
<span className="flex flex-row">
|
|
||||||
<Avatar
|
|
||||||
component="span"
|
|
||||||
name={deployment.commitUserName}
|
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
|
||||||
className="mr-1 h-4 w-4 self-center"
|
|
||||||
/>
|
|
||||||
<Text className="self-center text-sm">
|
|
||||||
{deployment.commitUserName} updated just now
|
|
||||||
</Text>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkStatusOfTheApplication(
|
export function checkStatusOfTheApplication(
|
||||||
stateHistory: ApplicationState[] | [],
|
stateHistory: ApplicationState[] | [],
|
||||||
) {
|
) {
|
||||||
@@ -103,7 +55,7 @@ export function RenderWorkspacesWithApps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceProjects = workspace.applications
|
const workspaceProjects = workspace.applications
|
||||||
.filter((app: Application) =>
|
.filter((app) =>
|
||||||
app.name.toLowerCase().includes(query.toLowerCase()),
|
app.name.toLowerCase().includes(query.toLowerCase()),
|
||||||
)
|
)
|
||||||
.sort((appA, appB) => {
|
.sort((appA, appB) => {
|
||||||
@@ -141,25 +93,23 @@ export function RenderWorkspacesWithApps({
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<List className="grid grid-flow-row border-y">
|
<List className="grid grid-flow-row border-y">
|
||||||
{workspaceProjects.map((app, index) => {
|
{workspaceProjects.map((app, index) => {
|
||||||
const isDeployingToProduction = app.deployments[0]
|
const [latestDeployment] = app.deployments;
|
||||||
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={app.slug}>
|
<Fragment key={app.slug}>
|
||||||
<ListItem.Root
|
<ListItem.Root
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<div className="grid grid-flow-col gap-px">
|
<div className="grid grid-flow-col gap-px">
|
||||||
{app.deployments[0] && (
|
{latestDeployment && (
|
||||||
<div className="mr-2 flex self-center align-middle">
|
<div className="mr-2 flex self-center align-middle">
|
||||||
<StatusCircle
|
<StatusCircle
|
||||||
status={
|
status={
|
||||||
app.deployments[0]
|
latestDeployment.deploymentStatus as DeploymentStatus
|
||||||
.deploymentStatus as DeploymentStatus
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StateBadge
|
<StateBadge
|
||||||
status={checkStatusOfTheApplication(
|
status={checkStatusOfTheApplication(
|
||||||
app.appStates,
|
app.appStates,
|
||||||
@@ -190,27 +140,10 @@ export function RenderWorkspacesWithApps({
|
|||||||
<ListItem.Text
|
<ListItem.Text
|
||||||
primary={app.name}
|
primary={app.name}
|
||||||
secondary={
|
secondary={
|
||||||
<>
|
<DeploymentStatusMessage
|
||||||
{isDeployingToProduction && (
|
appCreatedAt={app.createdAt}
|
||||||
<CurrentDeployment
|
deployment={latestDeployment}
|
||||||
deployment={app.deployments[0]}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeployingToProduction &&
|
|
||||||
app.deployments[0] && (
|
|
||||||
<LastSuccessfulDeployment
|
|
||||||
deployment={app.deployments[0]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeployingToProduction &&
|
|
||||||
!app.deployments[0] && (
|
|
||||||
<ApplicationCreatedAt
|
|
||||||
createdAt={app.createdAt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem.Button>
|
</ListItem.Button>
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ function ControlledAutocomplete(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
inputValue={
|
||||||
|
typeof field.value !== 'object' ? field.value.toString() : undefined
|
||||||
|
}
|
||||||
{...props}
|
{...props}
|
||||||
{...field}
|
{...field}
|
||||||
ref={mergeRefs([field.ref, ref])}
|
ref={mergeRefs([field.ref, ref])}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export default function DataGridPreviewCell<TData extends object>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { presignedUrl } = await appClient.storage
|
const { presignedUrl } = await appClient.storage
|
||||||
.setAdminSecret(currentApplication.hasuraGraphqlAdminSecret)
|
.setAdminSecret(currentApplication.config?.hasura.adminSecret)
|
||||||
.getPresignedUrl({ fileId: id });
|
.getPresignedUrl({ fileId: id });
|
||||||
|
|
||||||
if (!presignedUrl) {
|
if (!presignedUrl) {
|
||||||
|
|||||||
@@ -1,31 +1,8 @@
|
|||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
/**
|
|
||||||
* Available dialog types.
|
|
||||||
*/
|
|
||||||
export type DialogType =
|
|
||||||
| 'EDIT_WORKSPACE_NAME'
|
|
||||||
| 'CREATE_RECORD'
|
|
||||||
| 'CREATE_COLUMN'
|
|
||||||
| 'EDIT_COLUMN'
|
|
||||||
| 'CREATE_TABLE'
|
|
||||||
| 'EDIT_TABLE'
|
|
||||||
| 'EDIT_PERMISSIONS'
|
|
||||||
| 'CREATE_FOREIGN_KEY'
|
|
||||||
| 'EDIT_FOREIGN_KEY'
|
|
||||||
| 'CREATE_ROLE'
|
|
||||||
| 'EDIT_ROLE'
|
|
||||||
| 'CREATE_USER'
|
|
||||||
| 'CREATE_PERMISSION_VARIABLE'
|
|
||||||
| 'EDIT_PERMISSION_VARIABLE'
|
|
||||||
| 'CREATE_ENVIRONMENT_VARIABLE'
|
|
||||||
| 'EDIT_ENVIRONMENT_VARIABLE'
|
|
||||||
| 'EDIT_USER'
|
|
||||||
| 'EDIT_USER_PASSWORD'
|
|
||||||
| 'EDIT_JWT_SECRET';
|
|
||||||
|
|
||||||
export interface DialogConfig<TPayload = unknown> {
|
export interface DialogConfig<TPayload = unknown> {
|
||||||
/**
|
/**
|
||||||
* Title of the dialog.
|
* Title of the dialog.
|
||||||
@@ -41,21 +18,36 @@ export interface DialogConfig<TPayload = unknown> {
|
|||||||
payload?: TPayload;
|
payload?: TPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenDialogOptions {
|
||||||
|
/**
|
||||||
|
* Title of the dialog.
|
||||||
|
*/
|
||||||
|
title: ReactNode;
|
||||||
|
/**
|
||||||
|
* Component to render inside the dialog skeleton.
|
||||||
|
*/
|
||||||
|
component: ReactElement<{
|
||||||
|
location?: 'drawer' | 'dialog';
|
||||||
|
onCancel?: () => void;
|
||||||
|
onSubmit?: (args?: any) => Promise<any> | void;
|
||||||
|
}>;
|
||||||
|
/**
|
||||||
|
* Props to pass to the root dialog component.
|
||||||
|
*/
|
||||||
|
props?: Partial<CommonDialogProps>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DialogContextProps {
|
export interface DialogContextProps {
|
||||||
/**
|
/**
|
||||||
* Call this function to open a dialog.
|
* Call this function to open a dialog. It will automatically apply the
|
||||||
|
* necessary functionality to the dialog.
|
||||||
*/
|
*/
|
||||||
openDialog: <TPayload = unknown>(
|
openDialog: (options: OpenDialogOptions) => void;
|
||||||
type: DialogType,
|
|
||||||
config?: DialogConfig<TPayload>,
|
|
||||||
) => void;
|
|
||||||
/**
|
/**
|
||||||
* Call this function to open a drawer.
|
* Call this function to open a drawer. It will automatically apply the
|
||||||
|
* necessary functionality to the drawer.
|
||||||
*/
|
*/
|
||||||
openDrawer: <TPayload = unknown>(
|
openDrawer: (options: OpenDialogOptions) => void;
|
||||||
type: DialogType,
|
|
||||||
config?: DialogConfig<TPayload>,
|
|
||||||
) => void;
|
|
||||||
/**
|
/**
|
||||||
* Call this function to open an alert dialog.
|
* Call this function to open an alert dialog.
|
||||||
*/
|
*/
|
||||||
@@ -87,7 +79,7 @@ export interface DialogContextProps {
|
|||||||
*/
|
*/
|
||||||
onDirtyStateChange: (
|
onDirtyStateChange: (
|
||||||
isDirty: boolean,
|
isDirty: boolean,
|
||||||
location?: 'drawer' | 'dialog',
|
location?: DialogFormProps['location'],
|
||||||
) => void;
|
) => void;
|
||||||
/**
|
/**
|
||||||
* Call this function to open a dirty confirmation dialog.
|
* Call this function to open a dirty confirmation dialog.
|
||||||
|
|||||||
@@ -1,30 +1,12 @@
|
|||||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||||
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
|
|
||||||
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
|
|
||||||
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
|
|
||||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
|
||||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
|
||||||
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
|
||||||
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 CreateUserForm from '@/components/users/CreateUserForm';
|
|
||||||
import EditUserForm from '@/components/users/EditUserForm';
|
|
||||||
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
|
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
|
||||||
import AlertDialog from '@/ui/v2/AlertDialog';
|
import AlertDialog from '@/ui/v2/AlertDialog';
|
||||||
import { BaseDialog } from '@/ui/v2/Dialog';
|
import { BaseDialog } from '@/ui/v2/Dialog';
|
||||||
import Drawer from '@/ui/v2/Drawer';
|
import Drawer from '@/ui/v2/Drawer';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import type {
|
import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
|
||||||
BaseSyntheticEvent,
|
|
||||||
DetailedHTMLProps,
|
|
||||||
HTMLProps,
|
|
||||||
PropsWithChildren,
|
|
||||||
} from 'react';
|
|
||||||
import {
|
import {
|
||||||
|
cloneElement,
|
||||||
|
isValidElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -33,7 +15,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
import type { DialogConfig, DialogType } from './DialogContext';
|
import type { DialogConfig, OpenDialogOptions } from './DialogContext';
|
||||||
import DialogContext from './DialogContext';
|
import DialogContext from './DialogContext';
|
||||||
import {
|
import {
|
||||||
alertDialogReducer,
|
alertDialogReducer,
|
||||||
@@ -41,67 +23,11 @@ import {
|
|||||||
drawerReducer,
|
drawerReducer,
|
||||||
} from './dialogReducers';
|
} from './dialogReducers';
|
||||||
|
|
||||||
function LoadingComponent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> = {}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
{...props}
|
|
||||||
className={twMerge(
|
|
||||||
'grid items-center justify-center px-6 py-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ActivityIndicator
|
|
||||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
|
||||||
label="Loading form..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CreateRecordForm = dynamic(
|
|
||||||
() => import('@/components/dataBrowser/CreateRecordForm'),
|
|
||||||
{ ssr: false, loading: () => LoadingComponent() },
|
|
||||||
);
|
|
||||||
|
|
||||||
const CreateColumnForm = dynamic(
|
|
||||||
() => import('@/components/dataBrowser/CreateColumnForm'),
|
|
||||||
{ ssr: false, loading: () => LoadingComponent() },
|
|
||||||
);
|
|
||||||
|
|
||||||
const EditColumnForm = dynamic(
|
|
||||||
() => import('@/components/dataBrowser/EditColumnForm'),
|
|
||||||
{ ssr: false, loading: () => LoadingComponent() },
|
|
||||||
);
|
|
||||||
|
|
||||||
const CreateTableForm = dynamic(
|
|
||||||
() => import('@/components/dataBrowser/CreateTableForm'),
|
|
||||||
{ ssr: false, loading: () => LoadingComponent() },
|
|
||||||
);
|
|
||||||
|
|
||||||
const EditTableForm = dynamic(
|
|
||||||
() => import('@/components/dataBrowser/EditTableForm'),
|
|
||||||
{ ssr: false, loading: () => LoadingComponent() },
|
|
||||||
);
|
|
||||||
|
|
||||||
const EditPermissionsForm = dynamic(
|
|
||||||
() => import('@/components/dataBrowser/EditPermissionsForm'),
|
|
||||||
{ ssr: false, loading: () => LoadingComponent() },
|
|
||||||
);
|
|
||||||
|
|
||||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{
|
{ open: dialogOpen, title: dialogTitle, activeDialog, dialogProps },
|
||||||
open: dialogOpen,
|
|
||||||
activeDialogType,
|
|
||||||
dialogProps,
|
|
||||||
title: dialogTitle,
|
|
||||||
payload: dialogPayload,
|
|
||||||
},
|
|
||||||
dialogDispatch,
|
dialogDispatch,
|
||||||
] = useReducer(dialogReducer, {
|
] = useReducer(dialogReducer, {
|
||||||
open: false,
|
open: false,
|
||||||
@@ -110,10 +36,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
open: drawerOpen,
|
open: drawerOpen,
|
||||||
activeDialogType: activeDrawerType,
|
|
||||||
dialogProps: drawerProps,
|
|
||||||
title: drawerTitle,
|
title: drawerTitle,
|
||||||
payload: drawerPayload,
|
activeDialog: activeDrawer,
|
||||||
|
dialogProps: drawerProps,
|
||||||
},
|
},
|
||||||
drawerDispatch,
|
drawerDispatch,
|
||||||
] = useReducer(drawerReducer, {
|
] = useReducer(drawerReducer, {
|
||||||
@@ -136,12 +61,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
const isDialogDirty = useRef(false);
|
const isDialogDirty = useRef(false);
|
||||||
const [showDirtyConfirmation, setShowDirtyConfirmation] = useState(false);
|
const [showDirtyConfirmation, setShowDirtyConfirmation] = useState(false);
|
||||||
|
|
||||||
const openDialog = useCallback(
|
const openDialog = useCallback((options: OpenDialogOptions) => {
|
||||||
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => {
|
dialogDispatch({ type: 'OPEN_DIALOG', payload: options });
|
||||||
dialogDispatch({ type: 'OPEN_DIALOG', payload: { type, config } });
|
}, []);
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeDialog = useCallback(() => {
|
const closeDialog = useCallback(() => {
|
||||||
dialogDispatch({ type: 'HIDE_DIALOG' });
|
dialogDispatch({ type: 'HIDE_DIALOG' });
|
||||||
@@ -152,12 +74,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
dialogDispatch({ type: 'CLEAR_DIALOG_CONTENT' });
|
dialogDispatch({ type: 'CLEAR_DIALOG_CONTENT' });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const openDrawer = useCallback(
|
const openDrawer = useCallback((options: OpenDialogOptions) => {
|
||||||
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => {
|
drawerDispatch({ type: 'OPEN_DRAWER', payload: options });
|
||||||
drawerDispatch({ type: 'OPEN_DRAWER', payload: { type, config } });
|
}, []);
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const closeDrawer = useCallback(() => {
|
const closeDrawer = useCallback(() => {
|
||||||
drawerDispatch({ type: 'HIDE_DRAWER' });
|
drawerDispatch({ type: 'HIDE_DRAWER' });
|
||||||
@@ -228,9 +147,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
[closeDialog, openDirtyConfirmation],
|
[closeDialog, openDirtyConfirmation],
|
||||||
);
|
);
|
||||||
|
|
||||||
// We are coupling this logic with the location of the dialog content which is
|
|
||||||
// not ideal. We shoule figure out a better logic for tracking the dirty
|
|
||||||
// state in the future.
|
|
||||||
const onDirtyStateChange = useCallback(
|
const onDirtyStateChange = useCallback(
|
||||||
(dirty: boolean, location: 'drawer' | 'dialog' = 'drawer') => {
|
(dirty: boolean, location: 'drawer' | 'dialog' = 'drawer') => {
|
||||||
if (location === 'dialog') {
|
if (location === 'dialog') {
|
||||||
@@ -271,25 +187,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sharedDialogProps = {
|
|
||||||
...dialogPayload,
|
|
||||||
onSubmit: async (values: any) => {
|
|
||||||
await dialogPayload?.onSubmit?.(values);
|
|
||||||
|
|
||||||
closeDialog();
|
|
||||||
},
|
|
||||||
onCancel: closeDialogWithDirtyGuard,
|
|
||||||
};
|
|
||||||
|
|
||||||
const sharedDrawerProps = {
|
|
||||||
onSubmit: async () => {
|
|
||||||
await drawerPayload?.onSubmit();
|
|
||||||
|
|
||||||
closeDrawer();
|
|
||||||
},
|
|
||||||
onCancel: closeDrawerWithDirtyGuard,
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleCloseDrawerAndDialog() {
|
function handleCloseDrawerAndDialog() {
|
||||||
if (isDrawerDirty.current || isDialogDirty.current) {
|
if (isDrawerDirty.current || isDialogDirty.current) {
|
||||||
@@ -367,56 +264,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
<RetryableErrorBoundary
|
<RetryableErrorBoundary
|
||||||
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
||||||
>
|
>
|
||||||
{activeDialogType === 'EDIT_WORKSPACE_NAME' && (
|
{isValidElement(activeDialog)
|
||||||
<EditWorkspaceNameForm {...sharedDialogProps} />
|
? cloneElement(activeDialog, {
|
||||||
)}
|
...activeDialog.props,
|
||||||
|
location: 'dialog',
|
||||||
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
|
onSubmit: async (values?: any) => {
|
||||||
<CreateForeignKeyForm {...sharedDialogProps} />
|
await activeDialog?.props?.onSubmit?.(values);
|
||||||
)}
|
closeDialog();
|
||||||
|
},
|
||||||
{activeDialogType === 'EDIT_FOREIGN_KEY' && (
|
onCancel: () => {
|
||||||
<EditForeignKeyForm {...sharedDialogProps} />
|
activeDialog?.props?.onCancel?.();
|
||||||
)}
|
closeDialogWithDirtyGuard();
|
||||||
|
},
|
||||||
{activeDialogType === 'CREATE_ROLE' && (
|
})
|
||||||
<CreateRoleForm {...sharedDialogProps} />
|
: null}
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDialogType === 'EDIT_ROLE' && (
|
|
||||||
<EditRoleForm {...sharedDialogProps} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDialogType === 'CREATE_USER' && (
|
|
||||||
<CreateUserForm {...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} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDialogType === 'EDIT_USER_PASSWORD' && (
|
|
||||||
<EditUserPasswordForm
|
|
||||||
{...sharedDialogProps}
|
|
||||||
user={sharedDialogProps?.user}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDialogType === 'EDIT_JWT_SECRET' && (
|
|
||||||
<EditJwtSecretForm {...sharedDialogProps} />
|
|
||||||
)}
|
|
||||||
</RetryableErrorBoundary>
|
</RetryableErrorBoundary>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
@@ -436,51 +297,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RetryableErrorBoundary>
|
<RetryableErrorBoundary>
|
||||||
{activeDrawerType === 'CREATE_RECORD' && (
|
{isValidElement(activeDrawer)
|
||||||
<CreateRecordForm
|
? cloneElement(activeDrawer, {
|
||||||
{...sharedDrawerProps}
|
...activeDrawer.props,
|
||||||
columns={drawerPayload?.columns}
|
location: 'drawer',
|
||||||
/>
|
onSubmit: async (values?: any) => {
|
||||||
)}
|
await activeDrawer?.props?.onSubmit?.(values);
|
||||||
|
closeDrawer();
|
||||||
{activeDrawerType === 'CREATE_COLUMN' && (
|
},
|
||||||
<CreateColumnForm {...sharedDrawerProps} />
|
onCancel: () => {
|
||||||
)}
|
activeDrawer?.props?.onCancel?.();
|
||||||
|
closeDrawerWithDirtyGuard();
|
||||||
{activeDrawerType === 'EDIT_COLUMN' && (
|
},
|
||||||
<EditColumnForm
|
})
|
||||||
{...sharedDrawerProps}
|
: null}
|
||||||
column={drawerPayload?.column}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDrawerType === 'CREATE_TABLE' && (
|
|
||||||
<CreateTableForm
|
|
||||||
{...sharedDrawerProps}
|
|
||||||
schema={drawerPayload?.schema}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDrawerType === 'EDIT_TABLE' && (
|
|
||||||
<EditTableForm
|
|
||||||
{...sharedDrawerProps}
|
|
||||||
table={drawerPayload?.table}
|
|
||||||
schema={drawerPayload?.schema}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDrawerType === 'EDIT_PERMISSIONS' && (
|
|
||||||
<EditPermissionsForm
|
|
||||||
{...sharedDrawerProps}
|
|
||||||
disabled={drawerPayload?.disabled}
|
|
||||||
schema={drawerPayload?.schema}
|
|
||||||
table={drawerPayload?.table}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeDrawerType === 'EDIT_USER' && (
|
|
||||||
<EditUserForm {...sharedDrawerProps} {...drawerPayload} />
|
|
||||||
)}
|
|
||||||
</RetryableErrorBoundary>
|
</RetryableErrorBoundary>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import type { DialogConfig, DialogType } from './DialogContext';
|
import type { DialogConfig, OpenDialogOptions } from './DialogContext';
|
||||||
|
|
||||||
export interface DialogState {
|
export interface DialogState {
|
||||||
/**
|
/**
|
||||||
@@ -12,9 +12,13 @@ export interface DialogState {
|
|||||||
*/
|
*/
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
/**
|
/**
|
||||||
* Type of the currently active dialog.
|
* Component to render inside the dialog skeleton.
|
||||||
*/
|
*/
|
||||||
activeDialogType?: DialogType;
|
activeDialog?: ReactElement<{
|
||||||
|
location?: 'drawer' | 'dialog';
|
||||||
|
onCancel?: () => void;
|
||||||
|
onSubmit?: (args?: any) => Promise<any> | void;
|
||||||
|
}>;
|
||||||
/**
|
/**
|
||||||
* Props passed to the currently active dialog.
|
* Props passed to the currently active dialog.
|
||||||
*/
|
*/
|
||||||
@@ -27,10 +31,7 @@ export interface DialogState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DialogAction =
|
export type DialogAction =
|
||||||
| {
|
| { type: 'OPEN_DIALOG'; payload: OpenDialogOptions }
|
||||||
type: 'OPEN_DIALOG';
|
|
||||||
payload: { type: DialogType; config?: DialogConfig };
|
|
||||||
}
|
|
||||||
| { type: 'HIDE_DIALOG' }
|
| { type: 'HIDE_DIALOG' }
|
||||||
| { type: 'CLEAR_DIALOG_CONTENT' };
|
| { type: 'CLEAR_DIALOG_CONTENT' };
|
||||||
|
|
||||||
@@ -50,10 +51,9 @@ export function dialogReducer(
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
open: true,
|
open: true,
|
||||||
activeDialogType: action.payload?.type,
|
title: action.payload.title,
|
||||||
dialogProps: action.payload.config?.props,
|
activeDialog: action.payload.component,
|
||||||
title: action.payload.config?.title,
|
dialogProps: action.payload.props,
|
||||||
payload: action.payload.config?.payload,
|
|
||||||
};
|
};
|
||||||
case 'HIDE_DIALOG':
|
case 'HIDE_DIALOG':
|
||||||
return {
|
return {
|
||||||
@@ -64,8 +64,7 @@ export function dialogReducer(
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
title: undefined,
|
title: undefined,
|
||||||
payload: undefined,
|
activeDialog: undefined,
|
||||||
activeDialogType: undefined,
|
|
||||||
dialogProps: undefined,
|
dialogProps: undefined,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
@@ -74,10 +73,7 @@ export function dialogReducer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DrawerAction =
|
export type DrawerAction =
|
||||||
| {
|
| { type: 'OPEN_DRAWER'; payload: OpenDialogOptions }
|
||||||
type: 'OPEN_DRAWER';
|
|
||||||
payload: { type: DialogType; config?: DialogConfig };
|
|
||||||
}
|
|
||||||
| { type: 'HIDE_DRAWER' }
|
| { type: 'HIDE_DRAWER' }
|
||||||
| { type: 'CLEAR_DRAWER_CONTENT' };
|
| { type: 'CLEAR_DRAWER_CONTENT' };
|
||||||
|
|
||||||
@@ -97,10 +93,9 @@ export function drawerReducer(
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
open: true,
|
open: true,
|
||||||
activeDialogType: action.payload?.type,
|
title: action.payload.title,
|
||||||
dialogProps: action.payload.config?.props,
|
activeDialog: action.payload.component,
|
||||||
title: action.payload.config?.title,
|
dialogProps: action.payload.props,
|
||||||
payload: action.payload.config?.payload,
|
|
||||||
};
|
};
|
||||||
case 'HIDE_DRAWER':
|
case 'HIDE_DRAWER':
|
||||||
return {
|
return {
|
||||||
@@ -111,8 +106,7 @@ export function drawerReducer(
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
title: undefined,
|
title: undefined,
|
||||||
payload: undefined,
|
activeDialog: undefined,
|
||||||
activeDialogType: undefined,
|
|
||||||
dialogProps: undefined,
|
dialogProps: undefined,
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { BoxProps } from '@/ui/v2/Box';
|
import type { BoxProps } from '@/ui/v2/Box';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
export interface FormProps extends BoxProps {
|
export interface FormProps extends BoxProps {
|
||||||
@@ -11,6 +12,7 @@ export interface FormProps extends BoxProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||||
|
const formRef = useRef<HTMLDivElement>();
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
@@ -25,6 +27,15 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitButton = Array.from(
|
||||||
|
formRef.current.getElementsByTagName('button'),
|
||||||
|
).find((item) => item.type === 'submit');
|
||||||
|
|
||||||
|
// Disabling submit if the submit button is disabled
|
||||||
|
if (submitButton?.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
handleSubmit(onSubmit)(event);
|
handleSubmit(onSubmit)(event);
|
||||||
@@ -35,6 +46,7 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
|||||||
// so keyboard events must be handled on the form element itself.
|
// so keyboard events must be handled on the form element itself.
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
<Box
|
<Box
|
||||||
|
ref={formRef}
|
||||||
component="form"
|
component="form"
|
||||||
{...props}
|
{...props}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import type { BoxProps } from '@/ui/v2/Box';
|
||||||
|
import Box from '@/ui/v2/Box';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface FormActivityIndicatorProps extends BoxProps {}
|
||||||
|
|
||||||
|
export default function FormActivityIndicator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: FormActivityIndicatorProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
{...props}
|
||||||
|
className={twMerge(
|
||||||
|
'grid items-center justify-center px-6 py-4',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ActivityIndicator
|
||||||
|
circularProgressProps={{ className: 'w-5 h-5' }}
|
||||||
|
label="Loading form..."
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './FormActivityIndicator';
|
||||||
|
export { default } from './FormActivityIndicator';
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import Breadcrumbs from '@/components/common/Breadcrumbs';
|
import Breadcrumbs from '@/components/common/Breadcrumbs';
|
||||||
import FeedbackForm from '@/components/common/FeedbackForm';
|
import FeedbackForm from '@/components/common/FeedbackForm';
|
||||||
|
import LocalAccountMenu from '@/components/common/LocalAccountMenu';
|
||||||
import Logo from '@/components/common/Logo';
|
import Logo from '@/components/common/Logo';
|
||||||
import MobileNav from '@/components/common/MobileNav';
|
import MobileNav from '@/components/common/MobileNav';
|
||||||
import NavLink from '@/components/common/NavLink';
|
import NavLink from '@/components/common/NavLink';
|
||||||
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
|
||||||
import { AccountMenu } from '@/components/dashboard/AccountMenu';
|
import { AccountMenu } from '@/components/dashboard/AccountMenu';
|
||||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
@@ -23,6 +23,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
component="header"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'z-40 grid w-full transform-gpu grid-flow-col items-center justify-between gap-2 border-b-1 px-4 py-3',
|
'z-40 grid w-full transform-gpu grid-flow-col items-center justify-between gap-2 border-b-1 px-4 py-3',
|
||||||
className,
|
className,
|
||||||
@@ -73,7 +74,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
|||||||
Docs
|
Docs
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{isPlatform ? <AccountMenu /> : <ThemeSwitcher className="w-52" />}
|
{isPlatform ? <AccountMenu /> : <LocalAccountMenu />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MobileNav className="sm:hidden" />
|
<MobileNav className="sm:hidden" />
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ function IconLink(
|
|||||||
: [icon.props?.sx]),
|
: [icon.props?.sx]),
|
||||||
{
|
{
|
||||||
color: (theme) => {
|
color: (theme) => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return theme.palette.mode === 'dark'
|
||||||
|
? 'text.secondary'
|
||||||
|
: 'text.primary';
|
||||||
|
}
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
return 'primary.main';
|
return 'primary.main';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
|
||||||
|
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||||
|
import IconButton from '@/ui/v2/IconButton';
|
||||||
|
import UserIcon from '@/ui/v2/icons/UserIcon';
|
||||||
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
|
export default function LocalAccountMenu() {
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown.Root className="justify-self-center">
|
||||||
|
<Dropdown.Trigger hideChevron asChild>
|
||||||
|
<IconButton
|
||||||
|
variant="borderless"
|
||||||
|
color="secondary"
|
||||||
|
className="h-7 w-7 rounded-full"
|
||||||
|
sx={{
|
||||||
|
backgroundColor: (theme) => `${theme.palette.grey[300]} !important`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserIcon className="h-4 w-4" />
|
||||||
|
</IconButton>
|
||||||
|
</Dropdown.Trigger>
|
||||||
|
|
||||||
|
<Dropdown.Content
|
||||||
|
PaperProps={{
|
||||||
|
className: 'mt-1 p-6 grid grid-flow-row gap-4 w-full max-w-xs',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeSwitcher label="Theme" />
|
||||||
|
|
||||||
|
<Text className="text-center text-xs" color="disabled">
|
||||||
|
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||||
|
</Text>
|
||||||
|
</Dropdown.Content>
|
||||||
|
</Dropdown.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './LocalAccountMenu';
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import { Alert } from '@/ui/Alert';
|
||||||
|
|
||||||
|
export default function MaintenanceAlert() {
|
||||||
|
const { maintenanceActive, maintenanceEndDate } = useUI();
|
||||||
|
|
||||||
|
if (!maintenanceActive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateTimeFormat = Intl.DateTimeFormat(undefined, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = dateTimeFormat.formatToParts(maintenanceEndDate);
|
||||||
|
|
||||||
|
const year = parts.find((part) => part.type === 'year')?.value;
|
||||||
|
const month = parts.find((part) => part.type === 'month')?.value;
|
||||||
|
const day = parts.find((part) => part.type === 'day')?.value;
|
||||||
|
const hour = parts.find((part) => part.type === 'hour')?.value;
|
||||||
|
const minute = parts.find((part) => part.type === 'minute')?.value;
|
||||||
|
const timeZone = parts.find((part) => part.type === 'timeZoneName')?.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert severity="warning" className="mt-4">
|
||||||
|
<p>
|
||||||
|
We're currently doing maintenance on our infrastructure. Project
|
||||||
|
creation and project settings are temporarily disabled during the
|
||||||
|
maintenance period.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{maintenanceEndDate && (
|
||||||
|
<p>
|
||||||
|
Maintenance is expected to be completed at {year}-{month}-{day} {hour}
|
||||||
|
:{minute} {timeZone}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './MaintenanceAlert';
|
||||||
@@ -20,6 +20,7 @@ import type { ListItemButtonProps } from '@/ui/v2/ListItem';
|
|||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import { useSignOut } from '@nhost/nextjs';
|
import { useSignOut } from '@nhost/nextjs';
|
||||||
|
import getConfig from 'next/config';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { cloneElement, Fragment, isValidElement, useState } from 'react';
|
import { cloneElement, Fragment, isValidElement, useState } from 'react';
|
||||||
@@ -89,6 +90,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
|||||||
const { signOut } = useSignOut();
|
const { signOut } = useSignOut();
|
||||||
const { setUserContext } = useUserDataContext();
|
const { setUserContext } = useUserDataContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -256,6 +258,10 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
|
|||||||
</ListItem.Button>
|
</ListItem.Button>
|
||||||
</ListItem.Root>
|
</ListItem.Root>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
<Text className="text-center text-xs" color="secondary">
|
||||||
|
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||||
|
</Text>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ export default function ThemeSwitcher({
|
|||||||
|
|
||||||
onChange?.(event, value);
|
onChange?.(event, value);
|
||||||
}}
|
}}
|
||||||
|
slotProps={{
|
||||||
|
listbox: { className: 'min-w-0 w-full' },
|
||||||
|
popper: {
|
||||||
|
disablePortal: false,
|
||||||
|
className: 'z-[10000] w-[270px] w-full',
|
||||||
|
},
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Option value="light">Light</Option>
|
<Option value="light">Light</Option>
|
||||||
<Option value="dark">Dark</Option>
|
<Option value="dark">Dark</Option>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Text from '@/ui/v2/Text';
|
|||||||
import { nhost } from '@/utils/nhost';
|
import { nhost } from '@/utils/nhost';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useUserData } from '@nhost/nextjs';
|
import { useUserData } from '@nhost/nextjs';
|
||||||
|
import getConfig from 'next/config';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -24,23 +25,10 @@ function AccountMenuContent({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const { handleClose } = useDropdown();
|
const { handleClose } = useDropdown();
|
||||||
|
const { publicRuntimeConfig } = getConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="relative grid w-account grid-flow-row gap-5 p-6">
|
<Box className="relative grid w-full grid-flow-row gap-5 p-6">
|
||||||
<Button
|
|
||||||
variant="borderless"
|
|
||||||
color="secondary"
|
|
||||||
className="absolute top-6 right-4 grid grid-flow-col items-center gap-px self-start font-medium"
|
|
||||||
onClick={async () => {
|
|
||||||
await nhost.auth.signOut();
|
|
||||||
router.push('/signin');
|
|
||||||
await client.resetStore();
|
|
||||||
}}
|
|
||||||
endIcon={<PowerIcon className="mr-1 h-4 w-4" />}
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-flow-row justify-center">
|
<div className="grid grid-flow-row justify-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
className="mx-auto mb-2 h-16 w-16 rounded-full"
|
className="mx-auto mb-2 h-16 w-16 rounded-full"
|
||||||
@@ -72,9 +60,26 @@ function AccountMenuContent({
|
|||||||
<Button color="error" disabled>
|
<Button color="error" disabled>
|
||||||
Remove Account
|
Remove Account
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await nhost.auth.signOut();
|
||||||
|
router.push('/signin');
|
||||||
|
await client.resetStore();
|
||||||
|
}}
|
||||||
|
endIcon={<PowerIcon className="mr-1 h-4 w-4" />}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeSwitcher label="Theme" fullWidth />
|
<ThemeSwitcher label="Theme" />
|
||||||
|
|
||||||
|
<Text className="text-center text-xs" color="disabled">
|
||||||
|
Dashboard Version: {publicRuntimeConfig?.version || 'n/a'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -107,7 +112,7 @@ export function AccountMenu() {
|
|||||||
/>
|
/>
|
||||||
</Dropdown.Trigger>
|
</Dropdown.Trigger>
|
||||||
|
|
||||||
<Dropdown.Content PaperProps={{ className: 'mt-1' }}>
|
<Dropdown.Content PaperProps={{ className: 'mt-1 max-w-xs w-full' }}>
|
||||||
<AccountMenuContent
|
<AccountMenuContent
|
||||||
onChangePasswordClick={() => setChangePasswordModal(true)}
|
onChangePasswordClick={() => setChangePasswordModal(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -22,7 +23,7 @@ import ForeignKeyEditor from './ForeignKeyEditor';
|
|||||||
|
|
||||||
export type BaseColumnFormValues = DatabaseColumn;
|
export type BaseColumnFormValues = DatabaseColumn;
|
||||||
|
|
||||||
export interface BaseColumnFormProps {
|
export interface BaseColumnFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +61,7 @@ export default function BaseColumnForm({
|
|||||||
onSubmit: handleExternalSubmit,
|
onSubmit: handleExternalSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: BaseColumnFormProps) {
|
}: BaseColumnFormProps) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
|
|
||||||
@@ -91,8 +93,8 @@ export default function BaseColumnForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'drawer');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
|
||||||
|
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
|
||||||
import type { DatabaseColumn } from '@/types/dataBrowser';
|
import type { DatabaseColumn } from '@/types/dataBrowser';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -29,7 +30,7 @@ const ForeignKeyEditorInput = forwardRef(
|
|||||||
) => {
|
) => {
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
const { setValue } = useFormContext();
|
const { setValue } = useFormContext();
|
||||||
const column = useWatch<Partial<DatabaseColumn>>();
|
const column = useWatch() as DatabaseColumn;
|
||||||
const { foreignKeyRelation } = column;
|
const { foreignKeyRelation } = column;
|
||||||
|
|
||||||
if (!column.foreignKeyRelation) {
|
if (!column.foreignKeyRelation) {
|
||||||
@@ -39,8 +40,8 @@ const ForeignKeyEditorInput = forwardRef(
|
|||||||
className="py-1"
|
className="py-1"
|
||||||
disabled={!column.name || !column.type}
|
disabled={!column.name || !column.type}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
openDialog('CREATE_FOREIGN_KEY', {
|
openDialog({
|
||||||
title: (
|
title: (
|
||||||
<span className="grid grid-flow-row">
|
<span className="grid grid-flow-row">
|
||||||
<span>Add a Foreign Key Relation</span>
|
<span>Add a Foreign Key Relation</span>
|
||||||
@@ -51,16 +52,18 @@ const ForeignKeyEditorInput = forwardRef(
|
|||||||
</Text>
|
</Text>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
payload: {
|
component: (
|
||||||
selectedColumn: column.name,
|
<CreateForeignKeyForm
|
||||||
availableColumns: [column],
|
selectedColumn={column.name}
|
||||||
onSubmit: (values: BaseForeignKeyFormValues) => {
|
availableColumns={[column]}
|
||||||
setValue('foreignKeyRelation', values);
|
onSubmit={(values) => {
|
||||||
onCreateSubmit();
|
setValue('foreignKeyRelation', values);
|
||||||
},
|
onCreateSubmit();
|
||||||
},
|
}}
|
||||||
})
|
/>
|
||||||
}
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Add Foreign Key
|
Add Foreign Key
|
||||||
</Button>
|
</Button>
|
||||||
@@ -86,20 +89,22 @@ const ForeignKeyEditorInput = forwardRef(
|
|||||||
<div className="grid grid-flow-col">
|
<div className="grid grid-flow-col">
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onClick={() =>
|
onClick={() => {
|
||||||
openDialog('EDIT_FOREIGN_KEY', {
|
openDialog({
|
||||||
title: 'Edit Foreign Key Relation',
|
title: 'Edit Foreign Key Relation',
|
||||||
payload: {
|
component: (
|
||||||
foreignKeyRelation,
|
<EditForeignKeyForm
|
||||||
availableColumns: [column],
|
foreignKeyRelation={foreignKeyRelation}
|
||||||
selectedColumn: column.name,
|
selectedColumn={column.name}
|
||||||
onSubmit: (values: BaseForeignKeyFormValues) => {
|
availableColumns={[column]}
|
||||||
setValue('foreignKeyRelation', values);
|
onSubmit={(values) => {
|
||||||
onEditSubmit();
|
setValue('foreignKeyRelation', values);
|
||||||
},
|
onEditSubmit();
|
||||||
},
|
}}
|
||||||
})
|
/>
|
||||||
}
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
className="min-w-[initial] py-1 px-2"
|
className="min-w-[initial] py-1 px-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
|
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -23,7 +24,7 @@ export interface BaseForeignKeyFormValues extends ForeignKeyRelation {
|
|||||||
disableOriginColumn?: boolean;
|
disableOriginColumn?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseForeignKeyFormProps {
|
export interface BaseForeignKeyFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Available columns in the table.
|
* Available columns in the table.
|
||||||
*/
|
*/
|
||||||
@@ -64,6 +65,7 @@ export function BaseForeignKeyForm({
|
|||||||
onSubmit: handleExternalSubmit,
|
onSubmit: handleExternalSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: BaseForeignKeyFormProps) {
|
}: BaseForeignKeyFormProps) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
|
|
||||||
@@ -86,8 +88,8 @@ export function BaseForeignKeyForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'dialog');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
|
import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type {
|
import type {
|
||||||
ColumnInsertOptions,
|
ColumnInsertOptions,
|
||||||
DataBrowserGridColumn,
|
DataBrowserGridColumn,
|
||||||
@@ -10,7 +11,7 @@ import Button from '@/ui/v2/Button';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
export interface BaseRecordFormProps {
|
export interface BaseRecordFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* The columns of the table.
|
* The columns of the table.
|
||||||
*/
|
*/
|
||||||
@@ -36,6 +37,7 @@ export default function BaseRecordForm({
|
|||||||
onSubmit: handleExternalSubmit,
|
onSubmit: handleExternalSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: BaseRecordFormProps) {
|
}: BaseRecordFormProps) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
const { requiredColumns, optionalColumns } = columns.reduce(
|
const { requiredColumns, optionalColumns } = columns.reduce(
|
||||||
@@ -70,8 +72,8 @@ export default function BaseRecordForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'drawer');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
// Stores columns in a map to have constant time lookup. This is necessary
|
// Stores columns in a map to have constant time lookup. This is necessary
|
||||||
// for tables with many columns.
|
// for tables with many columns.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
|
import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
|
import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -30,7 +31,7 @@ export interface BaseTableFormValues
|
|||||||
foreignKeyRelations?: ForeignKeyRelation[];
|
foreignKeyRelations?: ForeignKeyRelation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseTableFormProps {
|
export interface BaseTableFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -99,7 +100,9 @@ function NameInput() {
|
|||||||
function FormFooter({
|
function FormFooter({
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText,
|
submitButtonText,
|
||||||
}: Pick<BaseTableFormProps, 'onCancel' | 'submitButtonText'>) {
|
location,
|
||||||
|
}: Pick<BaseTableFormProps, 'onCancel' | 'submitButtonText'> &
|
||||||
|
Pick<DialogFormProps, 'location'>) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
const { isSubmitting, dirtyFields } = useFormState();
|
const { isSubmitting, dirtyFields } = useFormState();
|
||||||
|
|
||||||
@@ -108,8 +111,8 @@ function FormFooter({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'drawer');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
|
||||||
@@ -135,6 +138,7 @@ function FormFooter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BaseTableForm({
|
export default function BaseTableForm({
|
||||||
|
location,
|
||||||
onSubmit: handleExternalSubmit,
|
onSubmit: handleExternalSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
@@ -168,7 +172,11 @@ export default function BaseTableForm({
|
|||||||
<ForeignKeyEditorSection />
|
<ForeignKeyEditorSection />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormFooter onCancel={onCancel} submitButtonText={submitButtonText} />
|
<FormFooter
|
||||||
|
onCancel={onCancel}
|
||||||
|
submitButtonText={submitButtonText}
|
||||||
|
location={location}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||||
|
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
|
||||||
|
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
|
||||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||||
@@ -68,18 +70,19 @@ export default function ForeignKeyEditorSection() {
|
|||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
const primaryKeyIndex = getValues('primaryKeyIndex');
|
const primaryKeyIndex = getValues('primaryKeyIndex');
|
||||||
|
|
||||||
openDialog('EDIT_FOREIGN_KEY', {
|
openDialog({
|
||||||
title: 'Edit Foreign Key Relation',
|
title: 'Edit Foreign Key Relation',
|
||||||
payload: {
|
component: (
|
||||||
foreignKeyRelation: fields[index],
|
<EditForeignKeyForm
|
||||||
availableColumns: columns.map((column, columnIndex) =>
|
foreignKeyRelation={fields[index] as ForeignKeyRelation}
|
||||||
columnIndex === primaryKeyIndex
|
availableColumns={columns.map((column, columnIndex) =>
|
||||||
? { ...column, isPrimary: true }
|
columnIndex === primaryKeyIndex
|
||||||
: column,
|
? { ...column, isPrimary: true }
|
||||||
),
|
: column,
|
||||||
onSubmit: (values: BaseForeignKeyFormValues) =>
|
)}
|
||||||
handleEdit(values, index),
|
onSubmit={(values) => handleEdit(values, index)}
|
||||||
},
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onDelete={() => remove(index)}
|
onDelete={() => remove(index)}
|
||||||
@@ -105,7 +108,7 @@ export default function ForeignKeyEditorSection() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const primaryKeyIndex = getValues('primaryKeyIndex');
|
const primaryKeyIndex = getValues('primaryKeyIndex');
|
||||||
|
|
||||||
openDialog('CREATE_FOREIGN_KEY', {
|
openDialog({
|
||||||
title: (
|
title: (
|
||||||
<span className="grid grid-flow-row">
|
<span className="grid grid-flow-row">
|
||||||
<span>Add a Foreign Key Relation</span>
|
<span>Add a Foreign Key Relation</span>
|
||||||
@@ -116,14 +119,16 @@ export default function ForeignKeyEditorSection() {
|
|||||||
</Text>
|
</Text>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
payload: {
|
component: (
|
||||||
availableColumns: columns.map((column, index) =>
|
<CreateForeignKeyForm
|
||||||
index === primaryKeyIndex
|
availableColumns={columns.map((column, index) =>
|
||||||
? { ...column, isPrimary: true }
|
index === primaryKeyIndex
|
||||||
: column,
|
? { ...column, isPrimary: true }
|
||||||
),
|
: column,
|
||||||
onSubmit: handleCreate,
|
)}
|
||||||
},
|
onSubmit={handleCreate}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
import permissionVariablesQuery from '@/utils/msw/mocks/graphql/permissionVariablesQuery';
|
||||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||||
import { render, screen } from '@/utils/testUtils';
|
import { render, screen } from '@/utils/testUtils';
|
||||||
@@ -6,7 +6,11 @@ import { setupServer } from 'msw/node';
|
|||||||
import { test, vi } from 'vitest';
|
import { test, vi } from 'vitest';
|
||||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||||
|
|
||||||
const server = setupServer(tableQuery, hasuraMetadataQuery, customClaimsQuery);
|
const server = setupServer(
|
||||||
|
tableQuery,
|
||||||
|
hasuraMetadataQuery,
|
||||||
|
permissionVariablesQuery,
|
||||||
|
);
|
||||||
|
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
|
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
|
||||||
afterEach(() => server.resetHandlers());
|
afterEach(() => server.resetHandlers());
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import { useRouter } from 'next/router';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface CreateColumnFormProps
|
export interface CreateColumnFormProps
|
||||||
extends Pick<BaseColumnFormProps, 'onCancel'> {
|
extends Pick<BaseColumnFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
onSubmit?: () => Promise<void>;
|
onSubmit?: (args?: any) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateColumnForm({
|
export default function CreateColumnForm({
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import { useState } from 'react';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface CreateForeignKeyFormProps
|
export interface CreateForeignKeyFormProps
|
||||||
extends Pick<BaseForeignKeyFormProps, 'onCancel' | 'availableColumns'> {
|
extends Pick<
|
||||||
|
BaseForeignKeyFormProps,
|
||||||
|
'onCancel' | 'availableColumns' | 'location'
|
||||||
|
> {
|
||||||
/**
|
/**
|
||||||
* Column selected by default.
|
* Column selected by default.
|
||||||
*/
|
*/
|
||||||
@@ -21,7 +24,7 @@ export interface CreateForeignKeyFormProps
|
|||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateForeignKeyForm({
|
export default function CreateForeignKeyForm({
|
||||||
@@ -51,9 +54,7 @@ export default function CreateForeignKeyForm({
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (onSubmit) {
|
await onSubmit?.(values);
|
||||||
await onSubmit(values);
|
|
||||||
}
|
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
if (submitError && submitError instanceof Error) {
|
if (submitError && submitError instanceof Error) {
|
||||||
setError(submitError);
|
setError(submitError);
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface CreateRecordFormProps
|
export interface CreateRecordFormProps
|
||||||
extends Pick<BaseRecordFormProps, 'columns' | 'onCancel'> {
|
extends Pick<BaseRecordFormProps, 'columns' | 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
onSubmit?: () => Promise<void>;
|
onSubmit?: (args?: any) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateRecordForm({
|
export default function CreateRecordForm({
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { useRouter } from 'next/router';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface CreateTableFormProps
|
export interface CreateTableFormProps
|
||||||
extends Pick<BaseTableFormProps, 'onCancel'> {
|
extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Schema where the table should be created.
|
* Schema where the table should be created.
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +25,7 @@ export interface CreateTableFormProps
|
|||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
onSubmit?: () => Promise<void>;
|
onSubmit?: (args?: any) => Promise<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateTableForm({
|
export default function CreateTableForm({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import DataGridDateCell from '@/components/common/DataGridDateCell';
|
|||||||
import DataGridNumericCell from '@/components/common/DataGridNumericCell';
|
import DataGridNumericCell from '@/components/common/DataGridNumericCell';
|
||||||
import DataGridTextCell from '@/components/common/DataGridTextCell';
|
import DataGridTextCell from '@/components/common/DataGridTextCell';
|
||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
|
||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||||
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
|
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
|
||||||
@@ -28,9 +29,25 @@ import {
|
|||||||
} from '@/utils/dataBrowser/postgresqlConstants';
|
} from '@/utils/dataBrowser/postgresqlConstants';
|
||||||
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
|
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const CreateColumnForm = dynamic(
|
||||||
|
() => import('@/components/dataBrowser/CreateColumnForm'),
|
||||||
|
{ ssr: false, loading: () => <FormActivityIndicator /> },
|
||||||
|
);
|
||||||
|
|
||||||
|
const EditColumnForm = dynamic(
|
||||||
|
() => import('@/components/dataBrowser/EditColumnForm'),
|
||||||
|
{ ssr: false, loading: () => <FormActivityIndicator /> },
|
||||||
|
);
|
||||||
|
|
||||||
|
const CreateRecordForm = dynamic(
|
||||||
|
() => import('@/components/dataBrowser/CreateRecordForm'),
|
||||||
|
{ ssr: false, loading: () => <FormActivityIndicator /> },
|
||||||
|
);
|
||||||
|
|
||||||
export interface DataBrowserGridProps extends Partial<DataGridProps<any>> {}
|
export interface DataBrowserGridProps extends Partial<DataGridProps<any>> {}
|
||||||
|
|
||||||
export function createDataGridColumn(
|
export function createDataGridColumn(
|
||||||
@@ -273,33 +290,36 @@ export default function DataBrowserGrid({
|
|||||||
const memoizedData = useMemo(() => rows, [rows]);
|
const memoizedData = useMemo(() => rows, [rows]);
|
||||||
|
|
||||||
async function handleInsertRowClick() {
|
async function handleInsertRowClick() {
|
||||||
openDrawer('CREATE_RECORD', {
|
openDrawer({
|
||||||
title: 'Insert a New Row',
|
title: 'Insert a New Row',
|
||||||
payload: {
|
component: (
|
||||||
columns: memoizedColumns,
|
<CreateRecordForm
|
||||||
onSubmit: refetch,
|
// TODO: Create proper typings for data browser columns
|
||||||
},
|
columns={memoizedColumns as unknown as DataBrowserGridColumn[]}
|
||||||
|
onSubmit={refetch}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleInsertColumnClick() {
|
async function handleInsertColumnClick() {
|
||||||
openDrawer('CREATE_COLUMN', {
|
openDrawer({
|
||||||
title: 'Insert a New Column',
|
title: 'Insert a New Column',
|
||||||
payload: {
|
component: <CreateColumnForm onSubmit={refetch} />,
|
||||||
onSubmit: refetch,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEditColumnClick(
|
async function handleEditColumnClick(
|
||||||
column: DataBrowserGridColumn<NormalizedQueryDataRow>,
|
column: DataBrowserGridColumn<NormalizedQueryDataRow>,
|
||||||
) {
|
) {
|
||||||
openDrawer('EDIT_COLUMN', {
|
openDrawer({
|
||||||
title: 'Edit Column',
|
title: 'Edit Column',
|
||||||
payload: {
|
component: (
|
||||||
column,
|
<EditColumnForm
|
||||||
onSubmit: () => queryClient.refetchQueries([currentTablePath]),
|
column={column}
|
||||||
},
|
onSubmit={() => queryClient.refetchQueries([currentTablePath])}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
|
||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
import NavLink from '@/components/common/NavLink';
|
import NavLink from '@/components/common/NavLink';
|
||||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||||
@@ -31,11 +32,36 @@ import Select from '@/ui/v2/Select';
|
|||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
|
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
const CreateTableForm = dynamic(
|
||||||
|
() => import('@/components/dataBrowser/CreateTableForm'),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <FormActivityIndicator />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const EditTableForm = dynamic(
|
||||||
|
() => import('@/components/dataBrowser/EditTableForm'),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <FormActivityIndicator />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const EditPermissionsForm = dynamic(
|
||||||
|
() => import('@/components/dataBrowser/EditPermissionsForm'),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <FormActivityIndicator />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> {
|
export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> {
|
||||||
/**
|
/**
|
||||||
* Function to be called when a sidebar item is clicked.
|
* Function to be called when a sidebar item is clicked.
|
||||||
@@ -200,7 +226,7 @@ function DataBrowserSidebarContent({
|
|||||||
table: string,
|
table: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
) {
|
) {
|
||||||
openDrawer('EDIT_PERMISSIONS', {
|
openDrawer({
|
||||||
title: (
|
title: (
|
||||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||||
Permissions
|
Permissions
|
||||||
@@ -208,22 +234,18 @@ function DataBrowserSidebarContent({
|
|||||||
<Chip label="Preview" size="small" color="info" component="span" />
|
<Chip label="Preview" size="small" color="info" component="span" />
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
|
component: (
|
||||||
|
<EditPermissionsForm
|
||||||
|
disabled={disabled}
|
||||||
|
schema={schema}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
),
|
||||||
props: {
|
props: {
|
||||||
PaperProps: {
|
PaperProps: {
|
||||||
className: 'lg:w-[65%] lg:max-w-7xl',
|
className: 'lg:w-[65%] lg:max-w-7xl',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
payload: {
|
|
||||||
onSubmit: async () => {
|
|
||||||
await queryClient.refetchQueries([
|
|
||||||
`${dataSourceSlug}.${schema}.${table}`,
|
|
||||||
]);
|
|
||||||
await refetch();
|
|
||||||
},
|
|
||||||
disabled,
|
|
||||||
schema,
|
|
||||||
table,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,9 +318,11 @@ function DataBrowserSidebarContent({
|
|||||||
endIcon={<PlusIcon />}
|
endIcon={<PlusIcon />}
|
||||||
className="mt-1 w-full justify-between px-2"
|
className="mt-1 w-full justify-between px-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openDrawer('CREATE_TABLE', {
|
openDrawer({
|
||||||
title: 'Create a New Table',
|
title: 'Create a New Table',
|
||||||
payload: { onSubmit: refetch, schema: selectedSchema },
|
component: (
|
||||||
|
<CreateTableForm onSubmit={refetch} schema={selectedSchema} />
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
onSidebarItemClick();
|
onSidebarItemClick();
|
||||||
@@ -328,69 +352,68 @@ function DataBrowserSidebarContent({
|
|||||||
className="group"
|
className="group"
|
||||||
key={tablePath}
|
key={tablePath}
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
!isSelectedSchemaLocked && (
|
<Dropdown.Root
|
||||||
<Dropdown.Root
|
id="table-management-menu"
|
||||||
id="table-management-menu"
|
onOpen={() => setSidebarMenuTable(tablePath)}
|
||||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
onClose={() => setSidebarMenuTable(undefined)}
|
||||||
onClose={() => setSidebarMenuTable(undefined)}
|
>
|
||||||
|
<Dropdown.Trigger
|
||||||
|
asChild
|
||||||
|
hideChevron
|
||||||
|
disabled={tablePath === removableTable}
|
||||||
>
|
>
|
||||||
<Dropdown.Trigger
|
<IconButton
|
||||||
asChild
|
variant="borderless"
|
||||||
hideChevron
|
color={isSelected ? 'primary' : 'secondary'}
|
||||||
disabled={tablePath === removableTable}
|
className={twMerge(
|
||||||
|
!isSelected &&
|
||||||
|
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<IconButton
|
<DotsHorizontalIcon />
|
||||||
variant="borderless"
|
</IconButton>
|
||||||
color={isSelected ? 'primary' : 'secondary'}
|
</Dropdown.Trigger>
|
||||||
className={twMerge(
|
|
||||||
!isSelected &&
|
<Dropdown.Content menu PaperProps={{ className: 'w-52' }}>
|
||||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
{isGitHubConnected ? (
|
||||||
)}
|
<Dropdown.Item
|
||||||
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
|
onClick={() =>
|
||||||
|
handleEditPermissionClick(
|
||||||
|
table.table_schema,
|
||||||
|
table.table_name,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DotsHorizontalIcon />
|
<UsersIcon
|
||||||
</IconButton>
|
className="h-4 w-4"
|
||||||
</Dropdown.Trigger>
|
sx={{ color: 'text.secondary' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<Dropdown.Content
|
<span>View Permissions</span>
|
||||||
menu
|
</Dropdown.Item>
|
||||||
PaperProps={{ className: 'w-52' }}
|
) : (
|
||||||
>
|
[
|
||||||
{isGitHubConnected ? (
|
!isSelectedSchemaLocked && (
|
||||||
<Dropdown.Item
|
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
|
||||||
onClick={() =>
|
|
||||||
handleEditPermissionClick(
|
|
||||||
table.table_schema,
|
|
||||||
table.table_name,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<UsersIcon
|
|
||||||
className="h-4 w-4"
|
|
||||||
sx={{ color: 'text.secondary' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span>View Permissions</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
) : (
|
|
||||||
[
|
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="edit-table"
|
key="edit-table"
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openDrawer('EDIT_TABLE', {
|
openDrawer({
|
||||||
title: 'Edit Table',
|
title: 'Edit Table',
|
||||||
payload: {
|
component: (
|
||||||
onSubmit: async () => {
|
<EditTableForm
|
||||||
await queryClient.refetchQueries([
|
onSubmit={async () => {
|
||||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
await queryClient.refetchQueries([
|
||||||
]);
|
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||||
await refetch();
|
]);
|
||||||
},
|
await refetch();
|
||||||
schema: table.table_schema,
|
}}
|
||||||
table,
|
schema={table.table_schema}
|
||||||
},
|
table={table}
|
||||||
|
/>
|
||||||
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -400,32 +423,38 @@ function DataBrowserSidebarContent({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span>Edit Table</span>
|
<span>Edit Table</span>
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>
|
||||||
|
),
|
||||||
|
!isSelectedSchemaLocked && (
|
||||||
<Divider
|
<Divider
|
||||||
key="edit-table-separator"
|
key="edit-table-separator"
|
||||||
component="li"
|
component="li"
|
||||||
/>,
|
/>
|
||||||
<Dropdown.Item
|
),
|
||||||
key="edit-permissions"
|
<Dropdown.Item
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
key="edit-permissions"
|
||||||
onClick={() =>
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
handleEditPermissionClick(
|
onClick={() =>
|
||||||
table.table_schema,
|
handleEditPermissionClick(
|
||||||
table.table_name,
|
table.table_schema,
|
||||||
)
|
table.table_name,
|
||||||
}
|
)
|
||||||
>
|
}
|
||||||
<UsersIcon
|
>
|
||||||
className="h-4 w-4"
|
<UsersIcon
|
||||||
sx={{ color: 'text.secondary' }}
|
className="h-4 w-4"
|
||||||
/>
|
sx={{ color: 'text.secondary' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<span>Edit Permissions</span>
|
<span>Edit Permissions</span>
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>,
|
||||||
|
!isSelectedSchemaLocked && (
|
||||||
<Divider
|
<Divider
|
||||||
key="edit-permissions-separator"
|
key="edit-permissions-separator"
|
||||||
component="li"
|
component="li"
|
||||||
/>,
|
/>
|
||||||
|
),
|
||||||
|
!isSelectedSchemaLocked && (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="delete-table"
|
key="delete-table"
|
||||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||||
@@ -443,12 +472,12 @@ function DataBrowserSidebarContent({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span>Delete Table</span>
|
<span>Delete Table</span>
|
||||||
</Dropdown.Item>,
|
</Dropdown.Item>
|
||||||
]
|
),
|
||||||
)}
|
]
|
||||||
</Dropdown.Content>
|
)}
|
||||||
</Dropdown.Root>
|
</Dropdown.Content>
|
||||||
)
|
</Dropdown.Root>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem.Button
|
<ListItem.Button
|
||||||
@@ -518,7 +547,7 @@ export default function DataBrowserSidebar({
|
|||||||
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
|
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isPlatform && !currentApplication?.hasuraGraphqlAdminSecret) {
|
if (isPlatform && !currentApplication?.config?.hasura.adminSecret) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useRouter } from 'next/router';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface EditColumnFormProps
|
export interface EditColumnFormProps
|
||||||
extends Pick<BaseColumnFormProps, 'onCancel'> {
|
extends Pick<BaseColumnFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Column to be edited.
|
* Column to be edited.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import { useState } from 'react';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface EditForeignKeyFormProps
|
export interface EditForeignKeyFormProps
|
||||||
extends Pick<BaseForeignKeyFormProps, 'onCancel' | 'availableColumns'> {
|
extends Pick<
|
||||||
|
BaseForeignKeyFormProps,
|
||||||
|
'onCancel' | 'availableColumns' | 'location'
|
||||||
|
> {
|
||||||
/**
|
/**
|
||||||
* Foreign key relation to be edited.
|
* Foreign key relation to be edited.
|
||||||
*/
|
*/
|
||||||
@@ -26,7 +29,7 @@ export interface EditForeignKeyFormProps
|
|||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EditForeignKeyForm({
|
export default function EditForeignKeyForm({
|
||||||
@@ -57,9 +60,7 @@ export default function EditForeignKeyForm({
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (onSubmit) {
|
await onSubmit?.(values);
|
||||||
await onSubmit(values);
|
|
||||||
}
|
|
||||||
} catch (submitError) {
|
} catch (submitError) {
|
||||||
if (submitError && submitError instanceof Error) {
|
if (submitError && submitError instanceof Error) {
|
||||||
setError(submitError);
|
setError(submitError);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
|
|||||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type {
|
import type {
|
||||||
DatabaseAccessLevel,
|
DatabaseAccessLevel,
|
||||||
DatabaseAction,
|
DatabaseAction,
|
||||||
@@ -30,7 +31,7 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
import RolePermissionEditorForm from './RolePermissionEditorForm';
|
import RolePermissionEditorForm from './RolePermissionEditorForm';
|
||||||
import RolePermissionsRow from './RolePermissionsRow';
|
import RolePermissionsRow from './RolePermissionsRow';
|
||||||
|
|
||||||
export interface EditPermissionsFormProps {
|
export interface EditPermissionsFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Determines whether the form is disabled or not.
|
* Determines whether the form is disabled or not.
|
||||||
*/
|
*/
|
||||||
@@ -54,6 +55,7 @@ export default function EditPermissionsForm({
|
|||||||
schema,
|
schema,
|
||||||
table,
|
table,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
location,
|
||||||
}: EditPermissionsFormProps) {
|
}: EditPermissionsFormProps) {
|
||||||
const [role, setRole] = useState<string>();
|
const [role, setRole] = useState<string>();
|
||||||
const [action, setAction] = useState<DatabaseAction>();
|
const [action, setAction] = useState<DatabaseAction>();
|
||||||
@@ -181,6 +183,7 @@ export default function EditPermissionsForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RolePermissionEditorForm
|
<RolePermissionEditorForm
|
||||||
|
location={location}
|
||||||
resourceVersion={metadata?.resourceVersion}
|
resourceVersion={metadata?.resourceVersion}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import HighlightedText from '@/components/common/HighlightedText';
|
import HighlightedText from '@/components/common/HighlightedText';
|
||||||
import useManagePermissionMutation from '@/hooks/dataBrowser/useManagePermissionMutation';
|
import useManagePermissionMutation from '@/hooks/dataBrowser/useManagePermissionMutation';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import type {
|
import type {
|
||||||
DatabaseAction,
|
DatabaseAction,
|
||||||
HasuraMetadataPermission,
|
HasuraMetadataPermission,
|
||||||
@@ -13,6 +14,7 @@ import Button from '@/ui/v2/Button';
|
|||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import convertToHasuraPermissions from '@/utils/dataBrowser/convertToHasuraPermissions';
|
import convertToHasuraPermissions from '@/utils/dataBrowser/convertToHasuraPermissions';
|
||||||
import convertToRuleGroup from '@/utils/dataBrowser/convertToRuleGroup';
|
import convertToRuleGroup from '@/utils/dataBrowser/convertToRuleGroup';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -72,7 +74,7 @@ export interface RolePermissionEditorFormValues {
|
|||||||
computedFields?: string[];
|
computedFields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RolePermissionEditorFormProps {
|
export interface RolePermissionEditorFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Determines whether or not the form is disabled.
|
* Determines whether or not the form is disabled.
|
||||||
*/
|
*/
|
||||||
@@ -169,6 +171,7 @@ export default function RolePermissionEditorForm({
|
|||||||
onCancel,
|
onCancel,
|
||||||
permission,
|
permission,
|
||||||
disabled,
|
disabled,
|
||||||
|
location,
|
||||||
}: RolePermissionEditorFormProps) {
|
}: RolePermissionEditorFormProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const {
|
const {
|
||||||
@@ -214,8 +217,8 @@ export default function RolePermissionEditorForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'drawer');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
async function handleSubmit(values: RolePermissionEditorFormValues) {
|
async function handleSubmit(values: RolePermissionEditorFormValues) {
|
||||||
const managePermissionPromise = managePermission({
|
const managePermissionPromise = managePermission({
|
||||||
@@ -245,7 +248,7 @@ export default function RolePermissionEditorForm({
|
|||||||
: permission?.check,
|
: permission?.check,
|
||||||
backend_only: values.backendOnly,
|
backend_only: values.backendOnly,
|
||||||
computed_fields:
|
computed_fields:
|
||||||
permission?.computed_fields.length > 0
|
permission?.computed_fields?.length > 0
|
||||||
? permission?.computed_fields
|
? permission?.computed_fields
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
@@ -256,12 +259,12 @@ export default function RolePermissionEditorForm({
|
|||||||
{
|
{
|
||||||
loading: 'Saving permission...',
|
loading: 'Saving permission...',
|
||||||
success: 'Permission has been saved successfully.',
|
success: 'Permission has been saved successfully.',
|
||||||
error: 'An error occurred while saving the permission.',
|
error: getServerError('An error occurred while saving the permission.'),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|
||||||
onDirtyStateChange(false, 'drawer');
|
onDirtyStateChange(false, location);
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +273,7 @@ export default function RolePermissionEditorForm({
|
|||||||
openDirtyConfirmation({
|
openDirtyConfirmation({
|
||||||
props: {
|
props: {
|
||||||
onPrimaryAction: () => {
|
onPrimaryAction: () => {
|
||||||
onDirtyStateChange(false, 'drawer');
|
onDirtyStateChange(false, location);
|
||||||
onCancel?.();
|
onCancel?.();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -295,12 +298,14 @@ export default function RolePermissionEditorForm({
|
|||||||
{
|
{
|
||||||
loading: 'Deleting permission...',
|
loading: 'Deleting permission...',
|
||||||
success: 'Permission has been deleted successfully.',
|
success: 'Permission has been deleted successfully.',
|
||||||
error: 'An error occurred while deleting the permission.',
|
error: getServerError(
|
||||||
|
'An error occurred while deleting the permission.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|
||||||
onDirtyStateChange(false, 'drawer');
|
onDirtyStateChange(false, location);
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import XIcon from '@/ui/v2/icons/XIcon';
|
|||||||
import InputLabel from '@/ui/v2/InputLabel';
|
import InputLabel from '@/ui/v2/InputLabel';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
import { useGetRolesPermissionsQuery } from '@/utils/__generated__/graphql';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||||
@@ -51,8 +51,8 @@ export default function ColumnPresetsSection({
|
|||||||
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
||||||
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data: customClaimsData } = useGetAppCustomClaimsQuery({
|
const { data: permissionVariablesData } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
skip: !currentApplication?.id,
|
skip: !currentApplication?.id,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
@@ -74,8 +74,8 @@ export default function ColumnPresetsSection({
|
|||||||
throw tableError;
|
throw tableError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionVariableOptions = getPermissionVariablesArray(
|
const permissionVariableOptions = getAllPermissionVariables(
|
||||||
customClaimsData?.app?.authJwtCustomClaims,
|
permissionVariablesData?.config?.auth?.session?.accessToken?.customClaims,
|
||||||
).map(({ key }) => ({
|
).map(({ key }) => ({
|
||||||
label: `X-Hasura-${key}`,
|
label: `X-Hasura-${key}`,
|
||||||
value: `X-Hasura-${key}`,
|
value: `X-Hasura-${key}`,
|
||||||
@@ -136,7 +136,7 @@ export default function ColumnPresetsSection({
|
|||||||
disableClearable={false}
|
disableClearable={false}
|
||||||
clearIcon={
|
clearIcon={
|
||||||
<XIcon
|
<XIcon
|
||||||
className="w-4 h-4 mt-px"
|
className="mt-px h-4 w-4"
|
||||||
sx={{ color: theme.palette.text.primary }}
|
sx={{ color: theme.palette.text.primary }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ export default function ColumnPresetsSection({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="shrink-0 grow-0 flex-[40px]"
|
className="flex-[40px] shrink-0 grow-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (fields.length === 1) {
|
if (fields.length === 1) {
|
||||||
remove(index);
|
remove(index);
|
||||||
@@ -199,7 +199,7 @@ export default function ColumnPresetsSection({
|
|||||||
remove(index);
|
remove(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XIcon className="w-4 h-4" />
|
<XIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Input from '@/ui/v2/Input';
|
|||||||
import Radio from '@/ui/v2/Radio';
|
import Radio from '@/ui/v2/Radio';
|
||||||
import RadioGroup from '@/ui/v2/RadioGroup';
|
import RadioGroup from '@/ui/v2/RadioGroup';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import type { FocusEvent } from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import PermissionSettingsSection from './PermissionSettingsSection';
|
import PermissionSettingsSection from './PermissionSettingsSection';
|
||||||
@@ -130,7 +131,13 @@ export default function RowPermissionsSection({
|
|||||||
|
|
||||||
{action === 'select' && (
|
{action === 'select' && (
|
||||||
<Input
|
<Input
|
||||||
{...register('limit')}
|
{...register('limit', {
|
||||||
|
onBlur: (event: FocusEvent<HTMLInputElement>) => {
|
||||||
|
if (!event.target.value) {
|
||||||
|
setValue('limit', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id="limit"
|
id="limit"
|
||||||
type="number"
|
type="number"
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ const baseValidationSchema = Yup.object().shape({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectValidationSchema = baseValidationSchema.shape({
|
const selectValidationSchema = baseValidationSchema.shape({
|
||||||
limit: Yup.number().min(0, 'Limit must not be negative.').nullable(true),
|
limit: Yup.number()
|
||||||
|
.label('Limit')
|
||||||
|
.min(0, 'Limit must not be negative.')
|
||||||
|
.nullable(true),
|
||||||
allowAggregations: Yup.boolean().nullable(true),
|
allowAggregations: Yup.boolean().nullable(true),
|
||||||
queryRootFields: Yup.array().of(Yup.string()).nullable(true),
|
queryRootFields: Yup.array().of(Yup.string()).nullable(true),
|
||||||
subscriptionRootFields: Yup.array().of(Yup.string()).nullable(true),
|
subscriptionRootFields: Yup.array().of(Yup.string()).nullable(true),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
export interface EditTableFormProps
|
export interface EditTableFormProps
|
||||||
extends Pick<BaseTableFormProps, 'onCancel'> {
|
extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Schema where the table is located.
|
* Schema where the table is located.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Form from '@/components/common/Form';
|
|||||||
import type { RuleGroup } from '@/types/dataBrowser';
|
import type { RuleGroup } from '@/types/dataBrowser';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
import permissionVariablesQuery from '@/utils/msw/mocks/graphql/permissionVariablesQuery';
|
||||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
@@ -36,7 +36,7 @@ const defaultParameters = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [tableQuery, hasuraMetadataQuery, customClaimsQuery],
|
handlers: [tableQuery, hasuraMetadataQuery, permissionVariablesQuery],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { HasuraOperator } from '@/types/dataBrowser';
|
import type { HasuraOperator } from '@/types/dataBrowser';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||||
import type { InputProps } from '@/ui/v2/Input';
|
import type { InputProps } from '@/ui/v2/Input';
|
||||||
import { inputClasses } from '@/ui/v2/Input';
|
import { inputClasses } from '@/ui/v2/Input';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
import { useGetRolesPermissionsQuery } from '@/utils/__generated__/graphql';
|
||||||
import { useController, useFormContext, useWatch } from 'react-hook-form';
|
import { useController, useFormContext, useWatch } from 'react-hook-form';
|
||||||
import useRuleGroupEditor from './useRuleGroupEditor';
|
import useRuleGroupEditor from './useRuleGroupEditor';
|
||||||
|
|
||||||
@@ -116,8 +117,8 @@ export default function RuleValueInput({
|
|||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
error: customClaimsError,
|
error: customClaimsError,
|
||||||
} = useGetAppCustomClaimsQuery({
|
} = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
skip: !isHasuraInput || !currentApplication?.id,
|
skip: !isHasuraInput || !currentApplication?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,8 +200,8 @@ export default function RuleValueInput({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableHasuraPermissionVariables = getPermissionVariablesArray(
|
const availableHasuraPermissionVariables = getAllPermissionVariables(
|
||||||
data?.app?.authJwtCustomClaims,
|
data?.config?.auth?.session?.accessToken?.customClaims,
|
||||||
).map(({ key }) => ({
|
).map(({ key }) => ({
|
||||||
value: `X-Hasura-${key}`,
|
value: `X-Hasura-${key}`,
|
||||||
label: `X-Hasura-${key}`,
|
label: `X-Hasura-${key}`,
|
||||||
@@ -211,12 +212,13 @@ export default function RuleValueInput({
|
|||||||
<ControlledAutocomplete
|
<ControlledAutocomplete
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
freeSolo={!isHasuraInput}
|
freeSolo={!isHasuraInput}
|
||||||
autoSelect={!isHasuraInput}
|
|
||||||
autoHighlight={isHasuraInput}
|
autoHighlight={isHasuraInput}
|
||||||
open
|
isOptionEqualToValue={(
|
||||||
isOptionEqualToValue={(option, value) => {
|
option,
|
||||||
if (typeof value === 'string') {
|
value: string | number | AutocompleteOption<string>,
|
||||||
return option.value.toLowerCase() === (value as string).toLowerCase();
|
) => {
|
||||||
|
if (typeof value !== 'object') {
|
||||||
|
return option.value.toLowerCase() === value?.toString().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
return option.value.toLowerCase() === value.value.toLowerCase();
|
return option.value.toLowerCase() === value.value.toLowerCase();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
|
|||||||
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Tooltip from '@/ui/v2/Tooltip';
|
import Tooltip from '@/ui/v2/Tooltip';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
||||||
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
|
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
|
||||||
@@ -122,7 +123,9 @@ export default function DeploymentListItem({
|
|||||||
{
|
{
|
||||||
loading: 'Scheduling deployment...',
|
loading: 'Scheduling deployment...',
|
||||||
success: 'Deployment has been scheduled successfully.',
|
success: 'Deployment has been scheduled successfully.',
|
||||||
error: 'An error occurred when scheduling deployment.',
|
error: getServerError(
|
||||||
|
'An error occurred when scheduling deployment.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { Deployment } from '@/types/application';
|
||||||
|
import { render, screen } from '@/utils/testUtils';
|
||||||
|
import { test, vi } from 'vitest';
|
||||||
|
import DeploymentStatusMessage from './DeploymentStatusMessage';
|
||||||
|
|
||||||
|
const defaultDeployment: Deployment = {
|
||||||
|
id: 'de305d54-75b4-431b-adb2-eb6b9e546013',
|
||||||
|
commitUserName: 'john.doe',
|
||||||
|
commitUserAvatarUrl: 'https://example.com/avatar.png',
|
||||||
|
deploymentStartedAt: '2023-02-24T12:00:00.000Z',
|
||||||
|
deploymentEndedAt: null,
|
||||||
|
deploymentStatus: 'SCHEDULED',
|
||||||
|
commitSHA: '1234567890',
|
||||||
|
commitMessage: 'Update README.md',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render the avatar of the user who deployed the application', () => {
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={defaultDeployment}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('img', {
|
||||||
|
name: `Avatar of ${defaultDeployment.commitUserName}`,
|
||||||
|
}),
|
||||||
|
).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
`background-image: url(${defaultDeployment.commitUserAvatarUrl});`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "updated just now" when the deployment is in progress and has not ended', () => {
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={defaultDeployment}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/updated just now/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "updated just now" when the deployment\'s status is DEPLOYED, but it doesn\'t have an end date for some reason', () => {
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={{
|
||||||
|
...defaultDeployment,
|
||||||
|
deploymentStatus: 'DEPLOYED',
|
||||||
|
deploymentEndedAt: null,
|
||||||
|
}}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/updated just now/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "deployed 1 day ago" when the deployment has ended', () => {
|
||||||
|
vi.setSystemTime(new Date('2023-02-25T12:25:00.000Z'));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={{
|
||||||
|
...defaultDeployment,
|
||||||
|
deploymentStatus: 'DEPLOYED',
|
||||||
|
deploymentEndedAt: '2023-02-24T12:15:00.000Z',
|
||||||
|
}}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/deployed 1 day ago/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "created 1 day ago" if the application does not have a deployment', () => {
|
||||||
|
vi.setSystemTime(new Date('2023-02-25T12:25:00.000Z'));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage deployment={null} appCreatedAt="2023-02-24" />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/created 1 day ago/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Deployment } from '@/types/application';
|
||||||
|
import { Avatar } from '@/ui/Avatar';
|
||||||
|
import Text from '@/ui/v2/Text';
|
||||||
|
import formatDistance from 'date-fns/formatDistance';
|
||||||
|
|
||||||
|
export interface DeploymentStatusMessageProps {
|
||||||
|
/**
|
||||||
|
* The deployment to render the status message for.
|
||||||
|
*/
|
||||||
|
deployment: Partial<Deployment>;
|
||||||
|
/**
|
||||||
|
* The date the application was created.
|
||||||
|
*/
|
||||||
|
appCreatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeploymentStatusMessage({
|
||||||
|
deployment,
|
||||||
|
appCreatedAt,
|
||||||
|
}: DeploymentStatusMessageProps) {
|
||||||
|
const isDeployingToProduction = [
|
||||||
|
'SCHEDULED',
|
||||||
|
'PENDING',
|
||||||
|
'DEPLOYING',
|
||||||
|
].includes(deployment?.deploymentStatus);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDeployingToProduction ||
|
||||||
|
(deployment && !deployment.deploymentEndedAt)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span className="flex flex-row">
|
||||||
|
<Avatar
|
||||||
|
component="span"
|
||||||
|
name={deployment.commitUserName}
|
||||||
|
avatarUrl={deployment.commitUserAvatarUrl}
|
||||||
|
className="mr-1 h-4 w-4 self-center"
|
||||||
|
/>
|
||||||
|
<Text component="span" className="self-center text-sm">
|
||||||
|
{deployment.commitUserName} updated just now
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDeployingToProduction && deployment?.deploymentEndedAt) {
|
||||||
|
return (
|
||||||
|
<span className="flex flex-row">
|
||||||
|
<Avatar
|
||||||
|
component="span"
|
||||||
|
name={deployment.commitUserName}
|
||||||
|
avatarUrl={deployment.commitUserAvatarUrl}
|
||||||
|
className="mr-1 h-4 w-4 self-center"
|
||||||
|
/>
|
||||||
|
<Text component="span" className="self-center text-sm">
|
||||||
|
{deployment.commitUserName} deployed{' '}
|
||||||
|
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text component="span" className="text-sm">
|
||||||
|
created{' '}
|
||||||
|
{formatDistance(new Date(appCreatedAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './DeploymentStatusMessage';
|
||||||
|
export { default } from './DeploymentStatusMessage';
|
||||||
@@ -262,7 +262,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
|||||||
.setAdminSecret(
|
.setAdminSecret(
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||||
? 'nhost-admin-secret'
|
? 'nhost-admin-secret'
|
||||||
: currentApplication.hasuraGraphqlAdminSecret,
|
: currentApplication.config?.hasura.adminSecret,
|
||||||
)
|
)
|
||||||
.upload({
|
.upload({
|
||||||
file,
|
file,
|
||||||
@@ -277,7 +277,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileError) {
|
if (fileError) {
|
||||||
throw fileError;
|
throw new Error(fileError.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
|
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function FilesDataGridControls({
|
|||||||
const storageWithAdminSecret = appClient.storage.setAdminSecret(
|
const storageWithAdminSecret = appClient.storage.setAdminSecret(
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||||
? 'nhost-admin-secret'
|
? 'nhost-admin-secret'
|
||||||
: currentApplication.hasuraGraphqlAdminSecret,
|
: currentApplication.config?.hasura.adminSecret,
|
||||||
);
|
);
|
||||||
|
|
||||||
// note: this is not an optimal solution, but we don't have a better way
|
// note: this is not an optimal solution, but we don't have a better way
|
||||||
@@ -120,7 +120,7 @@ export default function FilesDataGridControls({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{numberOfSelectedFiles > 0 ? (
|
{numberOfSelectedFiles > 0 ? (
|
||||||
<div className="mx-auto h-[40px] grid grid-flow-col justify-start items-center gap-2">
|
<div className="mx-auto grid h-[40px] grid-flow-col items-center justify-start gap-2">
|
||||||
<Chip
|
<Chip
|
||||||
color="info"
|
color="info"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { slugifyString } from '@/utils/helpers';
|
import { slugifyString } from '@/utils/helpers';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
refetchGetOneUserQuery,
|
refetchGetOneUserQuery,
|
||||||
@@ -11,11 +14,12 @@ import {
|
|||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useUserData } from '@nhost/nextjs';
|
import { useUserData } from '@nhost/nextjs';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface EditWorkspaceNameFormProps {
|
export interface EditWorkspaceNameFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* The current workspace name if this is an edit operation.
|
* The current workspace name if this is an edit operation.
|
||||||
*/
|
*/
|
||||||
@@ -44,14 +48,7 @@ export interface EditWorkspaceNameFormProps {
|
|||||||
onCancel?: VoidFunction;
|
onCancel?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditWorkspaceNameFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
|
||||||
* New workspace name.
|
|
||||||
*/
|
|
||||||
newWorkspaceName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
|
||||||
newWorkspaceName: Yup.string()
|
newWorkspaceName: Yup.string()
|
||||||
.required('Workspace name is required.')
|
.required('Workspace name is required.')
|
||||||
.min(4, 'The new Workspace name must be at least 4 characters.')
|
.min(4, 'The new Workspace name must be at least 4 characters.')
|
||||||
@@ -71,14 +68,20 @@ const validationSchema = Yup.object().shape({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function EditWorkspaceName({
|
export type EditWorkspaceNameFormValues = Yup.InferType<
|
||||||
|
typeof validationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default function EditWorkspaceNameForm({
|
||||||
disabled,
|
disabled,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
currentWorkspaceName,
|
currentWorkspaceName,
|
||||||
currentWorkspaceId,
|
currentWorkspaceId,
|
||||||
submitButtonText = 'Create',
|
submitButtonText = 'Create',
|
||||||
|
location,
|
||||||
}: EditWorkspaceNameFormProps) {
|
}: EditWorkspaceNameFormProps) {
|
||||||
|
const { onDirtyStateChange } = useDialog();
|
||||||
const currentUser = useUserData();
|
const currentUser = useUserData();
|
||||||
const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
|
const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
|
||||||
const [updateWorkspaceName] = useUpdateWorkspaceMutation({
|
const [updateWorkspaceName] = useUpdateWorkspaceMutation({
|
||||||
@@ -105,6 +108,10 @@ export default function EditWorkspaceName({
|
|||||||
} = form;
|
} = form;
|
||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyStateChange(isDirty, location);
|
||||||
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
newWorkspaceName,
|
newWorkspaceName,
|
||||||
}: EditWorkspaceNameFormValues) {
|
}: EditWorkspaceNameFormValues) {
|
||||||
@@ -112,6 +119,8 @@ export default function EditWorkspaceName({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (currentWorkspaceId) {
|
if (currentWorkspaceId) {
|
||||||
|
onDirtyStateChange(false, location);
|
||||||
|
|
||||||
// In this bit of code we spread the props of the current path (e.g. /workspace/...) and add one key-value pair: `mutating: true`.
|
// In this bit of code we spread the props of the current path (e.g. /workspace/...) and add one key-value pair: `mutating: true`.
|
||||||
// We want to indicate that the currently we're in the process of running a mutation state that will affect the routing behaviour of the website
|
// We want to indicate that the currently we're in the process of running a mutation state that will affect the routing behaviour of the website
|
||||||
// i.e. redirecting to 404 if there's no workspace/project with that slug.
|
// i.e. redirecting to 404 if there's no workspace/project with that slug.
|
||||||
@@ -133,7 +142,9 @@ export default function EditWorkspaceName({
|
|||||||
{
|
{
|
||||||
loading: 'Updating workspace name...',
|
loading: 'Updating workspace name...',
|
||||||
success: 'Workspace name has been updated successfully.',
|
success: 'Workspace name has been updated successfully.',
|
||||||
error: 'An error occurred while updating the workspace name.',
|
error: getServerError(
|
||||||
|
'An error occurred while updating the workspace name.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -160,7 +171,9 @@ export default function EditWorkspaceName({
|
|||||||
{
|
{
|
||||||
loading: 'Creating new workspace...',
|
loading: 'Creating new workspace...',
|
||||||
success: 'The new workspace has been created successfully.',
|
success: 'The new workspace has been created successfully.',
|
||||||
error: 'An error occurred while creating the new workspace.',
|
error: getServerError(
|
||||||
|
'An error occurred while creating the new workspace.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -186,6 +199,9 @@ export default function EditWorkspaceName({
|
|||||||
include: ['getOneUser'],
|
include: ['getOneUser'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The form has been submitted, it's not dirty anymore
|
||||||
|
onDirtyStateChange(false, location);
|
||||||
|
|
||||||
await router.push(slug);
|
await router.push(slug);
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
}
|
}
|
||||||
@@ -194,9 +210,9 @@ export default function EditWorkspaceName({
|
|||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="flex flex-col content-between flex-auto pt-2 pb-6 overflow-hidden"
|
className="flex flex-auto flex-col content-between overflow-hidden pt-2 pb-6"
|
||||||
>
|
>
|
||||||
<div className="flex-auto px-6 overflow-y-auto">
|
<div className="flex-auto overflow-y-auto px-6">
|
||||||
<Input
|
<Input
|
||||||
{...register('newWorkspaceName')}
|
{...register('newWorkspaceName')}
|
||||||
error={Boolean(errors.newWorkspaceName?.message)}
|
error={Boolean(errors.newWorkspaceName?.message)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
|
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
|
||||||
import SearchIcon from '@/ui/v2/icons/SearchIcon';
|
import SearchIcon from '@/ui/v2/icons/SearchIcon';
|
||||||
@@ -11,6 +12,8 @@ interface IndexHeaderAppsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
|
export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mb-6 grid w-full grid-flow-col place-content-between items-center py-2">
|
<div className="mx-auto mb-6 grid w-full grid-flow-col place-content-between items-center py-2">
|
||||||
<Text variant="h2" component="h1" className="hidden md:block">
|
<Text variant="h2" component="h1" className="hidden md:block">
|
||||||
@@ -36,6 +39,7 @@ export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
startIcon={<PlusCircleIcon />}
|
startIcon={<PlusCircleIcon />}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
New Project
|
New Project
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -58,16 +58,19 @@ export function InviteAnnounce() {
|
|||||||
error: null,
|
error: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
const res = await nhost.functions.call('/accept-workspace-invite', {
|
const { res, error: acceptError } = await nhost.functions.call(
|
||||||
workspaceMemberInviteId: invite.id,
|
'/accept-workspace-invite',
|
||||||
isAccepted: true,
|
{
|
||||||
});
|
workspaceMemberInviteId: invite.id,
|
||||||
|
isAccepted: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (res?.res?.status !== 200) {
|
if (res?.status !== 200) {
|
||||||
triggerToast('An error occurred when trying to accept the invitation.');
|
triggerToast('An error occurred when trying to accept the invitation.');
|
||||||
|
|
||||||
return setSubmitState({
|
return setSubmitState({
|
||||||
error: res.error,
|
error: new Error(acceptError.message),
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,7 +93,7 @@ export function InviteAnnounce() {
|
|||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await nhost.functions.call(
|
const { error: ignoreError } = await nhost.functions.call(
|
||||||
'/accept-workspace-invite',
|
'/accept-workspace-invite',
|
||||||
{
|
{
|
||||||
workspaceMemberInviteId: inviteId,
|
workspaceMemberInviteId: inviteId,
|
||||||
@@ -99,12 +102,12 @@ export function InviteAnnounce() {
|
|||||||
{ useAxios: false },
|
{ useAxios: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res?.error) {
|
if (ignoreError) {
|
||||||
triggerToast('An error occurred when trying to ignore the invitation.');
|
triggerToast('An error occurred when trying to ignore the invitation.');
|
||||||
|
|
||||||
setIgnoreState({
|
setIgnoreState({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: new Error(res.error.message),
|
error: new Error(ignoreError.message),
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { UserDataProvider } from '@/context/workspace1-context';
|
import { UserDataProvider } from '@/context/workspace1-context';
|
||||||
import type { Application } from '@/types/application';
|
import type { Project } from '@/types/application';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import type { Workspace } from '@/types/workspace';
|
import type { Workspace } from '@/types/workspace';
|
||||||
import { render, screen, waitForElementToBeRemoved } from '@/utils/testUtils';
|
import { render, screen, waitForElementToBeRemoved } from '@/utils/testUtils';
|
||||||
@@ -36,12 +36,11 @@ vi.mock('next/router', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockApplication: Application = {
|
const mockApplication: Project = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Test Application',
|
name: 'Test Application',
|
||||||
slug: 'test-application',
|
slug: 'test-application',
|
||||||
appStates: [],
|
appStates: [],
|
||||||
hasuraGraphqlAdminSecret: 'nhost-admin-secret',
|
|
||||||
subdomain: '',
|
subdomain: '',
|
||||||
isProvisioned: true,
|
isProvisioned: true,
|
||||||
region: {
|
region: {
|
||||||
@@ -56,6 +55,14 @@ const mockApplication: Application = {
|
|||||||
featureFlags: [],
|
featureFlags: [],
|
||||||
providersUpdated: true,
|
providersUpdated: true,
|
||||||
githubRepository: { fullName: 'test/git-project' },
|
githubRepository: { fullName: 'test/git-project' },
|
||||||
|
repositoryProductionBranch: null,
|
||||||
|
nhostBaseFolder: null,
|
||||||
|
plan: null,
|
||||||
|
config: {
|
||||||
|
hasura: {
|
||||||
|
adminSecret: 'nhost-admin-secret',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockWorkspace: Workspace = {
|
const mockWorkspace: Workspace = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import useGitHubModal from '@/components/applications/github/useGitHubModal';
|
import useGitHubModal from '@/components/applications/github/useGitHubModal';
|
||||||
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
|
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
|
||||||
import GithubIcon from '@/components/icons/GithubIcon';
|
import GithubIcon from '@/components/icons/GithubIcon';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
@@ -146,6 +147,7 @@ function OverviewDeploymentList() {
|
|||||||
export default function OverviewDeployments() {
|
export default function OverviewDeployments() {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { openGitHubModal } = useGitHubModal();
|
const { openGitHubModal } = useGitHubModal();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
const { githubRepository } = currentApplication || {};
|
const { githubRepository } = currentApplication || {};
|
||||||
|
|
||||||
@@ -183,6 +185,7 @@ export default function OverviewDeployments() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={openGitHubModal}
|
onClick={openGitHubModal}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
<GithubIcon className="mr-1.5 h-4 w-4 self-center" />
|
<GithubIcon className="mr-1.5 h-4 w-4 self-center" />
|
||||||
Connect to GitHub
|
Connect to GitHub
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import GithubIcon from '@/components/icons/GithubIcon';
|
import GithubIcon from '@/components/icons/GithubIcon';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -8,6 +9,7 @@ import NavLink from 'next/link';
|
|||||||
export default function OverviewRepository() {
|
export default function OverviewRepository() {
|
||||||
const { currentWorkspace, currentApplication } =
|
const { currentWorkspace, currentApplication } =
|
||||||
useCurrentWorkspaceAndApplication();
|
useCurrentWorkspaceAndApplication();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -28,6 +30,7 @@ export default function OverviewRepository() {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
className="w-full border-1 hover:border-1"
|
className="w-full border-1 hover:border-1"
|
||||||
startIcon={<GithubIcon />}
|
startIcon={<GithubIcon />}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Connect to GitHub
|
Connect to GitHub
|
||||||
</Button>
|
</Button>
|
||||||
@@ -39,7 +42,7 @@ export default function OverviewRepository() {
|
|||||||
sx={{ backgroundColor: 'grey.200' }}
|
sx={{ backgroundColor: 'grey.200' }}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="grid grid-flow-col gap-1.5 ml-2"
|
className="ml-2 grid grid-flow-col gap-1.5"
|
||||||
sx={{ backgroundColor: 'transparent' }}
|
sx={{ backgroundColor: 'transparent' }}
|
||||||
>
|
>
|
||||||
<GithubIcon className="h-4 w-4 self-center" />
|
<GithubIcon className="h-4 w-4 self-center" />
|
||||||
@@ -52,7 +55,11 @@ export default function OverviewRepository() {
|
|||||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
|
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
|
||||||
passHref
|
passHref
|
||||||
>
|
>
|
||||||
<Button variant="borderless" size="small">
|
<Button
|
||||||
|
variant="borderless"
|
||||||
|
size="small"
|
||||||
|
disabled={maintenanceActive}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
|
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
|
||||||
|
|
||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -16,6 +17,7 @@ export default function OverviewTopBar() {
|
|||||||
useCurrentWorkspaceAndApplication();
|
useCurrentWorkspaceAndApplication();
|
||||||
const isPro = !currentApplication?.plan?.isFree;
|
const isPro = !currentApplication?.plan?.isFree;
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
if (!isPlatform) {
|
if (!isPlatform) {
|
||||||
return (
|
return (
|
||||||
@@ -104,6 +106,7 @@ export default function OverviewTopBar() {
|
|||||||
endIcon={<CogIcon className="h-4 w-4" />}
|
endIcon={<CogIcon className="h-4 w-4" />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useResetPostgresPasswordMutation,
|
useResetPostgresPasswordMutation,
|
||||||
useUpdateApplicationMutation,
|
useUpdateApplicationMutation,
|
||||||
@@ -28,6 +29,7 @@ export interface ResetDatabasePasswordFormValues {
|
|||||||
|
|
||||||
export default function ResetDatabasePasswordSettings() {
|
export default function ResetDatabasePasswordSettings() {
|
||||||
const [updateApplication] = useUpdateApplicationMutation();
|
const [updateApplication] = useUpdateApplicationMutation();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
const form = useForm<ResetDatabasePasswordFormValues>({
|
const form = useForm<ResetDatabasePasswordFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
@@ -44,11 +46,10 @@ export default function ResetDatabasePasswordSettings() {
|
|||||||
setValue,
|
setValue,
|
||||||
getValues,
|
getValues,
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors, isDirty, isSubmitting },
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const [resetPostgresPasswordMutation, { loading }] =
|
const [resetPostgresPasswordMutation] = useResetPostgresPasswordMutation();
|
||||||
useResetPostgresPasswordMutation();
|
|
||||||
const user = useUserData();
|
const user = useUserData();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
@@ -99,12 +100,16 @@ export default function ResetDatabasePasswordSettings() {
|
|||||||
title="Reset Password"
|
title="Reset Password"
|
||||||
description="This password is used for accessing your database."
|
description="This password is used for accessing your database."
|
||||||
submitButtonText="Reset"
|
submitButtonText="Reset"
|
||||||
rootClassName="border-[#F87171]"
|
slotProps={{
|
||||||
primaryActionButtonProps={{
|
root: {
|
||||||
variant: 'contained',
|
sx: { borderColor: (theme) => theme.palette.error.main },
|
||||||
color: 'error',
|
},
|
||||||
disabled: Boolean(errors?.databasePassword),
|
submitButton: {
|
||||||
loading,
|
variant: 'contained',
|
||||||
|
color: 'error',
|
||||||
|
disabled: !isDirty || maintenanceActive,
|
||||||
|
loading: isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
className="grid grid-flow-row pb-4"
|
className="grid grid-flow-row pb-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ export default function SettingsSidebar({
|
|||||||
>
|
>
|
||||||
Environment Variables
|
Environment Variables
|
||||||
</SettingsNavLink>
|
</SettingsNavLink>
|
||||||
|
|
||||||
|
<SettingsNavLink
|
||||||
|
href="/secrets"
|
||||||
|
exact={false}
|
||||||
|
onClick={handleSelect}
|
||||||
|
className="hidden"
|
||||||
|
>
|
||||||
|
Secrets
|
||||||
|
</SettingsNavLink>
|
||||||
</List>
|
</List>
|
||||||
</nav>
|
</nav>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,68 +1,61 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface AllowedEmailSettingsFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* Determines whether or not the allowed email settings are enabled.
|
allowedEmails: Yup.string().label('Allowed Emails'),
|
||||||
*/
|
allowedEmailDomains: Yup.string().label('Allowed Email Domains'),
|
||||||
enabled: boolean;
|
});
|
||||||
/**
|
|
||||||
* Set of email that are allowed to be used for project's users authentication.
|
export type AllowedEmailSettingsFormValues = Yup.InferType<
|
||||||
*/
|
typeof validationSchema
|
||||||
authAccessControlAllowedEmails: string;
|
>;
|
||||||
/**
|
|
||||||
* Set of email domains that are allowed to be used for project's users authentication.
|
|
||||||
* @example 'nhost.io'
|
|
||||||
*/
|
|
||||||
authAccessControlAllowedEmailDomains: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AllowedEmailDomainsSettings() {
|
export default function AllowedEmailDomainsSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { email, emailDomains } = data?.config?.auth?.user || {};
|
||||||
|
|
||||||
const form = useForm<AllowedEmailSettingsFormValues>({
|
const form = useForm<AllowedEmailSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enabled:
|
enabled: email?.allowed?.length > 0 || emailDomains?.allowed?.length > 0,
|
||||||
Boolean(data?.app?.authAccessControlAllowedEmails) ||
|
allowedEmails: email?.allowed?.join(', ') || '',
|
||||||
Boolean(data?.app?.authAccessControlAllowedEmailDomains),
|
allowedEmailDomains: emailDomains?.allowed?.join(', ') || '',
|
||||||
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
|
|
||||||
authAccessControlAllowedEmailDomains:
|
|
||||||
data?.app?.authAccessControlAllowedEmailDomains,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, formState, setValue, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const enabled = watch('enabled');
|
const enabled = watch('enabled');
|
||||||
|
|
||||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!data.app?.authAccessControlAllowedEmails &&
|
|
||||||
!data.app?.authAccessControlAllowedEmailDomains
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue('enabled', true, { shouldDirty: false });
|
|
||||||
}, [data.app, setValue]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -80,29 +73,51 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
const handleAllowedEmailDomainsChange = async (
|
const handleAllowedEmailDomainsChange = async (
|
||||||
values: AllowedEmailSettingsFormValues,
|
values: AllowedEmailSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authAccessControlAllowedEmails: values.enabled
|
auth: {
|
||||||
? values.authAccessControlAllowedEmails
|
user: {
|
||||||
: '',
|
email: {
|
||||||
authAccessControlAllowedEmailDomains: values.enabled
|
blocked: email.blocked,
|
||||||
? values.authAccessControlAllowedEmailDomains
|
allowed:
|
||||||
: '',
|
values.enabled && values.allowedEmails
|
||||||
|
? values.allowedEmails
|
||||||
|
.split(',')
|
||||||
|
.map((allowedEmail) => allowedEmail.trim())
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
emailDomains: {
|
||||||
|
blocked: emailDomains.blocked,
|
||||||
|
allowed:
|
||||||
|
values.enabled && values.allowedEmailDomains
|
||||||
|
? values.allowedEmailDomains
|
||||||
|
.split(',')
|
||||||
|
.map((allowedEmailDomain) => allowedEmailDomain.trim())
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Allowed email settings are being updated...`,
|
{
|
||||||
success: `Allowed email settings have been updated successfully.`,
|
loading: `Allowed email settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's allowed email settings.`,
|
success: `Allowed email settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's allowed email settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error
|
||||||
|
}
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
};
|
};
|
||||||
@@ -115,15 +130,12 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
description="Allow specific email addresses and domains to sign up."
|
description="Allow specific email addresses and domains to sign up."
|
||||||
slotProps={{
|
slotProps={{
|
||||||
submitButton: {
|
submitButton: {
|
||||||
disabled: !formState.isValid || !isDirty,
|
disabled: !isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains"
|
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains"
|
||||||
enabled={enabled}
|
switchId="enabled"
|
||||||
onEnabledChange={(switchEnabled) =>
|
|
||||||
setValue('enabled', switchEnabled, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
||||||
@@ -131,9 +143,9 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlAllowedEmails')}
|
{...register('allowedEmails')}
|
||||||
name="authAccessControlAllowedEmails"
|
name="allowedEmails"
|
||||||
id="authAccessControlAllowedEmails"
|
id="allowedEmails"
|
||||||
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
label="Allowed Emails (comma separated)"
|
label="Allowed Emails (comma separated)"
|
||||||
@@ -141,9 +153,9 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlAllowedEmailDomains')}
|
{...register('allowedEmailDomains')}
|
||||||
name="authAccessControlAllowedEmailDomains"
|
name="allowedEmailDomains"
|
||||||
id="authAccessControlAllowedEmailDomains"
|
id="allowedEmailDomains"
|
||||||
label="Allowed Email Domains (comma sepated list)"
|
label="Allowed Email Domains (comma sepated list)"
|
||||||
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface AllowedRedirectURLFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
allowedUrls: Yup.string().label('Allowed Redirect URLs'),
|
||||||
* Set of URLs that are allowed to be redirected to after project's users authentication.
|
});
|
||||||
*/
|
|
||||||
authAccessControlAllowedRedirectUrls: string;
|
export type AllowedRedirectURLFormValues = Yup.InferType<
|
||||||
}
|
typeof validationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function AllowedRedirectURLsSettings() {
|
export default function AllowedRedirectURLsSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { allowedUrls } = data?.config?.auth?.redirections || {};
|
||||||
|
|
||||||
const form = useForm<AllowedRedirectURLFormValues>({
|
const form = useForm<AllowedRedirectURLFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authAccessControlAllowedRedirectUrls:
|
allowedUrls: allowedUrls?.join(', ') || '',
|
||||||
data?.app?.authAccessControlAllowedRedirectUrls,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -52,26 +65,38 @@ export default function AllowedRedirectURLsSettings() {
|
|||||||
const handleAllowedRedirectURLsChange = async (
|
const handleAllowedRedirectURLsChange = async (
|
||||||
values: AllowedRedirectURLFormValues,
|
values: AllowedRedirectURLFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
redirections: {
|
||||||
|
allowedUrls: values.allowedUrls
|
||||||
|
? values.allowedUrls.split(',').map((url) => url.trim())
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Allowed redirect URL settings are being updated...`,
|
{
|
||||||
success: `Allowed redirect URL settings have been updated successfully.`,
|
loading: `Allowed redirect URL settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's allowed redirect URL settings.`,
|
success: `Allowed redirect URL settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's allowed redirect URL settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,17 +105,19 @@ export default function AllowedRedirectURLsSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Allowed Redirect URLs"
|
title="Allowed Redirect URLs"
|
||||||
description="Allowed URLs where users can be redirected to after authentication. Separate multiple redirect URLs with comma."
|
description="Allowed URLs where users can be redirected to after authentication. Separate multiple redirect URLs with comma."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls"
|
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls"
|
||||||
className="grid grid-flow-row px-4 lg:grid-cols-5"
|
className="grid grid-flow-row px-4 lg:grid-cols-5"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlAllowedRedirectUrls')}
|
{...register('allowedUrls')}
|
||||||
name="authAccessControlAllowedRedirectUrls"
|
name="allowedUrls"
|
||||||
id="authAccessControlAllowedRedirectUrls"
|
id="allowedUrls"
|
||||||
placeholder="http://localhost:3000, http://localhost:4000"
|
placeholder="http://localhost:3000, http://localhost:4000"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -1,67 +1,58 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BlockedEmailFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* Determines whether or not the blocked email settings are enabled.
|
blockedEmails: Yup.string().label('Blocked Emails'),
|
||||||
*/
|
blockedEmailDomains: Yup.string().label('Blocked Email Domains'),
|
||||||
enabled: boolean;
|
});
|
||||||
/**
|
|
||||||
* Set of emails that are blocked from registering to the user's project.
|
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
*/
|
|
||||||
authAccessControlBlockedEmails: string;
|
|
||||||
/**
|
|
||||||
* Set of email domains that are blocked from registering to the user's project.
|
|
||||||
*/
|
|
||||||
authAccessControlBlockedEmailDomains: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlockedEmailSettings() {
|
export default function BlockedEmailSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { email, emailDomains } = data?.config?.auth?.user || {};
|
||||||
|
|
||||||
const form = useForm<BlockedEmailFormValues>({
|
const form = useForm<BlockedEmailFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enabled:
|
enabled: email?.blocked?.length > 0 || emailDomains?.blocked?.length > 0,
|
||||||
Boolean(data?.app?.authAccessControlBlockedEmails) ||
|
blockedEmails: email?.blocked?.join(', ') || '',
|
||||||
Boolean(data?.app?.authAccessControlBlockedEmailDomains),
|
blockedEmailDomains: emailDomains?.blocked?.join(', ') || '',
|
||||||
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
|
|
||||||
authAccessControlBlockedEmailDomains:
|
|
||||||
data?.app?.authAccessControlBlockedEmailDomains,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, formState, setValue, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const enabled = watch('enabled');
|
const enabled = watch('enabled');
|
||||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!data.app?.authAccessControlBlockedEmails &&
|
|
||||||
!data.app?.authAccessControlBlockedEmailDomains
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue('enabled', true, { shouldDirty: false });
|
|
||||||
}, [data.app, setValue]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -79,31 +70,63 @@ export default function BlockedEmailSettings() {
|
|||||||
const handleAllowedEmailDomainsChange = async (
|
const handleAllowedEmailDomainsChange = async (
|
||||||
values: BlockedEmailFormValues,
|
values: BlockedEmailFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authAccessControlBlockedEmails: values.enabled
|
auth: {
|
||||||
? values.authAccessControlBlockedEmails
|
user: {
|
||||||
: '',
|
email: {
|
||||||
authAccessControlBlockedEmailDomains: values.enabled
|
allowed: email.allowed,
|
||||||
? values.authAccessControlBlockedEmailDomains
|
blocked:
|
||||||
: '',
|
values.enabled && values.blockedEmails
|
||||||
|
? [
|
||||||
|
...new Set(
|
||||||
|
values.blockedEmails
|
||||||
|
.split(',')
|
||||||
|
.map((blockedEmail) => blockedEmail.trim()),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
emailDomains: {
|
||||||
|
allowed: emailDomains.allowed,
|
||||||
|
blocked:
|
||||||
|
values.enabled && values.blockedEmailDomains
|
||||||
|
? [
|
||||||
|
...new Set(
|
||||||
|
values.blockedEmailDomains
|
||||||
|
.split(',')
|
||||||
|
.map((blockedEmailDomain) =>
|
||||||
|
blockedEmailDomain.trim(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Blocked email and domain settings are being updated...`,
|
{
|
||||||
success: `Blocked email and domain settings have been updated successfully.`,
|
loading: `Blocked email and domain settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's blocked email and domain settings.`,
|
success: `Blocked email and domain settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's blocked email and domain settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,15 +137,12 @@ export default function BlockedEmailSettings() {
|
|||||||
description="Block specific email addresses and domains to sign up."
|
description="Block specific email addresses and domains to sign up."
|
||||||
slotProps={{
|
slotProps={{
|
||||||
submitButton: {
|
submitButton: {
|
||||||
disabled: !formState.isValid || !isDirty,
|
disabled: !isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains"
|
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains"
|
||||||
enabled={enabled}
|
switchId="enabled"
|
||||||
onEnabledChange={(switchEnabled) =>
|
|
||||||
setValue('enabled', switchEnabled, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
||||||
@@ -130,9 +150,9 @@ export default function BlockedEmailSettings() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlBlockedEmails')}
|
{...register('blockedEmails')}
|
||||||
name="authAccessControlBlockedEmails"
|
name="blockedEmails"
|
||||||
id="authAccessControlBlockedEmails"
|
id="blockedEmails"
|
||||||
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
label="Blocked Emails (comma separated)"
|
label="Blocked Emails (comma separated)"
|
||||||
@@ -140,9 +160,9 @@ export default function BlockedEmailSettings() {
|
|||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlBlockedEmailDomains')}
|
{...register('blockedEmailDomains')}
|
||||||
name="authAccessControlBlockedEmailDomains"
|
name="blockedEmailDomains"
|
||||||
id="authAccessControlBlockedEmailDomains"
|
id="blockedEmailDomains"
|
||||||
label="Blocked Email Domains (comma sepated list)"
|
label="Blocked Email Domains (comma sepated list)"
|
||||||
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
|
|||||||
@@ -1,36 +1,47 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface ClientURLFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
clientUrl: Yup.string().label('Client URL'),
|
||||||
* The URL of the frontend app of where users are redirected after authenticating.
|
});
|
||||||
*/
|
|
||||||
authClientUrl: string;
|
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export default function ClientURLSettings() {
|
export default function ClientURLSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['GetApp'] });
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-first',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { clientUrl, allowedUrls } = data?.config?.auth?.redirections || {};
|
||||||
|
|
||||||
const form = useForm<ClientURLFormValues>({
|
const form = useForm<ClientURLFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientUrl: data?.app?.authClientUrl,
|
clientUrl,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -50,26 +61,37 @@ export default function ClientURLSettings() {
|
|||||||
const { register, formState } = form;
|
const { register, formState } = form;
|
||||||
|
|
||||||
const handleClientURLChange = async (values: ClientURLFormValues) => {
|
const handleClientURLChange = async (values: ClientURLFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
redirections: {
|
||||||
|
...values,
|
||||||
|
allowedUrls,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Client URL is being updated...`,
|
{
|
||||||
success: `Client URL has been updated successfully.`,
|
loading: `Client URL is being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Client URL.`,
|
success: `Client URL has been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Client URL.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,22 +100,26 @@ export default function ClientURLSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Client URL"
|
title="Client URL"
|
||||||
description="This should be the URL of your frontend app where users are redirected after authenticating."
|
description="This should be the URL of your frontend app where users are redirected after authenticating."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#client-url"
|
docsLink="https://docs.nhost.io/authentication#client-url"
|
||||||
className="grid grid-flow-row lg:grid-cols-5"
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authClientUrl')}
|
{...register('clientUrl')}
|
||||||
name="authClientUrl"
|
name="clientUrl"
|
||||||
id="authClientUrl"
|
id="clientUrl"
|
||||||
placeholder="http://localhost:3000"
|
placeholder="http://localhost:3000"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
aria-label="Client URL"
|
aria-label="Client URL"
|
||||||
|
error={!!formState.errors?.clientUrl}
|
||||||
|
helperText={formState.errors?.clientUrl?.message}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,46 +1,44 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAuthSettingsQuery,
|
GetAuthenticationSettingsDocument,
|
||||||
useUpdateAppMutation,
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface DisableNewUsersFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
disabled: Yup.boolean(),
|
||||||
* Disable new users from signing up to this project
|
});
|
||||||
*/
|
|
||||||
authDisableNewUsers: boolean;
|
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export default function DisableNewUsersSettings() {
|
export default function DisableNewUsersSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useGetAuthSettingsQuery({
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
fetchPolicy: 'cache-only',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<DisableNewUsersFormValues>({
|
const form = useForm<DisableNewUsersFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authDisableNewUsers: data?.app?.authDisableNewUsers,
|
disabled: !!data?.config?.auth?.signUp?.enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset(() => ({
|
|
||||||
authDisableNewUsers: data?.app?.authDisableNewUsers,
|
|
||||||
}));
|
|
||||||
}, [data?.app?.authDisableNewUsers, form, form.reset]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -55,32 +53,41 @@ export default function DisableNewUsersSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState } = form;
|
||||||
const authDisableNewUsers = watch('authDisableNewUsers');
|
|
||||||
|
|
||||||
const handleDisableNewUsersChange = async (
|
const handleDisableNewUsersChange = async (
|
||||||
values: DisableNewUsersFormValues,
|
values: DisableNewUsersFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
signUp: {
|
||||||
|
enabled: !values.disabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Disabling new user sign ups...`,
|
{
|
||||||
success: `New user sign ups have been disabled successfully.`,
|
loading: `Disabling new user sign ups...`,
|
||||||
error: `An error occurred while trying to disable new user sign ups.`,
|
success: `New user sign ups have been disabled successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to disable new user sign ups.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -88,14 +95,15 @@ export default function DisableNewUsersSettings() {
|
|||||||
<Form onSubmit={handleDisableNewUsersChange}>
|
<Form onSubmit={handleDisableNewUsersChange}>
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Disable New Users"
|
title="Disable New Users"
|
||||||
description="If set, newly registered users are disabled and won’t be able to sign in."
|
description="If set, newly registered users are disabled and won't be able to sign in."
|
||||||
docsLink="https://docs.nhost.io/authentication#disable-new-users"
|
docsLink="https://docs.nhost.io/authentication#disable-new-users"
|
||||||
switchId="authDisableNewUsers"
|
switchId="disabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authDisableNewUsers}
|
slotProps={{
|
||||||
primaryActionButtonProps={{
|
submitButton: {
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,65 +1,63 @@
|
|||||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useGetGravatarSettingsQuery,
|
GetAuthenticationSettingsDocument,
|
||||||
useUpdateAppMutation,
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import {
|
import {
|
||||||
AUTH_GRAVATAR_DEFAULT,
|
AUTH_GRAVATAR_DEFAULT,
|
||||||
AUTH_GRAVATAR_RATING,
|
AUTH_GRAVATAR_RATING,
|
||||||
getToastStyleProps,
|
getToastStyleProps,
|
||||||
} from '@/utils/settings/settingsConstants';
|
} from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface GravatarFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* Gravatar image to use as default.
|
default: Yup.string().label('Default Gravatar'),
|
||||||
*/
|
rating: Yup.string().label('Gravatar Rating'),
|
||||||
authGravatarDefault: string;
|
});
|
||||||
/**
|
|
||||||
* Gravatar image rating.
|
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
*/
|
|
||||||
authGravatarRating: string;
|
|
||||||
/**
|
|
||||||
* Enable Gravatar for this project
|
|
||||||
*/
|
|
||||||
authGravatarEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GravatarSettings() {
|
export default function GravatarSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetGravatarSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: defaultGravatar,
|
||||||
|
rating,
|
||||||
|
enabled,
|
||||||
|
} = data?.config?.auth?.user?.gravatar || {};
|
||||||
|
|
||||||
const form = useForm<GravatarFormValues>({
|
const form = useForm<GravatarFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authGravatarDefault: data?.app?.authGravatarDefault || '',
|
default: defaultGravatar || '',
|
||||||
authGravatarRating: data?.app?.authGravatarRating || '',
|
rating: rating || '',
|
||||||
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset(() => ({
|
|
||||||
authGravatarDefault: data?.app?.authGravatarDefault || '',
|
|
||||||
authGravatarRating: data?.app?.authGravatarRating || '',
|
|
||||||
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
|
|
||||||
}));
|
|
||||||
}, [data?.app, form, form.reset]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -75,29 +73,39 @@ export default function GravatarSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authGravatarEnabled = watch('authGravatarEnabled');
|
const gravatarEnabled = watch('enabled') ?? false;
|
||||||
|
|
||||||
const handleGravatarSettingsChange = async (values: GravatarFormValues) => {
|
const handleGravatarSettingsChange = async (values: GravatarFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
user: {
|
||||||
|
gravatar: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Gravatar settings are being updated...`,
|
{
|
||||||
success: `Gravatar settings have been updated successfully.`,
|
loading: `Gravatar settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Gravatar settings.`,
|
success: `Gravatar settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Gravatar settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -106,22 +114,23 @@ export default function GravatarSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Gravatar"
|
title="Gravatar"
|
||||||
description="Use Gravatars for avatar URLs for users."
|
description="Use Gravatars for avatar URLs for users."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#gravatar"
|
docsLink="https://docs.nhost.io/authentication#gravatar"
|
||||||
switchId="authGravatarEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authGravatarEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid grid-flow-col grid-cols-5 grid-rows-2 gap-y-6',
|
'grid grid-flow-col grid-cols-5 grid-rows-2 gap-y-6',
|
||||||
!authGravatarEnabled && 'hidden',
|
!gravatarEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ControlledSelect
|
<ControlledSelect
|
||||||
{...register('authGravatarDefault')}
|
{...register('default')}
|
||||||
id="authGravatarDefault"
|
id="default"
|
||||||
className="col-span-5 lg:col-span-2"
|
className="col-span-5 lg:col-span-2"
|
||||||
placeholder="Default Gravatar"
|
placeholder="Default Gravatar"
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
@@ -135,8 +144,8 @@ export default function GravatarSettings() {
|
|||||||
))}
|
))}
|
||||||
</ControlledSelect>
|
</ControlledSelect>
|
||||||
<ControlledSelect
|
<ControlledSelect
|
||||||
{...register('authGravatarRating')}
|
{...register('rating')}
|
||||||
id="authGravatarRating"
|
id="rating"
|
||||||
className="col-span-5 lg:col-span-2"
|
className="col-span-5 lg:col-span-2"
|
||||||
placeholder="Gravatar Rating"
|
placeholder="Gravatar Rating"
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
|||||||
@@ -1,54 +1,52 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useGetAuthSettingsQuery,
|
GetAuthenticationSettingsDocument,
|
||||||
useUpdateAppMutation,
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface MFASettingsFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* One Time Password issuer
|
issuer: Yup.string().label('OTP Issuer').nullable().required(),
|
||||||
*/
|
});
|
||||||
authMfaTotpIssuer: string;
|
|
||||||
/**
|
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
* Enable Multi Factor Authentication for this project
|
|
||||||
*/
|
|
||||||
authMfaEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MFASettings() {
|
export default function MFASettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAuthSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { enabled, issuer } = data?.config?.auth?.totp || {};
|
||||||
|
|
||||||
const form = useForm<MFASettingsFormValues>({
|
const form = useForm<MFASettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
|
issuer,
|
||||||
authMfaEnabled: data?.app?.authMfaEnabled,
|
enabled,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset(() => ({
|
|
||||||
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
|
|
||||||
authMfaEnabled: data?.app?.authMfaEnabled,
|
|
||||||
}));
|
|
||||||
}, [data?.app, form, form.reset]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -64,29 +62,37 @@ export default function MFASettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authMfaEnabled = watch('authMfaEnabled');
|
const authMfaEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleMFASettingsChange = async (values: MFASettingsFormValues) => {
|
const handleMFASettingsChange = async (values: MFASettingsFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
totp: values,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Multi-factor authentication settings are being updated...`,
|
{
|
||||||
success: `Multi-factor authentication settings have been updated successfully.`,
|
loading: `Multi-factor authentication settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's multi-factor authentication settings.`,
|
success: `Multi-factor authentication settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's multi-factor authentication settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,13 +101,14 @@ export default function MFASettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Multi-Factor Authentication"
|
title="Multi-Factor Authentication"
|
||||||
description="Enable users to use MFA to sign in"
|
description="Enable users to use MFA to sign in"
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication"
|
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication"
|
||||||
switchId="authMfaEnabled"
|
switchId="enabled"
|
||||||
enabled={authMfaEnabled}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid grid-flow-row lg:grid-cols-5',
|
'grid grid-flow-row lg:grid-cols-5',
|
||||||
@@ -109,14 +116,16 @@ export default function MFASettings() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authMfaTotpIssuer')}
|
{...register('issuer')}
|
||||||
name="authMfaTotpIssuer"
|
name="issuer"
|
||||||
id="authMfaTotpIssuer"
|
id="issuer"
|
||||||
label="OTP Issuer"
|
label="OTP Issuer"
|
||||||
placeholder="Name of the One Time Password (OTP) issuer"
|
placeholder="Name of the One Time Password (OTP) issuer"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.issuer}
|
||||||
|
helperText={formState.errors?.issuer?.message}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
@@ -7,26 +8,7 @@ import { useEffect } from 'react';
|
|||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BaseEnvironmentVariableFormValues {
|
export interface BaseEnvironmentVariableFormProps extends DialogFormProps {
|
||||||
/**
|
|
||||||
* 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.
|
* Determines the mode of the form.
|
||||||
*
|
*
|
||||||
@@ -50,8 +32,11 @@ export interface BaseEnvironmentVariableFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
||||||
|
id: Yup.string().label('ID'),
|
||||||
name: Yup.string()
|
name: Yup.string()
|
||||||
.required('This field is required.')
|
.label('Name')
|
||||||
|
.nullable()
|
||||||
|
.required()
|
||||||
.test(
|
.test(
|
||||||
'isEnvVarPermitted',
|
'isEnvVarPermitted',
|
||||||
'This is a reserved name.',
|
'This is a reserved name.',
|
||||||
@@ -77,18 +62,24 @@ export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
|||||||
(prefix) => !value.startsWith(prefix),
|
(prefix) => !value.startsWith(prefix),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.test('isEnvVarValid', `The name must start with a letter.`, (value) =>
|
.test(
|
||||||
/^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/i.test(value),
|
'isEnvVarValid',
|
||||||
|
'A name must start with a letter and can only contain letters, numbers, and underscores.',
|
||||||
|
(value) => /^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/i.test(value),
|
||||||
),
|
),
|
||||||
devValue: Yup.string().required('This field is required.'),
|
value: Yup.string().label('Value').nullable().required(),
|
||||||
prodValue: Yup.string().required('This field is required.'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type BaseEnvironmentVariableFormValues = Yup.InferType<
|
||||||
|
typeof baseEnvironmentVariableFormValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function BaseEnvironmentVariableForm({
|
export default function BaseEnvironmentVariableForm({
|
||||||
mode = 'edit',
|
mode = 'edit',
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: BaseEnvironmentVariableFormProps) {
|
}: BaseEnvironmentVariableFormProps) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
const form = useFormContext<BaseEnvironmentVariableFormValues>();
|
const form = useFormContext<BaseEnvironmentVariableFormValues>();
|
||||||
@@ -103,8 +94,8 @@ export default function BaseEnvironmentVariableForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'dialog');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
||||||
@@ -115,21 +106,7 @@ export default function BaseEnvironmentVariableForm({
|
|||||||
|
|
||||||
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
||||||
<Input
|
<Input
|
||||||
{...register('name', {
|
{...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"
|
id="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder="EXAMPLE_NAME"
|
placeholder="EXAMPLE_NAME"
|
||||||
@@ -143,30 +120,18 @@ export default function BaseEnvironmentVariableForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
{...register('prodValue')}
|
{...register('value')}
|
||||||
id="prodValue"
|
id="value"
|
||||||
label="Production Value"
|
label="Value"
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
error={!!errors.prodValue}
|
error={!!errors.value}
|
||||||
helperText={errors?.prodValue?.message}
|
helperText={errors?.value?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={mode === 'edit'}
|
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">
|
<div className="grid grid-flow-row gap-2">
|
||||||
<Button type="submit" loading={isSubmitting}>
|
<Button type="submit" loading={isSubmitting}>
|
||||||
{submitButtonText}
|
{submitButtonText}
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ import BaseEnvironmentVariableForm, {
|
|||||||
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
|
GetEnvironmentVariablesDocument,
|
||||||
useGetEnvironmentVariablesQuery,
|
useGetEnvironmentVariablesQuery,
|
||||||
useInsertEnvironmentVariablesMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export interface CreateEnvironmentVariableFormProps
|
export interface CreateEnvironmentVariableFormProps
|
||||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> {
|
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -31,8 +33,7 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
const form = useForm<BaseEnvironmentVariableFormValues>({
|
const form = useForm<BaseEnvironmentVariableFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
devValue: '',
|
value: '',
|
||||||
prodValue: '',
|
|
||||||
},
|
},
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
||||||
@@ -41,14 +42,14 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [insertEnvironmentVariables] = useInsertEnvironmentVariablesMutation({
|
const availableEnvironmentVariables = data?.config?.global?.environment || [];
|
||||||
refetchQueries: ['getEnvironmentVariables'],
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -68,13 +69,10 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
name,
|
name,
|
||||||
prodValue,
|
value,
|
||||||
devValue,
|
|
||||||
}: BaseEnvironmentVariableFormValues) {
|
}: BaseEnvironmentVariableFormValues) {
|
||||||
if (
|
if (
|
||||||
data?.environmentVariables?.some(
|
availableEnvironmentVariables?.some((variable) => variable.name === name)
|
||||||
(environmentVariable) => environmentVariable.name === name,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
setError('name', {
|
setError('name', {
|
||||||
message: 'This environment variable already exists.',
|
message: 'This environment variable already exists.',
|
||||||
@@ -83,20 +81,34 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertEnvironmentVariablePromise = insertEnvironmentVariables({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
environmentVariables: [
|
appId: currentApplication?.id,
|
||||||
{ appId: currentApplication.id, name, prodValue, devValue },
|
config: {
|
||||||
],
|
global: {
|
||||||
|
environment: [
|
||||||
|
...(availableEnvironmentVariables?.map((variable) => ({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value,
|
||||||
|
})) || []),
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
insertEnvironmentVariablePromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Creating environment variable...',
|
loading: 'Creating environment variable...',
|
||||||
success: 'Environment variable has been created successfully.',
|
success: 'Environment variable has been created successfully.',
|
||||||
error: 'An error occurred while creating the environment variable.',
|
error: getServerError(
|
||||||
|
'An error occurred while creating the environment variable.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ import BaseEnvironmentVariableForm, {
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { EnvironmentVariable } from '@/types/application';
|
import type { EnvironmentVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
|
GetEnvironmentVariablesDocument,
|
||||||
useGetEnvironmentVariablesQuery,
|
useGetEnvironmentVariablesQuery,
|
||||||
useUpdateEnvironmentVariableMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export interface EditEnvironmentVariableFormProps
|
export interface EditEnvironmentVariableFormProps
|
||||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> {
|
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* The environment variable to edit.
|
* The environment variable to edit.
|
||||||
*/
|
*/
|
||||||
@@ -38,8 +40,7 @@ export default function EditEnvironmentVariableForm({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: originalEnvironmentVariable.id || '',
|
id: originalEnvironmentVariable.id || '',
|
||||||
name: originalEnvironmentVariable.name || '',
|
name: originalEnvironmentVariable.name || '',
|
||||||
devValue: originalEnvironmentVariable.devValue || '',
|
value: originalEnvironmentVariable.value || '',
|
||||||
prodValue: originalEnvironmentVariable.prodValue || '',
|
|
||||||
},
|
},
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
||||||
@@ -48,14 +49,14 @@ export default function EditEnvironmentVariableForm({
|
|||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateEnvironmentVariable] = useUpdateEnvironmentVariableMutation({
|
const availableEnvironmentVariables = data?.config?.global?.environment || [];
|
||||||
refetchQueries: ['getEnvironmentVariables'],
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -76,14 +77,13 @@ export default function EditEnvironmentVariableForm({
|
|||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
prodValue,
|
value,
|
||||||
devValue,
|
|
||||||
}: BaseEnvironmentVariableFormValues) {
|
}: BaseEnvironmentVariableFormValues) {
|
||||||
if (
|
if (
|
||||||
data?.environmentVariables?.some(
|
availableEnvironmentVariables.some(
|
||||||
(environmentVariable) =>
|
(variable) =>
|
||||||
environmentVariable.name === name &&
|
variable.name === name &&
|
||||||
environmentVariable.name !== originalEnvironmentVariable.name,
|
variable.name !== originalEnvironmentVariable.name,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setError('name', {
|
setError('name', {
|
||||||
@@ -93,22 +93,36 @@ export default function EditEnvironmentVariableForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateEnvironmentVariablePromise = updateEnvironmentVariable({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id,
|
appId: currentApplication?.id,
|
||||||
environmentVariable: {
|
config: {
|
||||||
prodValue,
|
global: {
|
||||||
devValue,
|
environment: [
|
||||||
|
...availableEnvironmentVariables
|
||||||
|
.filter((variable) => variable.id !== id)
|
||||||
|
.map((variable) => ({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateEnvironmentVariablePromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Updating environment variable...',
|
loading: 'Updating environment variable...',
|
||||||
success: 'Environment variable has been updated successfully.',
|
success: 'Environment variable has been updated successfully.',
|
||||||
error: 'An error occurred while updating the environment variable.',
|
error: getServerError(
|
||||||
|
'An error occurred while updating the environment variable.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
refetchGetAppInjectedVariablesQuery,
|
GetEnvironmentVariablesDocument,
|
||||||
useUpdateApplicationMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -14,7 +15,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
|||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface EditJwtSecretFormProps {
|
export interface EditJwtSecretFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Initial JWT secret.
|
* Initial JWT secret.
|
||||||
*/
|
*/
|
||||||
@@ -39,14 +40,7 @@ export interface EditJwtSecretFormProps {
|
|||||||
onCancel?: VoidFunction;
|
onCancel?: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditJwtSecretFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
|
||||||
* JWT secret.
|
|
||||||
*/
|
|
||||||
jwtSecret: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
|
||||||
jwtSecret: Yup.string()
|
jwtSecret: Yup.string()
|
||||||
.nullable()
|
.nullable()
|
||||||
.required('This field is required.')
|
.required('This field is required.')
|
||||||
@@ -60,18 +54,19 @@ const validationSchema = Yup.object().shape({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type EditJwtSecretFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function EditJwtSecretForm({
|
export default function EditJwtSecretForm({
|
||||||
disabled,
|
disabled,
|
||||||
jwtSecret,
|
jwtSecret,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: EditJwtSecretFormProps) {
|
}: EditJwtSecretFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApplication] = useUpdateApplicationMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
refetchGetAppInjectedVariablesQuery({ id: currentApplication?.id }),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
@@ -89,30 +84,42 @@ export default function EditJwtSecretForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'dialog');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
async function handleSubmit(values: EditJwtSecretFormValues) {
|
async function handleSubmit(values: EditJwtSecretFormValues) {
|
||||||
const updateAppPromise = updateApplication({
|
const parsedJwtSecret = JSON.parse(values.jwtSecret);
|
||||||
|
const isArray = Array.isArray(parsedJwtSecret);
|
||||||
|
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
appId: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
hasuraGraphqlJwtSecret: values.jwtSecret,
|
hasura: {
|
||||||
|
jwtSecrets: isArray ? parsedJwtSecret : [parsedJwtSecret],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppPromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Updating JWT secret...',
|
{
|
||||||
success: 'JWT secret has been updated successfully.',
|
loading: 'Updating JWT secret...',
|
||||||
error: 'An error occurred while updating the JWT secret.',
|
success: 'JWT secret has been updated successfully.',
|
||||||
},
|
error: (arg: Error) =>
|
||||||
getToastStyleProps(),
|
arg?.message
|
||||||
);
|
? `Error: ${arg.message}`
|
||||||
|
: 'An error occurred while updating the JWT secret.',
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
|
} catch {
|
||||||
|
// Note: error is handled above
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -121,7 +128,7 @@ export default function EditJwtSecretForm({
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="flex flex-auto flex-col content-between overflow-hidden pb-4"
|
className="flex flex-auto flex-col content-between overflow-hidden pb-4"
|
||||||
>
|
>
|
||||||
<div className="px-6 overflow-y-auto flex-auto">
|
<div className="flex-auto overflow-y-auto px-6">
|
||||||
<Input
|
<Input
|
||||||
{...register('jwtSecret')}
|
{...register('jwtSecret')}
|
||||||
error={Boolean(errors.jwtSecret?.message)}
|
error={Boolean(errors.jwtSecret?.message)}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||||
|
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { EnvironmentVariable } from '@/types/application';
|
import type { EnvironmentVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -13,34 +16,49 @@ import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
|||||||
import List from '@/ui/v2/List';
|
import List from '@/ui/v2/List';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useDeleteEnvironmentVariableMutation,
|
GetEnvironmentVariablesDocument,
|
||||||
useGetEnvironmentVariablesQuery,
|
useGetEnvironmentVariablesQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export interface PermissionVariableSettingsFormValues {
|
export interface EnvironmentVariableSettingsFormValues {
|
||||||
/**
|
/**
|
||||||
* Permission variables.
|
* Environment variables.
|
||||||
*/
|
*/
|
||||||
environmentVariables: EnvironmentVariable[];
|
environmentVariables: EnvironmentVariable[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EnvironmentVariableSettings() {
|
export default function EnvironmentVariableSettings() {
|
||||||
const { openDialog, openAlertDialog } = useDialog();
|
const { openDialog, openAlertDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
fetchPolicy: 'cache-only',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [deleteEnvironmentVariable] = useDeleteEnvironmentVariableMutation({
|
const availableEnvironmentVariables = [
|
||||||
refetchQueries: ['getEnvironmentVariables'],
|
...(data?.config?.global?.environment || []),
|
||||||
|
].sort((a, b) => {
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -57,26 +75,43 @@ export default function EnvironmentVariableSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteVariable({ id }: EnvironmentVariable) {
|
async function handleDeleteVariable({ id }: EnvironmentVariable) {
|
||||||
const deleteEnvironmentVariablePromise = deleteEnvironmentVariable({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id,
|
appId: currentApplication?.id,
|
||||||
|
config: {
|
||||||
|
global: {
|
||||||
|
environment: availableEnvironmentVariables
|
||||||
|
.filter((variable) => variable.id !== id)
|
||||||
|
.map((variable) => ({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
deleteEnvironmentVariablePromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Deleting environment variable...',
|
{
|
||||||
success: 'Environment variable has been deleted successfully.',
|
loading: 'Deleting environment variable...',
|
||||||
error: 'An error occurred while deleting the environment variable.',
|
success: 'Environment variable has been deleted successfully.',
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
'An error occurred while deleting the environment variable.',
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenCreator() {
|
function handleOpenCreator() {
|
||||||
openDialog('CREATE_ENVIRONMENT_VARIABLE', {
|
openDialog({
|
||||||
title: 'Create Environment Variable',
|
title: 'Create Environment Variable',
|
||||||
|
component: <CreateEnvironmentVariableForm />,
|
||||||
props: {
|
props: {
|
||||||
titleProps: { className: '!pb-0' },
|
titleProps: { className: '!pb-0' },
|
||||||
PaperProps: { className: 'gap-2 max-w-sm' },
|
PaperProps: { className: 'gap-2 max-w-sm' },
|
||||||
@@ -85,9 +120,13 @@ export default function EnvironmentVariableSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenEditor(originalVariable: EnvironmentVariable) {
|
function handleOpenEditor(originalVariable: EnvironmentVariable) {
|
||||||
openDialog('EDIT_ENVIRONMENT_VARIABLE', {
|
openDialog({
|
||||||
title: 'Edit Environment Variables',
|
title: 'Edit Environment Variable',
|
||||||
payload: { originalEnvironmentVariable: originalVariable },
|
component: (
|
||||||
|
<EditEnvironmentVariableForm
|
||||||
|
originalEnvironmentVariable={originalVariable}
|
||||||
|
/>
|
||||||
|
),
|
||||||
props: {
|
props: {
|
||||||
titleProps: { className: '!pb-0' },
|
titleProps: { className: '!pb-0' },
|
||||||
PaperProps: { className: 'gap-2 max-w-sm' },
|
PaperProps: { className: 'gap-2 max-w-sm' },
|
||||||
@@ -113,12 +152,6 @@ export default function EnvironmentVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableEnvironmentVariables =
|
|
||||||
[...data.environmentVariables].sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Project Environment Variables"
|
title="Project Environment Variables"
|
||||||
@@ -134,95 +167,79 @@ export default function EnvironmentVariableSettings() {
|
|||||||
>
|
>
|
||||||
<Box className="grid grid-cols-2 gap-2 border-b-1 px-4 py-3 lg:grid-cols-3">
|
<Box className="grid grid-cols-2 gap-2 border-b-1 px-4 py-3 lg:grid-cols-3">
|
||||||
<Text className="font-medium">Variable Name</Text>
|
<Text className="font-medium">Variable Name</Text>
|
||||||
<Text className="font-medium lg:col-span-2">Updated</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
{availableEnvironmentVariables.length > 0 && (
|
{availableEnvironmentVariables.length > 0 && (
|
||||||
<List>
|
<List>
|
||||||
{availableEnvironmentVariables.map((environmentVariable, index) => {
|
{availableEnvironmentVariables.map((environmentVariable, index) => (
|
||||||
const timestamp = formatDistanceToNowStrict(
|
<Fragment key={environmentVariable.id}>
|
||||||
parseISO(environmentVariable.updatedAt),
|
<ListItem.Root
|
||||||
{ addSuffix: true, roundingMethod: 'floor' },
|
className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3"
|
||||||
);
|
secondaryAction={
|
||||||
|
<Dropdown.Root>
|
||||||
return (
|
<Dropdown.Trigger
|
||||||
<Fragment key={environmentVariable.id}>
|
asChild
|
||||||
<ListItem.Root
|
hideChevron
|
||||||
className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3"
|
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||||
secondaryAction={
|
>
|
||||||
<Dropdown.Root>
|
<IconButton
|
||||||
<Dropdown.Trigger
|
variant="borderless"
|
||||||
asChild
|
color="secondary"
|
||||||
hideChevron
|
disabled={maintenanceActive}
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
|
||||||
>
|
>
|
||||||
<IconButton variant="borderless" color="secondary">
|
<DotsVerticalIcon />
|
||||||
<DotsVerticalIcon />
|
</IconButton>
|
||||||
</IconButton>
|
</Dropdown.Trigger>
|
||||||
</Dropdown.Trigger>
|
|
||||||
|
|
||||||
<Dropdown.Content
|
<Dropdown.Content
|
||||||
menu
|
menu
|
||||||
PaperProps={{ className: 'w-32' }}
|
PaperProps={{ className: 'w-32' }}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
}}
|
}}
|
||||||
transformOrigin={{
|
transformOrigin={{
|
||||||
vertical: 'top',
|
vertical: 'top',
|
||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={() => handleOpenEditor(environmentVariable)}
|
||||||
>
|
>
|
||||||
<Dropdown.Item
|
<Text className="font-medium">Edit</Text>
|
||||||
onClick={() =>
|
</Dropdown.Item>
|
||||||
handleOpenEditor(environmentVariable)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text className="font-medium">Edit</Text>
|
|
||||||
</Dropdown.Item>
|
|
||||||
|
|
||||||
<Divider component="li" />
|
<Divider component="li" />
|
||||||
|
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleConfirmDelete(environmentVariable)
|
handleConfirmDelete(environmentVariable)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text className="font-medium" color="error">
|
<Text className="font-medium" color="error">
|
||||||
Delete
|
Delete
|
||||||
</Text>
|
</Text>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Content>
|
</Dropdown.Content>
|
||||||
</Dropdown.Root>
|
</Dropdown.Root>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem.Text className="truncate">
|
<ListItem.Text className="truncate">
|
||||||
{environmentVariable.name}
|
{environmentVariable.name}
|
||||||
</ListItem.Text>
|
</ListItem.Text>
|
||||||
|
</ListItem.Root>
|
||||||
|
|
||||||
<Text
|
<Divider
|
||||||
variant="subtitle1"
|
component="li"
|
||||||
className="truncate lg:col-span-2"
|
className={twMerge(
|
||||||
>
|
index === availableEnvironmentVariables.length - 1
|
||||||
{timestamp === '0 seconds ago' ||
|
? '!mt-4'
|
||||||
timestamp === 'in 0 seconds'
|
: '!my-4',
|
||||||
? 'Now'
|
)}
|
||||||
: timestamp}
|
/>
|
||||||
</Text>
|
</Fragment>
|
||||||
</ListItem.Root>
|
))}
|
||||||
|
|
||||||
<Divider
|
|
||||||
component="li"
|
|
||||||
className={twMerge(
|
|
||||||
index === availableEnvironmentVariables.length - 1
|
|
||||||
? '!mt-4'
|
|
||||||
: '!my-4',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -231,6 +248,7 @@ export default function EnvironmentVariableSettings() {
|
|||||||
variant="borderless"
|
variant="borderless"
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
onClick={handleOpenCreator}
|
onClick={handleOpenCreator}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Create Environment Variable
|
Create Environment Variable
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
|
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||||
import { useAppClient } from '@/hooks/useAppClient';
|
import { useAppClient } from '@/hooks/useAppClient';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
@@ -20,7 +22,8 @@ import generateAppServiceUrl, {
|
|||||||
} from '@/utils/common/generateAppServiceUrl';
|
} from '@/utils/common/generateAppServiceUrl';
|
||||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||||
import { useGetAppInjectedVariablesQuery } from '@/utils/__generated__/graphql';
|
import getJwtSecretsWithoutFalsyValues from '@/utils/settings/getJwtSecretsWithoutFalsyValues';
|
||||||
|
import { useGetEnvironmentVariablesQuery } from '@/utils/__generated__/graphql';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
|
|
||||||
export default function SystemEnvironmentVariableSettings() {
|
export default function SystemEnvironmentVariableSettings() {
|
||||||
@@ -28,10 +31,22 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||||
|
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetAppInjectedVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { jwtSecrets, webhookSecret, adminSecret } = data?.config?.hasura || {};
|
||||||
|
const jwtSecretsWithoutFalsyValues = getJwtSecretsWithoutFalsyValues(
|
||||||
|
jwtSecrets || [],
|
||||||
|
);
|
||||||
|
const stringifiedJwtSecrets =
|
||||||
|
jwtSecretsWithoutFalsyValues.length === 1
|
||||||
|
? JSON.stringify(jwtSecretsWithoutFalsyValues[0], null, 2)
|
||||||
|
: JSON.stringify(jwtSecretsWithoutFalsyValues, null, 2);
|
||||||
|
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
|
||||||
const appClient = useAppClient();
|
const appClient = useAppClient();
|
||||||
@@ -50,7 +65,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showViewJwtSecretModal() {
|
function showViewJwtSecretModal() {
|
||||||
openDialog('EDIT_JWT_SECRET', {
|
openDialog({
|
||||||
title: (
|
title: (
|
||||||
<span className="grid grid-flow-row">
|
<span className="grid grid-flow-row">
|
||||||
<span>Auth JWT Secret</span>
|
<span>Auth JWT Secret</span>
|
||||||
@@ -61,15 +76,14 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
</Text>
|
</Text>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
payload: {
|
component: (
|
||||||
disabled: true,
|
<EditJwtSecretForm disabled jwtSecret={stringifiedJwtSecrets} />
|
||||||
jwtSecret: data?.app?.hasuraGraphqlJwtSecret,
|
),
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEditJwtSecretModal() {
|
function showEditJwtSecretModal() {
|
||||||
openDialog('EDIT_JWT_SECRET', {
|
openDialog({
|
||||||
title: (
|
title: (
|
||||||
<span className="grid grid-flow-row">
|
<span className="grid grid-flow-row">
|
||||||
<span>Edit JWT Secret</span>
|
<span>Edit JWT Secret</span>
|
||||||
@@ -80,9 +94,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
</Text>
|
</Text>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
payload: {
|
component: <EditJwtSecretForm jwtSecret={stringifiedJwtSecrets} />,
|
||||||
jwtSecret: data?.app?.hasuraGraphqlJwtSecret,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +119,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
||||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
|
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.httpUrl },
|
||||||
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
||||||
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
|
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
|
||||||
];
|
];
|
||||||
@@ -134,7 +146,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
<Text className="truncate" color="secondary">
|
<Text className="truncate" color="secondary">
|
||||||
{showAdminSecret ? (
|
{showAdminSecret ? (
|
||||||
<InlineCode className="!text-sm font-medium">
|
<InlineCode className="!text-sm font-medium">
|
||||||
{currentApplication?.hasuraGraphqlAdminSecret}
|
{adminSecret}
|
||||||
</InlineCode>
|
</InlineCode>
|
||||||
) : (
|
) : (
|
||||||
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
||||||
@@ -167,7 +179,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
<Text className="truncate" color="secondary">
|
<Text className="truncate" color="secondary">
|
||||||
{showWebhookSecret ? (
|
{showWebhookSecret ? (
|
||||||
<InlineCode className="!text-sm font-medium">
|
<InlineCode className="!text-sm font-medium">
|
||||||
{data?.app?.webhookSecret}
|
{webhookSecret}
|
||||||
</InlineCode>
|
</InlineCode>
|
||||||
) : (
|
) : (
|
||||||
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
||||||
@@ -231,6 +243,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
variant="borderless"
|
variant="borderless"
|
||||||
onClick={showEditJwtSecretModal}
|
onClick={showEditJwtSecretModal}
|
||||||
size="small"
|
size="small"
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Edit JWT Secret
|
Edit JWT Secret
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import { Alert } from '@/ui/Alert';
|
import { Alert } from '@/ui/Alert';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -20,6 +22,7 @@ export interface BaseDirectoryFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BaseDirectorySettings() {
|
export default function BaseDirectorySettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateApp] = useUpdateAppMutation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -54,7 +57,9 @@ export default function BaseDirectorySettings() {
|
|||||||
{
|
{
|
||||||
loading: `The base directory is being updated...`,
|
loading: `The base directory is being updated...`,
|
||||||
success: `The base directory has been updated successfully.`,
|
success: `The base directory has been updated successfully.`,
|
||||||
error: `An error occurred while trying to update the project's base directory.`,
|
error: getServerError(
|
||||||
|
`An error occurred while trying to update the project's base directory.`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -84,9 +89,11 @@ export default function BaseDirectorySettings() {
|
|||||||
<InlineCode className="text-xs">nhost</InlineCode> folder.
|
<InlineCode className="text-xs">nhost</InlineCode> folder.
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/github-integration#base-directory"
|
docsLink="https://docs.nhost.io/platform/github-integration#base-directory"
|
||||||
className="grid grid-flow-row lg:grid-cols-5"
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import { Alert } from '@/ui/Alert';
|
import { Alert } from '@/ui/Alert';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -19,6 +21,7 @@ export interface DeploymentBranchFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DeploymentBranchSettings() {
|
export default function DeploymentBranchSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateApp] = useUpdateAppMutation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -57,7 +60,9 @@ export default function DeploymentBranchSettings() {
|
|||||||
{
|
{
|
||||||
loading: `The deployment branch is being updated...`,
|
loading: `The deployment branch is being updated...`,
|
||||||
success: `The deployment branch has been updated successfully.`,
|
success: `The deployment branch has been updated successfully.`,
|
||||||
error: `An error occurred while trying to update the project's deployment branch.`,
|
error: getServerError(
|
||||||
|
`An error occurred while trying to update the project's deployment branch.`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -79,9 +84,11 @@ export default function DeploymentBranchSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Deployment Branch"
|
title="Deployment Branch"
|
||||||
description="All commits pushed to this deployment branch will trigger a deployment. You can switch to a different branch here."
|
description="All commits pushed to this deployment branch will trigger a deployment. You can switch to a different branch here."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/github-integration#deployment-branch"
|
docsLink="https://docs.nhost.io/platform/github-integration#deployment-branch"
|
||||||
className="grid grid-flow-row lg:grid-cols-5"
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
@@ -7,18 +8,7 @@ import { useEffect } from 'react';
|
|||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BasePermissionVariableFormValues {
|
export interface BasePermissionVariableFormProps extends DialogFormProps {
|
||||||
/**
|
|
||||||
* Permission variable key.
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
/**
|
|
||||||
* Permission variable value.
|
|
||||||
*/
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BasePermissionVariableFormProps {
|
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -36,14 +26,19 @@ export interface BasePermissionVariableFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const basePermissionVariableValidationSchema = Yup.object({
|
export const basePermissionVariableValidationSchema = Yup.object({
|
||||||
key: Yup.string().required('This field is required.'),
|
key: Yup.string().label('Field Name').nullable().required(),
|
||||||
value: Yup.string().required('This field is required.'),
|
value: Yup.string().label('Path').nullable().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type BasePermissionVariableFormValues = Yup.InferType<
|
||||||
|
typeof basePermissionVariableValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function BasePermissionVariableForm({
|
export default function BasePermissionVariableForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: BasePermissionVariableFormProps) {
|
}: BasePermissionVariableFormProps) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
const form = useFormContext<BasePermissionVariableFormValues>();
|
const form = useFormContext<BasePermissionVariableFormValues>();
|
||||||
@@ -56,8 +51,8 @@ export default function BasePermissionVariableForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'dialog');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-flow-row gap-2 px-6 pb-6">
|
<div className="grid grid-flow-row gap-2 px-6 pb-6">
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ import BasePermissionVariableForm, {
|
|||||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import getPermissionVariablesObject from '@/utils/settings/getPermissionVariablesObject';
|
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAppCustomClaimsQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export interface CreatePermissionVariableFormProps
|
export interface CreatePermissionVariableFormProps
|
||||||
extends Pick<BasePermissionVariableFormProps, 'onCancel'> {
|
extends Pick<BasePermissionVariableFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -32,11 +32,14 @@ export default function CreatePermissionVariableForm({
|
|||||||
}: CreatePermissionVariableFormProps) {
|
}: CreatePermissionVariableFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, error, loading } = useGetAppCustomClaimsQuery({
|
const { data, error, loading } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { customClaims: permissionVariables } =
|
||||||
|
data?.config?.auth?.session?.accessToken || {};
|
||||||
|
|
||||||
const form = useForm<BasePermissionVariableFormValues>({
|
const form = useForm<BasePermissionVariableFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
key: '',
|
key: '',
|
||||||
@@ -46,8 +49,8 @@ export default function CreatePermissionVariableForm({
|
|||||||
resolver: yupResolver(basePermissionVariableValidationSchema),
|
resolver: yupResolver(basePermissionVariableValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: ['getAppCustomClaims'],
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -61,9 +64,8 @@ export default function CreatePermissionVariableForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availablePermissionVariables = getPermissionVariablesArray(
|
const availablePermissionVariables =
|
||||||
data?.app?.authJwtCustomClaims,
|
getAllPermissionVariables(permissionVariables);
|
||||||
);
|
|
||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
key,
|
key,
|
||||||
@@ -79,26 +81,29 @@ export default function CreatePermissionVariableForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionVariablesObject = getPermissionVariablesObject(
|
const existingPermissionVariables =
|
||||||
availablePermissionVariables.filter(
|
permissionVariables?.map((permissionVariable) => ({
|
||||||
(permissionVariable) => !permissionVariable.isSystemClaim,
|
key: permissionVariable.key,
|
||||||
),
|
value: permissionVariable.value,
|
||||||
);
|
})) || [];
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authJwtCustomClaims: {
|
auth: {
|
||||||
...permissionVariablesObject,
|
session: {
|
||||||
[key]: value,
|
accessToken: {
|
||||||
|
customClaims: [...existingPermissionVariables, { key, value }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Creating permission variable...',
|
loading: 'Creating permission variable...',
|
||||||
success: 'Permission variable has been created successfully.',
|
success: 'Permission variable has been created successfully.',
|
||||||
|
|||||||
@@ -6,25 +6,25 @@ import BasePermissionVariableForm, {
|
|||||||
basePermissionVariableValidationSchema,
|
basePermissionVariableValidationSchema,
|
||||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { CustomClaim } from '@/types/application';
|
import type { PermissionVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import getPermissionVariables from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import getPermissionVariablesObject from '@/utils/settings/getPermissionVariablesObject';
|
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAppCustomClaimsQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export interface EditPermissionVariableFormProps
|
export interface EditPermissionVariableFormProps
|
||||||
extends Pick<BasePermissionVariableFormProps, 'onCancel'> {
|
extends Pick<BasePermissionVariableFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* The permission variable to be edited.
|
* The permission variable to be edited.
|
||||||
*/
|
*/
|
||||||
originalVariable: CustomClaim;
|
originalVariable: PermissionVariable;
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -38,11 +38,14 @@ export default function EditPermissionVariableForm({
|
|||||||
}: EditPermissionVariableFormProps) {
|
}: EditPermissionVariableFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, error, loading } = useGetAppCustomClaimsQuery({
|
const { data, error, loading } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { customClaims: permissionVariables } =
|
||||||
|
data?.config?.auth?.session?.accessToken || {};
|
||||||
|
|
||||||
const form = useForm<BasePermissionVariableFormValues>({
|
const form = useForm<BasePermissionVariableFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
key: originalVariable.key || '',
|
key: originalVariable.key || '',
|
||||||
@@ -52,8 +55,8 @@ export default function EditPermissionVariableForm({
|
|||||||
resolver: yupResolver(basePermissionVariableValidationSchema),
|
resolver: yupResolver(basePermissionVariableValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: ['getAppCustomClaims'],
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -67,9 +70,8 @@ export default function EditPermissionVariableForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availablePermissionVariables = getPermissionVariables(
|
const availablePermissionVariables =
|
||||||
data?.app?.authJwtCustomClaims,
|
getAllPermissionVariables(permissionVariables);
|
||||||
);
|
|
||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
key,
|
key,
|
||||||
@@ -92,36 +94,43 @@ export default function EditPermissionVariableForm({
|
|||||||
(permissionVariable) => permissionVariable.key === originalVariable.key,
|
(permissionVariable) => permissionVariable.key === originalVariable.key,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedPermissionVariables = availablePermissionVariables.map(
|
const updatedPermissionVariables = availablePermissionVariables
|
||||||
(permissionVariable, index) => {
|
.map((permissionVariable, index) => {
|
||||||
if (index === originalPermissionVariableIndex) {
|
if (permissionVariable.isSystemVariable) {
|
||||||
return { key, value };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return permissionVariable;
|
if (index === originalPermissionVariableIndex) {
|
||||||
},
|
return {
|
||||||
);
|
key,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const permissionVariablesObject = getPermissionVariablesObject(
|
return {
|
||||||
updatedPermissionVariables.filter(
|
key: permissionVariable.key,
|
||||||
(permissionVariable) => !permissionVariable.isSystemClaim,
|
value: permissionVariable.value,
|
||||||
),
|
};
|
||||||
);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authJwtCustomClaims: {
|
auth: {
|
||||||
...permissionVariablesObject,
|
session: {
|
||||||
[key]: value,
|
accessToken: {
|
||||||
|
customClaims: updatedPermissionVariables,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Updating permission variable...',
|
loading: 'Updating permission variable...',
|
||||||
success: 'Permission variable has been updated successfully.',
|
success: 'Permission variable has been updated successfully.',
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||||
|
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { CustomClaim } from '@/types/application';
|
import type { PermissionVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -15,35 +18,33 @@ import List from '@/ui/v2/List';
|
|||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import Tooltip from '@/ui/v2/Tooltip';
|
import Tooltip from '@/ui/v2/Tooltip';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAppCustomClaimsQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export interface PermissionVariableSettingsFormValues {
|
|
||||||
/**
|
|
||||||
* Permission variables.
|
|
||||||
*/
|
|
||||||
authJwtCustomClaims: CustomClaim[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PermissionVariableSettings() {
|
export default function PermissionVariableSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { openDialog, openAlertDialog } = useDialog();
|
const { openDialog, openAlertDialog } = useDialog();
|
||||||
|
|
||||||
const { data, loading, error } = useGetAppCustomClaimsQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
fetchPolicy: 'cache-only',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const { customClaims: permissionVariables } =
|
||||||
refetchQueries: ['getAppCustomClaims'],
|
data?.config?.auth?.session?.accessToken || {};
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -56,40 +57,44 @@ export default function PermissionVariableSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteVariable({ key }: CustomClaim) {
|
async function handleDeleteVariable({ id }: PermissionVariable) {
|
||||||
const filteredCustomClaims = Object.keys(
|
const updateConfigPromise = updateConfig({
|
||||||
data?.app?.authJwtCustomClaims,
|
|
||||||
).filter((customClaimKey) => customClaimKey !== key);
|
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authJwtCustomClaims: filteredCustomClaims.reduce(
|
auth: {
|
||||||
(customClaims, currentKey) => ({
|
session: {
|
||||||
...customClaims,
|
accessToken: {
|
||||||
[currentKey]: data?.app?.authJwtCustomClaims[currentKey],
|
customClaims: permissionVariables
|
||||||
}),
|
?.filter((permissionVariable) => permissionVariable.id !== id)
|
||||||
{},
|
.map((permissionVariable) => ({
|
||||||
),
|
key: permissionVariable.key,
|
||||||
|
value: permissionVariable.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Deleting permission variable...',
|
loading: 'Deleting permission variable...',
|
||||||
success: 'Permission variable has been deleted successfully.',
|
success: 'Permission variable has been deleted successfully.',
|
||||||
error: 'An error occurred while trying to delete permission variable.',
|
error: getServerError(
|
||||||
|
'An error occurred while trying to delete permission variable.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenCreator() {
|
function handleOpenCreator() {
|
||||||
openDialog('CREATE_PERMISSION_VARIABLE', {
|
openDialog({
|
||||||
title: 'Create Permission Variable',
|
title: 'Create Permission Variable',
|
||||||
|
component: <CreatePermissionVariableForm />,
|
||||||
props: {
|
props: {
|
||||||
titleProps: { className: '!pb-0' },
|
titleProps: { className: '!pb-0' },
|
||||||
PaperProps: { className: 'max-w-sm' },
|
PaperProps: { className: 'max-w-sm' },
|
||||||
@@ -97,10 +102,12 @@ export default function PermissionVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenEditor(originalVariable: CustomClaim) {
|
function handleOpenEditor(originalVariable: PermissionVariable) {
|
||||||
openDialog('EDIT_PERMISSION_VARIABLE', {
|
openDialog({
|
||||||
title: 'Edit Permission Variable',
|
title: 'Edit Permission Variable',
|
||||||
payload: { originalVariable },
|
component: (
|
||||||
|
<EditPermissionVariableForm originalVariable={originalVariable} />
|
||||||
|
),
|
||||||
props: {
|
props: {
|
||||||
titleProps: { className: '!pb-0' },
|
titleProps: { className: '!pb-0' },
|
||||||
PaperProps: { className: 'max-w-sm' },
|
PaperProps: { className: 'max-w-sm' },
|
||||||
@@ -108,7 +115,7 @@ export default function PermissionVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirmDelete(originalVariable: CustomClaim) {
|
function handleConfirmDelete(originalVariable: PermissionVariable) {
|
||||||
openAlertDialog({
|
openAlertDialog({
|
||||||
title: 'Delete Permission Variable',
|
title: 'Delete Permission Variable',
|
||||||
payload: (
|
payload: (
|
||||||
@@ -126,9 +133,8 @@ export default function PermissionVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const availablePermissionVariables = getPermissionVariablesArray(
|
const availablePermissionVariables =
|
||||||
data?.app?.authJwtCustomClaims,
|
getAllPermissionVariables(permissionVariables);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
@@ -136,7 +142,7 @@ export default function PermissionVariableSettings() {
|
|||||||
description="Permission variables are used to define permission rules in the GraphQL API."
|
description="Permission variables are used to define permission rules in the GraphQL API."
|
||||||
docsLink="https://docs.nhost.io/graphql/permissions"
|
docsLink="https://docs.nhost.io/graphql/permissions"
|
||||||
rootClassName="gap-0"
|
rootClassName="gap-0"
|
||||||
className="px-0 my-2"
|
className="my-2 px-0"
|
||||||
slotProps={{ submitButton: { className: 'invisible' } }}
|
slotProps={{ submitButton: { className: 'invisible' } }}
|
||||||
>
|
>
|
||||||
<Box className="grid grid-cols-2 border-b-1 px-4 py-3">
|
<Box className="grid grid-cols-2 border-b-1 px-4 py-3">
|
||||||
@@ -146,28 +152,33 @@ export default function PermissionVariableSettings() {
|
|||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
<List>
|
<List>
|
||||||
{availablePermissionVariables.map((customClaim, index) => (
|
{availablePermissionVariables.map((permissionVariable, index) => (
|
||||||
<Fragment key={customClaim.key}>
|
<Fragment key={permissionVariable.id}>
|
||||||
<ListItem.Root
|
<ListItem.Root
|
||||||
className="px-4 grid grid-cols-2"
|
className="grid grid-cols-2 px-4"
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<Dropdown.Root>
|
<Dropdown.Root>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
customClaim.isSystemClaim
|
permissionVariable.isSystemVariable
|
||||||
? "You can't edit system permission variables"
|
? "You can't edit system permission variables"
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
disableHoverListener={!customClaim.isSystemClaim}
|
disableHoverListener={
|
||||||
hasDisabledChildren={customClaim.isSystemClaim}
|
!permissionVariable.isSystemVariable
|
||||||
|
}
|
||||||
|
hasDisabledChildren={permissionVariable.isSystemVariable}
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<Dropdown.Trigger asChild hideChevron>
|
<Dropdown.Trigger asChild hideChevron>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
disabled={customClaim.isSystemClaim}
|
disabled={
|
||||||
|
permissionVariable.isSystemVariable ||
|
||||||
|
maintenanceActive
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DotsVerticalIcon />
|
<DotsVerticalIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -187,7 +198,7 @@ export default function PermissionVariableSettings() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => handleOpenEditor(customClaim)}
|
onClick={() => handleOpenEditor(permissionVariable)}
|
||||||
>
|
>
|
||||||
<Text className="font-medium">Edit</Text>
|
<Text className="font-medium">Edit</Text>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
@@ -195,7 +206,7 @@ export default function PermissionVariableSettings() {
|
|||||||
<Divider component="li" />
|
<Divider component="li" />
|
||||||
|
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => handleConfirmDelete(customClaim)}
|
onClick={() => handleConfirmDelete(permissionVariable)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
@@ -213,15 +224,17 @@ export default function PermissionVariableSettings() {
|
|||||||
<ListItem.Text
|
<ListItem.Text
|
||||||
primary={
|
primary={
|
||||||
<>
|
<>
|
||||||
X-Hasura-{customClaim.key}{' '}
|
X-Hasura-{permissionVariable.key}{' '}
|
||||||
{customClaim.isSystemClaim && (
|
{permissionVariable.isSystemVariable && (
|
||||||
<LockIcon className="w-4 h-4" />
|
<LockIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text className="font-medium">user.{customClaim.value}</Text>
|
<Text className="font-medium">
|
||||||
|
user.{permissionVariable.value}
|
||||||
|
</Text>
|
||||||
</ListItem.Root>
|
</ListItem.Root>
|
||||||
|
|
||||||
<Divider
|
<Divider
|
||||||
@@ -237,10 +250,11 @@ export default function PermissionVariableSettings() {
|
|||||||
</List>
|
</List>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="justify-self-start mx-4"
|
className="mx-4 justify-self-start"
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
onClick={handleOpenCreator}
|
onClick={handleOpenCreator}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Create Permission Variable
|
Create Permission Variable
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export * from './PermissionVariableSettings';
|
|
||||||
export { default } from './PermissionVariableSettings';
|
export { default } from './PermissionVariableSettings';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
|
import type { DialogFormProps } from '@/types/common';
|
||||||
import { Alert } from '@/ui/Alert';
|
import { Alert } from '@/ui/Alert';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
@@ -8,14 +9,7 @@ import { useEffect } from 'react';
|
|||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BaseRoleFormValues {
|
export interface BaseRoleFormProps extends DialogFormProps {
|
||||||
/**
|
|
||||||
* The name of the role.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseRoleFormProps {
|
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -36,10 +30,15 @@ export const baseRoleFormValidationSchema = Yup.object({
|
|||||||
name: Yup.string().required('This field is required.'),
|
name: Yup.string().required('This field is required.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type BaseRoleFormValues = Yup.InferType<
|
||||||
|
typeof baseRoleFormValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function BaseRoleForm({
|
export default function BaseRoleForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
|
location,
|
||||||
}: BaseRoleFormProps) {
|
}: BaseRoleFormProps) {
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
const form = useFormContext<BaseRoleFormValues>();
|
const form = useFormContext<BaseRoleFormValues>();
|
||||||
@@ -52,8 +51,8 @@ export default function BaseRoleForm({
|
|||||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDirtyStateChange(isDirty, 'dialog');
|
onDirtyStateChange(isDirty, location);
|
||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-flow-row gap-3 px-6 pb-6">
|
<div className="grid grid-flow-row gap-3 px-6 pb-6">
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ import BaseRoleForm, {
|
|||||||
} from '@/components/settings/roles/BaseRoleForm';
|
} from '@/components/settings/roles/BaseRoleForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetRolesQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export interface CreateRoleFormProps
|
export interface CreateRoleFormProps
|
||||||
extends Pick<BaseRoleFormProps, 'onCancel'> {
|
extends Pick<BaseRoleFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -30,10 +32,11 @@ export default function CreateRoleForm({
|
|||||||
...props
|
...props
|
||||||
}: CreateRoleFormProps) {
|
}: CreateRoleFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetRolesQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
const { allowed: allowedRoles } = data?.config?.auth?.user?.roles || {};
|
||||||
|
|
||||||
const form = useForm<BaseRoleFormValues>({
|
const form = useForm<BaseRoleFormValues>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
@@ -41,7 +44,9 @@ export default function CreateRoleForm({
|
|||||||
resolver: yupResolver(baseRoleFormValidationSchema),
|
resolver: yupResolver(baseRoleFormValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['getRoles'] });
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ActivityIndicator delay={1000} label="Loading roles..." />;
|
return <ActivityIndicator delay={1000} label="Loading roles..." />;
|
||||||
@@ -52,7 +57,7 @@ export default function CreateRoleForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
const availableRoles = getUserRoles(allowedRoles);
|
||||||
|
|
||||||
async function handleSubmit({ name }: BaseRoleFormValues) {
|
async function handleSubmit({ name }: BaseRoleFormValues) {
|
||||||
if (availableRoles.some((role) => role.name === name)) {
|
if (availableRoles.some((role) => role.name === name)) {
|
||||||
@@ -61,26 +66,40 @@ export default function CreateRoleForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updatedAllowedRoles = allowedRoles ? [...allowedRoles, name] : [name];
|
||||||
|
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authUserDefaultAllowedRoles: `${data?.app?.authUserDefaultAllowedRoles},${name}`,
|
auth: {
|
||||||
|
user: {
|
||||||
|
roles: {
|
||||||
|
allowed: updatedAllowedRoles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppPromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Creating role...',
|
{
|
||||||
success: 'Role has been created successfully.',
|
loading: 'Creating role...',
|
||||||
error: 'An error occurred while trying to create the role.',
|
success: 'Role has been created successfully.',
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
'An error occurred while trying to create the role.',
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
await onSubmit?.();
|
onSubmit?.();
|
||||||
|
} catch (updateConfigError) {
|
||||||
|
console.error(updateConfigError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,17 +8,20 @@ import BaseRoleForm, {
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { Role } from '@/types/application';
|
import type { Role } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetRolesQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
export interface EditRoleFormProps extends Pick<BaseRoleFormProps, 'onCancel'> {
|
export interface EditRoleFormProps
|
||||||
|
extends Pick<BaseRoleFormProps, 'onCancel' | 'location'> {
|
||||||
/**
|
/**
|
||||||
* The role to be edited.
|
* The role to be edited.
|
||||||
*/
|
*/
|
||||||
@@ -35,11 +38,14 @@ export default function EditRoleForm({
|
|||||||
...props
|
...props
|
||||||
}: EditRoleFormProps) {
|
}: EditRoleFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetRolesQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { allowed: allowedRoles, default: defaultRole } =
|
||||||
|
data?.config?.auth?.user?.roles || {};
|
||||||
|
|
||||||
const form = useForm<BaseRoleFormValues>({
|
const form = useForm<BaseRoleFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: originalRole.name || '',
|
name: originalRole.name || '',
|
||||||
@@ -48,8 +54,8 @@ export default function EditRoleForm({
|
|||||||
resolver: yupResolver(baseRoleFormValidationSchema),
|
resolver: yupResolver(baseRoleFormValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: ['getRoles'],
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -61,7 +67,7 @@ export default function EditRoleForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
const availableRoles = getUserRoles(allowedRoles);
|
||||||
|
|
||||||
async function handleSubmit({ name }: BaseRoleFormValues) {
|
async function handleSubmit({ name }: BaseRoleFormValues) {
|
||||||
if (
|
if (
|
||||||
@@ -74,47 +80,55 @@ export default function EditRoleForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAllowedRolesList =
|
const defaultAllowedRolesList = allowedRoles || [];
|
||||||
data?.app?.authUserDefaultAllowedRoles.split(',') || [];
|
|
||||||
|
|
||||||
const originalRoleIndex = defaultAllowedRolesList.findIndex(
|
const originalRoleIndex = defaultAllowedRolesList.findIndex(
|
||||||
(role) => role.trim() === originalRole.name,
|
(role) => role.trim() === originalRole.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedDefaultAllowedRoles = defaultAllowedRolesList
|
const updatedDefaultAllowedRoles = defaultAllowedRolesList.map(
|
||||||
.map((role, index) => {
|
(role, index) => {
|
||||||
if (index === originalRoleIndex) {
|
if (index === originalRoleIndex) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return role;
|
return role;
|
||||||
})
|
},
|
||||||
.join(',');
|
);
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authUserDefaultRole:
|
auth: {
|
||||||
data?.app?.authUserDefaultRole === originalRole.name
|
user: {
|
||||||
? name
|
roles: {
|
||||||
: data?.app?.authUserDefaultRole,
|
default: defaultRole === originalRole.name ? name : defaultRole,
|
||||||
authUserDefaultAllowedRoles: updatedDefaultAllowedRoles,
|
allowed: updatedDefaultAllowedRoles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppPromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Updating role...',
|
{
|
||||||
success: 'Role has been updated successfully.',
|
loading: 'Updating role...',
|
||||||
error: 'An error occurred while trying to update the role.',
|
success: 'Role has been updated successfully.',
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
'An error occurred while trying to update the role.',
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
await onSubmit?.();
|
onSubmit?.();
|
||||||
|
} catch (updateConfigError) {
|
||||||
|
console.error(updateConfigError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user