Compare commits
516 Commits
@nhost/rea
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43705b992d | ||
|
|
2e999e8715 | ||
|
|
0370696d5c | ||
|
|
f62131d55a | ||
|
|
86d077ac00 | ||
|
|
200e9f774c | ||
|
|
bc1235de3b | ||
|
|
fce58ebaea | ||
|
|
452e281120 | ||
|
|
9a338e54c9 | ||
|
|
baeebf980d | ||
|
|
ac92c6ee61 | ||
|
|
1ddaf680c0 | ||
|
|
c6e6194d8e | ||
|
|
83deea8b45 | ||
|
|
acbaabcf85 | ||
|
|
3534501f37 | ||
|
|
27bc23cbbc | ||
|
|
6450223558 | ||
|
|
a62a85a777 | ||
|
|
ae24f83953 | ||
|
|
fc60d7a782 | ||
|
|
6be8a998df | ||
|
|
ea091f6251 | ||
|
|
8175c052f7 | ||
|
|
e6605a6ed0 | ||
|
|
1cba0e6492 | ||
|
|
179c90fcdb | ||
|
|
85f0f943a1 | ||
|
|
c4c23fde31 | ||
|
|
e0b94c3e90 | ||
|
|
113d638532 | ||
|
|
d87448916f | ||
|
|
af4292658c | ||
|
|
f735bcd2ea | ||
|
|
66fb74af86 | ||
|
|
791eac30bb | ||
|
|
da4ad889d7 | ||
|
|
9ef111760c | ||
|
|
c2706c7d97 | ||
|
|
683b8768c4 | ||
|
|
6d9df237a8 | ||
|
|
220ae37aa7 | ||
|
|
d0d94d9239 | ||
|
|
aed3d1f147 | ||
|
|
d07bf08e45 | ||
|
|
f2183250d2 | ||
|
|
d2bb5ecfae | ||
|
|
02d0db0cf0 | ||
|
|
441005d5c3 | ||
|
|
eea8708549 | ||
|
|
5f3f9390aa | ||
|
|
1c5b0560ed | ||
|
|
1bfdf21b99 | ||
|
|
efd522a38a | ||
|
|
55c35fa9c5 | ||
|
|
d42c27ae99 | ||
|
|
927be4a2c9 | ||
|
|
e44352abbd | ||
|
|
f9289f3c32 | ||
|
|
8ff06e5637 | ||
|
|
49e4633bca | ||
|
|
7ae7a7206c | ||
|
|
43d7e7babf | ||
|
|
463a51ce7c | ||
|
|
86e9d9d47f | ||
|
|
f99b72cd7c | ||
|
|
0dc2f3ff29 | ||
|
|
d0f8081101 | ||
|
|
84ebfb79d0 | ||
|
|
3c78d0ef46 | ||
|
|
e9a26fc995 | ||
|
|
b0794507f5 | ||
|
|
824e222e9d | ||
|
|
16a99d7d0f | ||
|
|
cda5c3d274 | ||
|
|
3d3791286d | ||
|
|
ad28bf2166 | ||
|
|
17bfa83204 | ||
|
|
6cd64e76ff | ||
|
|
a4bf50cf23 | ||
|
|
113baafd84 | ||
|
|
87c2b31821 | ||
|
|
8a6bc3625c | ||
|
|
bdfda8aced | ||
|
|
ca090436af | ||
|
|
55f85a04ea | ||
|
|
73f95cfa3b | ||
|
|
dbd3ded515 | ||
|
|
5399fac211 | ||
|
|
52e3127a34 | ||
|
|
3fb12c189b | ||
|
|
c4d5366b22 | ||
|
|
bd68e916cf | ||
|
|
7cadd9447b | ||
|
|
b649f178e0 | ||
|
|
7432c6477c | ||
|
|
c3aa6126fe | ||
|
|
0f3cf887c1 | ||
|
|
5cd311b69a | ||
|
|
057fda178f | ||
|
|
241b14a004 | ||
|
|
1f5e1e3d42 | ||
|
|
5727b0b0fe | ||
|
|
10b56089fa | ||
|
|
973df1ed5a | ||
|
|
8f681b83e8 | ||
|
|
2f38ed56f5 | ||
|
|
21501624e6 | ||
|
|
464530dacb | ||
|
|
0f2fc3dfec | ||
|
|
5cb71f1dc8 | ||
|
|
83e0a4d33e | ||
|
|
16502ea175 | ||
|
|
beee0407df | ||
|
|
3990b1ffbb | ||
|
|
1fb03708e3 | ||
|
|
e9ef254c6d | ||
|
|
d42719ee65 | ||
|
|
72ff489ea8 | ||
|
|
c9bf2dde0e | ||
|
|
613533d377 | ||
|
|
8568354718 | ||
|
|
1be6d32455 | ||
|
|
812a6e5005 | ||
|
|
34cc230b61 | ||
|
|
898a7c835f | ||
|
|
7766624bc5 | ||
|
|
2e8f73df38 | ||
|
|
6a419e060e | ||
|
|
43480ca735 | ||
|
|
efc42d77fd | ||
|
|
31e2523eca | ||
|
|
fbf4f40ab7 | ||
|
|
cbe203e720 | ||
|
|
09af118452 | ||
|
|
20d0c3d09b | ||
|
|
378a6684b0 | ||
|
|
d92891b223 | ||
|
|
1999ae09e6 | ||
|
|
aef86dc822 | ||
|
|
0fe48a0833 | ||
|
|
7bbf6dbf1c | ||
|
|
a3499c4628 | ||
|
|
689dc873b3 | ||
|
|
a0747d02e0 | ||
|
|
be5bd1e446 | ||
|
|
52ccfdec89 | ||
|
|
2c60591580 | ||
|
|
3cac6f69bd | ||
|
|
71ff71ccd2 | ||
|
|
da575ca262 | ||
|
|
5020566725 | ||
|
|
eb5915aa03 | ||
|
|
458ee7fe6c | ||
|
|
ea7eb18f36 | ||
|
|
18f5414411 | ||
|
|
a7ce6d85f4 | ||
|
|
2f348c660a | ||
|
|
6140bc5b3b | ||
|
|
9f7780ec91 | ||
|
|
7c07d09ea4 | ||
|
|
13876ed523 | ||
|
|
abc7d0c7a5 | ||
|
|
074a36ea48 | ||
|
|
64e806dc27 | ||
|
|
bd0e9748b6 | ||
|
|
b21222b378 | ||
|
|
7e217db128 | ||
|
|
56c716d9fa | ||
|
|
14ecbd1fb9 | ||
|
|
a0242c4d6f | ||
|
|
4800b4a756 | ||
|
|
5b318d17d4 | ||
|
|
2f9be4f760 | ||
|
|
64777b6f30 | ||
|
|
7e1489353e | ||
|
|
c53306a497 | ||
|
|
83345579d0 | ||
|
|
2b4b9e0385 | ||
|
|
922349f550 | ||
|
|
d613f3fd04 | ||
|
|
4d8a47777e | ||
|
|
229a7ab1f7 | ||
|
|
3dabb7b53a | ||
|
|
abc3d6ce60 | ||
|
|
a529b654bc | ||
|
|
08d49bd1fd | ||
|
|
03435a2c66 | ||
|
|
66208d6840 | ||
|
|
5be9abb0fa | ||
|
|
8e504b5328 | ||
|
|
0e3eb7204a | ||
|
|
b112ba0af4 | ||
|
|
70cfeb1fcf | ||
|
|
e6d990faa7 | ||
|
|
b45da7e360 | ||
|
|
3116562b58 | ||
|
|
693e40d385 | ||
|
|
ff186a8d09 | ||
|
|
3061771908 | ||
|
|
c681cc9bef | ||
|
|
3a80504427 | ||
|
|
9a1aa7bb2e | ||
|
|
98345f2e78 | ||
|
|
f29abe6238 | ||
|
|
8956d47bce | ||
|
|
dd0738d5f7 | ||
|
|
11d77d6011 | ||
|
|
a78cd2f18f | ||
|
|
e025c5857f | ||
|
|
6ef340daad | ||
|
|
a96e3c9163 | ||
|
|
c3c95053dc | ||
|
|
b27e94c712 | ||
|
|
279cf78aa5 | ||
|
|
8817adddf6 | ||
|
|
229c47cf16 | ||
|
|
1388f11508 | ||
|
|
e5e705350d | ||
|
|
4f81b0695d | ||
|
|
c2bfed6e1f | ||
|
|
97dc261fcd | ||
|
|
4ca93c2773 | ||
|
|
9c5b6532d3 | ||
|
|
f8b32584b4 | ||
|
|
889df8ca4d | ||
|
|
b998e09e10 | ||
|
|
9e0486a362 | ||
|
|
74037cec68 | ||
|
|
800db1b300 | ||
|
|
a40baa8c63 | ||
|
|
5cc06609c2 | ||
|
|
819e68b501 | ||
|
|
efa68aab83 | ||
|
|
3a696d366a | ||
|
|
e3e21b6164 | ||
|
|
9259663c76 | ||
|
|
26dd7faf05 | ||
|
|
10cc213933 | ||
|
|
1b5cb93761 | ||
|
|
4157c012fd | ||
|
|
8de1be4910 | ||
|
|
9515096349 | ||
|
|
4dd5617855 | ||
|
|
d0f9ffba73 | ||
|
|
01dc358842 | ||
|
|
925a1808e6 | ||
|
|
ac80f88727 | ||
|
|
3078247629 | ||
|
|
144c0084d2 | ||
|
|
bbdfb77a07 | ||
|
|
b6df9e2e8c | ||
|
|
48f15eb849 | ||
|
|
141642d40d | ||
|
|
def4a3a2ea | ||
|
|
fcb4d167e7 | ||
|
|
b5a9c1be47 | ||
|
|
c5137c6c45 | ||
|
|
297c2a965d | ||
|
|
5b5e7d9640 | ||
|
|
b876a4ada1 | ||
|
|
c02e0c63f2 | ||
|
|
7e064355ba | ||
|
|
788482fab2 | ||
|
|
16d94821b8 | ||
|
|
0f6ece6b8c | ||
|
|
5c4ab54c90 | ||
|
|
793e7392da | ||
|
|
5941568bbb | ||
|
|
1a9e1fde1d | ||
|
|
a9bbd1303e | ||
|
|
b0ed2b6f14 | ||
|
|
9194be4816 | ||
|
|
3e951eab4f | ||
|
|
cd7a198715 | ||
|
|
7c4d05a25e | ||
|
|
199fd0d491 | ||
|
|
32c0632526 | ||
|
|
19cca7f45d | ||
|
|
cd4b58674a | ||
|
|
191580a819 | ||
|
|
4a57861354 | ||
|
|
d96b817476 | ||
|
|
db6db8d860 | ||
|
|
fe405ba123 | ||
|
|
972a5f652f | ||
|
|
7a2c140524 | ||
|
|
3d717d68a9 | ||
|
|
13b6a47bef | ||
|
|
2a6caa47bd | ||
|
|
4f20d8640d | ||
|
|
5e8ae336a2 | ||
|
|
1ff73f4f00 | ||
|
|
7ce44ae1b1 | ||
|
|
1b847617b6 | ||
|
|
df53ec2954 | ||
|
|
7b4c32816e | ||
|
|
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 | ||
|
|
357e0933ff | ||
|
|
baa1937d06 | ||
|
|
03e5662df9 | ||
|
|
e0711bdfc8 | ||
|
|
caca27fde3 | ||
|
|
b0d51033c6 | ||
|
|
088f9394fc | ||
|
|
a91361f971 | ||
|
|
a4f5be6ab9 | ||
|
|
dbbccbf1cd | ||
|
|
7d8f82b99d | ||
|
|
a70dc7b352 | ||
|
|
54df0df42b | ||
|
|
0bbb2598fd | ||
|
|
e10480b761 | ||
|
|
1343abbe50 | ||
|
|
4ba34cc827 | ||
|
|
fa37546139 | ||
|
|
42ece48ce3 | ||
|
|
112526a984 | ||
|
|
f97ab31f69 | ||
|
|
21dc1ecd6b | ||
|
|
c11adbe3e2 | ||
|
|
6078e9c207 | ||
|
|
450582dc43 | ||
|
|
ddec0e1be1 | ||
|
|
698154b24b | ||
|
|
9c87b0f67b | ||
|
|
85afe3d216 | ||
|
|
0773e5215f | ||
|
|
10f25fcc4e | ||
|
|
2ee90d6ea3 | ||
|
|
5db5323a1d | ||
|
|
0c74806245 | ||
|
|
5df84d7f50 | ||
|
|
d58de8fcf8 | ||
|
|
ed93d4b583 | ||
|
|
1ec1953eaa | ||
|
|
63e9c3933e | ||
|
|
85674c4d90 | ||
|
|
5a1d3b9bfc | ||
|
|
942570ed29 | ||
|
|
1b1620f633 | ||
|
|
e8e8d661e1 | ||
|
|
ba08ec7f5c | ||
|
|
fb0c98c21d | ||
|
|
c9f575c40c | ||
|
|
6c8bed7ecc | ||
|
|
3058eee48f | ||
|
|
d5a712f7ef | ||
|
|
83422f5ee6 | ||
|
|
51909a6a8f | ||
|
|
2f30797556 | ||
|
|
0b8f7d1661 | ||
|
|
52ee9d84b6 | ||
|
|
d9612b28b0 | ||
|
|
0034791493 | ||
|
|
80fed14a6b | ||
|
|
d457ada435 | ||
|
|
b41e5a9df5 | ||
|
|
0c8ace1bd4 | ||
|
|
3f800a068b | ||
|
|
7d490fe569 | ||
|
|
d6527122db | ||
|
|
3211140dec | ||
|
|
469352cd81 | ||
|
|
88400f6b7c | ||
|
|
f8c8a06d71 | ||
|
|
ebc1730fce | ||
|
|
c1cd1e813c | ||
|
|
e08a074474 | ||
|
|
2f819865bc | ||
|
|
3888f3041f | ||
|
|
bacb1b9720 | ||
|
|
e119e4fc18 | ||
|
|
569c4004f6 | ||
|
|
95932fa3f2 | ||
|
|
99402b77d1 | ||
|
|
f6fb2cd8e6 | ||
|
|
5c2cf59b41 | ||
|
|
a6d31dc260 | ||
|
|
b1fe2be963 | ||
|
|
872e50b635 | ||
|
|
bd73557a47 | ||
|
|
9b6e8ab3bc | ||
|
|
c95bab70c2 | ||
|
|
fe0742e278 | ||
|
|
ded57d3b24 | ||
|
|
c30abaea22 | ||
|
|
d2c4b7cad1 | ||
|
|
59d737696a | ||
|
|
22de3214f1 | ||
|
|
cf880f992f | ||
|
|
195adfb04a | ||
|
|
aee4cdcb72 | ||
|
|
87af60cc03 | ||
|
|
3d8dd39995 | ||
|
|
65687beecc | ||
|
|
62aa859737 | ||
|
|
d8c2d369aa | ||
|
|
a4e4926aeb | ||
|
|
35cd76e562 | ||
|
|
266bbe837d | ||
|
|
caf785a938 | ||
|
|
9bc3e755df | ||
|
|
638a7ac11d | ||
|
|
4e49c8db50 | ||
|
|
210f65b4db | ||
|
|
1b6482126f | ||
|
|
96f9c1a55d | ||
|
|
731460b20d | ||
|
|
1537d46b1d | ||
|
|
632def158d | ||
|
|
39271a67e2 | ||
|
|
9e25c4f386 | ||
|
|
dd58a4ac7f | ||
|
|
b9c3567baa | ||
|
|
108937789a | ||
|
|
e651745a7e | ||
|
|
699debb2b8 | ||
|
|
3e08dc7f8c | ||
|
|
6928b48781 | ||
|
|
02886350ff | ||
|
|
b3672f8246 | ||
|
|
6091b4a8e8 | ||
|
|
82ddcbd180 | ||
|
|
8aa7aafa3b | ||
|
|
183cb4b26a | ||
|
|
3a7377c6e2 | ||
|
|
1529f58c33 | ||
|
|
95af5421d1 | ||
|
|
feb39404db | ||
|
|
15b3100c63 | ||
|
|
f7ef7d106d | ||
|
|
0dbbcc5595 | ||
|
|
816f8d069d | ||
|
|
72c31622cd | ||
|
|
6959461e3f | ||
|
|
103472ac77 | ||
|
|
2ebf99ff8f | ||
|
|
c13e492bbf | ||
|
|
63476a2351 | ||
|
|
782252c059 | ||
|
|
e86978a1ff | ||
|
|
84cfd11953 | ||
|
|
9a43e136f6 | ||
|
|
e9cff26fa0 | ||
|
|
3d32bca2b3 | ||
|
|
4021feaf38 | ||
|
|
306ec74356 | ||
|
|
2764a1c4b6 | ||
|
|
4b2d2a4f55 | ||
|
|
eba2bd05b8 | ||
|
|
84a1b28261 | ||
|
|
ff3b9df41e | ||
|
|
a0901914ac | ||
|
|
f1272947dd | ||
|
|
e5041bfd30 | ||
|
|
3d7cc74feb | ||
|
|
d007202783 | ||
|
|
38e92a705d | ||
|
|
74cc63833a | ||
|
|
425320bbb5 | ||
|
|
499352ad8a | ||
|
|
26d577d7ae | ||
|
|
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 |
@@ -7,4 +7,4 @@
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
}
|
||||
7
.github/actions/nhost-cli/action.yaml
vendored
7
.github/actions/nhost-cli/action.yaml
vendored
@@ -35,8 +35,11 @@ runs:
|
||||
fi
|
||||
- name: Install Nhost CLI
|
||||
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
|
||||
shell: bash
|
||||
run: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
|
||||
uses: nick-fields/retry@v2
|
||||
with:
|
||||
timeout_minutes: 3
|
||||
max_attempts: 10
|
||||
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
|
||||
|
||||
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*/**/*'
|
||||
|
||||
9
.github/renovate.json
vendored
9
.github/renovate.json
vendored
@@ -8,9 +8,16 @@
|
||||
},
|
||||
"ignoreDeps": [
|
||||
"pnpm",
|
||||
"node"
|
||||
"node",
|
||||
"@types/node"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"enabledManagers": [
|
||||
"npm",
|
||||
"dockerfile",
|
||||
"docker-compose",
|
||||
"github-actions"
|
||||
]
|
||||
}
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
BUILD: 'all'
|
||||
- name: Check if the pnpm lockfile changed
|
||||
id: changed-lockfile
|
||||
uses: tj-actions/changed-files@v34
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: pnpm-lock.yaml
|
||||
# * Determine a pnpm filter argument for packages that have been modified.
|
||||
|
||||
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 }}
|
||||
29
.github/workflows/renovate.yaml
vendored
29
.github/workflows/renovate.yaml
vendored
@@ -21,6 +21,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
@@ -61,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 }}
|
||||
|
||||
456
README.md
456
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,437 +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/badgifter">
|
||||
<img src="https://avatars.githubusercontent.com/u/50094885?v=4" width="100;" alt="badgifter"/>
|
||||
<br />
|
||||
<sub><b>Bad Gifter</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/heygambo">
|
||||
<img src="https://avatars.githubusercontent.com/u/449438?v=4" width="100;" alt="heygambo"/>
|
||||
<br />
|
||||
<sub><b>Christian Gambardella</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dbarrosop">
|
||||
<img src="https://avatars.githubusercontent.com/u/6246622?v=4" width="100;" alt="dbarrosop"/>
|
||||
<br />
|
||||
<sub><b>David Barroso</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<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/wollerman">
|
||||
<img src="https://avatars.githubusercontent.com/u/1610241?v=4" width="100;" alt="wollerman"/>
|
||||
<br />
|
||||
<sub><b>Matt Wollerman</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mdp18">
|
||||
<img src="https://avatars.githubusercontent.com/u/11698527?v=4" width="100;" alt="mdp18"/>
|
||||
<br />
|
||||
<sub><b>Max</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/mustafa-hanif">
|
||||
<img src="https://avatars.githubusercontent.com/u/30019262?v=4" width="100;" alt="mustafa-hanif"/>
|
||||
<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>
|
||||
<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></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alexander-mart">
|
||||
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
|
||||
<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>
|
||||
<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></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
<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>
|
||||
<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></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/nkhdo">
|
||||
<img src="https://avatars.githubusercontent.com/u/26102306?v=4" width="100;" alt="nkhdo"/>
|
||||
<br />
|
||||
<sub><b>Hoang Do</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/iangabrielsanchez">
|
||||
<img src="https://avatars.githubusercontent.com/u/9511946?v=4" width="100;" alt="iangabrielsanchez"/>
|
||||
<br />
|
||||
<sub><b>Ian Gabriel Sanchez</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/eltociear">
|
||||
<img src="https://avatars.githubusercontent.com/u/22633385?v=4" width="100;" alt="eltociear"/>
|
||||
<br />
|
||||
<sub><b>Ikko Ashimine</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<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></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MarcelloTheArcane">
|
||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||
<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>
|
||||
<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></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/atapas">
|
||||
<img src="https://avatars.githubusercontent.com/u/3633137?v=4" width="100;" alt="atapas"/>
|
||||
<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>
|
||||
<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,112 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 200e9f77: chore(deps): update dependency @types/react-dom to v18.0.10
|
||||
- Updated dependencies [200e9f77]
|
||||
- @nhost/nextjs@1.13.2
|
||||
- @nhost/react-apollo@4.13.2
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dbd3ded5: fix(dashboard): workspaces creation, new form, correct redirects.
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 85f0f943: fix(dashboard): don't break the table creation process
|
||||
|
||||
## 0.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [d42c27ae]
|
||||
- Updated dependencies [927be4a2]
|
||||
- @nhost/nextjs@1.13.1
|
||||
- @nhost/react-apollo@4.13.1
|
||||
|
||||
## 0.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d0f80811: fix(dashboard): don't show error when signing out the user
|
||||
|
||||
## 0.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d92891b2: feat(dashboard): add Permission Editor to the Database section
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3d379128: fix(dashboard): create new user
|
||||
- @nhost/react-apollo@4.13.0
|
||||
- @nhost/nextjs@1.13.0
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7cadd944: fix(dashboard): display Twitter provider settings
|
||||
|
||||
## 0.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9a1aa7bb: add functions to the log dashboard
|
||||
- f29abe62: feat(dashboard): Users Management v2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7766624b: feat(dashboard): add JWT secret editor modal
|
||||
- @nhost/react-apollo@4.12.1
|
||||
- @nhost/nextjs@1.12.1
|
||||
|
||||
## 0.7.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- dd0738d5: fix(dashboard): provisioning status polling
|
||||
|
||||
## 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
|
||||
|
||||
@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN yarn global add turbo@1
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -66,6 +66,11 @@ module.exports = withBundleAnalyzer({
|
||||
destination: '/:workspaceSlug/:appSlug/settings/environment-variables',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/:workspaceSlug/:appSlug/users/:userId',
|
||||
destination: '/:workspaceSlug/:appSlug/users?userId=:userId',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.7.8",
|
||||
"version": "0.9.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -13,11 +13,11 @@
|
||||
"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": {
|
||||
"@apollo/client": "^3.6.2",
|
||||
"@apollo/client": "^3.7.3",
|
||||
"@codemirror/language": "^6.3.0",
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.5",
|
||||
@@ -34,10 +34,7 @@
|
||||
"@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",
|
||||
@@ -91,31 +88,31 @@
|
||||
"@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",
|
||||
"@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/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",
|
||||
"@types/react-dom": "18.0.10",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@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.27.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
@@ -132,19 +129,21 @@
|
||||
"jsdom": "^20.0.3",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.49.0",
|
||||
"msw-storybook-addon": "^1.6.3",
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"react-date-fns-hooks": "^0.9.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.27.0",
|
||||
"webpack": "^5.75.0"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -158,5 +157,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
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,119 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { inputErrorMessages } from '@/utils/getErrorMessage';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useUpdateWorkspaceMutation } from '@/utils/__generated__/graphql';
|
||||
import router from 'next/router';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type ChangeWorkspaceNameProps = {
|
||||
close: VoidFunction;
|
||||
};
|
||||
|
||||
export default function ChangeWorkspaceName({
|
||||
close,
|
||||
}: ChangeWorkspaceNameProps) {
|
||||
const { currentWorkspace } = useCurrentWorkspaceAndApplication();
|
||||
const [newWorkspaceName, setNewWorkspaceName] = useState(
|
||||
currentWorkspace.name,
|
||||
);
|
||||
const [workspaceError, setWorkspaceError] = useState<string>('');
|
||||
|
||||
const [updateWorkspace, { loading: mutationLoading, error: mutationError }] =
|
||||
useUpdateWorkspaceMutation({
|
||||
refetchQueries: [],
|
||||
});
|
||||
|
||||
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
inputErrorMessages(
|
||||
event.target.value,
|
||||
setNewWorkspaceName,
|
||||
setWorkspaceError,
|
||||
'Workspace',
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
const name = newWorkspaceName;
|
||||
const slug = slugifyString(name);
|
||||
|
||||
if (slug.length < 4 || slug.length > 32) {
|
||||
setWorkspaceError('Slug should be within 4 and 32 characters.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateWorkspace({
|
||||
variables: {
|
||||
id: currentWorkspace.id,
|
||||
workspace: {
|
||||
name,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
close();
|
||||
triggerToast('Workspace name changed');
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
`Error trying to remove workspace: ${currentWorkspace.id} - ${error.message}`,
|
||||
);
|
||||
}
|
||||
await router.push(slug);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h3" component="h2">
|
||||
Change Workspace Name
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-4 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="workspaceName"
|
||||
label="New Workspace Name"
|
||||
onChange={handleChange}
|
||||
value={newWorkspaceName}
|
||||
placeholder="New workspace name"
|
||||
fullWidth
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
helperText={`https://app.nhost.io/${slugifyString(
|
||||
newWorkspaceName || '',
|
||||
)}`}
|
||||
/>
|
||||
|
||||
{workspaceError && <Alert severity="error">{workspaceError}</Alert>}
|
||||
|
||||
{mutationError && (
|
||||
<Alert severity="error">{mutationError.toString()}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={mutationLoading || !!workspaceError}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,9 +32,12 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
|
||||
useState<ConnectGithubModalState>('CONNECTING');
|
||||
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
|
||||
|
||||
const { data, loading, error } = useGetGithubRepositoriesQuery({
|
||||
pollInterval: 2000,
|
||||
});
|
||||
const { data, loading, error, startPolling } =
|
||||
useGetGithubRepositoriesQuery();
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(2000);
|
||||
}, [startPolling]);
|
||||
|
||||
const handleSelectAnotherRepository = () => {
|
||||
setSelectedRepoId(null);
|
||||
|
||||
@@ -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 { generateAppServiceUrl } 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,13 +29,15 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? LOCAL_HASURA_URL
|
||||
: `${generateAppServiceUrl(
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${LOCAL_HASURA_URL}/console`
|
||||
: generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region.awsName,
|
||||
'hasura',
|
||||
)}/console`;
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
|
||||
@@ -13,14 +13,17 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
|
||||
|
||||
const { data } = useGetFunctionLogQuery({
|
||||
const { data, startPolling } = useGetFunctionLogQuery({
|
||||
variables: {
|
||||
subdomain: currentApplication.subdomain,
|
||||
functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
|
||||
},
|
||||
pollInterval: 3000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(3000);
|
||||
}, [startPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data.getFunctionLogs.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLCl
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import Select from '@/ui/v2/Select';
|
||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||
import type { RemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
|
||||
import { DEFAULT_ROLES } from './utils';
|
||||
|
||||
@@ -57,7 +57,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user: RemoteAppGetUsersQuery['users'][number] = users.find(
|
||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
|
||||
({ id }) => id === userId,
|
||||
);
|
||||
|
||||
|
||||
@@ -37,20 +37,29 @@ 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
|
||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, options, reason, details) => {
|
||||
setValue(controllerProps?.name || name, options);
|
||||
setValue?.(controllerProps?.name || name, options, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, options, reason, details);
|
||||
|
||||
@@ -42,13 +42,16 @@ function ControlledSwitch(
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(e) => {
|
||||
setValue(controllerProps?.name || name, e.target.checked, {
|
||||
onChange={(event) => {
|
||||
setValue(controllerProps?.name || name, event.target.checked, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event);
|
||||
}
|
||||
}}
|
||||
checked={field.value || false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -35,15 +35,15 @@ function InsertPlaceholderTableRow({
|
||||
}: InsertPlaceholderTableRowProps) {
|
||||
return (
|
||||
<div
|
||||
className="h-12 border-r-1 border-b-1 border-gray-200 bg-white"
|
||||
className="h-12 bg-white border-gray-200 border-r-1 border-b-1"
|
||||
{...props}
|
||||
>
|
||||
<Button
|
||||
onClick={onInsertRow}
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="h-full w-full justify-start rounded-none px-2 py-3 text-xs font-normal hover:shadow-none focus:shadow-none focus:outline-none"
|
||||
startIcon={<PlusIcon className="h-4 w-4 text-greyscaleGrey" />}
|
||||
className="justify-start w-full h-full px-2 py-3 text-xs font-normal rounded-none hover:shadow-none focus:shadow-none focus:outline-none"
|
||||
startIcon={<PlusIcon className="w-4 h-4 text-greyscaleGrey" />}
|
||||
>
|
||||
Insert New Row
|
||||
</Button>
|
||||
@@ -181,7 +181,7 @@ export default function DataGridBody<T extends object>({
|
||||
return (
|
||||
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
|
||||
{rows.length === 0 && !loading && (
|
||||
<div className="flex flex-nowrap pr-5">
|
||||
<div className="flex pr-5 flex-nowrap">
|
||||
{onInsertRow ? (
|
||||
<InsertPlaceholderTableRow
|
||||
style={{
|
||||
@@ -279,7 +279,7 @@ export default function DataGridBody<T extends object>({
|
||||
})}
|
||||
|
||||
{allowInsertColumn && (
|
||||
<div className="h-12 w-25 border-r-1 border-b-1 border-gray-200 bg-white" />
|
||||
<div className="h-12 bg-white border-gray-200 w-25 border-r-1 border-b-1" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -6,19 +6,25 @@ import { createContext } from 'react';
|
||||
* Available dialog types.
|
||||
*/
|
||||
export type DialogType =
|
||||
| 'EDIT_WORKSPACE_NAME'
|
||||
| 'CREATE_RECORD'
|
||||
| 'CREATE_COLUMN'
|
||||
| 'EDIT_COLUMN'
|
||||
| 'CREATE_TABLE'
|
||||
| 'EDIT_TABLE'
|
||||
| 'EDIT_PERMISSIONS'
|
||||
| 'CREATE_FOREIGN_KEY'
|
||||
| 'EDIT_FOREIGN_KEY'
|
||||
| 'CREATE_ROLE'
|
||||
| 'EDIT_ROLE'
|
||||
| 'CREATE_USER'
|
||||
| 'CREATE_PERMISSION_VARIABLE'
|
||||
| 'EDIT_PERMISSION_VARIABLE'
|
||||
| 'CREATE_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_ENVIRONMENT_VARIABLE';
|
||||
| 'EDIT_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_USER'
|
||||
| 'EDIT_USER_PASSWORD'
|
||||
| 'EDIT_JWT_SECRET';
|
||||
|
||||
export interface DialogConfig<TPayload = unknown> {
|
||||
/**
|
||||
@@ -62,6 +68,16 @@ export interface DialogContextProps {
|
||||
* Call this function to close the active drawer.
|
||||
*/
|
||||
closeDrawer: VoidFunction;
|
||||
/**
|
||||
* Call this function to check if the form is dirty and close the active dialog
|
||||
* if the form is pristine.
|
||||
*/
|
||||
closeDialogWithDirtyGuard: VoidFunction;
|
||||
/**
|
||||
* Call this function to check if the form is dirty and close the active drawer
|
||||
* if the form is pristine.
|
||||
*/
|
||||
closeDrawerWithDirtyGuard: VoidFunction;
|
||||
/**
|
||||
* Call this function to close the active alert dialog.
|
||||
*/
|
||||
@@ -73,6 +89,10 @@ export interface DialogContextProps {
|
||||
isDirty: boolean,
|
||||
location?: 'drawer' | 'dialog',
|
||||
) => void;
|
||||
/**
|
||||
* Call this function to open a dirty confirmation dialog.
|
||||
*/
|
||||
openDirtyConfirmation: (config?: Partial<DialogConfig<string>>) => void;
|
||||
}
|
||||
|
||||
export default createContext<DialogContextProps>({
|
||||
@@ -81,6 +101,9 @@ export default createContext<DialogContextProps>({
|
||||
openAlertDialog: () => {},
|
||||
closeDialog: () => {},
|
||||
closeDrawer: () => {},
|
||||
closeDialogWithDirtyGuard: () => {},
|
||||
closeDrawerWithDirtyGuard: () => {},
|
||||
closeAlertDialog: () => {},
|
||||
onDirtyStateChange: () => {},
|
||||
openDirtyConfirmation: () => {},
|
||||
});
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
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 EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
|
||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
||||
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
||||
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
|
||||
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
|
||||
import CreateUserForm from '@/components/users/CreateUserForm';
|
||||
import EditUserForm from '@/components/users/EditUserForm';
|
||||
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import AlertDialog from '@/ui/v2/AlertDialog';
|
||||
import { 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 +62,38 @@ 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() },
|
||||
);
|
||||
|
||||
const EditPermissionsForm = dynamic(
|
||||
() => import('@/components/dataBrowser/EditPermissionsForm'),
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const router = useRouter();
|
||||
|
||||
const [
|
||||
{
|
||||
open: dialogOpen,
|
||||
@@ -161,42 +181,52 @@ 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,
|
||||
});
|
||||
}
|
||||
|
||||
function closeDrawerWithDirtyGuard(event?: BaseSyntheticEvent) {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
}
|
||||
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,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
closeDrawer();
|
||||
}
|
||||
const closeDrawerWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
}
|
||||
|
||||
function closeDialogWithDirtyGuard(event?: BaseSyntheticEvent) {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
}
|
||||
closeDrawer();
|
||||
},
|
||||
[closeDrawer, openDirtyConfirmation],
|
||||
);
|
||||
|
||||
closeDialog();
|
||||
}
|
||||
const closeDialogWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog();
|
||||
},
|
||||
[closeDialog, openDirtyConfirmation],
|
||||
);
|
||||
|
||||
// We are coupling this logic with the location of the dialog content which is
|
||||
// not ideal. We shoule figure out a better logic for tracking the dirty
|
||||
@@ -223,10 +253,22 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
openAlertDialog,
|
||||
closeDialog,
|
||||
closeDrawer,
|
||||
closeDialogWithDirtyGuard,
|
||||
closeDrawerWithDirtyGuard,
|
||||
closeAlertDialog,
|
||||
onDirtyStateChange,
|
||||
openDirtyConfirmation,
|
||||
}),
|
||||
[closeDialog, closeDrawer, onDirtyStateChange, openDialog, openDrawer],
|
||||
[
|
||||
closeDialog,
|
||||
closeDialogWithDirtyGuard,
|
||||
closeDrawer,
|
||||
closeDrawerWithDirtyGuard,
|
||||
onDirtyStateChange,
|
||||
openDialog,
|
||||
openDirtyConfirmation,
|
||||
openDrawer,
|
||||
],
|
||||
);
|
||||
|
||||
const sharedDialogProps = {
|
||||
@@ -248,6 +290,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
|
||||
@@ -299,6 +367,10 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<RetryableErrorBoundary
|
||||
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
||||
>
|
||||
{activeDialogType === 'EDIT_WORKSPACE_NAME' && (
|
||||
<EditWorkspaceNameForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
|
||||
<CreateForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
@@ -315,6 +387,10 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<EditRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_USER' && (
|
||||
<CreateUserForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
|
||||
<CreatePermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
@@ -330,17 +406,34 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
|
||||
<EditEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_USER_PASSWORD' && (
|
||||
<EditUserPasswordForm
|
||||
{...sharedDialogProps}
|
||||
user={sharedDialogProps?.user}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_JWT_SECRET' && (
|
||||
<EditJwtSecretForm {...sharedDialogProps} />
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</BaseDialog>
|
||||
|
||||
<Drawer
|
||||
anchor="right"
|
||||
{...drawerProps}
|
||||
title={drawerTitle}
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawerWithDirtyGuard}
|
||||
SlideProps={{ onExited: clearDrawerContent, unmountOnExit: false }}
|
||||
anchor="right"
|
||||
PaperProps={{ className: 'max-w-2.5xl w-full' }}
|
||||
PaperProps={{
|
||||
...drawerProps?.PaperProps,
|
||||
className: twMerge(
|
||||
'max-w-2.5xl w-full',
|
||||
drawerProps?.PaperProps?.className,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
{activeDrawerType === 'CREATE_RECORD' && (
|
||||
@@ -375,6 +468,19 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
schema={drawerPayload?.schema}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_PERMISSIONS' && (
|
||||
<EditPermissionsForm
|
||||
{...sharedDrawerProps}
|
||||
disabled={drawerPayload?.disabled}
|
||||
schema={drawerPayload?.schema}
|
||||
table={drawerPayload?.table}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_USER' && (
|
||||
<EditUserForm {...sharedDrawerProps} {...drawerPayload} />
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default function HighlightedText({
|
||||
children,
|
||||
}: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<InlineCode className="text-greyscaleDark bg-primary-light font-display text-sm">
|
||||
{children}
|
||||
</InlineCode>
|
||||
);
|
||||
}
|
||||
1
dashboard/src/components/common/HighlightedText/index.ts
Normal file
1
dashboard/src/components/common/HighlightedText/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './HighlightedText';
|
||||
@@ -8,7 +8,7 @@ function InlineCode({ className, children, ...props }: InlineCodeProps) {
|
||||
return (
|
||||
<code
|
||||
className={twMerge(
|
||||
'inline-grid h-full max-h-[18px] max-w-xs items-center truncate rounded-sm bg-gray-100 px-1 font-mono text-[11px] text-gray-600',
|
||||
'inline-grid max-w-xs items-center truncate rounded-sm bg-gray-100 px-1 font-mono text-[11px] text-greyscaleMedium',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
138
dashboard/src/components/common/Pagination/Pagination.tsx
Normal file
138
dashboard/src/components/common/Pagination/Pagination.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { ButtonProps } from '@/ui/v2/Button';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import ChevronLeftIcon from '@/ui/v2/icons/ChevronLeftIcon';
|
||||
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
||||
import type { DetailedHTMLProps, HTMLProps } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type PaginationProps = DetailedHTMLProps<
|
||||
HTMLProps<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
/**
|
||||
* Total number of pages.
|
||||
*/
|
||||
totalNrOfPages: number;
|
||||
/**
|
||||
* Number of total elements per page.
|
||||
*/
|
||||
elementsPerPage?: number;
|
||||
/**
|
||||
* Total number of elements.
|
||||
*/
|
||||
totalNrOfElements: number;
|
||||
/**
|
||||
* Current page number.
|
||||
*/
|
||||
currentPageNumber: number;
|
||||
/**
|
||||
* Function to be called when navigating to the previous page.
|
||||
*/
|
||||
onPrevPageClick: VoidFunction;
|
||||
/**
|
||||
* Function to be called when navigating to the next page.
|
||||
*/
|
||||
onNextPageClick: VoidFunction;
|
||||
/**
|
||||
* Function to be called when a new page number is submitted.
|
||||
*/
|
||||
onPageChange: (page: number) => void;
|
||||
/**
|
||||
* Props for component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props to be passed to the next button component.
|
||||
*/
|
||||
nextButton?: Partial<ButtonProps>;
|
||||
/**
|
||||
* Props to be passed to the previous button component.
|
||||
*/
|
||||
prevButton?: Partial<ButtonProps>;
|
||||
};
|
||||
};
|
||||
|
||||
export default function Pagination({
|
||||
className,
|
||||
totalNrOfPages,
|
||||
currentPageNumber,
|
||||
onPrevPageClick,
|
||||
onNextPageClick,
|
||||
slotProps,
|
||||
elementsPerPage,
|
||||
onPageChange,
|
||||
totalNrOfElements,
|
||||
...props
|
||||
}: PaginationProps) {
|
||||
return (
|
||||
<div
|
||||
className={twMerge('grid grid-flow-col items-center gap-2', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="grid justify-start grid-flow-col gap-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="block text-xs"
|
||||
disabled={currentPageNumber === 1}
|
||||
aria-label="Previous page"
|
||||
onClick={onPrevPageClick}
|
||||
>
|
||||
<ChevronLeftIcon className="w-4 h-4" />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<div className="grid items-center grid-cols-3 gap-1 text-center grid-col !text-greyscaleGreyDark">
|
||||
<Text className="text-xs align-middle ">Page</Text>
|
||||
<Input
|
||||
value={currentPageNumber}
|
||||
onChange={(e) => {
|
||||
const page = parseInt(e.target.value, 10);
|
||||
if (page > 0 && page <= totalNrOfPages) {
|
||||
onPageChange(page);
|
||||
}
|
||||
}}
|
||||
disabled={totalNrOfPages === 1}
|
||||
color="secondary"
|
||||
slotProps={{
|
||||
inputRoot: {
|
||||
className: 'w-4 h-2.5 text-center !text-[11.5px]',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Text className="self-center text-xs align-middle text-greyscaleGreyDark">
|
||||
of {totalNrOfPages}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="text-xs"
|
||||
aria-label="Next page"
|
||||
disabled={currentPageNumber === totalNrOfPages}
|
||||
onClick={onNextPageClick}
|
||||
{...slotProps?.nextButton}
|
||||
>
|
||||
Next
|
||||
<ChevronRightIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-end text-center gap-x-1">
|
||||
<Text className="text-xs text-greyscaleGreyDark">
|
||||
{currentPageNumber === 1 && currentPageNumber}
|
||||
{currentPageNumber === 2 && elementsPerPage + currentPageNumber - 1}
|
||||
{currentPageNumber > 2 &&
|
||||
(currentPageNumber - 1) * elementsPerPage + 1}{' '}
|
||||
-{' '}
|
||||
{totalNrOfElements < currentPageNumber * elementsPerPage
|
||||
? totalNrOfElements
|
||||
: currentPageNumber * elementsPerPage}{' '}
|
||||
of {totalNrOfElements} users
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
dashboard/src/components/common/Pagination/index.ts
Normal file
3
dashboard/src/components/common/Pagination/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './Pagination';
|
||||
export { default } from './Pagination';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { ChangePasswordModal } from '@/components/applications/ChangePasswordModal';
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { useUserDataContext } from '@/context/workspace1-context';
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import { Dropdown, useDropdown } from '@/ui/v2/Dropdown';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { emptyWorkspace } from '@/utils/helpers';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -22,9 +20,8 @@ function AccountMenuContent({
|
||||
}: AccountMenuContentProps) {
|
||||
const user = useUserData();
|
||||
const router = useRouter();
|
||||
const client = useApolloClient();
|
||||
const [clicked, setClicked] = useState(false);
|
||||
const { setWorkspaceContext } = useWorkspaceContext();
|
||||
const { setUserContext } = useUserDataContext();
|
||||
const { handleClose } = useDropdown();
|
||||
|
||||
return (
|
||||
@@ -34,10 +31,9 @@ function AccountMenuContent({
|
||||
color="secondary"
|
||||
className="absolute top-6 right-4 grid grid-flow-col items-center gap-1 self-start font-medium"
|
||||
onClick={async () => {
|
||||
setWorkspaceContext(emptyWorkspace());
|
||||
setUserContext({ workspaces: [] });
|
||||
nhost.auth.signOut();
|
||||
router.push('/signin');
|
||||
await nhost.auth.signOut();
|
||||
await client.resetStore();
|
||||
}}
|
||||
aria-label="Sign Out"
|
||||
>
|
||||
|
||||
@@ -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';
|
||||
@@ -118,6 +118,7 @@ export default function BaseColumnForm({
|
||||
variant="inline"
|
||||
className="col-span-8 py-3"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<ControlledAutocomplete
|
||||
@@ -272,6 +273,7 @@ export default function BaseColumnForm({
|
||||
error={Boolean(errors.comment)}
|
||||
variant="inline"
|
||||
className="col-span-8 py-3"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -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';
|
||||
@@ -88,6 +88,7 @@ function NameInput() {
|
||||
error={Boolean(errors.name)}
|
||||
variant="inline"
|
||||
className="col-span-8 py-3"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -70,6 +70,7 @@ function NameInput({ index }: FieldArrayInputProps) {
|
||||
}
|
||||
},
|
||||
})}
|
||||
autoComplete="off"
|
||||
aria-label="Name"
|
||||
placeholder="Enter name"
|
||||
hideEmptyHelperText
|
||||
@@ -82,7 +82,7 @@ export default function ColumnEditorTable() {
|
||||
startIcon={<PlusIcon />}
|
||||
size="small"
|
||||
>
|
||||
Add column
|
||||
Add Column
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -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,406 @@
|
||||
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 { UseAsyncValueOptions } from './useAsyncValue';
|
||||
import useAsyncValue from './useAsyncValue';
|
||||
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?: UseAsyncValueOptions['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<{
|
||||
schema: string;
|
||||
table: string;
|
||||
name: string;
|
||||
}>();
|
||||
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,
|
||||
preventRowFetching: true,
|
||||
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,
|
||||
} = useAsyncValue({
|
||||
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: !props.disabled,
|
||||
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={() => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (props.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
}}
|
||||
error={Boolean(tableError || metadataError) || props.error}
|
||||
helperText={
|
||||
String(tableError || metadataError || '') || props.helperText
|
||||
}
|
||||
onChange={(event) => setInputValue(event.target.value)}
|
||||
value={inputValue}
|
||||
startAdornment={
|
||||
selectedColumn || relationshipDotNotation ? (
|
||||
<Text
|
||||
className={twMerge(
|
||||
'!ml-2 lg:max-w-[200px] flex-shrink-0 truncate',
|
||||
props.disabled && 'text-greyscaleGrey',
|
||||
)}
|
||||
>
|
||||
<span className="text-greyscaleGrey">{defaultTable}</span>.
|
||||
{relationshipDotNotation && (
|
||||
<>
|
||||
<span className="hidden lg:inline">
|
||||
{getTruncatedText(relationshipDotNotation, 15, 'end')}.
|
||||
</span>
|
||||
|
||||
<span className="inline lg:hidden">
|
||||
{getTruncatedText(relationshipDotNotation, 35, 'end')}.
|
||||
</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,302 @@
|
||||
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 UseAsyncValueOptions {
|
||||
/**
|
||||
* 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 useAsyncValue({
|
||||
selectedSchema,
|
||||
selectedTable,
|
||||
initialValue,
|
||||
isTableLoading,
|
||||
isMetadataLoading,
|
||||
tableData,
|
||||
metadata,
|
||||
onInitialized,
|
||||
}: UseAsyncValueOptions) {
|
||||
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 = 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 ||
|
||||
asyncTablePath !== currentTablePath
|
||||
) {
|
||||
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,
|
||||
manual_configuration: metadataManualConfiguration,
|
||||
} = currentRelationship.using || {};
|
||||
|
||||
if (metadataManualConfiguration) {
|
||||
setAsyncTablePath(
|
||||
`${metadataManualConfiguration.remote_table.schema}.${metadataManualConfiguration.remote_table.name}`,
|
||||
);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: metadataManualConfiguration.remote_table.schema || 'public',
|
||||
table: metadataManualConfiguration.remote_table.name,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// In some cases the metadata already contains the schema and table name
|
||||
if (metadataConstraint && 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 normalizedColumnName = columnName.replace(/"/g, '');
|
||||
const { foreign_key_constraint_on, manual_configuration } =
|
||||
currentRelationship.using || {};
|
||||
|
||||
if (!foreign_key_constraint_on && !manual_configuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (manual_configuration) {
|
||||
return Object.keys(manual_configuration.column_mapping).includes(
|
||||
normalizedColumnName,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof foreign_key_constraint_on === 'string') {
|
||||
return foreign_key_constraint_on === normalizedColumnName;
|
||||
}
|
||||
|
||||
return foreign_key_constraint_on.column === normalizedColumnName;
|
||||
},
|
||||
);
|
||||
|
||||
if (!foreignKeyRelation) {
|
||||
setRemainingColumnPath([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedSchema = foreignKeyRelation.referencedSchema?.replace(
|
||||
/(\\"|")/g,
|
||||
'',
|
||||
);
|
||||
const normalizedTable = foreignKeyRelation.referencedTable?.replace(
|
||||
/(\\"|")/g,
|
||||
'',
|
||||
);
|
||||
|
||||
setAsyncTablePath(`${normalizedSchema || 'public'}.${normalizedTable}`);
|
||||
|
||||
setSelectedRelationships((currentRelationships) => [
|
||||
...currentRelationships,
|
||||
{
|
||||
schema: normalizedSchema || 'public',
|
||||
table: normalizedTable,
|
||||
name: nextPath,
|
||||
},
|
||||
]);
|
||||
|
||||
setRemainingColumnPath((columnPath) => columnPath.slice(1));
|
||||
}, [
|
||||
currentTablePath,
|
||||
asyncTablePath,
|
||||
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:
|
||||
initialized && selectedRelationships?.length > 0
|
||||
? relationshipDotNotation
|
||||
: '',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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 || []),
|
||||
].reduce((relationships, currentRelationship) => {
|
||||
const { foreign_key_constraint_on, manual_configuration } =
|
||||
currentRelationship?.using || {};
|
||||
|
||||
if (manual_configuration) {
|
||||
return [
|
||||
...relationships,
|
||||
...Object.keys(manual_configuration.column_mapping).map((column) => ({
|
||||
schema: manual_configuration.remote_table?.schema || 'public',
|
||||
table: manual_configuration.remote_table?.name,
|
||||
column,
|
||||
name: currentRelationship.name,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof foreign_key_constraint_on === 'string') {
|
||||
return [
|
||||
...relationships,
|
||||
{
|
||||
schema: selectedSchema,
|
||||
table: selectedTable,
|
||||
column: foreign_key_constraint_on,
|
||||
name: currentRelationship.name,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...relationships,
|
||||
{
|
||||
schema: foreign_key_constraint_on.table.schema,
|
||||
table: foreign_key_constraint_on.table.name,
|
||||
column: foreign_key_constraint_on.column,
|
||||
name: currentRelationship.name,
|
||||
},
|
||||
];
|
||||
}, [] as { schema: string; table: string; column: string; name: string }[]);
|
||||
|
||||
return [
|
||||
...columnOptions,
|
||||
...objectAndArrayRelationships.map((relationship) => ({
|
||||
label: relationship.name,
|
||||
value: relationship.name,
|
||||
group: 'relationships',
|
||||
metadata: {
|
||||
target: {
|
||||
schema: relationship.schema,
|
||||
table: relationship.table,
|
||||
column: relationship.column,
|
||||
...(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 {
|
||||
@@ -348,7 +348,7 @@ export default function DataBrowserGrid({
|
||||
description={
|
||||
<span>
|
||||
Schema{' '}
|
||||
<InlineCode className="max-h-[32px] bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
<InlineCode className="bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
{metadata.schema || schemaSlug}
|
||||
</InlineCode>{' '}
|
||||
does not exist.
|
||||
@@ -365,7 +365,7 @@ export default function DataBrowserGrid({
|
||||
description={
|
||||
<span>
|
||||
Table{' '}
|
||||
<InlineCode className="max-h-[32px] bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
<InlineCode className="bg-gray-200 bg-opacity-80 px-1.5 text-sm">
|
||||
{metadata.schema || schemaSlug}.{metadata.table || tableSlug}
|
||||
</InlineCode>{' '}
|
||||
does not exist.
|
||||
@@ -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,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
@@ -8,6 +9,7 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
|
||||
import FloatingActionButton from '@/ui/FloatingActionButton';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Chip from '@/ui/v2/Chip';
|
||||
import Divider from '@/ui/v2/Divider';
|
||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
@@ -17,6 +19,7 @@ import LockIcon from '@/ui/v2/icons/LockIcon';
|
||||
import PencilIcon from '@/ui/v2/icons/PencilIcon';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import TrashIcon from '@/ui/v2/icons/TrashIcon';
|
||||
import UsersIcon from '@/ui/v2/icons/UsersIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import List from '@/ui/v2/List';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
@@ -194,6 +197,40 @@ function DataBrowserSidebarContent({
|
||||
});
|
||||
}
|
||||
|
||||
function handleEditPermissionClick(
|
||||
schema: string,
|
||||
table: string,
|
||||
disabled?: boolean,
|
||||
) {
|
||||
openDrawer('EDIT_PERMISSIONS', {
|
||||
title: (
|
||||
<span className="inline-grid grid-flow-col gap-2 items-center">
|
||||
Permissions
|
||||
<InlineCode className="!text-sm+ font-normal text-greyscaleMedium">
|
||||
{table}
|
||||
</InlineCode>
|
||||
<Chip label="Preview" size="small" color="info" component="span" />
|
||||
</span>
|
||||
),
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'lg:w-[65%] lg:max-w-7xl',
|
||||
},
|
||||
},
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${schema}.${table}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
disabled,
|
||||
schema,
|
||||
table,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-1">
|
||||
{schemas && schemas.length > 0 && (
|
||||
@@ -318,9 +355,7 @@ function DataBrowserSidebarContent({
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
disabled={
|
||||
tablePath === removableTable || isGitHubConnected
|
||||
}
|
||||
disabled={tablePath === removableTable}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
@@ -329,7 +364,6 @@ function DataBrowserSidebarContent({
|
||||
!isSelected &&
|
||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||
)}
|
||||
disabled={isGitHubConnected}
|
||||
>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
@@ -339,44 +373,84 @@ function DataBrowserSidebarContent({
|
||||
menu
|
||||
PaperProps={{ className: 'w-52' }}
|
||||
>
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
openDrawer('EDIT_TABLE', {
|
||||
title: 'Edit Table',
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
schema: table.table_schema,
|
||||
table,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<PencilIcon className="h-4 w-4 text-gray-700" />
|
||||
{isGitHubConnected ? (
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
true,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon className="h-4 w-4 text-gray-700" />
|
||||
|
||||
<span>Edit Table</span>
|
||||
</Dropdown.Item>
|
||||
<span>View Permissions</span>
|
||||
</Dropdown.Item>
|
||||
) : (
|
||||
[
|
||||
<Dropdown.Item
|
||||
key="edit-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
openDrawer('EDIT_TABLE', {
|
||||
title: 'Edit Table',
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
schema: table.table_schema,
|
||||
table,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<PencilIcon className="h-4 w-4 text-gray-700" />
|
||||
|
||||
<Divider component="li" />
|
||||
<span>Edit Table</span>
|
||||
</Dropdown.Item>,
|
||||
<Divider
|
||||
key="edit-table-separator"
|
||||
component="li"
|
||||
/>,
|
||||
<Dropdown.Item
|
||||
key="edit-permissions"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon className="h-4 w-4 text-gray-700" />
|
||||
|
||||
<Dropdown.Item
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium text-red"
|
||||
onClick={() =>
|
||||
handleDeleteTableClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 text-red" />
|
||||
<span>Edit Permissions</span>
|
||||
</Dropdown.Item>,
|
||||
<Divider
|
||||
key="edit-permissions-separator"
|
||||
component="li"
|
||||
/>,
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium text-red"
|
||||
onClick={() =>
|
||||
handleDeleteTableClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<TrashIcon className="h-4 w-4 text-red" />
|
||||
|
||||
<span>Delete Table</span>
|
||||
</Dropdown.Item>
|
||||
<span>Delete Table</span>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
)}
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
)
|
||||
@@ -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';
|
||||
@@ -127,7 +127,7 @@ export default function DatabaseRecordInputGroup({
|
||||
<span>{columnId}</span>
|
||||
</span>
|
||||
|
||||
<InlineCode>
|
||||
<InlineCode className="h-[18px]">
|
||||
{specificType}
|
||||
{maxLength ? `(${maxLength})` : null}
|
||||
</InlineCode>
|
||||
@@ -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';
|
||||
@@ -0,0 +1,348 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import type {
|
||||
DatabaseAccessLevel,
|
||||
DatabaseAction,
|
||||
HasuraMetadataPermission,
|
||||
} from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import FullPermissionIcon from '@/ui/v2/icons/FullPermissionIcon';
|
||||
import NoPermissionIcon from '@/ui/v2/icons/NoPermissionIcon';
|
||||
import PartialPermissionIcon from '@/ui/v2/icons/PartialPermissionIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Table from '@/ui/v2/Table';
|
||||
import TableBody from '@/ui/v2/TableBody';
|
||||
import TableCell from '@/ui/v2/TableCell';
|
||||
import TableContainer from '@/ui/v2/TableContainer';
|
||||
import TableHead from '@/ui/v2/TableHead';
|
||||
import TableRow from '@/ui/v2/TableRow';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useGetRemoteAppRolesQuery } from '@/utils/__generated__/graphql';
|
||||
import NavLink from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import RolePermissionEditorForm from './RolePermissionEditorForm';
|
||||
import RolePermissionsRow from './RolePermissionsRow';
|
||||
|
||||
export interface EditPermissionsFormProps {
|
||||
/**
|
||||
* Determines whether the form is disabled or not.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The schema that is being edited.
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* The table that is being edited.
|
||||
*/
|
||||
table: string;
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export default function EditPermissionsForm({
|
||||
disabled,
|
||||
schema,
|
||||
table,
|
||||
onCancel,
|
||||
}: EditPermissionsFormProps) {
|
||||
const [role, setRole] = useState<string>();
|
||||
const [action, setAction] = useState<DatabaseAction>();
|
||||
|
||||
const { closeDrawerWithDirtyGuard } = useDialog();
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
|
||||
const client = useRemoteApplicationGQLClient();
|
||||
const {
|
||||
data: rolesData,
|
||||
loading: rolesLoading,
|
||||
error: rolesError,
|
||||
} = useGetRemoteAppRolesQuery({ client });
|
||||
|
||||
const {
|
||||
data: tableData,
|
||||
status: tableStatus,
|
||||
error: tableError,
|
||||
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
||||
|
||||
const {
|
||||
data: metadata,
|
||||
status: metadataStatus,
|
||||
error: metadataError,
|
||||
} = useMetadataQuery([`default.metadata`]);
|
||||
|
||||
if (tableStatus === 'loading') {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ActivityIndicator label="Loading table..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tableError) {
|
||||
throw tableError;
|
||||
}
|
||||
|
||||
if (metadataStatus === 'loading') {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ActivityIndicator label="Loading table metadata..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (metadataError) {
|
||||
throw metadataError;
|
||||
}
|
||||
|
||||
if (rolesLoading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<ActivityIndicator label="Loading available roles..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (rolesError) {
|
||||
throw rolesError;
|
||||
}
|
||||
|
||||
const availableRoles = [
|
||||
'public',
|
||||
...(rolesData?.authRoles?.map(({ role: authRole }) => authRole) || []),
|
||||
];
|
||||
|
||||
const metadataForTable = metadata?.tables?.find(
|
||||
({ table: currentTable }) =>
|
||||
currentTable.name === table && currentTable.schema === schema,
|
||||
);
|
||||
|
||||
const availableColumns =
|
||||
tableData?.columns.map((column) => column.column_name) || [];
|
||||
|
||||
function handleSubmit() {
|
||||
setRole(undefined);
|
||||
setAction(undefined);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setRole(undefined);
|
||||
setAction(undefined);
|
||||
}
|
||||
|
||||
function getAccessLevel(
|
||||
permission?: HasuraMetadataPermission['permission'],
|
||||
): DatabaseAccessLevel {
|
||||
if (
|
||||
!permission ||
|
||||
(!permission?.check && !permission && permission?.columns?.length === 0)
|
||||
) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
const sortedTableColumns = [...availableColumns].sort();
|
||||
const isAllColumnSelected =
|
||||
sortedTableColumns.length === permission?.columns?.length &&
|
||||
[...(permission?.columns || [])]
|
||||
.sort()
|
||||
.every(
|
||||
(permissionColumn, index) =>
|
||||
permissionColumn === sortedTableColumns[index],
|
||||
);
|
||||
|
||||
if (
|
||||
Object.keys(permission?.check || {}).length === 0 &&
|
||||
Object.keys(permission?.filter || {}).length === 0 &&
|
||||
isAllColumnSelected
|
||||
) {
|
||||
return 'full';
|
||||
}
|
||||
|
||||
return 'partial';
|
||||
}
|
||||
|
||||
if (role && action) {
|
||||
const permissionsForAction = {
|
||||
insert: metadataForTable?.insert_permissions,
|
||||
select: metadataForTable?.select_permissions,
|
||||
update: metadataForTable?.update_permissions,
|
||||
delete: metadataForTable?.delete_permissions,
|
||||
};
|
||||
|
||||
return (
|
||||
<RolePermissionEditorForm
|
||||
disabled={disabled}
|
||||
schema={schema}
|
||||
table={table}
|
||||
role={role}
|
||||
action={action}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleCancel}
|
||||
permission={
|
||||
permissionsForAction[action]?.find(
|
||||
({ role: currentRole }) => currentRole === role,
|
||||
)?.permission
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-auto flex-col content-between overflow-hidden border-t-1 border-gray-200 bg-[#fafafa]">
|
||||
<div className="flex-auto">
|
||||
<section className="grid grid-flow-row gap-6 content-start overflow-y-auto p-6 bg-white border-b-1 border-gray-200">
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Text component="h2" className="!font-bold">
|
||||
Roles & Actions overview
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Rules for each role and action can be set by clicking on the
|
||||
corresponding cell.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-col gap-4 items-center justify-start">
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="!text-greyscaleDark grid items-center grid-flow-col gap-1"
|
||||
>
|
||||
full access <FullPermissionIcon />
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="!text-greyscaleDark grid items-center grid-flow-col gap-1"
|
||||
>
|
||||
partial access <PartialPermissionIcon />
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
variant="subtitle2"
|
||||
className="!text-greyscaleDark grid items-center grid-flow-col gap-1"
|
||||
>
|
||||
no access <NoPermissionIcon />
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHead className="block">
|
||||
<TableRow className="grid grid-cols-5 items-center">
|
||||
<TableCell className="border-b-0 p-2">Role</TableCell>
|
||||
|
||||
<TableCell className="border-b-0 p-2 text-center">
|
||||
Insert
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="border-b-0 p-2 text-center">
|
||||
Select
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="border-b-0 p-2 text-center">
|
||||
Update
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="border-b-0 p-2 text-center">
|
||||
Delete
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
<TableBody className="rounded-sm+ block border-1">
|
||||
<RolePermissionsRow
|
||||
name="admin"
|
||||
disabled
|
||||
accessLevels={{
|
||||
insert: 'full',
|
||||
select: 'full',
|
||||
update: 'full',
|
||||
delete: 'full',
|
||||
}}
|
||||
/>
|
||||
|
||||
{availableRoles.map((currentRole, index) => {
|
||||
const insertPermissions =
|
||||
metadataForTable?.insert_permissions?.find(
|
||||
({ role: permissionRole }) =>
|
||||
permissionRole === currentRole,
|
||||
);
|
||||
|
||||
const selectPermissions =
|
||||
metadataForTable?.select_permissions?.find(
|
||||
({ role: permissionRole }) =>
|
||||
permissionRole === currentRole,
|
||||
);
|
||||
|
||||
const updatePermissions =
|
||||
metadataForTable?.update_permissions?.find(
|
||||
({ role: permissionRole }) =>
|
||||
permissionRole === currentRole,
|
||||
);
|
||||
|
||||
const deletePermissions =
|
||||
metadataForTable?.delete_permissions?.find(
|
||||
({ role: permissionRole }) =>
|
||||
permissionRole === currentRole,
|
||||
);
|
||||
|
||||
return (
|
||||
<RolePermissionsRow
|
||||
name={currentRole}
|
||||
key={currentRole}
|
||||
className={twMerge(
|
||||
index === availableRoles.length - 1 && 'border-b-0',
|
||||
)}
|
||||
onActionSelect={(selectedAction) => {
|
||||
setRole(currentRole);
|
||||
setAction(selectedAction);
|
||||
}}
|
||||
accessLevels={{
|
||||
insert: getAccessLevel(insertPermissions?.permission),
|
||||
select: getAccessLevel(selectPermissions?.permission),
|
||||
update: getAccessLevel(updatePermissions?.permission),
|
||||
delete: getAccessLevel(deletePermissions?.permission),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<Alert className="text-left">
|
||||
Please go to the{' '}
|
||||
<NavLink
|
||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/roles-and-permissions`}
|
||||
passHref
|
||||
>
|
||||
<Link
|
||||
href="settings/roles-and-permissions"
|
||||
underline="hover"
|
||||
onClick={closeDrawerWithDirtyGuard}
|
||||
>
|
||||
Settings page
|
||||
</Link>
|
||||
</NavLink>{' '}
|
||||
to add and delete roles.
|
||||
</Alert>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 border-gray-200 p-2 bg-white">
|
||||
<Button variant="borderless" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import HighlightedText from '@/components/common/HighlightedText';
|
||||
import useManagePermissionMutation from '@/hooks/dataBrowser/useManagePermissionMutation';
|
||||
import type {
|
||||
DatabaseAction,
|
||||
HasuraMetadataPermission,
|
||||
RuleGroup,
|
||||
} from '@/types/dataBrowser';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import convertToHasuraPermissions from '@/utils/dataBrowser/convertToHasuraPermissions';
|
||||
import convertToRuleGroup from '@/utils/dataBrowser/convertToRuleGroup';
|
||||
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import AggregationQuerySection from './sections/AggregationQuerySection';
|
||||
import BackendOnlySection from './sections/BackendOnlySection';
|
||||
import ColumnPermissionsSection from './sections/ColumnPermissionsSection';
|
||||
import type { ColumnPreset } from './sections/ColumnPresetsSection';
|
||||
import ColumnPresetsSection from './sections/ColumnPresetsSection';
|
||||
import PermissionSettingsSection from './sections/PermissionSettingsSection';
|
||||
import RootFieldPermissionsSection from './sections/RootFieldPermissionsSection';
|
||||
import RowPermissionsSection from './sections/RowPermissionsSection';
|
||||
import validationSchemas from './validationSchemas';
|
||||
|
||||
export interface RolePermissionEditorFormValues {
|
||||
/**
|
||||
* The permission filter to be applied for the role.
|
||||
*/
|
||||
filter: Record<string, any> | {};
|
||||
/**
|
||||
* The allowed columns to CRUD for the role.
|
||||
*/
|
||||
columns?: string[];
|
||||
/**
|
||||
* The number of rows to be returned for the role.
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* Whether the role is allowed to perform aggregations.
|
||||
*/
|
||||
allowAggregations?: boolean;
|
||||
/**
|
||||
* Whether the role is allowed to have access to special fields.
|
||||
*/
|
||||
enableRootFieldCustomization?: boolean;
|
||||
/**
|
||||
* The allowed root fields in queries and mutations for the role.
|
||||
*/
|
||||
queryRootFields?: string[];
|
||||
/**
|
||||
* The allowed root fields in subscriptions for the role.
|
||||
*/
|
||||
subscriptionRootFields?: string[];
|
||||
/**
|
||||
* Column presets for the role.
|
||||
*/
|
||||
columnPresets?: ColumnPreset[];
|
||||
/**
|
||||
* Whether the mutation should be restricted to trusted backends.
|
||||
*/
|
||||
backendOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface RolePermissionEditorFormProps {
|
||||
/**
|
||||
* Determines whether or not the form is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The schema that is being edited.
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* The table that is being edited.
|
||||
*/
|
||||
table: string;
|
||||
/**
|
||||
* The role that is being edited.
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* The action that is being edited.
|
||||
*/
|
||||
action: DatabaseAction;
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: VoidFunction;
|
||||
/**
|
||||
* Function to be called when the editing is cancelled.
|
||||
*/
|
||||
onCancel: VoidFunction;
|
||||
/**
|
||||
* The existing permissions for the role and action.
|
||||
*/
|
||||
permission?: HasuraMetadataPermission['permission'];
|
||||
}
|
||||
|
||||
function getDefaultRuleGroup(
|
||||
action: DatabaseAction,
|
||||
permission: HasuraMetadataPermission['permission'],
|
||||
): RuleGroup | {} {
|
||||
if (!permission) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (action === 'insert') {
|
||||
return convertToRuleGroup(permission.check);
|
||||
}
|
||||
|
||||
return convertToRuleGroup(permission.filter);
|
||||
}
|
||||
|
||||
function getColumnPresets(data: Record<string, any>): ColumnPreset[] {
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return [{ column: '', value: '' }];
|
||||
}
|
||||
|
||||
return Object.keys(data).map((key) => ({
|
||||
column: key,
|
||||
value: data[key],
|
||||
}));
|
||||
}
|
||||
|
||||
function convertToColumnPresetObject(
|
||||
columnPresets: ColumnPreset[],
|
||||
): Record<string, any> {
|
||||
if (columnPresets?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const returnValue = columnPresets.reduce((data, { column, value }) => {
|
||||
if (column) {
|
||||
return { ...data, [column]: value };
|
||||
}
|
||||
|
||||
return data;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(returnValue).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export default function RolePermissionEditorForm({
|
||||
schema,
|
||||
table,
|
||||
role,
|
||||
action,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
permission,
|
||||
disabled,
|
||||
}: RolePermissionEditorFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
mutateAsync: managePermission,
|
||||
error,
|
||||
reset: resetError,
|
||||
isLoading,
|
||||
} = useManagePermissionMutation({
|
||||
schema,
|
||||
table,
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['default.metadata'] });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<RolePermissionEditorFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
filter: getDefaultRuleGroup(action, permission),
|
||||
columns: permission?.columns || [],
|
||||
limit: permission?.limit || null,
|
||||
allowAggregations: permission?.allow_aggregations || false,
|
||||
enableRootFieldCustomization:
|
||||
permission?.query_root_fields?.length > 0 ||
|
||||
permission?.subscription_root_fields?.length > 0,
|
||||
queryRootFields: permission?.query_root_fields || [],
|
||||
subscriptionRootFields: permission?.subscription_root_fields || [],
|
||||
columnPresets: getColumnPresets(permission?.set || {}),
|
||||
backendOnly: permission?.backend_only || false,
|
||||
},
|
||||
resolver: yupResolver(validationSchemas[action]),
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { dirtyFields, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const { onDirtyStateChange, openDirtyConfirmation, openAlertDialog } =
|
||||
useDialog();
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'drawer');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
|
||||
async function handleSubmit(values: RolePermissionEditorFormValues) {
|
||||
const managePermissionPromise = managePermission({
|
||||
role,
|
||||
action,
|
||||
mode: permission ? 'update' : 'insert',
|
||||
originalPermission: permission,
|
||||
permission: {
|
||||
set: convertToColumnPresetObject(values.columnPresets),
|
||||
columns: values.columns,
|
||||
limit: values.limit,
|
||||
allow_aggregations: values.allowAggregations,
|
||||
query_root_fields: values.queryRootFields,
|
||||
subscription_root_fields: values.subscriptionRootFields,
|
||||
filter:
|
||||
action !== 'insert'
|
||||
? convertToHasuraPermissions(values.filter as RuleGroup)
|
||||
: permission?.filter,
|
||||
check:
|
||||
action === 'insert'
|
||||
? convertToHasuraPermissions(values.filter as RuleGroup)
|
||||
: permission?.check,
|
||||
backend_only: values.backendOnly,
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
managePermissionPromise,
|
||||
{
|
||||
loading: 'Saving permission...',
|
||||
success: 'Permission has been saved successfully.',
|
||||
error: 'An error occurred while saving the permission.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
onDirtyStateChange(false, 'drawer');
|
||||
onSubmit?.();
|
||||
}
|
||||
|
||||
function handleCancelClick() {
|
||||
if (isDirty) {
|
||||
openDirtyConfirmation({
|
||||
props: {
|
||||
onPrimaryAction: () => {
|
||||
onDirtyStateChange(false, 'drawer');
|
||||
onCancel?.();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
onCancel?.();
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
const deletePermissionPromise = managePermission({
|
||||
role,
|
||||
action,
|
||||
originalPermission: permission,
|
||||
mode: 'delete',
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
deletePermissionPromise,
|
||||
{
|
||||
loading: 'Deleting permission...',
|
||||
success: 'Permission has been deleted successfully.',
|
||||
error: 'An error occurred while deleting the permission.',
|
||||
},
|
||||
toastStyleProps,
|
||||
);
|
||||
|
||||
onDirtyStateChange(false, 'drawer');
|
||||
onSubmit?.();
|
||||
}
|
||||
|
||||
function handleDeleteClick() {
|
||||
openAlertDialog({
|
||||
title: 'Delete permissions',
|
||||
payload: (
|
||||
<span>
|
||||
Are you sure you want to delete the{' '}
|
||||
<HighlightedText>{action}</HighlightedText> permissions of{' '}
|
||||
<HighlightedText>{role}</HighlightedText>?
|
||||
</span>
|
||||
),
|
||||
props: {
|
||||
primaryButtonText: 'Delete',
|
||||
primaryButtonColor: 'error',
|
||||
onPrimaryAction: handleDelete,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
{error && error instanceof Error && (
|
||||
<div className="-mt-3 mb-4 px-6">
|
||||
<Alert
|
||||
severity="error"
|
||||
className="grid grid-flow-col items-center justify-between px-4 py-3"
|
||||
>
|
||||
<span className="text-left">
|
||||
<strong>Error:</strong> {error.message}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="p-1"
|
||||
onClick={resetError}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-auto flex-col content-between overflow-hidden border-t-1 border-gray-200 bg-[#fafafa]"
|
||||
>
|
||||
<div className="grid grid-flow-row gap-6 content-start flex-auto py-4 overflow-auto">
|
||||
<PermissionSettingsSection
|
||||
title="Selected role & action"
|
||||
className="justify-between grid-flow-col"
|
||||
>
|
||||
<div className="grid grid-flow-col gap-4">
|
||||
<Text>
|
||||
Role: <HighlightedText>{role}</HighlightedText>
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Action: <HighlightedText>{action}</HighlightedText>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Button variant="borderless" onClick={handleCancelClick}>
|
||||
Change
|
||||
</Button>
|
||||
</PermissionSettingsSection>
|
||||
|
||||
<RowPermissionsSection
|
||||
disabled={disabled}
|
||||
role={role}
|
||||
action={action}
|
||||
schema={schema}
|
||||
table={table}
|
||||
/>
|
||||
|
||||
{action !== 'delete' && (
|
||||
<ColumnPermissionsSection
|
||||
disabled={disabled}
|
||||
role={role}
|
||||
action={action}
|
||||
schema={schema}
|
||||
table={table}
|
||||
/>
|
||||
)}
|
||||
|
||||
{action === 'select' && (
|
||||
<>
|
||||
<AggregationQuerySection role={role} disabled={disabled} />
|
||||
<RootFieldPermissionsSection disabled={disabled} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{(action === 'insert' || action === 'update') && (
|
||||
<ColumnPresetsSection
|
||||
schema={schema}
|
||||
table={table}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{action !== 'select' && <BackendOnlySection disabled={disabled} />}
|
||||
</div>
|
||||
|
||||
<div className="grid flex-shrink-0 sm:grid-flow-col sm:justify-between gap-2 border-t-1 border-gray-200 p-2 bg-white">
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={handleCancelClick}
|
||||
tabIndex={isDirty ? -1 : 0}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{!disabled && (
|
||||
<div className="grid grid-flow-row sm:grid-flow-col gap-2">
|
||||
{Boolean(permission) && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Delete Permissions
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className="justify-self-end"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { DatabaseAccessLevel, DatabaseAction } from '@/types/dataBrowser';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import FullPermissionIcon from '@/ui/v2/icons/FullPermissionIcon';
|
||||
import NoPermissionIcon from '@/ui/v2/icons/NoPermissionIcon';
|
||||
import PartialPermissionIcon from '@/ui/v2/icons/PartialPermissionIcon';
|
||||
import type { TableCellProps } from '@/ui/v2/TableCell';
|
||||
import TableCell from '@/ui/v2/TableCell';
|
||||
import type { TableRowProps } from '@/ui/v2/TableRow';
|
||||
import TableRow from '@/ui/v2/TableRow';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface RolePermissionsProps extends TableRowProps {
|
||||
/**
|
||||
* Role name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Determines whether or not the actions are disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Access types for specific operations.
|
||||
*/
|
||||
accessLevels?: Record<DatabaseAction, DatabaseAccessLevel>;
|
||||
/**
|
||||
* Function to be called when the user wants to open the settings for an
|
||||
* operation.
|
||||
*/
|
||||
onActionSelect?: (action: DatabaseAction) => void;
|
||||
/**
|
||||
* Props passed to individual component slots.
|
||||
*/
|
||||
slotProps?: {
|
||||
/**
|
||||
* Props passed to every cell in the table row.
|
||||
*/
|
||||
cell?: Partial<TableCellProps>;
|
||||
};
|
||||
}
|
||||
|
||||
function AccessLevelIcon({ level }: { level: DatabaseAccessLevel }) {
|
||||
if (level === 'none') {
|
||||
return <NoPermissionIcon />;
|
||||
}
|
||||
|
||||
if (level === 'partial') {
|
||||
return <PartialPermissionIcon />;
|
||||
}
|
||||
|
||||
return <FullPermissionIcon />;
|
||||
}
|
||||
|
||||
export default function RolePermissions({
|
||||
name,
|
||||
disabled,
|
||||
accessLevels = {
|
||||
insert: 'none',
|
||||
select: 'none',
|
||||
update: 'none',
|
||||
delete: 'none',
|
||||
},
|
||||
onActionSelect,
|
||||
slotProps,
|
||||
className,
|
||||
...props
|
||||
}: RolePermissionsProps) {
|
||||
const cellProps = slotProps?.cell || {};
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className={twMerge(
|
||||
'grid grid-cols-5 items-center justify-items-stretch border-b-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<TableCell
|
||||
{...cellProps}
|
||||
className={twMerge(
|
||||
'block p-2 border-0 truncate border-r-1',
|
||||
cellProps.className,
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
{...cellProps}
|
||||
className={twMerge(
|
||||
'inline-grid items-center p-0 border-0 text-center w-full h-full border-r-1',
|
||||
disabled && 'justify-center',
|
||||
cellProps.className,
|
||||
)}
|
||||
>
|
||||
{disabled ? (
|
||||
<AccessLevelIcon level={accessLevels.insert} />
|
||||
) : (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="w-full h-full rounded-none"
|
||||
onClick={() => onActionSelect('insert')}
|
||||
>
|
||||
<AccessLevelIcon level={accessLevels.insert} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
{...cellProps}
|
||||
className={twMerge(
|
||||
'inline-grid items-center p-0 border-0 text-center w-full h-full border-r-1',
|
||||
disabled && 'justify-center',
|
||||
cellProps.className,
|
||||
)}
|
||||
>
|
||||
{disabled ? (
|
||||
<AccessLevelIcon level={accessLevels.select} />
|
||||
) : (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="w-full h-full rounded-none"
|
||||
onClick={() => onActionSelect('select')}
|
||||
>
|
||||
<AccessLevelIcon level={accessLevels.select} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
{...cellProps}
|
||||
className={twMerge(
|
||||
'inline-grid items-center p-0 border-0 text-center w-full h-full border-r-1',
|
||||
disabled && 'justify-center',
|
||||
cellProps.className,
|
||||
)}
|
||||
>
|
||||
{disabled ? (
|
||||
<AccessLevelIcon level={accessLevels.update} />
|
||||
) : (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="w-full h-full rounded-none"
|
||||
onClick={() => onActionSelect('update')}
|
||||
>
|
||||
<AccessLevelIcon level={accessLevels.update} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell
|
||||
{...cellProps}
|
||||
className={twMerge(
|
||||
'inline-grid items-center p-0 border-0 text-center w-full h-full',
|
||||
disabled && 'justify-center',
|
||||
cellProps.className,
|
||||
)}
|
||||
>
|
||||
{disabled ? (
|
||||
<AccessLevelIcon level={accessLevels.delete} />
|
||||
) : (
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="w-full h-full rounded-none"
|
||||
onClick={() => onActionSelect('delete')}
|
||||
>
|
||||
<AccessLevelIcon level={accessLevels.delete} />
|
||||
</IconButton>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './EditPermissionsForm';
|
||||
export { default } from './EditPermissionsForm';
|
||||
@@ -0,0 +1,64 @@
|
||||
import ControlledSwitch from '@/components/common/ControlledSwitch';
|
||||
import HighlightedText from '@/components/common/HighlightedText';
|
||||
import type { RolePermissionEditorFormValues } from '@/components/dataBrowser/EditPermissionsForm/RolePermissionEditorForm';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import PermissionSettingsSection from './PermissionSettingsSection';
|
||||
|
||||
export interface AggregationQuerySectionProps {
|
||||
/**
|
||||
* The role that is being edited.
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* Determines whether or not the section is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function AggregationQuerySection({
|
||||
role,
|
||||
disabled,
|
||||
}: AggregationQuerySectionProps) {
|
||||
const { setValue, getValues } =
|
||||
useFormContext<RolePermissionEditorFormValues>();
|
||||
|
||||
return (
|
||||
<PermissionSettingsSection title="Aggregation queries permissions">
|
||||
<Text variant="subtitle1">
|
||||
Allow queries with aggregate functions like sum, count, avg, max, min,
|
||||
etc.
|
||||
</Text>
|
||||
|
||||
<ControlledSwitch
|
||||
disabled={disabled}
|
||||
name="allowAggregations"
|
||||
label={
|
||||
<Text variant="subtitle1" component="span">
|
||||
Allow <HighlightedText>{role}</HighlightedText> to make aggregation
|
||||
queries
|
||||
</Text>
|
||||
}
|
||||
onChange={(event) => {
|
||||
if (event.target.checked) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(
|
||||
'queryRootFields',
|
||||
getValues('queryRootFields')?.filter(
|
||||
(field) => field !== 'select_aggregate',
|
||||
) || [],
|
||||
);
|
||||
|
||||
setValue(
|
||||
'subscriptionRootFields',
|
||||
getValues('subscriptionRootFields')?.filter(
|
||||
(field) => field !== 'select_aggregate',
|
||||
) || [],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</PermissionSettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import ControlledSwitch from '@/components/common/ControlledSwitch';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import PermissionSettingsSection from './PermissionSettingsSection';
|
||||
|
||||
export interface BackendOnlySectionProps {
|
||||
/**
|
||||
* Determines whether or not the section is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function BackendOnlySection({
|
||||
disabled,
|
||||
}: BackendOnlySectionProps) {
|
||||
return (
|
||||
<PermissionSettingsSection title="Backend only">
|
||||
<Text variant="subtitle1">
|
||||
When enabled, this mutation is accessible only via 'trusted
|
||||
backends'.
|
||||
</Text>
|
||||
|
||||
<ControlledSwitch
|
||||
disabled={disabled}
|
||||
name="backendOnly"
|
||||
label={
|
||||
<Text variant="subtitle1" component="span">
|
||||
Allow from backends only
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</PermissionSettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import HighlightedText from '@/components/common/HighlightedText';
|
||||
import type { RolePermissionEditorFormValues } from '@/components/dataBrowser/EditPermissionsForm/RolePermissionEditorForm';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import type { DatabaseAction } from '@/types/dataBrowser';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Checkbox from '@/ui/v2/Checkbox';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import PermissionSettingsSection from './PermissionSettingsSection';
|
||||
|
||||
export interface ColumnPermissionsSectionProps {
|
||||
/**
|
||||
* The role that is being edited.
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* The action that is being edited.
|
||||
*/
|
||||
action: DatabaseAction;
|
||||
/**
|
||||
* The schema that is being edited.
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* The table that is being edited.
|
||||
*/
|
||||
table: string;
|
||||
/**
|
||||
* Determines whether or not the section is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ColumnPermissionsSection({
|
||||
role,
|
||||
action,
|
||||
schema,
|
||||
table,
|
||||
disabled,
|
||||
}: ColumnPermissionsSectionProps) {
|
||||
const { register, setValue } =
|
||||
useFormContext<RolePermissionEditorFormValues>();
|
||||
const selectedColumns = useWatch({ name: 'columns' }) as string[];
|
||||
|
||||
const {
|
||||
data: tableData,
|
||||
status: tableStatus,
|
||||
error: tableError,
|
||||
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
||||
|
||||
if (tableError) {
|
||||
throw tableError;
|
||||
}
|
||||
|
||||
const isAllSelected = selectedColumns?.length === tableData?.columns?.length;
|
||||
|
||||
return (
|
||||
<PermissionSettingsSection title={`Column ${action} permissions`}>
|
||||
<div className="grid grid-flow-col justify-between gap-2 items-center">
|
||||
<Text>
|
||||
Allow role <HighlightedText>{role}</HighlightedText> to{' '}
|
||||
<HighlightedText>{action}</HighlightedText> columns:
|
||||
</Text>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (isAllSelected) {
|
||||
setValue('columns', []);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(
|
||||
'columns',
|
||||
tableData?.columns?.map((column) => column.column_name),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{isAllSelected ? 'Deselect All' : 'Select All'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{tableStatus === 'loading' && (
|
||||
<ActivityIndicator label="Loading columns..." />
|
||||
)}
|
||||
|
||||
{tableStatus === 'success' && (
|
||||
<div className="flex flex-row gap-6 justify-start flex-wrap items-center">
|
||||
{tableData?.columns?.map((column) => (
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
name="columns"
|
||||
value={column.column_name}
|
||||
label={column.column_name}
|
||||
key={column.column_name}
|
||||
checked={selectedColumns.includes(column.column_name)}
|
||||
{...register('columns')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Text variant="subtitle1">
|
||||
For <strong>relationships</strong>, set permissions for the
|
||||
corresponding tables/views.
|
||||
</Text>
|
||||
</PermissionSettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import type { RolePermissionEditorFormValues } from '@/components/dataBrowser/EditPermissionsForm/RolePermissionEditorForm';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import Autocomplete from '@/ui/v2/Autocomplete';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import IconButton from '@/ui/v2/IconButton';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
import XIcon from '@/ui/v2/icons/XIcon';
|
||||
import InputLabel from '@/ui/v2/InputLabel';
|
||||
import Option from '@/ui/v2/Option';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import PermissionSettingsSection from './PermissionSettingsSection';
|
||||
|
||||
export interface ColumnPreset {
|
||||
column: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface ColumnPresetSectionProps {
|
||||
/**
|
||||
* Schema to use for fetching available columns.
|
||||
*/
|
||||
schema: string;
|
||||
/**
|
||||
* Table to use for fetching available columns.
|
||||
*/
|
||||
table: string;
|
||||
/**
|
||||
* Determines whether or not the section is disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ColumnPresetsSection({
|
||||
schema,
|
||||
table,
|
||||
disabled,
|
||||
}: ColumnPresetSectionProps) {
|
||||
const theme = useTheme();
|
||||
const {
|
||||
data: tableData,
|
||||
status: tableStatus,
|
||||
error: tableError,
|
||||
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
||||
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { data: customClaimsData } = useGetAppCustomClaimsQuery({
|
||||
variables: { id: currentApplication?.id },
|
||||
skip: !currentApplication?.id,
|
||||
});
|
||||
const {
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useFormContext<RolePermissionEditorFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({ name: 'columnPresets' });
|
||||
const columnPresets = useWatch({ name: 'columnPresets' }) as ColumnPreset[];
|
||||
|
||||
const allColumnNames: string[] =
|
||||
tableData?.columns.map((column) => column.column_name) || [];
|
||||
const selectedColumns = fields as (ColumnPreset & { id: string })[];
|
||||
const selectedColumnsMap = columnPresets.reduce(
|
||||
(map, { column }) => map.set(column, true),
|
||||
new Map<string, boolean>(),
|
||||
);
|
||||
|
||||
if (tableError) {
|
||||
throw tableError;
|
||||
}
|
||||
|
||||
const permissionVariableOptions = getPermissionVariablesArray(
|
||||
customClaimsData?.app?.authJwtCustomClaims,
|
||||
).map(({ key }) => ({
|
||||
label: `X-Hasura-${key}`,
|
||||
value: `X-Hasura-${key}`,
|
||||
group: 'Permission variables',
|
||||
}));
|
||||
|
||||
return (
|
||||
<PermissionSettingsSection title="Column presets" className="gap-6">
|
||||
<Text variant="subtitle1">
|
||||
Set static values or session variables as pre-determined values for
|
||||
columns while inserting.
|
||||
</Text>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-cols-[1fr_1fr_40px] gap-2">
|
||||
<InputLabel as="span">Column Name</InputLabel>
|
||||
<InputLabel as="span">Column Value</InputLabel>
|
||||
</div>
|
||||
|
||||
{tableStatus === 'loading' && (
|
||||
<ActivityIndicator label="Loading columns..." />
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-4">
|
||||
{tableStatus === 'success' &&
|
||||
selectedColumns.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="grid grid-cols-[1fr_1fr_40px] gap-2"
|
||||
>
|
||||
<ControlledSelect
|
||||
disabled={disabled}
|
||||
name={`columnPresets.${index}.column`}
|
||||
error={Boolean(
|
||||
errors?.columnPresets?.at(index).column?.message,
|
||||
)}
|
||||
>
|
||||
{allColumnNames.map((column) => (
|
||||
<Option
|
||||
value={column}
|
||||
disabled={selectedColumnsMap.has(column)}
|
||||
key={column}
|
||||
>
|
||||
{column}
|
||||
</Option>
|
||||
))}
|
||||
</ControlledSelect>
|
||||
|
||||
<Autocomplete
|
||||
disabled={disabled}
|
||||
options={permissionVariableOptions}
|
||||
groupBy={(option) => option.group}
|
||||
name={`columnPresets.${index}.value`}
|
||||
inputValue={field.value}
|
||||
value={field.value}
|
||||
freeSolo
|
||||
fullWidth
|
||||
disableClearable={false}
|
||||
clearIcon={
|
||||
<XIcon
|
||||
className="w-4 h-4 mt-px"
|
||||
sx={{ color: theme.palette.text.primary }}
|
||||
/>
|
||||
}
|
||||
autoSelect
|
||||
autoHighlight={false}
|
||||
error={Boolean(
|
||||
errors?.columnPresets?.at(index).value?.message,
|
||||
)}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof value === 'string') {
|
||||
return (
|
||||
option.value.toLowerCase() ===
|
||||
(value as string).toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
option.value.toLowerCase() === value.value.toLowerCase()
|
||||
);
|
||||
}}
|
||||
onChange={(_event, _value, reason, details) => {
|
||||
if (reason === 'clear') {
|
||||
setValue(`columnPresets.${index}.value`, null, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(
|
||||
`columnPresets.${index}.value`,
|
||||
typeof details.option === 'string'
|
||||
? details.option
|
||||
: details.option.value,
|
||||
{ shouldDirty: true },
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="shrink-0 grow-0 flex-[40px]"
|
||||
onClick={() => {
|
||||
if (fields.length === 1) {
|
||||
remove(index);
|
||||
append({ column: '', value: '' });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
remove(index);
|
||||
}}
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
startIcon={<PlusIcon />}
|
||||
size="small"
|
||||
onClick={() => append({ column: '', value: '' })}
|
||||
disabled={
|
||||
selectedColumns.length === allColumnNames.length || disabled
|
||||
}
|
||||
className="justify-self-start"
|
||||
>
|
||||
Add Column
|
||||
</Button>
|
||||
</div>
|
||||
</PermissionSettingsSection>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user