Compare commits
405 Commits
@nhost/vue
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bbf6dbf1c | ||
|
|
689dc873b3 | ||
|
|
a0747d02e0 | ||
|
|
be5bd1e446 | ||
|
|
2c60591580 | ||
|
|
6140bc5b3b | ||
|
|
9f7780ec91 | ||
|
|
abc7d0c7a5 | ||
|
|
074a36ea48 | ||
|
|
64e806dc27 | ||
|
|
bd0e9748b6 | ||
|
|
b21222b378 | ||
|
|
7e217db128 | ||
|
|
56c716d9fa | ||
|
|
14ecbd1fb9 | ||
|
|
a0242c4d6f | ||
|
|
4800b4a756 | ||
|
|
5b318d17d4 | ||
|
|
2f9be4f760 | ||
|
|
64777b6f30 | ||
|
|
7e1489353e | ||
|
|
c53306a497 | ||
|
|
83345579d0 | ||
|
|
2b4b9e0385 | ||
|
|
922349f550 | ||
|
|
d613f3fd04 | ||
|
|
4d8a47777e | ||
|
|
229a7ab1f7 | ||
|
|
3dabb7b53a | ||
|
|
abc3d6ce60 | ||
|
|
08d49bd1fd | ||
|
|
03435a2c66 | ||
|
|
66208d6840 | ||
|
|
5be9abb0fa | ||
|
|
8e504b5328 | ||
|
|
0e3eb7204a | ||
|
|
a78cd2f18f | ||
|
|
6ef340daad | ||
|
|
a96e3c9163 | ||
|
|
9c5b6532d3 | ||
|
|
889df8ca4d | ||
|
|
b998e09e10 | ||
|
|
9e0486a362 | ||
|
|
74037cec68 | ||
|
|
819e68b501 | ||
|
|
10cc213933 | ||
|
|
4157c012fd | ||
|
|
9515096349 | ||
|
|
4dd5617855 | ||
|
|
01dc358842 | ||
|
|
925a1808e6 | ||
|
|
ac80f88727 | ||
|
|
3078247629 | ||
|
|
144c0084d2 | ||
|
|
bbdfb77a07 | ||
|
|
b5a9c1be47 | ||
|
|
5b5e7d9640 | ||
|
|
c02e0c63f2 | ||
|
|
788482fab2 | ||
|
|
16d94821b8 | ||
|
|
5c4ab54c90 | ||
|
|
5941568bbb | ||
|
|
1a9e1fde1d | ||
|
|
32c0632526 | ||
|
|
19cca7f45d | ||
|
|
191580a819 | ||
|
|
4a57861354 | ||
|
|
d96b817476 | ||
|
|
db6db8d860 | ||
|
|
fe405ba123 | ||
|
|
972a5f652f | ||
|
|
7a2c140524 | ||
|
|
3d717d68a9 | ||
|
|
13b6a47bef | ||
|
|
2a6caa47bd | ||
|
|
dbbfaef451 | ||
|
|
b4c07f1723 | ||
|
|
e983eb53d9 | ||
|
|
49867fdcf7 | ||
|
|
cc428d73ee | ||
|
|
940a1db0fc | ||
|
|
d70f29a408 | ||
|
|
52cb055520 | ||
|
|
8c406237a2 | ||
|
|
d036e282e5 | ||
|
|
39530cd8e8 | ||
|
|
8b58627608 | ||
|
|
c4e2d87e5c | ||
|
|
66b0378d38 | ||
|
|
22d7a36247 | ||
|
|
7d2eb2de66 | ||
|
|
4bba002c30 | ||
|
|
881a3344d4 | ||
|
|
d1562d33fb | ||
|
|
21ced66f22 | ||
|
|
2a2d86904d | ||
|
|
82d46f716b | ||
|
|
5e26810868 | ||
|
|
d590258371 | ||
|
|
697ef57cb8 | ||
|
|
93002dc8c3 | ||
|
|
baa1937d06 | ||
|
|
03e5662df9 | ||
|
|
e0711bdfc8 | ||
|
|
caca27fde3 | ||
|
|
b0d51033c6 | ||
|
|
088f9394fc | ||
|
|
a91361f971 | ||
|
|
a4f5be6ab9 | ||
|
|
dbbccbf1cd | ||
|
|
a70dc7b352 | ||
|
|
54df0df42b | ||
|
|
0bbb2598fd | ||
|
|
4ba34cc827 | ||
|
|
42ece48ce3 | ||
|
|
f97ab31f69 | ||
|
|
21dc1ecd6b | ||
|
|
c11adbe3e2 | ||
|
|
6078e9c207 | ||
|
|
450582dc43 | ||
|
|
ddec0e1be1 | ||
|
|
698154b24b | ||
|
|
9c87b0f67b | ||
|
|
85afe3d216 | ||
|
|
0773e5215f | ||
|
|
10f25fcc4e | ||
|
|
2ee90d6ea3 | ||
|
|
5db5323a1d | ||
|
|
ed93d4b583 | ||
|
|
1ec1953eaa | ||
|
|
63e9c3933e | ||
|
|
1b1620f633 | ||
|
|
e8e8d661e1 | ||
|
|
ba08ec7f5c | ||
|
|
fb0c98c21d | ||
|
|
c9f575c40c | ||
|
|
6c8bed7ecc | ||
|
|
d5a712f7ef | ||
|
|
83422f5ee6 | ||
|
|
51909a6a8f | ||
|
|
2f30797556 | ||
|
|
0b8f7d1661 | ||
|
|
52ee9d84b6 | ||
|
|
d9612b28b0 | ||
|
|
0034791493 | ||
|
|
80fed14a6b | ||
|
|
d457ada435 | ||
|
|
b41e5a9df5 | ||
|
|
0c8ace1bd4 | ||
|
|
3f800a068b | ||
|
|
7d490fe569 | ||
|
|
d6527122db | ||
|
|
3211140dec | ||
|
|
469352cd81 | ||
|
|
88400f6b7c | ||
|
|
f8c8a06d71 | ||
|
|
ebc1730fce | ||
|
|
c1cd1e813c | ||
|
|
e08a074474 | ||
|
|
2f819865bc | ||
|
|
3888f3041f | ||
|
|
569c4004f6 | ||
|
|
95932fa3f2 | ||
|
|
99402b77d1 | ||
|
|
f6fb2cd8e6 | ||
|
|
5c2cf59b41 | ||
|
|
a6d31dc260 | ||
|
|
872e50b635 | ||
|
|
bd73557a47 | ||
|
|
c95bab70c2 | ||
|
|
52d4b5de45 | ||
|
|
fe0742e278 | ||
|
|
ded57d3b24 | ||
|
|
c30abaea22 | ||
|
|
6b4ab50f74 | ||
|
|
ceba605d0b | ||
|
|
9249a85ee5 | ||
|
|
22de3214f1 | ||
|
|
cf880f992f | ||
|
|
f4d70f88e9 | ||
|
|
0d09b80b12 | ||
|
|
195adfb04a | ||
|
|
aee4cdcb72 | ||
|
|
b09930c8a4 | ||
|
|
687951281e | ||
|
|
c0f05acd9b | ||
|
|
87af60cc03 | ||
|
|
3d8dd39995 | ||
|
|
65687beecc | ||
|
|
62aa859737 | ||
|
|
d8c2d369aa | ||
|
|
a4e4926aeb | ||
|
|
9bc3e755df | ||
|
|
4a9471cc16 | ||
|
|
638a7ac11d | ||
|
|
567e370bdc | ||
|
|
a91f2db0e2 | ||
|
|
4e49c8db50 | ||
|
|
210f65b4db | ||
|
|
1b6482126f | ||
|
|
699debb2b8 | ||
|
|
3e08dc7f8c | ||
|
|
6928b48781 | ||
|
|
02886350ff | ||
|
|
b3672f8246 | ||
|
|
aea99ad2c8 | ||
|
|
594488e435 | ||
|
|
bb83b0f81a | ||
|
|
0384d7c7c4 | ||
|
|
7e356a9604 | ||
|
|
013e55a307 | ||
|
|
2a71257cde | ||
|
|
583a4401d0 | ||
|
|
914e91a0b0 | ||
|
|
98698213e2 | ||
|
|
756daa97cd | ||
|
|
ab5a2b119c | ||
|
|
ffdcce1463 | ||
|
|
0dbbcc5595 | ||
|
|
a3dcb6106e | ||
|
|
208d224763 | ||
|
|
816f8d069d | ||
|
|
2ebf99ff8f | ||
|
|
c13e492bbf | ||
|
|
564d000bfc | ||
|
|
63476a2351 | ||
|
|
266fda07ab | ||
|
|
782252c059 | ||
|
|
e86978a1ff | ||
|
|
84cfd11953 | ||
|
|
9a43e136f6 | ||
|
|
e9cff26fa0 | ||
|
|
3d32bca2b3 | ||
|
|
4021feaf38 | ||
|
|
6174e1ddcc | ||
|
|
af2e3eae37 | ||
|
|
d2b4c126f3 | ||
|
|
7f2e182c47 | ||
|
|
306ec74356 | ||
|
|
ae40bd54d4 | ||
|
|
b5cc47078a | ||
|
|
7f251111e2 | ||
|
|
c03dacc3a3 | ||
|
|
8b9e1a0ce8 | ||
|
|
cf9cfec330 | ||
|
|
1c94f56c59 | ||
|
|
f06d5deba3 | ||
|
|
8ff58d7f23 | ||
|
|
8dd1c7415b | ||
|
|
ebd2749e38 | ||
|
|
80b604adda | ||
|
|
9d73050792 | ||
|
|
2a86b8876c | ||
|
|
91a1a41f5d | ||
|
|
22e9c27c81 | ||
|
|
2d2beb53d2 | ||
|
|
b403b0d6a0 | ||
|
|
4dbac55cb4 | ||
|
|
c6e31ac741 | ||
|
|
0d3e8b3992 | ||
|
|
b2afd14d61 | ||
|
|
f28f28b6ee | ||
|
|
834b959271 | ||
|
|
4dbc9ccc87 | ||
|
|
2764a1c4b6 | ||
|
|
1666ca2ec5 | ||
|
|
346791d4d5 | ||
|
|
94bdafe22f | ||
|
|
33782e9d41 | ||
|
|
ea02e1e104 | ||
|
|
98bf6e3792 | ||
|
|
d9dcafd643 | ||
|
|
4f3d97b5ad | ||
|
|
d1801ceae9 | ||
|
|
b6f9fe6304 | ||
|
|
15b652d7e0 | ||
|
|
cdc0047cb7 | ||
|
|
7c0e71e8be | ||
|
|
175d6d8cbc | ||
|
|
2ae277409a | ||
|
|
ada10170b7 | ||
|
|
0ed77cbe8b | ||
|
|
1abc68992f | ||
|
|
795962e3c2 | ||
|
|
ba998eb632 | ||
|
|
d3fc1bbeb9 | ||
|
|
7b9c2016d0 | ||
|
|
ebf4070be6 | ||
|
|
59e3f6abc6 | ||
|
|
0c9a03a7ff | ||
|
|
42cbe27914 | ||
|
|
d7e7b0e51b | ||
|
|
036181dd75 | ||
|
|
2828a9fe01 | ||
|
|
88bc71dffc | ||
|
|
17699870a6 | ||
|
|
5c0304ab73 | ||
|
|
637265e3d9 | ||
|
|
229604d8e1 | ||
|
|
1165a33079 | ||
|
|
82c0dd9d87 | ||
|
|
a9bfab1778 | ||
|
|
60b7a664d2 | ||
|
|
4f708d04d6 | ||
|
|
a02af56056 | ||
|
|
575136dcfb | ||
|
|
2bc6346cbc | ||
|
|
91cd494a3d | ||
|
|
d57e0a5287 | ||
|
|
dd5e7093f0 | ||
|
|
3e9bb84f07 | ||
|
|
6665b58ec8 | ||
|
|
456e893cd6 | ||
|
|
fd76170ca6 | ||
|
|
682aef2d94 | ||
|
|
cb53f71d4a | ||
|
|
4fc0b40cb4 | ||
|
|
9da2d01c55 | ||
|
|
0d5e7850f8 | ||
|
|
8ed965c669 | ||
|
|
cb284b40b1 | ||
|
|
c59f622feb | ||
|
|
9a30edd038 | ||
|
|
4b2d2a4f55 | ||
|
|
eba2bd05b8 | ||
|
|
84a1b28261 | ||
|
|
ff3b9df41e | ||
|
|
b8b4e36175 | ||
|
|
6e47ef68d5 | ||
|
|
2697414637 | ||
|
|
e753b2faed | ||
|
|
6f62ec7d2a | ||
|
|
d365ef1953 | ||
|
|
99ee9fd10d | ||
|
|
9608a327c9 | ||
|
|
492b83ef58 | ||
|
|
09f53ae43f | ||
|
|
7724ac7e06 | ||
|
|
02343fe171 | ||
|
|
98dd8d039c | ||
|
|
456e057497 | ||
|
|
93e9b58a58 | ||
|
|
019a7c2335 | ||
|
|
a0901914ac | ||
|
|
64882a8e16 | ||
|
|
1ed572fe39 | ||
|
|
e06271a8ae | ||
|
|
bca239ebb2 | ||
|
|
e33caa046d | ||
|
|
cbbd331341 | ||
|
|
c41bfaffdd | ||
|
|
f2596b0b14 | ||
|
|
4d0c3111d1 | ||
|
|
7ff9644ac7 | ||
|
|
87fdaa7144 | ||
|
|
132a4f4be9 | ||
|
|
6b31e31430 | ||
|
|
9ba2208dd7 | ||
|
|
f1272947dd | ||
|
|
e5041bfd30 | ||
|
|
3d7cc74feb | ||
|
|
d007202783 | ||
|
|
38e92a705d | ||
|
|
74cc63833a | ||
|
|
425320bbb5 | ||
|
|
499352ad8a | ||
|
|
023060cee6 | ||
|
|
db57572f38 | ||
|
|
c9f88326b2 | ||
|
|
ac8efcbdd5 | ||
|
|
9bc346e8d4 | ||
|
|
efed987d31 | ||
|
|
bdab7da7d3 | ||
|
|
c2d9993968 | ||
|
|
508ba62207 | ||
|
|
a3318de06e | ||
|
|
26d577d7ae | ||
|
|
fa9f7ca052 | ||
|
|
622c48a94b | ||
|
|
e1a87a05b1 | ||
|
|
2148317282 | ||
|
|
5f9c6c8346 | ||
|
|
2e30371086 | ||
|
|
57db5b83d4 | ||
|
|
66659bb293 | ||
|
|
579d4f3170 | ||
|
|
bb56548603 | ||
|
|
3cab18713a | ||
|
|
fb94dae43a | ||
|
|
f694846eae | ||
|
|
322ab50138 | ||
|
|
435efd2bc5 | ||
|
|
5501a5937e | ||
|
|
80a6808a82 | ||
|
|
987bd70312 | ||
|
|
feb22e62c1 | ||
|
|
e7d4c77a6d | ||
|
|
628e32464d | ||
|
|
dc82043254 | ||
|
|
997e9d58a8 | ||
|
|
8a3f1706fe | ||
|
|
12cbe4d534 | ||
|
|
9f21931201 | ||
|
|
09351e1910 | ||
|
|
1c2ea5a407 |
@@ -2,9 +2,22 @@
|
||||
"$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"linked": [],
|
||||
"linked": [
|
||||
[
|
||||
"@nhost/nextjs",
|
||||
"@nhost/react",
|
||||
"@nhost/vue",
|
||||
"@nhost/nhost-js",
|
||||
"@nhost/hasura-auth-js",
|
||||
"@nhost/hasura-storage-js"
|
||||
],
|
||||
[
|
||||
"@nhost/react-apollo",
|
||||
"@nhost/apollo"
|
||||
]
|
||||
],
|
||||
"access": "restricted",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
}
|
||||
108
.github/actions/nhost-cli/README.md
vendored
Normal file
108
.github/actions/nhost-cli/README.md
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Nhost CLI GitHub Action
|
||||
|
||||
## Usage
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
```
|
||||
|
||||
### Install the CLI and start the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
```
|
||||
|
||||
### Set another working directory
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: examples/react-apollo
|
||||
start: true
|
||||
```
|
||||
|
||||
### Don't wait for the app to be ready
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI and start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
wait: false
|
||||
```
|
||||
|
||||
### Stop the app
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Start app
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
start: true
|
||||
- name: Do something
|
||||
cmd: echo "do something"
|
||||
- name: Stop
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
stop: true
|
||||
```
|
||||
|
||||
### Install a given value of the CLI
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
```
|
||||
|
||||
### Inject values into nhost/config.yaml
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.16.1
|
||||
```
|
||||
78
.github/actions/nhost-cli/action.yaml
vendored
Normal file
78
.github/actions/nhost-cli/action.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Nhost CLI
|
||||
description: 'Action to install the Nhost CLI and to run an application'
|
||||
inputs:
|
||||
start:
|
||||
description: "Start the application. If false, the application won't be started"
|
||||
default: 'false'
|
||||
wait:
|
||||
description: 'If starting the application, wait until it is ready'
|
||||
default: 'true'
|
||||
stop:
|
||||
description: 'Stop the application'
|
||||
default: 'false'
|
||||
path:
|
||||
description: 'Path to the application'
|
||||
default: '.'
|
||||
version:
|
||||
description: 'Version of the Nhost CLI'
|
||||
default: 'latest'
|
||||
config:
|
||||
description: 'Values to be injected into nhost/config.yaml'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Check if Nhost CLI is already installed
|
||||
id: check-nhost-cli
|
||||
shell: bash
|
||||
# TODO check if the version is the same
|
||||
run: |
|
||||
if [ -z "$(which nhost)" ]
|
||||
then
|
||||
echo "installed=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "installed=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
- name: Install Nhost CLI
|
||||
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 3
|
||||
max_attempts: 3
|
||||
command: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
- name: Set custom configuration
|
||||
if: ${{ inputs.config }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: config="${{ inputs.config }}" yq -i '. *= env(config)' nhost/config.yaml
|
||||
- name: Start the application
|
||||
if: ${{ inputs.start == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost dev --no-browser &
|
||||
- name: Wait for the app to be ready
|
||||
id: wait
|
||||
if: ${{ inputs.start == 'true' && inputs.wait == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
continue-on-error: true
|
||||
run: |
|
||||
curl -sSf --connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry 300 \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time 300 \
|
||||
--retry-connrefused \
|
||||
'http://localhost:9695' > /dev/null
|
||||
- name: Log on failure
|
||||
if: steps.wait.outcome == 'failure'
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: |
|
||||
nhost logs
|
||||
exit 1
|
||||
- name: Stop the application
|
||||
if: ${{ inputs.stop == 'true' }}
|
||||
shell: bash
|
||||
working-directory: ${{ inputs.path }}
|
||||
run: nhost down
|
||||
7
.github/labeler.yml
vendored
7
.github/labeler.yml
vendored
@@ -12,11 +12,14 @@ examples:
|
||||
sdk:
|
||||
- packages/**/*
|
||||
|
||||
integrations:
|
||||
- integrations/**/*
|
||||
|
||||
react:
|
||||
- '{packages,examples}/*react*/**/*'
|
||||
- '{packages,examples,integrations}/*react*/**/*'
|
||||
|
||||
nextjs:
|
||||
- '{packages,examples}/*next*/**/*'
|
||||
|
||||
vue:
|
||||
- '{packages,examples}/*vue*/**/*'
|
||||
- '{packages,examples,integrations}/*vue*/**/*'
|
||||
|
||||
12
.github/renovate.json
vendored
12
.github/renovate.json
vendored
@@ -3,11 +3,21 @@
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"docker-compose": {
|
||||
"enabled": true
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"pnpm",
|
||||
"node"
|
||||
"node",
|
||||
"@types/node"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"npm",
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions"
|
||||
]
|
||||
}
|
||||
19
.github/workflows/ci.yaml
vendored
19
.github/workflows/ci.yaml
vendored
@@ -36,13 +36,24 @@ jobs:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
# * If the lockfile has changed, we don't filter anything in order to run all the e2e tests.
|
||||
- name: filter packages
|
||||
id: filter-packages
|
||||
if: steps.changed-lockfile.outputs.any_changed != 'true' && github.event_name == 'pull_request'
|
||||
run: echo "filter=${{ format('--filter=...[origin/{0}]', github.base_ref) }}" >> $GITHUB_OUTPUT
|
||||
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
||||
# * In a PR, only include packages that have been modified, and their dependencies
|
||||
- name: List examples with an e2e script
|
||||
id: set-matrix
|
||||
run: |
|
||||
FILTER_MODIFIED="${{ github.event_name == 'pull_request' && format('--filter=...[origin/{0}]', github.base_ref) || '' }}"
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' $FILTER_MODIFIED \
|
||||
PACKAGES=$(pnpm recursive list --depth -1 --parseable --filter='!nhost-root' ${{ steps.filter-packages.outputs.filter }} \
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp)
|
||||
@@ -51,7 +62,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
e2e:
|
||||
name: 'e2e: ${{ matrix.package.name }}'
|
||||
name: 'e2e (${{ matrix.package.path }})'
|
||||
needs: build
|
||||
if: ${{ needs.build.outputs.matrix != '[]' && needs.build.outputs.matrix != '' }}
|
||||
strategy:
|
||||
@@ -71,7 +82,7 @@ jobs:
|
||||
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
||||
- name: Install Nhost CLI
|
||||
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
|
||||
run: curl -L https://raw.githubusercontent.com/nhost/cli/main/get.sh | bash
|
||||
uses: ./.github/actions/nhost-cli
|
||||
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
|
||||
- name: Run e2e test
|
||||
run: pnpm --filter="${{ matrix.package.name }}" run e2e
|
||||
|
||||
15
.github/workflows/contributors.yaml
vendored
15
.github/workflows/contributors.yaml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Add contributors
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
contrib-readme-job:
|
||||
runs-on: ubuntu-latest
|
||||
name: A job to automate contrib in readme
|
||||
steps:
|
||||
- name: Contribute List
|
||||
uses: akhilmhdh/contributors-readme-action@v2.3.6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
1
.github/workflows/labeler.yaml
vendored
1
.github/workflows/labeler.yaml
vendored
@@ -12,3 +12,4 @@ jobs:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: '${{ secrets.GH_PAT }}'
|
||||
sync-labels: ''
|
||||
|
||||
30
.github/workflows/renovate.yaml
vendored
30
.github/workflows/renovate.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
- name: Determine bumps
|
||||
id: bumps
|
||||
run: |
|
||||
LAST_NON_PR_SHA=$(git log --no-merges main origin/${{ github.head_ref }} --format=format:%h | head -2 | tail -1)
|
||||
LAST_NON_PR_SHA=$(git log --no-merges main origin/${{ github.head_ref }} --format=format:%h -- | head -2 | tail -1)
|
||||
echo "result<<EOF" >> $GITHUB_OUTPUT
|
||||
pnpm recursive list --depth -1 --parseable \
|
||||
--filter='!nhost-root' \
|
||||
@@ -62,8 +62,28 @@ jobs:
|
||||
|
||||
${{ github.event.pull_request.title }}
|
||||
EOF
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
if: steps.bumps.outputs.result != ''
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
commit_message: ${{ github.event.pull_request.title }}
|
||||
branch: main
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
commit-message: ${{ github.event.pull_request.title }}
|
||||
branch: renovate-changesets
|
||||
delete-branch: true
|
||||
title: 'chore: create changesest from Renovate bumps'
|
||||
labels: |
|
||||
dependencies
|
||||
body: |
|
||||
This PR creates the changesets from the Renovate dependencies that have been merged to main.
|
||||
- name: Enable Pull Request Automerge
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
uses: peter-evans/enable-pull-request-automerge@v2
|
||||
with:
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
- name: Auto approve
|
||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||
uses: juliangruber/approve-pull-request-action@v2
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PAT }}
|
||||
number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||
|
||||
104
.github/workflows/test-nhost-cli-action.yaml
vendored
Normal file
104
.github/workflows/test-nhost-cli-action.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
name: Test Nhost CLI action
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- '.github/actions/nhost-cli/**'
|
||||
- '!.github/actions/nhost-cli/**/*.md'
|
||||
|
||||
jobs:
|
||||
install:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
- name: should succeed running the nhost command
|
||||
run: nhost
|
||||
|
||||
start:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
- name: should be running
|
||||
run: curl -sSf 'http://localhost:9695' > /dev/null
|
||||
|
||||
stop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI, start and stop the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
stop: true
|
||||
- name: should have no live docker container
|
||||
run: |
|
||||
if [ -z "docker ps -q" ]; then
|
||||
echo "Some docker containers are still running"
|
||||
docker ps
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and start the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
wait: false
|
||||
- name: should not be ready
|
||||
run: curl -sSf -o /dev/null 'http://localhost:9695' > /dev/null && exit 1 || true
|
||||
- name: should eventually be ready
|
||||
run: |
|
||||
curl -sSf --connect-timeout 3 \
|
||||
--max-time 5 \
|
||||
--retry 300 \
|
||||
--retry-delay 1 \
|
||||
--retry-max-time 300 \
|
||||
--retry-connrefused \
|
||||
'http://localhost:9695' > /dev/null
|
||||
|
||||
config:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI and run the application
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
path: packages/nhost-js
|
||||
start: true
|
||||
config: |
|
||||
services:
|
||||
auth:
|
||||
image: nhost/hasura-auth:0.15.0
|
||||
- name: should find the injected hasura-auth version
|
||||
run: |
|
||||
VERSION=$(curl -sSf 'http://localhost:1337/v1/auth/version')
|
||||
EXPECTED_VERSION='{"version":"v0.15.0"}'
|
||||
if [ "$VERSION" != "$EXPECTED_VERSION" ]; then
|
||||
echo "Expected version $EXPECTED_VERSION but got $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install the Nhost CLI
|
||||
uses: ./.github/actions/nhost-cli
|
||||
with:
|
||||
version: v0.8.10
|
||||
- name: should find the correct version
|
||||
run: nhost version | head -n 1 | grep v0.8.10 || exit 1
|
||||
@@ -14,4 +14,5 @@ package.json
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
*.d.ts
|
||||
.next
|
||||
.next
|
||||
**/pnpm-lock.yaml
|
||||
435
README.md
435
README.md
@@ -101,14 +101,17 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript SDK](https://docs.nhost.io/reference/javascript)
|
||||
- [Dart and Flutter SDK](https://github.com/nhost/nhost-dart)
|
||||
- [Nhost React](https://docs.nhost.io/reference/react)
|
||||
- [Nhost Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
- [Nhost Vue](https://docs.nhost.io/reference/vue)
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript)
|
||||
- [Dart and Flutter](https://github.com/nhost/nhost-dart)
|
||||
- [React](https://docs.nhost.io/reference/react)
|
||||
- [Next.js](https://docs.nhost.io/reference/nextjs)
|
||||
- [Vue](https://docs.nhost.io/reference/vue)
|
||||
|
||||
## Integrations
|
||||
|
||||
- [Apollo](./integrations/apollo#nhostapollo)
|
||||
- [React Apollo](./integrations/react-apollo#nhostreact-apollo)
|
||||
- [React URQL](./integrations/react-urql#nhostreact-urql)
|
||||
- [Stripe GraphQL API](./integrations/stripe-graphql-js#nhoststripe-graphql-js)
|
||||
- [Google Translation GraphQL API](./integrations/google-translation#nhostgoogle-translation)
|
||||
|
||||
@@ -127,8 +130,8 @@ Also, follow Nhost on [GitHub Discussions](https://github.com/nhost/nhost/discus
|
||||
|
||||
This repository, and most of our other open source projects, are licensed under the MIT license.
|
||||
|
||||
<a href="https://runacap.com/ross-index/q1-2022/" target="_blank" rel="noopener">
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2022/06/ROSS_badge_black_Q1_2022.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q1 2022 | Runa Capital" width="260" height="56" />
|
||||
<a href="https://runacap.com/ross-index/" target="_blank" rel="noopener" >
|
||||
<img style="width: 260px; height: 56px" src="https://runacap.com/wp-content/uploads/2022/06/ROSS_black_edition_badge.svg" alt="ROSS Index - Fastest Growing Open-Source Startups | Runa Capital" width="260" height="56" />
|
||||
</a>
|
||||
|
||||
### How to contribute
|
||||
@@ -141,416 +144,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
|
||||
### Contributors
|
||||
|
||||
<!-- readme: contributors -start -->
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/plmercereau">
|
||||
<img src="https://avatars.githubusercontent.com/u/24897252?v=4" width="100;" alt="plmercereau"/>
|
||||
<br />
|
||||
<sub><b>Pilou</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elitan">
|
||||
<img src="https://avatars.githubusercontent.com/u/331818?v=4" width="100;" alt="elitan"/>
|
||||
<br />
|
||||
<sub><b>Johan Eliasson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/szilarddoro">
|
||||
<img src="https://avatars.githubusercontent.com/u/310881?v=4" width="100;" alt="szilarddoro"/>
|
||||
<br />
|
||||
<sub><b>Szilárd Dóró</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nunopato">
|
||||
<img src="https://avatars.githubusercontent.com/u/1523504?v=4" width="100;" alt="nunopato"/>
|
||||
<br />
|
||||
<sub><b>Nuno Pato</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gdangelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/4352286?v=4" width="100;" alt="gdangelo"/>
|
||||
<br />
|
||||
<sub><b>Grégory D'Angelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/guicurcio">
|
||||
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
|
||||
<br />
|
||||
<sub><b>Guido Curcio</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subatuba21">
|
||||
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
|
||||
<br />
|
||||
<sub><b>Subha Das</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/sebagudelo">
|
||||
<img src="https://avatars.githubusercontent.com/u/43288271?v=4" width="100;" alt="sebagudelo"/>
|
||||
<br />
|
||||
<sub><b>Sebagudelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mrinalwahal">
|
||||
<img src="https://avatars.githubusercontent.com/u/9859731?v=4" width="100;" alt="mrinalwahal"/>
|
||||
<br />
|
||||
<sub><b>Mrinal Wahal</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/timpratim">
|
||||
<img src="https://avatars.githubusercontent.com/u/32492961?v=4" width="100;" alt="timpratim"/>
|
||||
<br />
|
||||
<sub><b>Pratim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrtze">
|
||||
<img src="https://avatars.githubusercontent.com/u/3797215?v=4" width="100;" alt="chrtze"/>
|
||||
<br />
|
||||
<sub><b>Christopher Möller</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/GavanWilhite">
|
||||
<img src="https://avatars.githubusercontent.com/u/2085119?v=4" width="100;" alt="GavanWilhite"/>
|
||||
<br />
|
||||
<sub><b>Gavan Wilhite</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FuzzyReason">
|
||||
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
|
||||
<br />
|
||||
<sub><b>Vadim Smirnov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/macmac49">
|
||||
<img src="https://avatars.githubusercontent.com/u/831190?v=4" width="100;" alt="macmac49"/>
|
||||
<br />
|
||||
<sub><b>Macmac49</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subhendukundu">
|
||||
<img src="https://avatars.githubusercontent.com/u/20059141?v=4" width="100;" alt="subhendukundu"/>
|
||||
<br />
|
||||
<sub><b>Subhendu Kundu</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/heygambo">
|
||||
<img src="https://avatars.githubusercontent.com/u/449438?v=4" width="100;" alt="heygambo"/>
|
||||
<br />
|
||||
<sub><b>Christian Gambardella</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dbarrosop">
|
||||
<img src="https://avatars.githubusercontent.com/u/6246622?v=4" width="100;" alt="dbarrosop"/>
|
||||
<br />
|
||||
<sub><b>David Barroso</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/hajek-raven">
|
||||
<img src="https://avatars.githubusercontent.com/u/7288737?v=4" width="100;" alt="hajek-raven"/>
|
||||
<br />
|
||||
<sub><b>Filip Hájek</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MelodicCrypter">
|
||||
<img src="https://avatars.githubusercontent.com/u/18341500?v=4" width="100;" alt="MelodicCrypter"/>
|
||||
<br />
|
||||
<sub><b>Hugh Caluscusin</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jerryjappinen">
|
||||
<img src="https://avatars.githubusercontent.com/u/1101002?v=4" width="100;" alt="jerryjappinen"/>
|
||||
<br />
|
||||
<sub><b>Jerry Jäppinen</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mdp18">
|
||||
<img src="https://avatars.githubusercontent.com/u/11698527?v=4" width="100;" alt="mdp18"/>
|
||||
<br />
|
||||
<sub><b>Max</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mustafa-hanif">
|
||||
<img src="https://avatars.githubusercontent.com/u/30019262?v=4" width="100;" alt="mustafa-hanif"/>
|
||||
<br />
|
||||
<sub><b>Mustafa Hanif</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nbourdin">
|
||||
<img src="https://avatars.githubusercontent.com/u/5602476?v=4" width="100;" alt="nbourdin"/>
|
||||
<br />
|
||||
<sub><b>Nicolas Bourdin</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/piromsurang">
|
||||
<img src="https://avatars.githubusercontent.com/u/17776837?v=4" width="100;" alt="piromsurang"/>
|
||||
<br />
|
||||
<sub><b>Piromsurang Rungserichai</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Savinvadim1312">
|
||||
<img src="https://avatars.githubusercontent.com/u/16936043?v=4" width="100;" alt="Savinvadim1312"/>
|
||||
<br />
|
||||
<sub><b>Savin Vadim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Svarto">
|
||||
<img src="https://avatars.githubusercontent.com/u/24279217?v=4" width="100;" alt="Svarto"/>
|
||||
<br />
|
||||
<sub><b>Svarto</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/muttenzer">
|
||||
<img src="https://avatars.githubusercontent.com/u/49474412?v=4" width="100;" alt="muttenzer"/>
|
||||
<br />
|
||||
<sub><b>Muttenzer</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alexander-mart">
|
||||
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
|
||||
<br />
|
||||
<sub><b>Alexander Mart</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ahmic">
|
||||
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
|
||||
<br />
|
||||
<sub><b>Amir Ahmic</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/akd-io">
|
||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||
<br />
|
||||
<sub><b>Anders Kjær Damgaard</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Sonichigo">
|
||||
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
|
||||
<br />
|
||||
<sub><b>Animesh Pathak</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrisli-03">
|
||||
<img src="https://avatars.githubusercontent.com/u/11177048?v=4" width="100;" alt="chrisli-03"/>
|
||||
<br />
|
||||
<sub><b>Chris</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/massless">
|
||||
<img src="https://avatars.githubusercontent.com/u/44389?v=4" width="100;" alt="massless"/>
|
||||
<br />
|
||||
<sub><b>Chris Wetherell</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
<br />
|
||||
<sub><b>Colin Broderick</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/daguitosama">
|
||||
<img src="https://avatars.githubusercontent.com/u/34744883?v=4" width="100;" alt="daguitosama"/>
|
||||
<br />
|
||||
<sub><b>Dago</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dminkovsky">
|
||||
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
||||
<br />
|
||||
<sub><b>Dmitry Minkovsky</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dohomi">
|
||||
<img src="https://avatars.githubusercontent.com/u/489221?v=4" width="100;" alt="dohomi"/>
|
||||
<br />
|
||||
<sub><b>Dominic Garms</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/gaurav1999">
|
||||
<img src="https://avatars.githubusercontent.com/u/20752142?v=4" width="100;" alt="gaurav1999"/>
|
||||
<br />
|
||||
<sub><b>Gaurav Agrawal</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alveshelio">
|
||||
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
||||
<br />
|
||||
<sub><b>Helio Alves</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nkhdo">
|
||||
<img src="https://avatars.githubusercontent.com/u/26102306?v=4" width="100;" alt="nkhdo"/>
|
||||
<br />
|
||||
<sub><b>Hoang Do</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/eltociear">
|
||||
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
|
||||
<br />
|
||||
<sub><b>Ikko Ashimine</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jladuval">
|
||||
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
||||
<br />
|
||||
<sub><b>Jacob Duval</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/leothorp">
|
||||
<img src="https://avatars.githubusercontent.com/u/12928449?v=4" width="100;" alt="leothorp"/>
|
||||
<br />
|
||||
<sub><b>Leo Thorp</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/LucasBois1">
|
||||
<img src="https://avatars.githubusercontent.com/u/44686060?v=4" width="100;" alt="LucasBois1"/>
|
||||
<br />
|
||||
<sub><b>Lucas Bois</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MarcelloTheArcane">
|
||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||
<br />
|
||||
<sub><b>Max Reynolds</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nachoaldamav">
|
||||
<img src="https://avatars.githubusercontent.com/u/22749943?v=4" width="100;" alt="nachoaldamav"/>
|
||||
<br />
|
||||
<sub><b>Nacho Aldama</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ghoshnirmalya">
|
||||
<img src="https://avatars.githubusercontent.com/u/6391763?v=4" width="100;" alt="ghoshnirmalya"/>
|
||||
<br />
|
||||
<sub><b>Nirmalya Ghosh</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/quentin-decre">
|
||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||
<br />
|
||||
<sub><b>Quentin Decré</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elephant3">
|
||||
<img src="https://avatars.githubusercontent.com/u/48279149?v=4" width="100;" alt="elephant3"/>
|
||||
<br />
|
||||
<sub><b>Siarhei Lipchyk</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/altschuler">
|
||||
<img src="https://avatars.githubusercontent.com/u/956928?v=4" width="100;" alt="altschuler"/>
|
||||
<br />
|
||||
<sub><b>Simon Altschuler</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/atapas">
|
||||
<img src="https://avatars.githubusercontent.com/u/3633137?v=4" width="100;" alt="atapas"/>
|
||||
<br />
|
||||
<sub><b>Tapas Adhikary</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/uulwake">
|
||||
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
|
||||
<br />
|
||||
<sub><b>Ulrich Wake</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/kwarabei">
|
||||
<img src="https://avatars.githubusercontent.com/u/102731455?v=4" width="100;" alt="kwarabei"/>
|
||||
<br />
|
||||
<sub><b>Vadim</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/TheRedLancer">
|
||||
<img src="https://avatars.githubusercontent.com/u/58493767?v=4" width="100;" alt="TheRedLancer"/>
|
||||
<br />
|
||||
<sub><b>Zach Burnaby</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/komninoschat">
|
||||
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
||||
<br />
|
||||
<sub><b>Komninos</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/meesvandongen">
|
||||
<img src="https://avatars.githubusercontent.com/u/35409045?v=4" width="100;" alt="meesvandongen"/>
|
||||
<br />
|
||||
<sub><b>Meesvandongen</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
</table>
|
||||
<!-- readme: contributors -end -->
|
||||
<a href="https://github.com/nhost/nhost/graphs/contributors">
|
||||
<p align="center">
|
||||
<img width="720" src="https://contrib.rocks/image?repo=nhost/nhost" alt="A table of avatars from the project's contributors" />
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -19,7 +19,9 @@ module.exports = {
|
||||
'*.spec.ts',
|
||||
'*.spec.tsx',
|
||||
'tests/**/*.ts',
|
||||
'tests/**/*.d.ts'
|
||||
'tests/**/*.d.ts',
|
||||
'e2e/**/*.ts',
|
||||
'e2e/**/*.d.ts'
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'cypress'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
module.exports = {
|
||||
'(packages|integrations)/(docgen|hasura-auth-js|hasura-storage-js|nextjs|nhost-js|react|core|vue)/src/**/*.{js,ts,jsx,tsx}':
|
||||
['pnpm docgen', 'git add docs'],
|
||||
'(nhost-cloud.yaml|**/nhost/config.yaml)': () => [
|
||||
'pnpm sync-versions',
|
||||
"git add ':(glob)**/nhost/config.yaml'"
|
||||
|
||||
@@ -28,22 +28,44 @@
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"],
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"typeRoots": [
|
||||
"./node_modules/@types", "**/*/dist", "**/*/build", "**/*/.next", "**/*/umd"
|
||||
"./node_modules/@types",
|
||||
"**/*/dist",
|
||||
"**/*/build",
|
||||
"**/*/.next",
|
||||
"**/*/umd"
|
||||
],
|
||||
"paths": {
|
||||
"@nhost/apollo": ["../packages/apollo/src/index.ts"],
|
||||
"@nhost/core": ["../packages/core/src/index.ts"],
|
||||
"@nhost/docgen": ["../packages/docgen/src/index.ts"],
|
||||
"@nhost/hasura-auth-js": ["../packages/hasura-auth-js/src/index.ts"],
|
||||
"@nhost/hasura-storage-js": ["../packages/hasura-storage-js/src/index.ts"],
|
||||
"@nhost/nextjs": ["../packages/nextjs/src/index.ts"],
|
||||
"@nhost/nhost-js": ["../packages/nhost-js/src/index.ts"],
|
||||
"@nhost/react": ["../packages/react/src/index.ts"],
|
||||
"@nhost/react-apollo": ["../packages/react-apollo/src/index.ts"],
|
||||
"@nhost/react-auth": ["../packages/react-auth/src/index.ts"],
|
||||
"@nhost/vue": ["../packages/vue/src/index.ts"]
|
||||
"@nhost/apollo": [
|
||||
"../integrations/apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-storage-js": [
|
||||
"../packages/hasura-storage-js/src/index.ts"
|
||||
],
|
||||
"@nhost/nextjs": [
|
||||
"../packages/nextjs/src/index.ts"
|
||||
],
|
||||
"@nhost/nhost-js": [
|
||||
"../packages/nhost-js/src/index.ts"
|
||||
],
|
||||
"@nhost/react": [
|
||||
"../packages/react/src/index.ts"
|
||||
],
|
||||
"@nhost/react-apollo": [
|
||||
"../integrations/react-apollo/src/index.ts"
|
||||
],
|
||||
"@nhost/vue": [
|
||||
"../packages/vue/src/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
@@ -18,7 +19,9 @@ export default defineConfig({
|
||||
tsconfigPaths(),
|
||||
dts({
|
||||
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
||||
entryRoot: 'src'
|
||||
entryRoot: 'src',
|
||||
// Was defaulting to true until version 1.7
|
||||
skipDiagnostics: true
|
||||
})
|
||||
],
|
||||
test: {
|
||||
@@ -41,6 +44,12 @@ export default defineConfig({
|
||||
},
|
||||
rollupOptions: {
|
||||
external: (id) => deps.some((dep) => id.startsWith(dep)),
|
||||
plugins: [
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
'exports.hasOwnProperty(': 'Object.prototype.hasOwnProperty.call(exports,'
|
||||
})
|
||||
],
|
||||
output: {
|
||||
globals: {
|
||||
graphql: 'graphql',
|
||||
|
||||
@@ -6,6 +6,7 @@ module.exports = {
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-addon-next-router',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
@@ -38,4 +39,10 @@ module.exports = {
|
||||
},
|
||||
};
|
||||
},
|
||||
env: (config) => ({
|
||||
...config,
|
||||
NEXT_PUBLIC_ENV: 'dev',
|
||||
NEXT_PUBLIC_NHOST_BACKEND_URL: 'http://localhost:1337',
|
||||
NEXT_PUBLIC_NHOST_PLATFORM: 'false',
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,9 +2,25 @@ import '@fontsource/inter';
|
||||
import '@fontsource/inter/500.css';
|
||||
import '@fontsource/inter/700.css';
|
||||
import { CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import { NhostApolloProvider } from '@nhost/react-apollo';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Buffer } from 'buffer';
|
||||
import { initialize, mswDecorator } from 'msw-storybook-addon';
|
||||
import { RouterContext } from 'next/dist/shared/lib/router-context';
|
||||
import '../src/styles/globals.css';
|
||||
import defaultTheme from '../src/theme/default';
|
||||
|
||||
global.Buffer = Buffer;
|
||||
|
||||
initialize({ onUnhandledRequest: 'bypass' });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const parameters = {
|
||||
nextRouter: {
|
||||
Provider: RouterContext.Provider,
|
||||
isReady: true,
|
||||
},
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
@@ -14,11 +30,25 @@ export const parameters = {
|
||||
},
|
||||
};
|
||||
|
||||
export const withMuiTheme = (Story) => (
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
export const decorators = [withMuiTheme];
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<ThemeProvider theme={defaultTheme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Story />
|
||||
</QueryClientProvider>
|
||||
),
|
||||
(Story) => (
|
||||
<NhostApolloProvider
|
||||
fetchPolicy="cache-first"
|
||||
graphqlUrl="http://localhost:1337/v1/graphql"
|
||||
>
|
||||
<Story />
|
||||
</NhostApolloProvider>
|
||||
),
|
||||
mswDecorator,
|
||||
];
|
||||
|
||||
@@ -1,5 +1,80 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.7.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b21222b3: chore(deps): update dependency @types/node to v16
|
||||
- 9e0486a3: fix(dashboard): close modals when navigating
|
||||
- Updated dependencies [b21222b3]
|
||||
- Updated dependencies [65687bee]
|
||||
- Updated dependencies [54df0df4]
|
||||
- @nhost/nextjs@1.12.0
|
||||
- @nhost/react-apollo@4.12.0
|
||||
|
||||
## 0.7.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d6527122: fix(dashboard): use correct service URLs
|
||||
|
||||
## 0.7.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [57db5b83]
|
||||
- @nhost/nextjs@1.11.0
|
||||
- @nhost/nhost-js@1.7.0
|
||||
- @nhost/react@0.17.0
|
||||
- @nhost/react-apollo@4.11.0
|
||||
|
||||
## 0.7.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a6d31dc2: fix(dashboard): don't break the UI when project is not loaded yet
|
||||
|
||||
## 0.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7f251111: Use `NhostProvider` instead of `NhostReactProvider` and `NhostNextProvider`
|
||||
|
||||
`NhostReactProvider` and `NhostNextProvider` are now deprecated
|
||||
|
||||
- f4d70f88: fix(dashboard): do not break when region is nullish
|
||||
- 4a9471cc: Windows Live Provider displayed link updated to match backend url
|
||||
- 594488e4: fix(dashboard): do not show error when submitting Apple provider settings
|
||||
- Updated dependencies [7f251111]
|
||||
- @nhost/nextjs@1.10.0
|
||||
- @nhost/react@0.16.0
|
||||
- @nhost/react-apollo@4.10.0
|
||||
|
||||
## 0.7.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 80b604ad: fix(dashboard): use correct Hasura slug
|
||||
|
||||
## 0.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2d2beb53: fix(dashboard): prevent error on GraphQL page
|
||||
- ac8efcbd: chore(dashboard): deprecate old DNS name
|
||||
|
||||
## 0.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 132a4f4b: chore(dashboard): remove unused dependencies
|
||||
- 132a4f4b: chore(deps): synchronize @types/react-dom and @types/react versions
|
||||
- db57572f: fix(dashboard): correct section paddings when no env vars
|
||||
- Updated dependencies [132a4f4b]
|
||||
- @nhost/react@0.15.2
|
||||
- @nhost/react-apollo@4.9.2
|
||||
- @nhost/nextjs@1.9.3
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -37,6 +37,14 @@ NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
```
|
||||
|
||||
### Storybook
|
||||
|
||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||
|
||||
```bash
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
### Full list of environment variables
|
||||
|
||||
| Name | Description |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.7.4",
|
||||
"version": "0.7.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -13,7 +13,7 @@
|
||||
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
|
||||
"nhost:dev": "nhost dev -d",
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -25,7 +25,7 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@graphiql/react": "^0.14.0",
|
||||
"@graphiql/react": "^0.15.0",
|
||||
"@graphiql/toolkit": "^0.8.0",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
@@ -34,12 +34,11 @@
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/system": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^5.0.8",
|
||||
"@nhost/core": "workspace:*",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/nhost-js": "workspace:*",
|
||||
"@nhost/react": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@segment/snippet": "^4.15.3",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
@@ -51,21 +50,20 @@
|
||||
"cross-fetch": "^3.1.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.1.0",
|
||||
"graphiql": "^2.2.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^4.3.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"next": "^12.3.1",
|
||||
"next-seo": "^5.14.1",
|
||||
"node-pg-format": "^1.3.5",
|
||||
"pluralize": "^8.0.0",
|
||||
"prettysize": "^2.0.0",
|
||||
"randomstring": "^1.2.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-is": "17.0.2",
|
||||
@@ -75,7 +73,6 @@
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.31.2",
|
||||
"slugify": "^1.6.5",
|
||||
"smartlook-client": "^6.0.0",
|
||||
"stripe": "^10.17.0",
|
||||
"tailwind-merge": "^1.8.0",
|
||||
"utility-types": "^3.10.0",
|
||||
@@ -91,24 +88,21 @@
|
||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-interactions": "^6.5.14",
|
||||
"@storybook/addon-links": "^6.5.14",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/builder-webpack5": "^6.5.14",
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^18.11.9",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.25",
|
||||
"@types/react-dom": "18.0.9",
|
||||
@@ -117,15 +111,12 @@
|
||||
"@types/validator": "^13.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"@vitest/coverage-c8": "^0.25.2",
|
||||
"@vitejs/plugin-react": "^3.0.0",
|
||||
"@vitest/coverage-c8": "^0.26.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"concurrently": "^6.3.0",
|
||||
"critters": "^0.0.16",
|
||||
"csstype": "^3.0.10",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
@@ -135,25 +126,24 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^4.18.2",
|
||||
"express-validator": "^6.14.2",
|
||||
"jsdom": "^20.0.3",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.49.0",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"postcss": "^8.4.19",
|
||||
"postmark": "^2.7.8",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"storybook-addon-next-router": "^4.0.1",
|
||||
"tailwindcss": "^3.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.4",
|
||||
"vite-tsconfig-paths": "^3.6.0",
|
||||
"vitest": "^0.25.2",
|
||||
"vite": "^4.0.2",
|
||||
"vite-tsconfig-paths": "^4.0.3",
|
||||
"vitest": "^0.26.2",
|
||||
"webpack": "^5.75.0"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -167,5 +157,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
44
dashboard/public/assets/BR.svg
Normal file
44
dashboard/public/assets/BR.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 19 KiB |
303
dashboard/public/mockServiceWorker.js
Normal file
303
dashboard/public/mockServiceWorker.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
|
||||
/**
|
||||
* Mock Service Worker (0.49.0).
|
||||
* @see https://github.com/mswjs/msw
|
||||
* - Please do NOT modify this file.
|
||||
* - Please do NOT serve this file on production.
|
||||
*/
|
||||
|
||||
const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70'
|
||||
const activeClientIds = new Set()
|
||||
|
||||
self.addEventListener('install', function () {
|
||||
self.skipWaiting()
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('message', async function (event) {
|
||||
const clientId = event.source.id
|
||||
|
||||
if (!clientId || !self.clients) {
|
||||
return
|
||||
}
|
||||
|
||||
const client = await self.clients.get(clientId)
|
||||
|
||||
if (!client) {
|
||||
return
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
switch (event.data) {
|
||||
case 'KEEPALIVE_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'KEEPALIVE_RESPONSE',
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'INTEGRITY_CHECK_REQUEST': {
|
||||
sendToClient(client, {
|
||||
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||
payload: INTEGRITY_CHECKSUM,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_ACTIVATE': {
|
||||
activeClientIds.add(clientId)
|
||||
|
||||
sendToClient(client, {
|
||||
type: 'MOCKING_ENABLED',
|
||||
payload: true,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'MOCK_DEACTIVATE': {
|
||||
activeClientIds.delete(clientId)
|
||||
break
|
||||
}
|
||||
|
||||
case 'CLIENT_CLOSED': {
|
||||
activeClientIds.delete(clientId)
|
||||
|
||||
const remainingClients = allClients.filter((client) => {
|
||||
return client.id !== clientId
|
||||
})
|
||||
|
||||
// Unregister itself when there are no more clients
|
||||
if (remainingClients.length === 0) {
|
||||
self.registration.unregister()
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const { request } = event
|
||||
const accept = request.headers.get('accept') || ''
|
||||
|
||||
// Bypass server-sent events.
|
||||
if (accept.includes('text/event-stream')) {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass navigation requests.
|
||||
if (request.mode === 'navigate') {
|
||||
return
|
||||
}
|
||||
|
||||
// Opening the DevTools triggers the "only-if-cached" request
|
||||
// that cannot be handled by the worker. Bypass such requests.
|
||||
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||
return
|
||||
}
|
||||
|
||||
// Bypass all requests when there are no active clients.
|
||||
// Prevents the self-unregistered worked from handling requests
|
||||
// after it's been deleted (still remains active until the next reload).
|
||||
if (activeClientIds.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Generate unique request ID.
|
||||
const requestId = Math.random().toString(16).slice(2)
|
||||
|
||||
event.respondWith(
|
||||
handleRequest(event, requestId).catch((error) => {
|
||||
if (error.name === 'NetworkError') {
|
||||
console.warn(
|
||||
'[MSW] Successfully emulated a network error for the "%s %s" request.',
|
||||
request.method,
|
||||
request.url,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// At this point, any exception indicates an issue with the original request/response.
|
||||
console.error(
|
||||
`\
|
||||
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
|
||||
request.method,
|
||||
request.url,
|
||||
`${error.name}: ${error.message}`,
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
async function handleRequest(event, requestId) {
|
||||
const client = await resolveMainClient(event)
|
||||
const response = await getResponse(event, client, requestId)
|
||||
|
||||
// Send back the response clone for the "response:*" life-cycle events.
|
||||
// Ensure MSW is active and ready to handle the message, otherwise
|
||||
// this message will pend indefinitely.
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
;(async function () {
|
||||
const clonedResponse = response.clone()
|
||||
sendToClient(client, {
|
||||
type: 'RESPONSE',
|
||||
payload: {
|
||||
requestId,
|
||||
type: clonedResponse.type,
|
||||
ok: clonedResponse.ok,
|
||||
status: clonedResponse.status,
|
||||
statusText: clonedResponse.statusText,
|
||||
body:
|
||||
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||
headers: Object.fromEntries(clonedResponse.headers.entries()),
|
||||
redirected: clonedResponse.redirected,
|
||||
},
|
||||
})
|
||||
})()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Resolve the main client for the given event.
|
||||
// Client that issues a request doesn't necessarily equal the client
|
||||
// that registered the worker. It's with the latter the worker should
|
||||
// communicate with during the response resolving phase.
|
||||
async function resolveMainClient(event) {
|
||||
const client = await self.clients.get(event.clientId)
|
||||
|
||||
if (client?.frameType === 'top-level') {
|
||||
return client
|
||||
}
|
||||
|
||||
const allClients = await self.clients.matchAll({
|
||||
type: 'window',
|
||||
})
|
||||
|
||||
return allClients
|
||||
.filter((client) => {
|
||||
// Get only those clients that are currently visible.
|
||||
return client.visibilityState === 'visible'
|
||||
})
|
||||
.find((client) => {
|
||||
// Find the client ID that's recorded in the
|
||||
// set of clients that have registered the worker.
|
||||
return activeClientIds.has(client.id)
|
||||
})
|
||||
}
|
||||
|
||||
async function getResponse(event, client, requestId) {
|
||||
const { request } = event
|
||||
const clonedRequest = request.clone()
|
||||
|
||||
function passthrough() {
|
||||
// Clone the request because it might've been already used
|
||||
// (i.e. its body has been read and sent to the client).
|
||||
const headers = Object.fromEntries(clonedRequest.headers.entries())
|
||||
|
||||
// Remove MSW-specific request headers so the bypassed requests
|
||||
// comply with the server's CORS preflight check.
|
||||
// Operate with the headers as an object because request "Headers"
|
||||
// are immutable.
|
||||
delete headers['x-msw-bypass']
|
||||
|
||||
return fetch(clonedRequest, { headers })
|
||||
}
|
||||
|
||||
// Bypass mocking when the client is not active.
|
||||
if (!client) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass initial page load requests (i.e. static assets).
|
||||
// The absence of the immediate/parent client in the map of the active clients
|
||||
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||
// and is not ready to handle requests.
|
||||
if (!activeClientIds.has(client.id)) {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Bypass requests with the explicit bypass header.
|
||||
// Such requests can be issued by "ctx.fetch()".
|
||||
if (request.headers.get('x-msw-bypass') === 'true') {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
// Notify the client that a request has been intercepted.
|
||||
const clientMessage = await sendToClient(client, {
|
||||
type: 'REQUEST',
|
||||
payload: {
|
||||
id: requestId,
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: Object.fromEntries(request.headers.entries()),
|
||||
cache: request.cache,
|
||||
mode: request.mode,
|
||||
credentials: request.credentials,
|
||||
destination: request.destination,
|
||||
integrity: request.integrity,
|
||||
redirect: request.redirect,
|
||||
referrer: request.referrer,
|
||||
referrerPolicy: request.referrerPolicy,
|
||||
body: await request.text(),
|
||||
bodyUsed: request.bodyUsed,
|
||||
keepalive: request.keepalive,
|
||||
},
|
||||
})
|
||||
|
||||
switch (clientMessage.type) {
|
||||
case 'MOCK_RESPONSE': {
|
||||
return respondWithMock(clientMessage.data)
|
||||
}
|
||||
|
||||
case 'MOCK_NOT_FOUND': {
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
case 'NETWORK_ERROR': {
|
||||
const { name, message } = clientMessage.data
|
||||
const networkError = new Error(message)
|
||||
networkError.name = name
|
||||
|
||||
// Rejecting a "respondWith" promise emulates a network error.
|
||||
throw networkError
|
||||
}
|
||||
}
|
||||
|
||||
return passthrough()
|
||||
}
|
||||
|
||||
function sendToClient(client, message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const channel = new MessageChannel()
|
||||
|
||||
channel.port1.onmessage = (event) => {
|
||||
if (event.data && event.data.error) {
|
||||
return reject(event.data.error)
|
||||
}
|
||||
|
||||
resolve(event.data)
|
||||
}
|
||||
|
||||
client.postMessage(message, [channel.port2])
|
||||
})
|
||||
}
|
||||
|
||||
function sleep(timeMs) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, timeMs)
|
||||
})
|
||||
}
|
||||
|
||||
async function respondWithMock(response) {
|
||||
await sleep(response.delay)
|
||||
return new Response(response.body, response)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
useInsertFeatureFlagMutation,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useUserEmail } from '@nhost/react';
|
||||
import { useUserEmail } from '@nhost/nextjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import ApplicationInfo from './ApplicationInfo';
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
|
||||
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import generateAppServiceUrl, {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
} from '@/utils/common/generateAppServiceUrl';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HasuraDataProps {
|
||||
@@ -15,6 +19,7 @@ interface HasuraDataProps {
|
||||
|
||||
export function HasuraData({ close }: HasuraDataProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
if (
|
||||
!currentApplication?.subdomain ||
|
||||
@@ -24,9 +29,15 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? LOCAL_HASURA_URL
|
||||
: generateRemoteAppUrl(currentApplication.subdomain);
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${LOCAL_HASURA_URL}/console`
|
||||
: generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
'hasura',
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
@@ -60,7 +71,7 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Link
|
||||
href={`${hasuraUrl}/console`}
|
||||
href={hasuraUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="grid grid-flow-col items-center justify-center gap-1 rounded-[4px] bg-btn p-2 text-sm+ font-medium text-white hover:ring-2 motion-safe:transition-all"
|
||||
|
||||
@@ -37,20 +37,27 @@ function ControlledAutocomplete(
|
||||
}: ControlledAutocompleteProps<AutocompleteOption>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const form = useFormContext();
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
...(controllerProps || {}),
|
||||
name: controllerProps?.name || name || '',
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
throw new Error('ControlledAutocomplete must be used in a FormContext.');
|
||||
}
|
||||
|
||||
const { setValue } = form || {};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
{...props}
|
||||
{...field}
|
||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, options, reason, details) => {
|
||||
setValue(controllerProps?.name || name, options);
|
||||
setValue?.(controllerProps?.name || name, options);
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, options, reason, details);
|
||||
|
||||
@@ -2,11 +2,11 @@ import DataGridBody from '@/components/common/DataGridBody';
|
||||
import DataGridFrame from '@/components/common/DataGridFrame';
|
||||
import type { DataGridHeaderProps } from '@/components/common/DataGridHeader';
|
||||
import DataGridHeader from '@/components/common/DataGridHeader';
|
||||
import DataBrowserEmptyState from '@/components/data-browser/DataBrowserEmptyState';
|
||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||
import { DataGridProvider } from '@/context/DataGridContext';
|
||||
import type { UseDataGridOptions } from '@/hooks/useDataGrid';
|
||||
import useDataGrid from '@/hooks/useDataGrid';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useEffect, useRef } from 'react';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DataGridProps } from '@/components/common/DataGrid';
|
||||
import DataGridCell from '@/components/common/DataGridCell';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ColumnType,
|
||||
DataBrowserGridCell,
|
||||
DataBrowserGridCellProps,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import Tooltip, { useTooltip } from '@/ui/v2/Tooltip';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataGridProps } from '@/components/common/DataGrid';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import CreateForeignKeyForm from '@/components/data-browser/CreateForeignKeyForm';
|
||||
import EditForeignKeyForm from '@/components/data-browser/EditForeignKeyForm';
|
||||
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
|
||||
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
|
||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||
@@ -12,13 +12,21 @@ import AlertDialog from '@/ui/v2/AlertDialog';
|
||||
import { BaseDialog } from '@/ui/v2/Dialog';
|
||||
import Drawer from '@/ui/v2/Drawer';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import type {
|
||||
BaseSyntheticEvent,
|
||||
DetailedHTMLProps,
|
||||
HTMLProps,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import DialogContext from './DialogContext';
|
||||
@@ -49,31 +57,33 @@ function LoadingComponent({
|
||||
}
|
||||
|
||||
const CreateRecordForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateRecordForm'),
|
||||
() => import('@/components/dataBrowser/CreateRecordForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateColumnForm'),
|
||||
() => import('@/components/dataBrowser/CreateColumnForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/EditColumnForm'),
|
||||
() => import('@/components/dataBrowser/EditColumnForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateTableForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateTableForm'),
|
||||
() => import('@/components/dataBrowser/CreateTableForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditTableForm = dynamic(
|
||||
() => import('@/components/data-browser/EditTableForm'),
|
||||
() => import('@/components/dataBrowser/EditTableForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const router = useRouter();
|
||||
|
||||
const [
|
||||
{
|
||||
open: dialogOpen,
|
||||
@@ -161,26 +171,29 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
alertDialogDispatch({ type: 'CLEAR_ALERT_CONTENT' });
|
||||
}
|
||||
|
||||
function openDirtyConfirmation(config?: Partial<DialogConfig<string>>) {
|
||||
const { props, ...restConfig } = config || {};
|
||||
const openDirtyConfirmation = useCallback(
|
||||
(config?: Partial<DialogConfig<string>>) => {
|
||||
const { props, ...restConfig } = config || {};
|
||||
|
||||
openAlertDialog({
|
||||
...config,
|
||||
title: 'Unsaved changes',
|
||||
payload:
|
||||
'You have unsaved local changes. Are you sure you want to discard them?',
|
||||
props: {
|
||||
...props,
|
||||
primaryButtonText: 'Discard',
|
||||
primaryButtonColor: 'error',
|
||||
},
|
||||
...restConfig,
|
||||
});
|
||||
}
|
||||
setShowDirtyConfirmation(true);
|
||||
openAlertDialog({
|
||||
...config,
|
||||
title: 'Unsaved changes',
|
||||
payload:
|
||||
'You have unsaved local changes. Are you sure you want to discard them?',
|
||||
props: {
|
||||
...props,
|
||||
primaryButtonText: 'Discard',
|
||||
primaryButtonColor: 'error',
|
||||
},
|
||||
...restConfig,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
function closeDrawerWithDirtyGuard(event?: BaseSyntheticEvent) {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
}
|
||||
@@ -190,7 +203,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
|
||||
function closeDialogWithDirtyGuard(event?: BaseSyntheticEvent) {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
}
|
||||
@@ -248,6 +260,32 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
onCancel: closeDrawerWithDirtyGuard,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function handleCloseDrawerAndDialog() {
|
||||
if (isDrawerDirty.current || isDialogDirty.current) {
|
||||
openDirtyConfirmation({
|
||||
props: {
|
||||
onPrimaryAction: () => {
|
||||
closeDialog();
|
||||
closeDrawer();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
throw new Error('Unsaved changes');
|
||||
}
|
||||
|
||||
closeDrawer();
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
router?.events?.on?.('routeChangeStart', handleCloseDrawerAndDialog);
|
||||
|
||||
return () => {
|
||||
router?.events?.off?.('routeChangeStart', handleCloseDrawerAndDialog);
|
||||
};
|
||||
}, [closeDialog, closeDrawer, openDirtyConfirmation, router.events]);
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={contextValue}>
|
||||
<AlertDialog
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import type { PropsWithoutRef } from 'react';
|
||||
|
||||
import type { ReadOnlyToggleProps } from './ReadOnlyToggle';
|
||||
import ReadOnlyToggle from './ReadOnlyToggle';
|
||||
|
||||
export default {
|
||||
title: 'Common Components / ReadOnlyToggle',
|
||||
component: ReadOnlyToggle,
|
||||
argTypes: {
|
||||
checked: {
|
||||
options: [null, true, false],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ReadOnlyToggle>;
|
||||
|
||||
const Template: ComponentStory<typeof ReadOnlyToggle> = function Template(
|
||||
args: PropsWithoutRef<ReadOnlyToggleProps>,
|
||||
) {
|
||||
return <ReadOnlyToggle {...args} />;
|
||||
};
|
||||
|
||||
export const Null = Template.bind({});
|
||||
Null.args = {
|
||||
checked: null,
|
||||
};
|
||||
|
||||
export const True = Template.bind({});
|
||||
True.args = {
|
||||
checked: true,
|
||||
};
|
||||
|
||||
export const False = Template.bind({});
|
||||
False.args = {
|
||||
checked: false,
|
||||
};
|
||||
|
||||
export const CustomClasses = Template.bind({});
|
||||
CustomClasses.args = {
|
||||
checked: true,
|
||||
className: '!bg-red',
|
||||
slotProps: {
|
||||
label: {
|
||||
className: '!text-sm !text-white',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,39 +1,79 @@
|
||||
import type { ForwardedRef } from 'react';
|
||||
import type { TextProps } from '@/ui/v2/Text';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
const ReadOnlyToggle = forwardRef(
|
||||
(
|
||||
{ checked }: { checked: boolean | null },
|
||||
ref: ForwardedRef<HTMLSpanElement>,
|
||||
) => (
|
||||
export interface ReadOnlyToggleProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement> {
|
||||
/**
|
||||
* Determines whether the toggle is checked or not.
|
||||
*/
|
||||
checked?: boolean | null;
|
||||
/**
|
||||
* Props passed to specific component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props passed to the root `<span />` element.
|
||||
*/
|
||||
root?: DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>;
|
||||
/**
|
||||
* Props passed to the label.
|
||||
*/
|
||||
label?: TextProps;
|
||||
};
|
||||
}
|
||||
|
||||
function ReadOnlyToggle(
|
||||
{ checked, className, slotProps = {}, ...props }: ReadOnlyToggleProps,
|
||||
ref: ForwardedRef<HTMLSpanElement>,
|
||||
) {
|
||||
return (
|
||||
<span
|
||||
className="inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5"
|
||||
{...props}
|
||||
{...(slotProps?.root || {})}
|
||||
className={twMerge(
|
||||
'inline-grid h-full w-full grid-flow-col items-center justify-start gap-1.5',
|
||||
slotProps?.root?.className,
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
'box-border inline-grid h-3 w-5 items-center rounded-full px-0.5',
|
||||
checked === true && 'justify-end bg-greyscaleDark',
|
||||
checked === true &&
|
||||
'border-1 border-transparent justify-end bg-greyscaleDark',
|
||||
checked === false && 'border-1 border-greyscaleDark',
|
||||
checked === null && 'border-1 border-greyscaleDark',
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={twMerge(
|
||||
'inline rounded-full',
|
||||
'inline-block rounded-full',
|
||||
checked === true && 'h-2 w-2 bg-white',
|
||||
checked === false && 'h-2 w-2 bg-greyscaleDark',
|
||||
checked === null && 'h-px w-2 justify-self-center bg-greyscaleDark',
|
||||
checked === null &&
|
||||
'h-px my-px w-2 justify-self-center bg-greyscaleDark',
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span className="truncate text-xs font-normal">{String(checked)}</span>
|
||||
<Text
|
||||
{...(slotProps?.label || {})}
|
||||
component="span"
|
||||
className={twMerge(
|
||||
'truncate !text-xs font-normal',
|
||||
slotProps?.label?.className,
|
||||
)}
|
||||
>
|
||||
{String(checked)}
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
ReadOnlyToggle.displayName = 'NhostReadOnlyToggle';
|
||||
|
||||
export default ReadOnlyToggle;
|
||||
export default forwardRef(ReadOnlyToggle);
|
||||
|
||||
@@ -3,7 +3,7 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/data-browser';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import { OptionBase } from '@/ui/v2/Option';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/data-browser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn } from '@/types/data-browser';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import LinkIcon from '@/ui/v2/icons/LinkIcon';
|
||||
@@ -2,7 +2,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import Option from '@/ui/v2/Option';
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ControlledSelectProps } from '@/components/common/ControlledSelect';
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { NormalizedQueryDataRow } from '@/types/data-browser';
|
||||
import type { NormalizedQueryDataRow } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import type { ForwardedRef, PropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
@@ -1,5 +1,5 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { NormalizedQueryDataRow } from '@/types/data-browser';
|
||||
import type { NormalizedQueryDataRow } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import { useFormContext, useFormState, useWatch } from 'react-hook-form';
|
||||
import type { BaseForeignKeyFormValues } from './BaseForeignKeyForm';
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import DatabaseRecordInputGroup from '@/components/data-browser/DatabaseRecordInputGroup';
|
||||
import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
|
||||
import type {
|
||||
ColumnInsertOptions,
|
||||
DataBrowserGridColumn,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import { baseColumnValidationSchema } from '@/components/data-browser/BaseColumnForm';
|
||||
import type { DatabaseTable, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
|
||||
import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import { useEffect } from 'react';
|
||||
@@ -1,7 +1,7 @@
|
||||
import ControlledAutocomplete from '@/components/common/ControlledAutocomplete';
|
||||
import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { ColumnType, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { ColumnType, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import type { ButtonProps } from '@/ui/v2/Button';
|
||||
import type { CheckboxProps } from '@/ui/v2/Checkbox';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import LinkIcon from '@/ui/v2/icons/LinkIcon';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/data-browser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/data-browser';
|
||||
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import InputLabel from '@/ui/v2/InputLabel';
|
||||
@@ -1,5 +1,5 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/data-browser';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import { identityTypes } from '@/utils/dataBrowser/postgresqlConstants';
|
||||
import { useMemo } from 'react';
|
||||
@@ -1,5 +1,5 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { DatabaseColumn } from '@/types/data-browser';
|
||||
import type { DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import { useMemo } from 'react';
|
||||
import { useFormState, useWatch } from 'react-hook-form';
|
||||
@@ -0,0 +1,129 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { ColumnAutocompleteProps } from './ColumnAutocomplete';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / ColumnAutocomplete',
|
||||
component: ColumnAutocomplete,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof ColumnAutocomplete>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tableQuery, hasuraMetadataQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof ColumnAutocomplete> = function Template(
|
||||
args: ColumnAutocompleteProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>('');
|
||||
|
||||
const form = useForm<{ firstReference: string; secondReference: string }>({
|
||||
defaultValues: {
|
||||
firstReference: null,
|
||||
secondReference: null,
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(values: {
|
||||
firstReference: string;
|
||||
secondReference: string;
|
||||
}) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
name="firstReference"
|
||||
label="First Reference"
|
||||
onChange={(_event, newValue) =>
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('firstReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ColumnAutocomplete
|
||||
{...args}
|
||||
name="secondReference"
|
||||
label="Second Reference"
|
||||
onChange={(_event, newValue) =>
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
})
|
||||
}
|
||||
onInitialized={(newValue) => {
|
||||
form.setValue('secondReference', newValue.value, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({});
|
||||
Basic.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
};
|
||||
Basic.parameters = defaultParameters;
|
||||
|
||||
export const DefaultValue = Template.bind({});
|
||||
DefaultValue.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
value: 'author.id',
|
||||
};
|
||||
DefaultValue.parameters = defaultParameters;
|
||||
|
||||
export const DisabledRelationships = Template.bind({});
|
||||
DisabledRelationships.args = {
|
||||
schema: 'public',
|
||||
table: 'books',
|
||||
disableRelationships: true,
|
||||
};
|
||||
DisabledRelationships.parameters = defaultParameters;
|
||||
@@ -0,0 +1,33 @@
|
||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import { render, screen } from '@/utils/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { test, vi } from 'vitest';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
const server = setupServer(tableQuery, hasuraMetadataQuery, customClaimsQuery);
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('should render a combobox', () => {
|
||||
render(
|
||||
<ColumnAutocomplete
|
||||
schema="public"
|
||||
table="books"
|
||||
label="Column Autocomplete"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole('combobox', { name: /column autocomplete/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Note: Network requests don't go through in tests, so we can't test the
|
||||
// autocomplete functionality for now.
|
||||
@@ -0,0 +1,382 @@
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import { AutocompletePopper } from '@/ui/v2/Autocomplete';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import ArrowLeftIcon from '@/ui/v2/icons/ArrowLeftIcon';
|
||||
import type { InputProps } from '@/ui/v2/Input';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import List from '@/ui/v2/List';
|
||||
import { OptionBase } from '@/ui/v2/Option';
|
||||
import { OptionGroupBase } from '@/ui/v2/OptionGroup';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import getTruncatedText from '@/utils/common/getTruncatedText';
|
||||
import type { AutocompleteGroupedOption } from '@mui/base/AutocompleteUnstyled';
|
||||
import { useAutocomplete } from '@mui/base/AutocompleteUnstyled';
|
||||
import type { AutocompleteRenderGroupParams } from '@mui/material/Autocomplete';
|
||||
import { autocompleteClasses } from '@mui/material/Autocomplete';
|
||||
import type {
|
||||
ForwardedRef,
|
||||
HTMLAttributes,
|
||||
PropsWithoutRef,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import { forwardRef, useEffect, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { UseAsyncInitialValueOptions } from './useAsyncInitialValue';
|
||||
import useAsyncInitialValue from './useAsyncInitialValue';
|
||||
import type { UseColumnGroupsOptions } from './useColumnGroups';
|
||||
import useColumnGroups from './useColumnGroups';
|
||||
|
||||
export interface ColumnAutocompleteProps
|
||||
extends Omit<PropsWithoutRef<InputProps>, 'onChange'> {
|
||||
/**
|
||||
* Schema where the `table` is located.
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* Table to get the columns from.
|
||||
*/
|
||||
table: string;
|
||||
/**
|
||||
* Function to be called when the value changes.
|
||||
*/
|
||||
onChange?: (
|
||||
event: SyntheticEvent,
|
||||
value: {
|
||||
value: string;
|
||||
columnMetadata?: Record<string, any>;
|
||||
disableReset?: boolean;
|
||||
},
|
||||
) => void;
|
||||
/**
|
||||
* Function to be called when the input is asynchronously initialized.
|
||||
*/
|
||||
onInitialized?: UseAsyncInitialValueOptions['onInitialized'];
|
||||
/**
|
||||
* Class name to be applied to the root element.
|
||||
*/
|
||||
rootClassName?: string;
|
||||
/**
|
||||
* Determines if the autocomplete should allow relationships.
|
||||
*/
|
||||
disableRelationships?: UseColumnGroupsOptions['disableRelationships'];
|
||||
}
|
||||
|
||||
function renderGroup(params: AutocompleteRenderGroupParams) {
|
||||
return (
|
||||
<li key={params.key}>
|
||||
<OptionGroupBase>{params.group}</OptionGroupBase>
|
||||
|
||||
<List>{params.children}</List>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function renderOption(
|
||||
option: AutocompleteOption<string>,
|
||||
optionProps: HTMLAttributes<HTMLLIElement>,
|
||||
) {
|
||||
return (
|
||||
<OptionBase
|
||||
{...optionProps}
|
||||
className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5"
|
||||
>
|
||||
<span>{option.label}</span>
|
||||
|
||||
{option.group === 'columns' && (
|
||||
<InlineCode>{option.metadata?.type || option.value}</InlineCode>
|
||||
)}
|
||||
</OptionBase>
|
||||
);
|
||||
}
|
||||
|
||||
function ColumnAutocomplete(
|
||||
{
|
||||
rootClassName,
|
||||
schema: defaultSchema,
|
||||
table: defaultTable,
|
||||
value: externalValue,
|
||||
disableRelationships,
|
||||
onChange,
|
||||
onInitialized,
|
||||
...props
|
||||
}: ColumnAutocompleteProps,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeRelationship, setActiveRelationship] = useState<any>();
|
||||
const selectedSchema = activeRelationship?.schema || defaultSchema;
|
||||
const selectedTable = activeRelationship?.table || defaultTable;
|
||||
|
||||
const {
|
||||
data: tableData,
|
||||
status: tableStatus,
|
||||
error: tableError,
|
||||
isFetching: isTableFetching,
|
||||
} = useTableQuery([`default.${selectedSchema}.${selectedTable}`], {
|
||||
schema: selectedSchema,
|
||||
table: selectedTable,
|
||||
queryOptions: { refetchOnWindowFocus: false },
|
||||
});
|
||||
|
||||
const {
|
||||
data: metadata,
|
||||
status: metadataStatus,
|
||||
error: metadataError,
|
||||
isFetching: isMetadataFetching,
|
||||
} = useMetadataQuery([`default.metadata`], {
|
||||
queryOptions: { refetchOnWindowFocus: false },
|
||||
});
|
||||
|
||||
const {
|
||||
initialized,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
selectedColumn,
|
||||
setSelectedColumn,
|
||||
selectedRelationships,
|
||||
setSelectedRelationships,
|
||||
relationshipDotNotation,
|
||||
activeRelationship: asyncActiveRelationship,
|
||||
} = useAsyncInitialValue({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
initialValue: externalValue as string,
|
||||
isTableLoading: tableStatus === 'loading' || isTableFetching,
|
||||
isMetadataLoading: metadataStatus === 'loading' || isMetadataFetching,
|
||||
tableData,
|
||||
metadata,
|
||||
onInitialized,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setActiveRelationship(asyncActiveRelationship);
|
||||
}, [asyncActiveRelationship]);
|
||||
|
||||
function isOptionEqualToValue(
|
||||
option: AutocompleteOption,
|
||||
value: NonNullable<string | AutocompleteOption>,
|
||||
) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return option.value === value;
|
||||
}
|
||||
|
||||
return option.value === value.value && option.custom === value.custom;
|
||||
}
|
||||
|
||||
function handleChange(
|
||||
event: SyntheticEvent,
|
||||
value: NonNullable<string | AutocompleteOption>,
|
||||
) {
|
||||
if (typeof value === 'string' || Array.isArray(value) || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('group' in value && value.group === 'columns') {
|
||||
setSelectedColumn(value);
|
||||
setOpen(false);
|
||||
setInputValue(value.value);
|
||||
|
||||
onChange?.(event, {
|
||||
value:
|
||||
selectedRelationships.length > 0
|
||||
? [relationshipDotNotation, value.value].join('.')
|
||||
: value.value,
|
||||
columnMetadata: value.metadata,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setInputValue('');
|
||||
setSelectedColumn(null);
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
value.metadata?.target,
|
||||
]);
|
||||
}
|
||||
|
||||
const options = useColumnGroups({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
tableData,
|
||||
metadata,
|
||||
disableRelationships,
|
||||
});
|
||||
|
||||
const {
|
||||
popupOpen,
|
||||
anchorEl,
|
||||
setAnchorEl,
|
||||
getRootProps,
|
||||
getInputLabelProps,
|
||||
getInputProps,
|
||||
getListboxProps,
|
||||
getOptionProps,
|
||||
groupedOptions,
|
||||
} = useAutocomplete({
|
||||
open,
|
||||
inputValue,
|
||||
options,
|
||||
id: props?.name,
|
||||
openOnFocus: true,
|
||||
disableCloseOnSelect: true,
|
||||
value: selectedColumn,
|
||||
onClose: () => setOpen(false),
|
||||
groupBy: (option) => option.group,
|
||||
isOptionEqualToValue,
|
||||
onChange: handleChange,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...getRootProps()} className={rootClassName}>
|
||||
<Input
|
||||
{...props}
|
||||
ref={ref}
|
||||
fullWidth
|
||||
slotProps={{
|
||||
...(props.slotProps || {}),
|
||||
label: getInputLabelProps(),
|
||||
input: { ...(props.slotProps?.input || {}), ref: setAnchorEl },
|
||||
inputRoot: {
|
||||
...getInputProps(),
|
||||
className: twMerge(
|
||||
Boolean(selectedColumn) || Boolean(relationshipDotNotation)
|
||||
? '!pl-0'
|
||||
: null,
|
||||
props.slotProps?.inputRoot?.className,
|
||||
),
|
||||
},
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onClick={() => setOpen(true)}
|
||||
error={Boolean(tableError || metadataError)}
|
||||
helperText={String(tableError || metadataError || '')}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
value={inputValue}
|
||||
startAdornment={
|
||||
selectedColumn || relationshipDotNotation ? (
|
||||
<Text className="!ml-2 lg:max-w-[200px] flex-shrink-0 truncate">
|
||||
<span className="text-greyscaleGrey">{defaultTable}</span>.
|
||||
{relationshipDotNotation && (
|
||||
<>
|
||||
<span className="hidden lg:inline">
|
||||
{getTruncatedText(relationshipDotNotation, 15, 'start')}.
|
||||
</span>
|
||||
|
||||
<span className="inline lg:hidden">
|
||||
{relationshipDotNotation}.
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
endAdornment={
|
||||
tableStatus === 'loading' ||
|
||||
metadataStatus === 'loading' ||
|
||||
!initialized ? (
|
||||
<ActivityIndicator className="mr-2" delay={500} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AutocompletePopper
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
modifiers={[{ name: 'offset', options: { offset: [0, 10] } }]}
|
||||
placement="bottom-start"
|
||||
open={popupOpen}
|
||||
anchorEl={anchorEl}
|
||||
style={{ width: anchorEl?.parentElement?.clientWidth }}
|
||||
>
|
||||
<div className={autocompleteClasses.paper}>
|
||||
<div className="px-3 py-2.5 border-b-1 border-greyscale-100 grid grid-flow-col gap-2 justify-start items-center">
|
||||
{selectedRelationships.length > 0 && (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
setInputValue('');
|
||||
setSelectedColumn(null);
|
||||
setSelectedRelationships((activeRelationships) =>
|
||||
activeRelationships.slice(0, -1),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4" />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
<Text className="truncate direction-rtl text-left">
|
||||
<span className="!text-greyscaleMedium">{defaultTable}</span>
|
||||
|
||||
{relationshipDotNotation && (
|
||||
<>
|
||||
<span className="hidden lg:inline">
|
||||
.{getTruncatedText(relationshipDotNotation, 20, 'start')}
|
||||
</span>
|
||||
|
||||
<span className="inline lg:hidden">
|
||||
.{relationshipDotNotation}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{(tableStatus === 'loading' ||
|
||||
metadataStatus === 'loading' ||
|
||||
!initialized) && (
|
||||
<div className="p-2">
|
||||
<ActivityIndicator label="Loading..." />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupedOptions.length > 0 && (
|
||||
<List
|
||||
{...getListboxProps()}
|
||||
className={autocompleteClasses.listbox}
|
||||
>
|
||||
{(
|
||||
groupedOptions as AutocompleteGroupedOption<
|
||||
typeof options[number]
|
||||
>[]
|
||||
).map((optionGroup) =>
|
||||
renderGroup({
|
||||
key: `${optionGroup.key}`,
|
||||
group: optionGroup.group,
|
||||
children: optionGroup.options.map((option, index) =>
|
||||
renderOption(
|
||||
option,
|
||||
getOptionProps({
|
||||
option,
|
||||
index: optionGroup.index + index,
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{groupedOptions.length === 0 && Boolean(anchorEl) && (
|
||||
<Text className={autocompleteClasses.noOptions}>No options</Text>
|
||||
)}
|
||||
</div>
|
||||
</AutocompletePopper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(ColumnAutocomplete);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ColumnAutocomplete';
|
||||
export { default } from './ColumnAutocomplete';
|
||||
@@ -0,0 +1,263 @@
|
||||
import type { FetchMetadataReturnType } from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import type { FetchTableReturnType } from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { HasuraMetadataTable } from '@/types/dataBrowser';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface UseAsyncInitialValueOptions {
|
||||
/**
|
||||
* Selected schema to be used to determine the initial value.
|
||||
*/
|
||||
selectedSchema?: string;
|
||||
/**
|
||||
* Selected table to be used to determine the initial value.
|
||||
*/
|
||||
selectedTable?: string;
|
||||
/**
|
||||
* Initial value to be used before the async value is loaded.
|
||||
*/
|
||||
initialValue?: string;
|
||||
/**
|
||||
* Determines whether or not the table data is loading.
|
||||
*/
|
||||
isTableLoading?: boolean;
|
||||
/**
|
||||
* Determines whether or not the metadata is loading.
|
||||
*/
|
||||
isMetadataLoading?: boolean;
|
||||
/**
|
||||
* Table data to be used to determine the initial value.
|
||||
*/
|
||||
tableData?: FetchTableReturnType;
|
||||
/**
|
||||
* Metadata to be used to determine the initial value.
|
||||
*/
|
||||
metadata?: FetchMetadataReturnType;
|
||||
/**
|
||||
* Function to be called when the input is asynchronously initialized.
|
||||
*/
|
||||
onInitialized?: (value: {
|
||||
value: string;
|
||||
columnMetadata?: Record<string, any>;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function useAsyncInitialValue({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
initialValue,
|
||||
isTableLoading,
|
||||
isMetadataLoading,
|
||||
tableData,
|
||||
metadata,
|
||||
onInitialized,
|
||||
}: UseAsyncInitialValueOptions) {
|
||||
const currentTablePath = `${selectedSchema}.${selectedTable}`;
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
// We might not going to have the most up-to-date table data because the
|
||||
// relationship is loaded asynchronously, so we need to make sure we don't
|
||||
// look for the column in a stale table when initializing
|
||||
const [asyncTablePath, setAsyncTablePath] = useState(currentTablePath);
|
||||
const [remainingColumnPath, setRemainingColumnPath] = useState(
|
||||
initialValue?.split('.') || [],
|
||||
);
|
||||
const [selectedRelationships, setSelectedRelationships] = useState<
|
||||
{ schema: string; table: string; name: string }[]
|
||||
>([]);
|
||||
const relationshipDotNotation =
|
||||
initialized && selectedRelationships?.length > 0
|
||||
? selectedRelationships.map((relationship) => relationship.name).join('.')
|
||||
: '';
|
||||
const [selectedColumn, setSelectedColumn] =
|
||||
useState<AutocompleteOption>(null);
|
||||
const activeRelationship =
|
||||
selectedRelationships[selectedRelationships.length - 1];
|
||||
|
||||
useEffect(() => {
|
||||
if (remainingColumnPath?.length > 0 || initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInitialized(true);
|
||||
|
||||
if (!selectedColumn) {
|
||||
return;
|
||||
}
|
||||
|
||||
onInitialized?.({
|
||||
value:
|
||||
selectedRelationships.length > 0
|
||||
? [relationshipDotNotation, selectedColumn.value].join('.')
|
||||
: selectedColumn.value,
|
||||
columnMetadata: selectedColumn.metadata,
|
||||
});
|
||||
}, [
|
||||
initialized,
|
||||
onInitialized,
|
||||
relationshipDotNotation,
|
||||
remainingColumnPath?.length,
|
||||
selectedColumn,
|
||||
selectedRelationships.length,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
remainingColumnPath?.length !== 1 ||
|
||||
isTableLoading ||
|
||||
!tableData?.columns ||
|
||||
asyncTablePath !== currentTablePath
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [activeColumn] = remainingColumnPath;
|
||||
|
||||
// If there is a single column in the path, it means that we can look for it
|
||||
// in the table columns
|
||||
if (
|
||||
!tableData?.columns.some((column) => column.column_name === activeColumn)
|
||||
) {
|
||||
setRemainingColumnPath([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedColumn({
|
||||
value: activeColumn,
|
||||
label: activeColumn,
|
||||
group: 'columns',
|
||||
metadata: tableData.columns.find(
|
||||
(column) => column.column_name === activeColumn,
|
||||
),
|
||||
});
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
setInputValue(activeColumn);
|
||||
}, [
|
||||
remainingColumnPath,
|
||||
isTableLoading,
|
||||
tableData?.columns,
|
||||
asyncTablePath,
|
||||
currentTablePath,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
remainingColumnPath.length < 2 ||
|
||||
isTableLoading ||
|
||||
isMetadataLoading ||
|
||||
!tableData?.columns
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataMap = metadata.tables.reduce(
|
||||
(map, metadataTable) =>
|
||||
map.set(
|
||||
`${metadataTable.table.schema}.${metadataTable.table.name}`,
|
||||
metadataTable,
|
||||
),
|
||||
new Map<string, HasuraMetadataTable>(),
|
||||
);
|
||||
|
||||
const [nextPath] = remainingColumnPath.slice(
|
||||
0,
|
||||
remainingColumnPath.length - 1,
|
||||
);
|
||||
|
||||
const tableMetadata = metadataMap.get(`${selectedSchema}.${selectedTable}`);
|
||||
const currentRelationship = [
|
||||
...(tableMetadata.object_relationships || []),
|
||||
...(tableMetadata.array_relationships || []),
|
||||
].find(({ name }) => name === nextPath);
|
||||
|
||||
if (!currentRelationship) {
|
||||
setRemainingColumnPath([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const { foreign_key_constraint_on: metadataConstraint } =
|
||||
currentRelationship.using || {};
|
||||
|
||||
// In some cases the metadata already contains the schema and table name
|
||||
if (typeof metadataConstraint !== 'string') {
|
||||
setAsyncTablePath(
|
||||
`${metadataConstraint.table.schema || 'public'}.${
|
||||
metadataConstraint.table.name
|
||||
}`,
|
||||
);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: metadataConstraint.table.schema || 'public',
|
||||
table: metadataConstraint.table.name,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const foreignKeyRelation = tableData?.foreignKeyRelations?.find(
|
||||
({ columnName }) => {
|
||||
const { foreign_key_constraint_on } = currentRelationship.using || {};
|
||||
|
||||
if (!foreign_key_constraint_on) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof foreign_key_constraint_on === 'string') {
|
||||
return foreign_key_constraint_on === columnName;
|
||||
}
|
||||
|
||||
return foreign_key_constraint_on.column === columnName;
|
||||
},
|
||||
);
|
||||
|
||||
if (!foreignKeyRelation) {
|
||||
setRemainingColumnPath([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setAsyncTablePath(
|
||||
`${foreignKeyRelation.referencedSchema || 'public'}.${
|
||||
foreignKeyRelation.referencedTable
|
||||
}`,
|
||||
);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: foreignKeyRelation.referencedSchema || 'public',
|
||||
table: foreignKeyRelation.referencedTable,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
}, [
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
metadata?.tables,
|
||||
tableData?.columns,
|
||||
tableData?.foreignKeyRelations,
|
||||
remainingColumnPath,
|
||||
isTableLoading,
|
||||
isMetadataLoading,
|
||||
]);
|
||||
|
||||
return {
|
||||
initialized,
|
||||
inputValue,
|
||||
setInputValue,
|
||||
activeRelationship,
|
||||
selectedRelationships: initialized ? selectedRelationships : [],
|
||||
selectedColumn: initialized ? selectedColumn : null,
|
||||
setSelectedRelationships,
|
||||
setSelectedColumn,
|
||||
relationshipDotNotation,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { FetchMetadataReturnType } from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import type { FetchTableReturnType } from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
|
||||
export interface UseColumnGroupsOptions {
|
||||
/**
|
||||
* Selected schema to be used to determines the column groups.
|
||||
*/
|
||||
selectedSchema?: string;
|
||||
/**
|
||||
* Selected table to be used to determine the column groups.
|
||||
*/
|
||||
selectedTable?: string;
|
||||
/**
|
||||
* Table data to be used to determine the column groups.
|
||||
*/
|
||||
tableData?: FetchTableReturnType;
|
||||
/**
|
||||
* Metadata to be used to determine the column groups.
|
||||
*/
|
||||
metadata?: FetchMetadataReturnType;
|
||||
/**
|
||||
* Determines whether or not to disable column groups.
|
||||
*/
|
||||
disableRelationships?: boolean;
|
||||
}
|
||||
|
||||
export default function useColumnGroups({
|
||||
selectedTable,
|
||||
selectedSchema,
|
||||
tableData,
|
||||
metadata,
|
||||
disableRelationships,
|
||||
}: UseColumnGroupsOptions) {
|
||||
const { columns, foreignKeyRelations } = tableData || {};
|
||||
|
||||
const columnTargetMap = foreignKeyRelations?.reduce(
|
||||
(map, currentRelation) =>
|
||||
map.set(currentRelation.columnName, {
|
||||
schema: currentRelation.referencedSchema || 'public',
|
||||
table: currentRelation.referencedTable,
|
||||
}),
|
||||
new Map<string, { schema: string; table: string }>(),
|
||||
);
|
||||
|
||||
const columnOptions: AutocompleteOption[] =
|
||||
columns?.map((column) => ({
|
||||
label: column.column_name,
|
||||
value: column.column_name,
|
||||
group: 'columns',
|
||||
metadata: column,
|
||||
})) || [];
|
||||
|
||||
if (disableRelationships) {
|
||||
return columnOptions;
|
||||
}
|
||||
|
||||
const { object_relationships, array_relationships } =
|
||||
metadata?.tables?.find(
|
||||
({ table: metadataTable }) =>
|
||||
metadataTable.name === selectedTable &&
|
||||
metadataTable.schema === selectedSchema,
|
||||
) || {};
|
||||
|
||||
const objectAndArrayRelationships = [
|
||||
...(object_relationships || []),
|
||||
...(array_relationships || []),
|
||||
].map((relationship) => {
|
||||
const { foreign_key_constraint_on } = relationship?.using || {};
|
||||
|
||||
if (typeof foreign_key_constraint_on === 'string') {
|
||||
return {
|
||||
schema: selectedSchema,
|
||||
table: selectedTable,
|
||||
column: foreign_key_constraint_on,
|
||||
name: relationship.name,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
schema: foreign_key_constraint_on.table.schema,
|
||||
table: foreign_key_constraint_on.table.name,
|
||||
column: foreign_key_constraint_on.column,
|
||||
name: relationship.name,
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
...columnOptions,
|
||||
...objectAndArrayRelationships.map((relationship) => ({
|
||||
label: relationship.name,
|
||||
value: relationship.name,
|
||||
group: 'relationships',
|
||||
metadata: {
|
||||
target: {
|
||||
...(columnTargetMap?.get(relationship.column) || {}),
|
||||
name: relationship.name,
|
||||
},
|
||||
},
|
||||
})),
|
||||
];
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {
|
||||
BaseColumnFormProps,
|
||||
BaseColumnFormValues,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import BaseColumnForm, {
|
||||
baseColumnValidationSchema,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import useCreateColumnMutation from '@/hooks/dataBrowser/useCreateColumnMutation';
|
||||
import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
@@ -1,11 +1,11 @@
|
||||
import type {
|
||||
BaseForeignKeyFormProps,
|
||||
BaseForeignKeyFormValues,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import {
|
||||
BaseForeignKeyForm,
|
||||
baseForeignKeyValidationSchema,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { BaseRecordFormProps } from '@/components/data-browser/BaseRecordForm';
|
||||
import BaseRecordForm from '@/components/data-browser/BaseRecordForm';
|
||||
import type { BaseRecordFormProps } from '@/components/dataBrowser/BaseRecordForm';
|
||||
import BaseRecordForm from '@/components/dataBrowser/BaseRecordForm';
|
||||
import useCreateRecordMutation from '@/hooks/dataBrowser/useCreateRecordMutation';
|
||||
import type { ColumnInsertOptions } from '@/types/data-browser';
|
||||
import type { ColumnInsertOptions } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { createDynamicValidationSchema } from '@/utils/dataBrowser/validationSchemaHelpers';
|
||||
@@ -1,14 +1,14 @@
|
||||
import type {
|
||||
BaseTableFormProps,
|
||||
BaseTableFormValues,
|
||||
} from '@/components/data-browser/BaseTableForm';
|
||||
} from '@/components/dataBrowser/BaseTableForm';
|
||||
import BaseTableForm, {
|
||||
baseTableValidationSchema,
|
||||
} from '@/components/data-browser/BaseTableForm';
|
||||
} from '@/components/dataBrowser/BaseTableForm';
|
||||
import useCreateTableMutation from '@/hooks/dataBrowser/useCreateTableMutation';
|
||||
import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import useTrackTableMutation from '@/hooks/dataBrowser/useTrackTableMutation';
|
||||
import type { DatabaseTable } from '@/types/data-browser';
|
||||
import type { DatabaseTable } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
@@ -6,8 +6,8 @@ import DataGridNumericCell from '@/components/common/DataGridNumericCell';
|
||||
import DataGridTextCell from '@/components/common/DataGridTextCell';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import DataBrowserEmptyState from '@/components/data-browser/DataBrowserEmptyState';
|
||||
import DataBrowserGridControls from '@/components/data-browser/DataBrowserGridControls';
|
||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
|
||||
import useDeleteColumnWithToastMutation from '@/hooks/dataBrowser/useDeleteColumnMutation/useDeleteColumnWithToastMutation';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { UpdateRecordVariables } from '@/hooks/dataBrowser/useUpdateRecordMutation';
|
||||
@@ -17,7 +17,7 @@ import useTablePath from '@/hooks/useTablePath';
|
||||
import type {
|
||||
DataBrowserGridColumn,
|
||||
NormalizedQueryDataRow,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import KeyIcon from '@/ui/v2/icons/KeyIcon';
|
||||
import normalizeDefaultValue from '@/utils/dataBrowser/normalizeDefaultValue';
|
||||
import {
|
||||
@@ -4,7 +4,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import useDeleteRecordMutation from '@/hooks/dataBrowser/useDeleteRecordMutation';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import useDataGridConfig from '@/hooks/useDataGridConfig';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import Chip from '@/ui/Chip';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DataBrowserSidebarProps } from '@/components/data-browser/DataBrowserSidebar';
|
||||
import DataBrowserSidebar from '@/components/data-browser/DataBrowserSidebar';
|
||||
import type { DataBrowserSidebarProps } from '@/components/dataBrowser/DataBrowserSidebar';
|
||||
import DataBrowserSidebar from '@/components/dataBrowser/DataBrowserSidebar';
|
||||
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
|
||||
import ProjectLayout from '@/components/layout/ProjectLayout';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -1,6 +1,6 @@
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import ReadOnlyToggle from '@/components/common/ReadOnlyToggle';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import KeyIcon from '@/ui/v2/icons/KeyIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Option from '@/ui/v2/Option';
|
||||
@@ -204,7 +204,7 @@ export default function DatabaseRecordInputGroup({
|
||||
multiline={isMultiline}
|
||||
rows={5}
|
||||
autoFocus={index === 0 && autoFocusFirstInput}
|
||||
componentsProps={{
|
||||
slotProps={{
|
||||
label: commonLabelProps,
|
||||
inputRoot: { step: 1 },
|
||||
}}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type {
|
||||
BaseColumnFormProps,
|
||||
BaseColumnFormValues,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import BaseColumnForm, {
|
||||
baseColumnValidationSchema,
|
||||
} from '@/components/data-browser/BaseColumnForm';
|
||||
} from '@/components/dataBrowser/BaseColumnForm';
|
||||
import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import useUpdateColumnMutation from '@/hooks/dataBrowser/useUpdateColumnMutation';
|
||||
import type { DataBrowserGridColumn } from '@/types/data-browser';
|
||||
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -1,12 +1,12 @@
|
||||
import type {
|
||||
BaseForeignKeyFormProps,
|
||||
BaseForeignKeyFormValues,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import {
|
||||
BaseForeignKeyForm,
|
||||
baseForeignKeyValidationSchema,
|
||||
} from '@/components/data-browser/BaseForeignKeyForm';
|
||||
import type { ForeignKeyRelation } from '@/types/data-browser';
|
||||
} from '@/components/dataBrowser/BaseForeignKeyForm';
|
||||
import type { ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
@@ -1,17 +1,17 @@
|
||||
import type {
|
||||
BaseTableFormProps,
|
||||
BaseTableFormValues,
|
||||
} from '@/components/data-browser/BaseTableForm';
|
||||
} from '@/components/dataBrowser/BaseTableForm';
|
||||
import BaseTableForm, {
|
||||
baseTableValidationSchema,
|
||||
} from '@/components/data-browser/BaseTableForm';
|
||||
} from '@/components/dataBrowser/BaseTableForm';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
|
||||
import useUpdateTableMutation from '@/hooks/dataBrowser/useUpdateTableMutation';
|
||||
import type {
|
||||
DatabaseTable,
|
||||
NormalizedQueryDataRow,
|
||||
} from '@/types/data-browser';
|
||||
} from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -0,0 +1,208 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
|
||||
import type { HasuraOperator } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useController, useFormContext } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import RuleRemoveButton from './RuleRemoveButton';
|
||||
import RuleValueInput from './RuleValueInput';
|
||||
|
||||
export interface RuleEditorRowProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
|
||||
/**
|
||||
* Name of the parent group editor.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Index of the rule.
|
||||
*/
|
||||
index: number;
|
||||
/**
|
||||
* Function to be called when the remove button is clicked.
|
||||
*/
|
||||
onRemove?: VoidFunction;
|
||||
/**
|
||||
* List of operators to be disabled for the rule editor.
|
||||
*
|
||||
* @default []
|
||||
*/
|
||||
disabledOperators?: HasuraOperator[];
|
||||
}
|
||||
|
||||
const commonOperators: {
|
||||
value: HasuraOperator;
|
||||
label?: string;
|
||||
helperText?: string;
|
||||
}[] = [
|
||||
{ value: '_eq', helperText: 'equal' },
|
||||
{ value: '_neq', helperText: 'not equal' },
|
||||
{ value: '_in_hasura', label: '_in', helperText: 'in (X-Hasura-)' },
|
||||
{ value: '_in', helperText: 'in (array)' },
|
||||
{ value: '_nin_hasura', label: '_nin', helperText: 'not in (X-Hasura-)' },
|
||||
{ value: '_nin', helperText: 'not in (array)' },
|
||||
{ value: '_gt', helperText: 'greater than' },
|
||||
{ value: '_lt', helperText: 'lower than' },
|
||||
{ value: '_gte', helperText: 'greater than or equal' },
|
||||
{ value: '_lte', helperText: 'lower than or equal' },
|
||||
{ value: '_ceq', helperText: 'equal to column' },
|
||||
{ value: '_cne', helperText: 'not equal to column' },
|
||||
{ value: '_cgt', helperText: 'greater than column' },
|
||||
{ value: '_clt', helperText: 'lower than column' },
|
||||
{ value: '_cgte', helperText: 'greater than or equal to column' },
|
||||
{ value: '_clte', helperText: 'lower than or equal to column' },
|
||||
{ value: '_is_null', helperText: 'null' },
|
||||
];
|
||||
|
||||
const textSpecificOperators: typeof commonOperators = [
|
||||
{ value: '_like', helperText: 'like' },
|
||||
{ value: '_nlike', helperText: 'not like' },
|
||||
{ value: '_ilike', helperText: 'like (case-insensitive)' },
|
||||
{ value: '_nilike', helperText: 'not like (case-insensitive)' },
|
||||
{ value: '_similar', helperText: 'similar' },
|
||||
{ value: '_nsimilar', helperText: 'not similar' },
|
||||
{ value: '_regex', helperText: 'matches regex' },
|
||||
{ value: '_nregex', helperText: `doesn't match regex` },
|
||||
{ value: '_iregex', helperText: 'matches case-insensitive regex' },
|
||||
{ value: '_niregex', helperText: `doesn't match case-insensitive regex` },
|
||||
];
|
||||
|
||||
function renderOption({
|
||||
value,
|
||||
label,
|
||||
helperText,
|
||||
}: typeof commonOperators[number]) {
|
||||
return (
|
||||
<Option key={value} value={value} className="grid grid-flow-col gap-2">
|
||||
<Text component="span" className="inline-block w-16">
|
||||
{label || value}
|
||||
</Text>
|
||||
|
||||
{helperText && (
|
||||
<Text component="span" className="!text-greyscaleGrey">
|
||||
{helperText}
|
||||
</Text>
|
||||
)}
|
||||
</Option>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RuleEditorRow({
|
||||
name,
|
||||
index,
|
||||
onRemove,
|
||||
className,
|
||||
disabledOperators = [],
|
||||
...props
|
||||
}: RuleEditorRowProps) {
|
||||
const {
|
||||
query: { schemaSlug, tableSlug },
|
||||
} = useRouter();
|
||||
const { control, setValue } = useFormContext();
|
||||
const rowName = `${name}.rules.${index}`;
|
||||
|
||||
const [selectedTablePath, setSelectedTablePath] = useState<string>('');
|
||||
const [selectedColumnType, setSelectedColumnType] = useState<string>('');
|
||||
const { field: autocompleteField } = useController({
|
||||
name: `${rowName}.column`,
|
||||
control,
|
||||
});
|
||||
|
||||
const disabledOperatorMap = disabledOperators.reduce(
|
||||
(map, currentOperator) => map.set(currentOperator, true),
|
||||
new Map<string, boolean>(),
|
||||
);
|
||||
|
||||
const availableOperators = [
|
||||
...commonOperators.filter(({ value }) => !disabledOperatorMap.has(value)),
|
||||
...(selectedColumnType === 'text'
|
||||
? textSpecificOperators.filter(
|
||||
({ value }) => !disabledOperatorMap.get(value),
|
||||
)
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'flex lg:flex-row flex-col items-stretch lg:max-h-10 flex-1 space-y-1 lg:space-y-0',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ColumnAutocomplete
|
||||
{...autocompleteField}
|
||||
schema={schemaSlug as string}
|
||||
table={tableSlug as string}
|
||||
rootClassName="lg:flex-grow-0 lg:flex-shrink-0 lg:flex-[320px] h-10"
|
||||
slotProps={{ input: { className: 'bg-white lg:!rounded-r-none' } }}
|
||||
fullWidth
|
||||
onChange={(_event, { value, columnMetadata, disableReset }) => {
|
||||
setSelectedTablePath(
|
||||
`${columnMetadata.table_schema}.${columnMetadata.table_name}`,
|
||||
);
|
||||
setSelectedColumnType(columnMetadata?.udt_name);
|
||||
setValue(`${rowName}.column`, value, { shouldDirty: true });
|
||||
|
||||
if (disableReset) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(`${rowName}.operator`, '_eq', { shouldDirty: true });
|
||||
setValue(`${rowName}.value`, '', { shouldDirty: true });
|
||||
}}
|
||||
onInitialized={({ value, columnMetadata }) => {
|
||||
setSelectedTablePath(
|
||||
`${columnMetadata.table_schema}.${columnMetadata.table_name}`,
|
||||
);
|
||||
setSelectedColumnType(columnMetadata?.udt_name);
|
||||
setValue(`${rowName}.column`, value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
|
||||
<ControlledSelect
|
||||
name={`${rowName}.operator`}
|
||||
className="lg:flex-grow-0 lg:flex-shrink-0 lg:flex-[140px] h-10"
|
||||
slotProps={{ root: { className: 'bg-white lg:!rounded-none' } }}
|
||||
fullWidth
|
||||
onChange={(_event, value: HasuraOperator) => {
|
||||
if (!['_in', '_nin', '_in_hasura', '_nin_hasura'].includes(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === '_in_hasura' || value === '_nin_hasura') {
|
||||
setValue(`${rowName}.value`, null, { shouldDirty: true });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(`${rowName}.value`, [], { shouldDirty: true });
|
||||
}}
|
||||
renderValue={(option) => {
|
||||
if (!option?.value) {
|
||||
return <span />;
|
||||
}
|
||||
|
||||
if (option.value === '_in_hasura') {
|
||||
return <span>_in</span>;
|
||||
}
|
||||
|
||||
if (option.value === '_nin_hasura') {
|
||||
return <span>_nin</span>;
|
||||
}
|
||||
|
||||
return <span>{option.value}</span>;
|
||||
}}
|
||||
>
|
||||
{availableOperators.map(renderOption)}
|
||||
</ControlledSelect>
|
||||
|
||||
<RuleValueInput selectedTablePath={selectedTablePath} name={rowName} />
|
||||
|
||||
<RuleRemoveButton onRemove={onRemove} name={name} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { RuleGroup } from '@/types/dataBrowser';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface RuleGroupControlsProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
|
||||
/**
|
||||
* Name of the rule group to control.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Determines whether or not select should be shown or just a label with the
|
||||
* operator name.
|
||||
*/
|
||||
showSelect?: boolean;
|
||||
}
|
||||
|
||||
const operatorDictionary: Record<RuleGroup['operator'], string> = {
|
||||
_and: 'and',
|
||||
_or: 'or',
|
||||
};
|
||||
|
||||
export default function RuleGroupControls({
|
||||
name,
|
||||
showSelect,
|
||||
className,
|
||||
...props
|
||||
}: RuleGroupControlsProps) {
|
||||
const currentOperator: RuleGroup['operator'] = useWatch({
|
||||
name: `${name}.operator`,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge('grid grid-flow-row gap-2 content-start', className)}
|
||||
{...props}
|
||||
>
|
||||
{showSelect ? (
|
||||
<ControlledSelect
|
||||
name={`${name}.operator`}
|
||||
slotProps={{ root: { className: 'bg-white' } }}
|
||||
fullWidth
|
||||
>
|
||||
<Option value="_and">and</Option>
|
||||
<Option value="_or">or</Option>
|
||||
</ControlledSelect>
|
||||
) : (
|
||||
<Text className="p-2 !font-medium">
|
||||
{operatorDictionary[currentOperator]}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import type { RuleGroup } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { RuleGroupEditorProps } from './RuleGroupEditor';
|
||||
import RuleGroupEditor from './RuleGroupEditor';
|
||||
|
||||
export default {
|
||||
title: 'Data Browser / RuleGroupEditor',
|
||||
component: RuleGroupEditor,
|
||||
parameters: {
|
||||
docs: {
|
||||
source: {
|
||||
type: 'code',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof RuleGroupEditor>;
|
||||
|
||||
const defaultParameters = {
|
||||
nextRouter: {
|
||||
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
|
||||
asPath: '/workspace/app/database/browser/default/public/users',
|
||||
query: {
|
||||
workspaceSlug: 'workspace',
|
||||
appSlug: 'app',
|
||||
dataSourceSlug: 'default',
|
||||
schemaSlug: 'public',
|
||||
tableSlug: 'books',
|
||||
},
|
||||
},
|
||||
msw: {
|
||||
handlers: [tableQuery, hasuraMetadataQuery, customClaimsQuery],
|
||||
},
|
||||
};
|
||||
|
||||
const Template: ComponentStory<typeof RuleGroupEditor> = function Template(
|
||||
args: RuleGroupEditorProps,
|
||||
) {
|
||||
const [submittedValues, setSubmittedValues] = useState<string>();
|
||||
|
||||
const form = useForm<{ ruleGroupEditor: RuleGroup }>({
|
||||
defaultValues: {
|
||||
ruleGroupEditor: {
|
||||
operator: '_and',
|
||||
rules: [{ column: '', operator: '_eq', value: '' }],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
});
|
||||
|
||||
function handleSubmit(values: { ruleGroupEditor: RuleGroup }) {
|
||||
setSubmittedValues(JSON.stringify(values, null, 2));
|
||||
}
|
||||
|
||||
// note: Storybook passes `onRemove` as a prop, but we don't want to use it
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<RuleGroupEditor {...args} name="ruleGroupEditor" onRemove={null} />
|
||||
|
||||
<Button type="submit" className="justify-self-start">
|
||||
Submit
|
||||
</Button>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
|
||||
<Text component="pre" className="!font-mono !text-gray-700">
|
||||
{submittedValues || 'The form has not been submitted yet.'}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {};
|
||||
Default.parameters = defaultParameters;
|
||||
|
||||
export const DisabledOperators = Template.bind({});
|
||||
DisabledOperators.args = {
|
||||
disabledOperators: ['_in_hasura', '_nin_hasura', '_is_null'],
|
||||
};
|
||||
DisabledOperators.parameters = defaultParameters;
|
||||
@@ -0,0 +1,193 @@
|
||||
import type { Rule, RuleGroup } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { RuleEditorRowProps } from './RuleEditorRow';
|
||||
import RuleEditorRow from './RuleEditorRow';
|
||||
import RuleGroupControls from './RuleGroupControls';
|
||||
|
||||
export interface RuleGroupEditorProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>,
|
||||
Pick<RuleEditorRowProps, 'disabledOperators'> {
|
||||
/**
|
||||
* Name of the group editor.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Function to be called when the remove button is clicked.
|
||||
*/
|
||||
onRemove?: VoidFunction;
|
||||
/**
|
||||
* Determines whether or not remove should be disabled for the rule group.
|
||||
*/
|
||||
disableRemove?: boolean;
|
||||
/**
|
||||
* Group editor depth.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
depth?: number;
|
||||
/**
|
||||
* Maximum depth of the group editor.
|
||||
*
|
||||
* @default 7
|
||||
*/
|
||||
maxDepth?: number;
|
||||
}
|
||||
|
||||
export default function RuleGroupEditor({
|
||||
onRemove,
|
||||
name,
|
||||
className,
|
||||
disableRemove,
|
||||
disabledOperators = [],
|
||||
depth = 0,
|
||||
maxDepth = 7,
|
||||
...props
|
||||
}: RuleGroupEditorProps) {
|
||||
const form = useFormContext();
|
||||
|
||||
const { control } = form;
|
||||
|
||||
// Note: Reason for the type cast to `never`
|
||||
// https://github.com/react-hook-form/react-hook-form/issues/4055#issuecomment-950145092
|
||||
const {
|
||||
fields: rules,
|
||||
append: appendRule,
|
||||
remove: removeRule,
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: `${name}.rules`,
|
||||
} as never);
|
||||
|
||||
// Note: Reason for the type cast to `never`
|
||||
// https://github.com/react-hook-form/react-hook-form/issues/4055#issuecomment-950145092
|
||||
const {
|
||||
fields: groups,
|
||||
append: appendGroup,
|
||||
remove: removeGroup,
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: `${name}.groups`,
|
||||
} as never);
|
||||
|
||||
if (!form) {
|
||||
throw new Error('RuleGroupEditor must be used in a FormContext.');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
'rounded-lg px-2',
|
||||
depth === 0 && 'bg-greyscale-50',
|
||||
depth === 1 && 'bg-greyscale-100',
|
||||
depth === 2 && 'bg-greyscale-200',
|
||||
depth === 3 && 'bg-greyscale-300',
|
||||
depth === 4 && 'bg-greyscale-400',
|
||||
depth === 5 && 'bg-greyscale-500',
|
||||
depth >= 6 && 'bg-greyscale-600',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-col flex-auto space-y-4 lg:space-y-2 py-4">
|
||||
{(rules as (Rule & { id: string })[]).map((rule, ruleIndex) => (
|
||||
<div className="flex flex-row flex-auto" key={rule.id}>
|
||||
<div className="flex-[70px] flex-grow-0 flex-shrink-0 mr-2">
|
||||
{ruleIndex === 0 && (
|
||||
<Text className="p-2 !font-medium">Where</Text>
|
||||
)}
|
||||
|
||||
{ruleIndex > 0 && (
|
||||
<RuleGroupControls name={name} showSelect={ruleIndex === 1} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<RuleEditorRow
|
||||
name={name}
|
||||
index={ruleIndex}
|
||||
onRemove={() => removeRule(ruleIndex)}
|
||||
className="flex-auto"
|
||||
disabledOperators={disabledOperators}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{(groups as (RuleGroup & { id: string })[]).map(
|
||||
(ruleGroup, ruleGroupIndex) => (
|
||||
<div
|
||||
className="flex flex-row flex-auto items-start mt-2"
|
||||
key={ruleGroup.id}
|
||||
>
|
||||
<div className="flex-[70px] flex-grow-0 flex-shrink-0 mr-2">
|
||||
{rules.length === 0 && ruleGroupIndex === 0 && (
|
||||
<Text className="p-2 !font-medium">Where</Text>
|
||||
)}
|
||||
|
||||
<RuleGroupControls
|
||||
name={name}
|
||||
showSelect={
|
||||
(rules.length === 0 && ruleGroupIndex === 1) ||
|
||||
(rules.length === 1 && ruleGroupIndex === 0)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RuleGroupEditor
|
||||
onRemove={() => removeGroup(ruleGroupIndex)}
|
||||
disableRemove={rules.length === 0 && groups.length === 1}
|
||||
disabledOperators={disabledOperators}
|
||||
name={`${name}.groups.${ruleGroupIndex}`}
|
||||
className="flex-auto"
|
||||
depth={depth + 1}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row lg:grid-flow-col lg:justify-between gap-2 pb-2">
|
||||
<div className="grid grid-flow-row lg:grid-flow-col gap-2 lg:justify-start">
|
||||
<Button
|
||||
startIcon={<PlusIcon />}
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
appendRule({ column: '', operator: '_eq', value: '' })
|
||||
}
|
||||
>
|
||||
New Rule
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
startIcon={<PlusIcon />}
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
appendGroup({
|
||||
operator: '_and',
|
||||
rules: [{ column: '', operator: '_eq', value: '' }],
|
||||
groups: [],
|
||||
})
|
||||
}
|
||||
disabled={depth >= maxDepth - 1}
|
||||
>
|
||||
New Group
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{onRemove && (
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={onRemove}
|
||||
disabled={disableRemove}
|
||||
>
|
||||
Delete Group
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Rule, RuleGroup } from '@/types/dataBrowser';
|
||||
import type { ButtonProps } from '@/ui/v2/Button';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import XIcon from '@/ui/v2/icons/XIcon';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface RuleRemoveButtonProps extends ButtonProps {
|
||||
/**
|
||||
* Name of the parent group editor.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Function to be called when the remove button is clicked.
|
||||
*/
|
||||
onRemove?: VoidFunction;
|
||||
}
|
||||
|
||||
function RuleRemoveButton({
|
||||
name,
|
||||
onRemove,
|
||||
className,
|
||||
...props
|
||||
}: RuleRemoveButtonProps) {
|
||||
const rules: Rule[] = useWatch({ name: `${name}.rules` });
|
||||
const groups: RuleGroup[] = useWatch({ name: `${name}.groups` });
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className={twMerge(
|
||||
'!bg-white lg:!rounded-l-none lg:flex-grow-0 lg:flex-shrink-0 lg:flex-[40px] !min-w-0 h-10',
|
||||
className,
|
||||
)}
|
||||
disabled={rules.length === 1 && groups.length === 0}
|
||||
aria-label="Remove Rule"
|
||||
{...props}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<XIcon className="!w-4 !h-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default RuleRemoveButton;
|
||||
@@ -0,0 +1,177 @@
|
||||
import ControlledAutocomplete from '@/components/common/ControlledAutocomplete';
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import ReadOnlyToggle from '@/components/common/ReadOnlyToggle';
|
||||
import type { ColumnAutocompleteProps } from '@/components/dataBrowser/ColumnAutocomplete';
|
||||
import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { HasuraOperator } from '@/types/dataBrowser';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useController, useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
export interface RuleValueInputProps {
|
||||
/**
|
||||
* Name of the parent group editor.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Path of the table selected through the column input.
|
||||
*/
|
||||
selectedTablePath?: string;
|
||||
}
|
||||
|
||||
function ColumnSelectorInput({
|
||||
name,
|
||||
selectedTablePath,
|
||||
schema,
|
||||
table,
|
||||
...props
|
||||
}: ColumnAutocompleteProps & { selectedTablePath: string }) {
|
||||
const { setValue, control } = useFormContext();
|
||||
const { field } = useController({
|
||||
name,
|
||||
control,
|
||||
});
|
||||
|
||||
return (
|
||||
<ColumnAutocomplete
|
||||
{...props}
|
||||
{...field}
|
||||
value={
|
||||
// this array can either be ['$', 'columnName'] or ['columnName']
|
||||
Array.isArray(field.value) ? field.value.slice(-1)[0] : field.value
|
||||
}
|
||||
schema={schema}
|
||||
table={table}
|
||||
disableRelationships
|
||||
rootClassName="flex-auto"
|
||||
slotProps={{
|
||||
input: { className: 'lg:!rounded-none !bg-white !z-10' },
|
||||
}}
|
||||
onChange={(_event, { value }) => {
|
||||
if (selectedTablePath === `${schema}.${table}`) {
|
||||
setValue(name, [value], { shouldDirty: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// For more information, see https://github.com/hasura/graphql-engine/issues/3459#issuecomment-1085666541
|
||||
setValue(name, ['$', value], { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RuleValueInput({
|
||||
name,
|
||||
selectedTablePath,
|
||||
}: RuleValueInputProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { setValue } = useFormContext();
|
||||
const inputName = `${name}.value`;
|
||||
const operator: HasuraOperator = useWatch({ name: `${name}.operator` });
|
||||
const isHasuraInput = operator === '_in_hasura' || operator === '_nin_hasura';
|
||||
const {
|
||||
query: { schemaSlug, tableSlug },
|
||||
} = useRouter();
|
||||
|
||||
const { data, loading, error } = useGetAppCustomClaimsQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
skip: !isHasuraInput,
|
||||
});
|
||||
|
||||
if (operator === '_is_null') {
|
||||
return (
|
||||
<ControlledSelect
|
||||
name={inputName}
|
||||
className="flex-auto"
|
||||
fullWidth
|
||||
slotProps={{ root: { className: 'bg-white lg:!rounded-none h-10' } }}
|
||||
>
|
||||
<Option value="true">
|
||||
<ReadOnlyToggle
|
||||
checked
|
||||
slotProps={{ label: { className: '!text-sm' } }}
|
||||
/>
|
||||
</Option>
|
||||
|
||||
<Option value="false">
|
||||
<ReadOnlyToggle
|
||||
checked={false}
|
||||
slotProps={{ label: { className: '!text-sm' } }}
|
||||
/>
|
||||
</Option>
|
||||
</ControlledSelect>
|
||||
);
|
||||
}
|
||||
|
||||
if (operator === '_in' || operator === '_nin') {
|
||||
return (
|
||||
<ControlledAutocomplete
|
||||
name={inputName}
|
||||
multiple
|
||||
freeSolo
|
||||
limitTags={5}
|
||||
className="flex-auto"
|
||||
slotProps={{ input: { className: 'lg:!rounded-none !bg-white !z-10' } }}
|
||||
options={[]}
|
||||
fullWidth
|
||||
filterSelectedOptions
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (['_ceq', '_cne', '_cgt', '_clt', '_cgte', '_clte'].includes(operator)) {
|
||||
return (
|
||||
<ColumnSelectorInput
|
||||
selectedTablePath={selectedTablePath}
|
||||
schema={schemaSlug as string}
|
||||
table={tableSlug as string}
|
||||
name={inputName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const availableHasuraPermissionVariables = !loading
|
||||
? getPermissionVariablesArray(data?.app?.authJwtCustomClaims).map(
|
||||
({ key }) => ({
|
||||
value: `X-Hasura-${key}`,
|
||||
label: `X-Hasura-${key}`,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<ControlledAutocomplete
|
||||
freeSolo={!isHasuraInput}
|
||||
name={inputName}
|
||||
className="flex-auto"
|
||||
slotProps={{
|
||||
input: { className: 'lg:!rounded-none !bg-white' },
|
||||
formControl: { className: '!bg-transparent' },
|
||||
}}
|
||||
fullWidth
|
||||
loading={loading}
|
||||
loadingText={<ActivityIndicator label="Loading..." />}
|
||||
error={!!error}
|
||||
helperText={error?.message}
|
||||
options={
|
||||
isHasuraInput
|
||||
? availableHasuraPermissionVariables
|
||||
: [{ value: 'X-Hasura-User-Id', label: 'X-Hasura-User-Id' }]
|
||||
}
|
||||
onChange={(_event, _value, reason, details) => {
|
||||
if (
|
||||
reason !== 'selectOption' &&
|
||||
details.option.value !== 'X-Hasura-User-Id'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(inputName, details.option.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './RuleGroupEditor';
|
||||
export { default } from './RuleGroupEditor';
|
||||
@@ -61,10 +61,8 @@ function LogsTimePicker({
|
||||
value={format(selectedDate, 'HH:mm:ss')}
|
||||
style={{ width: '135px' }}
|
||||
id="time-picker"
|
||||
componentsProps={{
|
||||
formControl: {
|
||||
className: 'grid grid-flow-col gap-x-3',
|
||||
},
|
||||
slotProps={{
|
||||
formControl: { className: 'grid grid-flow-col gap-x-3' },
|
||||
label: { sx: { fontSize: '14px' } },
|
||||
}}
|
||||
onChange={handleTimePicking}
|
||||
|
||||
@@ -5,6 +5,9 @@ import Image from 'next/image';
|
||||
|
||||
export default function OverviewProjectInfo() {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { region, subdomain } = currentApplication || {};
|
||||
const isRegionAvailable =
|
||||
region?.awsName && region?.countryCode && region?.city;
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row content-start gap-6">
|
||||
@@ -16,28 +19,28 @@ export default function OverviewProjectInfo() {
|
||||
<div className="grid grid-flow-row gap-3">
|
||||
<InfoCard
|
||||
title="Region"
|
||||
value={currentApplication.region?.awsName}
|
||||
value={region?.awsName}
|
||||
customValue={
|
||||
currentApplication.region && (
|
||||
region.countryCode &&
|
||||
region.city && (
|
||||
<div className="grid grid-flow-col items-center gap-1 self-center">
|
||||
<Image
|
||||
src={`/assets/${currentApplication.region.countryCode}.svg`}
|
||||
alt={`Logo of ${currentApplication.region.countryCode}`}
|
||||
src={`/assets/${region.countryCode}.svg`}
|
||||
alt={`Logo of ${region.countryCode}`}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
|
||||
<Text className="text-sm font-medium text-greyscaleDark">
|
||||
{currentApplication.region.city} (
|
||||
{currentApplication.region.awsName})
|
||||
{region.city} ({region.awsName})
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
disableCopy={!currentApplication.region}
|
||||
disableCopy={!isRegionAvailable}
|
||||
/>
|
||||
|
||||
<InfoCard title="Subdomain" value={currentApplication.subdomain} />
|
||||
<InfoCard title="Subdomain" value={subdomain} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatab
|
||||
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useUserData } from '@nhost/react';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function ResetDatabasePasswordSettings() {
|
||||
error={Boolean(errors?.databasePassword)}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
componentsProps={{
|
||||
slotProps={{
|
||||
input: { className: 'lg:w-1/2' },
|
||||
helperText: { component: 'div' },
|
||||
}}
|
||||
|
||||
@@ -125,7 +125,10 @@ export default function EnvironmentVariableSettings() {
|
||||
docsLink="https://docs.nhost.io/platform/environment-variables"
|
||||
docsTitle="Environment Variables"
|
||||
rootClassName="gap-0"
|
||||
className="px-0 my-2"
|
||||
className={twMerge(
|
||||
'px-0 my-2',
|
||||
availableEnvironmentVariables.length === 0 && 'gap-2',
|
||||
)}
|
||||
slotProps={{ submitButton: { className: 'hidden' } }}
|
||||
>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-3 gap-2 border-b-1 border-gray-200 px-4 py-3">
|
||||
@@ -134,91 +137,98 @@ export default function EnvironmentVariableSettings() {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<List>
|
||||
{availableEnvironmentVariables.map((environmentVariable, index) => {
|
||||
const timestamp = formatDistanceToNowStrict(
|
||||
parseISO(environmentVariable.updatedAt),
|
||||
{ addSuffix: true, roundingMethod: 'floor' },
|
||||
);
|
||||
{availableEnvironmentVariables.length > 0 && (
|
||||
<List>
|
||||
{availableEnvironmentVariables.map((environmentVariable, index) => {
|
||||
const timestamp = formatDistanceToNowStrict(
|
||||
parseISO(environmentVariable.updatedAt),
|
||||
{ addSuffix: true, roundingMethod: 'floor' },
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={environmentVariable.id}>
|
||||
<ListItem.Root
|
||||
className="px-4 grid grid-cols-2 lg:grid-cols-3 gap-2"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<IconButton variant="borderless" color="secondary">
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Dropdown.Item
|
||||
onClick={() => handleOpenEditor(environmentVariable)}
|
||||
return (
|
||||
<Fragment key={environmentVariable.id}>
|
||||
<ListItem.Root
|
||||
className="px-4 grid grid-cols-2 lg:grid-cols-3 gap-2"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||
>
|
||||
<Text className="font-medium">Edit</Text>
|
||||
</Dropdown.Item>
|
||||
<IconButton variant="borderless" color="secondary">
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Divider component="li" />
|
||||
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
handleConfirmDelete(environmentVariable)
|
||||
}
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-32' }}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="font-medium"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
handleOpenEditor(environmentVariable)
|
||||
}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text className="truncate">
|
||||
{environmentVariable.name}
|
||||
</ListItem.Text>
|
||||
<Text className="font-medium">Edit</Text>
|
||||
</Dropdown.Item>
|
||||
|
||||
<Text variant="subtitle1" className="lg:col-span-2 truncate">
|
||||
{timestamp === '0 seconds ago' ||
|
||||
timestamp === 'in 0 seconds'
|
||||
? 'Now'
|
||||
: timestamp}
|
||||
</Text>
|
||||
</ListItem.Root>
|
||||
<Divider component="li" />
|
||||
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availableEnvironmentVariables.length - 1
|
||||
? '!mt-4'
|
||||
: '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Dropdown.Item
|
||||
onClick={() =>
|
||||
handleConfirmDelete(environmentVariable)
|
||||
}
|
||||
>
|
||||
<Text
|
||||
className="font-medium"
|
||||
sx={{
|
||||
color: (theme) => theme.palette.error.main,
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Text className="truncate">
|
||||
{environmentVariable.name}
|
||||
</ListItem.Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle1"
|
||||
className="lg:col-span-2 truncate"
|
||||
>
|
||||
{timestamp === '0 seconds ago' ||
|
||||
timestamp === 'in 0 seconds'
|
||||
? 'Now'
|
||||
: timestamp}
|
||||
</Text>
|
||||
</ListItem.Root>
|
||||
|
||||
<Divider
|
||||
component="li"
|
||||
className={twMerge(
|
||||
index === availableEnvironmentVariables.length - 1
|
||||
? '!mt-4'
|
||||
: '!my-4',
|
||||
)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="justify-self-start mx-4"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useAppClient } from '@/hooks/useAppClient';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
@@ -13,6 +14,10 @@ import Input from '@/ui/v2/Input';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import generateAppServiceUrl, {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
} from '@/utils/common/generateAppServiceUrl';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { useGetAppInjectedVariablesQuery } from '@/utils/__generated__/graphql';
|
||||
@@ -27,8 +32,9 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
const { data, loading, error } = useGetAppInjectedVariablesQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
});
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const appClient = useAppClient({ start: false });
|
||||
const appClient = useAppClient();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -71,11 +77,6 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
});
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? LOCAL_HASURA_URL
|
||||
: generateRemoteAppUrl(currentApplication.subdomain);
|
||||
|
||||
const systemEnvironmentVariables = [
|
||||
{
|
||||
key: 'NHOST_BACKEND_URL',
|
||||
@@ -83,7 +84,19 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
},
|
||||
{ key: 'NHOST_SUBDOMAIN', value: currentApplication.subdomain },
|
||||
{ key: 'NHOST_REGION', value: currentApplication.region.awsName },
|
||||
{ key: 'NHOST_HASURA_URL', value: `${hasuraUrl}/console` },
|
||||
{
|
||||
key: 'NHOST_HASURA_URL',
|
||||
value:
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${LOCAL_HASURA_URL}/console`
|
||||
: generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
'hasura',
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
),
|
||||
},
|
||||
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
|
||||
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
||||
|
||||
@@ -14,7 +14,7 @@ import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import Tooltip from '@/ui/v2/Tooltip';
|
||||
import getPermissionVariables from '@/utils/settings/getPermissionVariablesArray';
|
||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
useGetAppCustomClaimsQuery,
|
||||
@@ -125,7 +125,7 @@ export default function PermissionVariableSettings() {
|
||||
});
|
||||
}
|
||||
|
||||
const availablePermissionVariables = getPermissionVariables(
|
||||
const availablePermissionVariables = getPermissionVariablesArray(
|
||||
data?.app?.authJwtCustomClaims,
|
||||
);
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import IconButton from '@/ui/v2/IconButton';
|
||||
import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -23,7 +23,6 @@ export interface AppleProviderFormValues {
|
||||
authAppleKeyId: string;
|
||||
authAppleClientId: string;
|
||||
authApplePrivateKey: string;
|
||||
authAppleScope: string;
|
||||
}
|
||||
|
||||
export default function AppleProviderSettings() {
|
||||
@@ -38,7 +37,6 @@ export default function AppleProviderSettings() {
|
||||
authAppleKeyId,
|
||||
authAppleClientId,
|
||||
authApplePrivateKey,
|
||||
authAppleScope,
|
||||
},
|
||||
},
|
||||
loading,
|
||||
@@ -57,7 +55,6 @@ export default function AppleProviderSettings() {
|
||||
authAppleKeyId,
|
||||
authAppleClientId,
|
||||
authApplePrivateKey,
|
||||
authAppleScope,
|
||||
authAppleEnabled,
|
||||
},
|
||||
});
|
||||
@@ -83,9 +80,7 @@ export default function AppleProviderSettings() {
|
||||
const updateAppMutation = updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
...values,
|
||||
},
|
||||
app: values,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -96,7 +91,7 @@ export default function AppleProviderSettings() {
|
||||
success: `Apple settings have been updated successfully.`,
|
||||
error: `An error occurred while trying to update the project's Apple settings.`,
|
||||
},
|
||||
{ ...toastStyleProps },
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
@@ -108,9 +103,11 @@ export default function AppleProviderSettings() {
|
||||
<SettingsContainer
|
||||
title="Apple"
|
||||
description="Allow users to sign in with Apple."
|
||||
primaryActionButtonProps={{
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isValid || !formState.isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-apple"
|
||||
docsTitle="how to sign in users with Apple"
|
||||
@@ -134,9 +131,9 @@ export default function AppleProviderSettings() {
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
<Input
|
||||
{...register('authAppleScope')}
|
||||
name="authAppleScope"
|
||||
id="authAppleScope"
|
||||
{...register('authAppleClientId')}
|
||||
name="authAppleClientId"
|
||||
id="authAppleClientId"
|
||||
label="Service ID"
|
||||
placeholder="Apple Service ID"
|
||||
className="col-span-1"
|
||||
@@ -168,9 +165,11 @@ export default function AppleProviderSettings() {
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
defaultValue={`${generateRemoteAppUrl(
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
)}/v1/auth/signin/provider/apple/callback`}
|
||||
currentApplication.region.awsName,
|
||||
'auth',
|
||||
)}/signin/provider/apple/callback`}
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
@@ -185,9 +184,11 @@ export default function AppleProviderSettings() {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateRemoteAppUrl(
|
||||
`${generateAppServiceUrl(
|
||||
currentApplication.subdomain,
|
||||
)}/v1/auth/signin/provider/apple/callback`,
|
||||
currentApplication.region.awsName,
|
||||
'auth',
|
||||
)}/signin/provider/apple/callback`,
|
||||
'Redirect URL',
|
||||
);
|
||||
}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user