Compare commits

...

317 Commits

Author SHA1 Message Date
Szilárd Dóró
b649f178e0 Merge pull request #1454 from nhost/changeset-release/main
chore: update versions
2023-01-04 14:43:52 +01:00
github-actions[bot]
1f5e1e3d42 chore: update versions 2023-01-03 18:20:01 +00:00
Nuno Pato
5727b0b0fe Merge pull request #1426 from nhost/feat/add-functions-to-logs-dashboard
feat(dashboard): add functions to logs
2023-01-03 17:18:24 -01:00
Pilou
2f38ed56f5 Merge pull request #1459 from nhost/fix/reuse-file-upload
fix: 🐛 allow useFileUpload to be reused
2023-01-03 13:41:20 +01:00
Guido Curcio
16502ea175 Merge pull request #1382 from nhost/feat/auth-management 2023-01-03 06:16:57 -05:00
Guido Curcio
beee0407df Merge branch 'main' into feat/auth-management 2023-01-03 05:13:46 -05:00
Guido Curcio
3990b1ffbb Update dashboard/src/components/users/CreateUserForm/CreateUserForm.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-03 06:12:22 -03:00
Guido Curcio
1fb03708e3 Update dashboard/src/components/users/EditUserPasswordForm/EditUserPasswordForm.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-03 06:12:15 -03:00
Szilárd Dóró
d42719ee65 Merge pull request #1453 from nhost/feat/custom-jwt-secret
feat(dashboard): add JWT secret editor modal
2023-01-03 09:30:09 +01:00
Guido Curcio
72ff489ea8 fix inconsistent padding in modals. 2023-01-03 03:43:11 -03:00
Pilou
c9bf2dde0e Merge pull request #1458 from nhost/refactor/do-not-over-export
Only export what is required by the user or `@nhost/nextjs`
2023-01-02 22:04:52 +01:00
Pierre-Louis Mercereau
613533d377 chore: do not cache documentation build 2023-01-02 21:22:54 +01:00
Pierre-Louis Mercereau
8568354718 fix: 🐛 allow useFileUpload to be reused 2023-01-02 21:03:58 +01:00
Pierre-Louis Mercereau
1be6d32455 Only export what is required by the user or @nhost/nextjs 2023-01-02 16:19:00 +01:00
Pilou
812a6e5005 Merge pull request #1452 from nhost/fix/improve-missing-react-provider-error
fix: improve missing React provider error
2023-01-02 15:54:48 +01:00
Szilárd Dóró
34cc230b61 fix(dashboard): improve responsive layout 2023-01-02 15:29:02 +01:00
Szilárd Dóró
898a7c835f chore(dashboard): improve JWT secret validation 2023-01-02 14:41:28 +01:00
Szilárd Dóró
7766624bc5 feat(dashboard): add JWT secret editor modal 2023-01-02 14:34:36 +01:00
Pierre-Louis Mercereau
2e8f73df38 chore: changeset 2023-01-02 14:25:29 +01:00
Pierre-Louis Mercereau
6a419e060e fix: improve missing React provider error 2023-01-02 14:23:49 +01:00
Guido Curcio
43480ca735 spacing on action buttons 2023-01-02 09:07:21 -03:00
Guido Curcio
efc42d77fd fix loading state on EditUserForm (drawer) 2023-01-02 08:42:45 -03:00
Guido Curcio
31e2523eca spacing on create user modal 2023-01-02 08:19:54 -03:00
Guido Curcio
fbf4f40ab7 Merge branch 'feat/auth-management' of https://github.com/nhost/nhost into feat/auth-management 2023-01-02 08:15:31 -03:00
Guido Curcio
cbe203e720 fix alt props and spacing on users table 2023-01-02 08:14:49 -03:00
Szilárd Dóró
378a6684b0 Merge pull request #1451 from nhost/changeset-release/main
chore: update versions
2023-01-02 11:33:20 +01:00
github-actions[bot]
1999ae09e6 chore: update versions 2023-01-02 09:47:28 +00:00
Szilárd Dóró
0fe48a0833 Merge pull request #1425 from nhost/fix/refresh-after-provisioning
fix(dashboard): provisioning status polling
2023-01-02 10:46:12 +01:00
Szilárd Dóró
7bbf6dbf1c Merge pull request #1387 from nhost/changeset-release/main
chore: update versions
2023-01-02 10:43:56 +01:00
Szilárd Dóró
689dc873b3 Update integrations/react-apollo/CHANGELOG.md
Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2023-01-02 10:07:28 +01:00
Szilárd Dóró
a0747d02e0 Update integrations/react-urql/CHANGELOG.md
Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2023-01-02 10:07:23 +01:00
Szilárd Dóró
be5bd1e446 Update integrations/apollo/CHANGELOG.md
Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2023-01-02 10:07:18 +01:00
Szilárd Dóró
52ccfdec89 Merge branch 'main' into fix/refresh-after-provisioning 2023-01-02 10:05:46 +01:00
Szilárd Dóró
2c60591580 fix(deps): revert major bumps 2023-01-02 10:02:12 +01:00
github-actions[bot]
6140bc5b3b chore: update versions 2022-12-27 13:49:10 +00:00
Pilou
9f7780ec91 Merge pull request #1438 from nhost/ci/renovate
ci: improve renovate configuration
2022-12-27 14:47:35 +01:00
Pilou
abc7d0c7a5 Merge pull request #1442 from nhost/chore/docgen-vite
chore: build docgen with vite
2022-12-27 09:44:03 +01:00
Pilou
074a36ea48 Merge pull request #1432 from nhost/fix/remove-accept-encoding-header
Fix/allow override of the accept-encoding header
2022-12-27 09:43:39 +01:00
Pierre-Louis Mercereau
64e806dc27 chore: build docgen with vite 2022-12-26 21:16:02 +01:00
Pilou
bd0e9748b6 Merge pull request #1440 from nhost/renovate-changesets
chore: create changesest from Renovate bumps
2022-12-26 21:04:45 +01:00
plmercereau
b21222b378 chore(deps): update dependency @types/node to v16 2022-12-26 20:01:13 +00:00
Pilou
7e217db128 Merge pull request #1294 from nhost/renovate/node-18.x
chore(deps): update dependency @types/node to v16
2022-12-26 20:59:25 +01:00
Pierre-Louis Mercereau
56c716d9fa Merge branch 'main' into renovate/node-18.x 2022-12-26 20:23:00 +01:00
Pilou
14ecbd1fb9 Merge pull request #1439 from nhost/renovate-changesets
chore: create changesest from Renovate bumps
2022-12-26 20:19:58 +01:00
plmercereau
a0242c4d6f chore(deps): update dependency tsup to v6 2022-12-26 19:13:36 +00:00
Pilou
4800b4a756 Merge pull request #1306 from nhost/renovate/tsup-6.x
chore(deps): update dependency tsup to v6
2022-12-26 20:12:14 +01:00
Pierre-Louis Mercereau
5b318d17d4 Merge branch 'main' into renovate/node-18.x 2022-12-26 20:08:46 +01:00
Pierre-Louis Mercereau
2f9be4f760 fix: @types/node v16 2022-12-26 20:06:00 +01:00
Pierre-Louis Mercereau
64777b6f30 ci: improve renovate configuration 2022-12-26 19:59:48 +01:00
Pilou
7e1489353e Merge pull request #1437 from nhost/ci/renovate-changeset-pr
ci: create renovate changeset pr
2022-12-26 19:44:20 +01:00
Pierre-Louis Mercereau
c53306a497 ci: correct PAT secret 2022-12-26 19:43:55 +01:00
Pierre-Louis Mercereau
83345579d0 ci: create renovate changeset pr 2022-12-26 19:41:08 +01:00
Pilou
2b4b9e0385 Merge pull request #1304 from nhost/renovate/graphql-16.x
chore(deps): update dependency graphql to v16
2022-12-26 19:08:25 +01:00
renovate[bot]
922349f550 chore(deps): update dependency graphql to v16 2022-12-26 17:47:32 +00:00
Pilou
d613f3fd04 Merge pull request #1434 from nhost/renovate/jsonwebtoken-9.x
ci: correct gh action
2022-12-26 18:41:24 +01:00
Pierre-Louis Mercereau
4d8a47777e ci: correct gh action 2022-12-26 18:40:44 +01:00
Pilou
229a7ab1f7 Merge pull request #1417 from nhost/renovate/jsonwebtoken-9.x
fix(deps): update dependency jsonwebtoken to v9
2022-12-26 18:38:09 +01:00
Pierre-Louis Mercereau
3dabb7b53a ci: adapt renovate action 2022-12-26 18:37:49 +01:00
renovate[bot]
abc3d6ce60 chore(deps): update dependency tsup to v6 2022-12-26 17:33:02 +00:00
Pilou
08d49bd1fd Merge pull request #1408 from nhost/renovate/vitest-monorepo
chore(deps): update dependency vitest to ^0.26.0
2022-12-26 18:24:07 +01:00
Pierre-Louis Mercereau
03435a2c66 chore: bump vitest to latest version 2022-12-26 14:55:39 +01:00
Pierre-Louis Mercereau
66208d6840 chore: allow custom values for Accept-Encoding 2022-12-26 13:15:04 +01:00
Pierre-Louis Mercereau
5be9abb0fa chore: changeset 2022-12-26 12:59:23 +01:00
Pierre-Louis Mercereau
8e504b5328 Merge branch 'main' into fix/remove-accept-encoding-header 2022-12-26 12:55:58 +01:00
renovate[bot]
0e3eb7204a fix(deps): update dependency jsonwebtoken to v9 2022-12-23 23:51:28 +00:00
Guido Curcio
70cfeb1fcf Update dashboard/src/components/users/UsersBody/UsersBody.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2022-12-23 12:38:12 -03:00
Guido Curcio
3116562b58 fix service urls; pagination props 2022-12-23 11:31:43 -03:00
Guido Curcio
693e40d385 Merge branch 'main' into feat/auth-management 2022-12-23 11:28:53 -03:00
Nuno Pato
9a1aa7bb2e changeset 2022-12-22 20:08:28 -01:00
Nuno Pato
98345f2e78 dashboard: add functions to the logs dashboard 2022-12-22 20:05:39 -01:00
Guido Curcio
f29abe6238 add changeset 2022-12-22 16:51:19 -03:00
Szilárd Dóró
8956d47bce fix(dashboard): start polling manually
reference issue: https://github.com/apollographql/apollo-client/issues/9819
2022-12-22 18:22:00 +01:00
Szilárd Dóró
dd0738d5f7 chore(dashboard): add changeset 2022-12-22 17:57:36 +01:00
Szilárd Dóró
11d77d6011 fix(dashboard): provisioning status polling 2022-12-22 17:53:07 +01:00
renovate[bot]
a78cd2f18f chore(deps): update dependency vitest to ^0.26.0 2022-12-22 16:01:51 +00:00
Pilou
6ef340daad Merge pull request #1424 from nhost/ci/retry-cli-install
ci: retry the Nhost CLI installation
2022-12-22 16:31:46 +01:00
Pierre-Louis Mercereau
a96e3c9163 ci: retry the Nhost CLI installation 2022-12-22 16:21:36 +01:00
Guido Curcio
229c47cf16 set up redirect from /users/<userId> -> users?userId=<userId> 2022-12-22 10:04:56 -03:00
Guido Curcio
e5e705350d fix: style inconsistencies in users page 2022-12-22 09:56:37 -03:00
Guido Curcio
4f81b0695d add responsive breakpoints to users's table, user's drawer 2022-12-22 09:51:47 -03:00
Szilárd Dóró
9c5b6532d3 Merge pull request #1422 from nhost/fix/persistent-drawer
fix(dashboard): close modals when navigating
2022-12-22 11:02:40 +01:00
Szilárd Dóró
889df8ca4d fix(dashboard): don't break unit tests 2022-12-22 10:54:02 +01:00
Pilou
b998e09e10 Merge pull request #1421 from nhost/chore/integrations-labeler
ci: label integrations
2022-12-22 10:48:33 +01:00
Szilárd Dóró
9e0486a362 chore(dashboard): add changeset 2022-12-22 10:01:12 +01:00
Szilárd Dóró
74037cec68 fix(dashboard): close modals when navigating 2022-12-22 10:00:21 +01:00
Guido Curcio
800db1b300 remove page query param from url if currentPage is equal to 1. 2022-12-22 03:02:53 -03:00
Guido Curcio
a40baa8c63 show up to 3 providers in users table, render a chip with remainder length 2022-12-22 02:23:17 -03:00
Guido Curcio
5cc06609c2 fix(EditUserForm): copy button. 2022-12-22 01:51:17 -03:00
Pierre-Louis Mercereau
819e68b501 ci: label integrations 2022-12-21 19:36:05 +01:00
Guido Curcio
efa68aab83 fix(roles): only show alert when editing role. 2022-12-21 14:15:48 -03:00
Guido Curcio
3a696d366a clicking on auth button of sidebar goes to page 1 2022-12-21 13:55:15 -03:00
Guido Curcio
e3e21b6164 upper bound on number of pages 2022-12-21 13:44:50 -03:00
Guido Curcio
9259663c76 missing key prop on providers 2022-12-21 13:43:28 -03:00
Guido Curcio
26dd7faf05 retrigger effect when nr of pages changes 2022-12-21 13:31:30 -03:00
Pilou
10cc213933 Merge pull request #1418 from nhost/renovate/mermaid-9.x
fix(deps): update dependency mermaid to v9
2022-12-21 16:50:53 +01:00
Guido Curcio
1b5cb93761 include alert when editing roles 2022-12-21 12:18:51 -03:00
Pilou
4157c012fd Merge pull request #1406 from nhost/chore/deprecate-axios
chore: deprecate axios
2022-12-21 16:02:26 +01:00
Pilou
9515096349 Update .changeset/kind-cycles-explain.md
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2022-12-21 15:38:33 +01:00
Pilou
4dd5617855 Update .changeset/kind-cycles-explain.md
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2022-12-21 15:37:29 +01:00
renovate[bot]
01dc358842 fix(deps): update dependency mermaid to v9 2022-12-21 13:22:15 +00:00
Pilou
925a1808e6 Merge pull request #1413 from nhost/renovate/vite-4.x
chore(deps): update dependency vite to v4
2022-12-21 14:10:23 +01:00
Pilou
ac80f88727 Merge pull request #1410 from nhost/chore/remove-query-string
chore: remove unused query-string dependency
2022-12-21 13:12:23 +01:00
Pilou
3078247629 Merge pull request #1412 from nhost/test/docker-compose
test(docker-compose): more retries
2022-12-21 13:12:05 +01:00
Pierre-Louis Mercereau
144c0084d2 chore: add Nhost prefix to client types 2022-12-21 12:27:19 +01:00
Pierre-Louis Mercereau
bbdfb77a07 refactor: types 2022-12-21 12:22:27 +01:00
Guido Curcio
b6df9e2e8c fix: correct onPageChange prop comments. 2022-12-21 08:21:39 -03:00
Guido Curcio
48f15eb849 remove unhelpful comments 2022-12-21 08:16:19 -03:00
Guido Curcio
141642d40d show only two providers in users table 2022-12-21 08:07:23 -03:00
Guido Curcio
def4a3a2ea fix: password change error handling, refetch, and closing dialog; createdAt in users body; use internal ban state for unbanning user; implement error handling for odd pages from URL & side-effect for user id queries; add comments to side-effects. 2022-12-21 08:02:50 -03:00
Pilou
b5a9c1be47 Update packages/nhost-js/src/clients/functions.ts
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2022-12-20 18:45:36 +01:00
Pierre-Louis Mercereau
5b5e7d9640 chore: update vite-related libraries 2022-12-20 15:38:28 +01:00
Guido Curcio
b876a4ada1 handle pagination between pages (parse & mutate url) 2022-12-20 11:02:35 -03:00
Pierre-Louis Mercereau
c02e0c63f2 refactor: simplify 2022-12-20 14:48:47 +01:00
Guido Curcio
7e064355ba show banned users on users table 2022-12-20 10:47:27 -03:00
renovate[bot]
788482fab2 chore(deps): update dependency vite to v4 2022-12-20 13:40:30 +00:00
Pilou
16d94821b8 Merge pull request #1411 from nhost/renovate/tj-actions-changed-files-35.x
chore(deps): update tj-actions/changed-files action to v35
2022-12-20 14:30:16 +01:00
Guido Curcio
0f6ece6b8c remove pagination when there are no users from filter. 2022-12-20 10:28:46 -03:00
Pierre-Louis Mercereau
5c4ab54c90 test(docker-compose): more retries 2022-12-20 14:28:05 +01:00
Guido Curcio
793e7392da error handling for creating users 2022-12-20 10:24:53 -03:00
renovate[bot]
5941568bbb chore(deps): update tj-actions/changed-files action to v35 2022-12-20 13:15:40 +00:00
Pierre-Louis Mercereau
1a9e1fde1d chore: remove unused query-string dependency 2022-12-20 14:12:29 +01:00
Guido Curcio
a9bbd1303e close drawer after deleting user; track disabled state of an user. 2022-12-20 09:30:41 -03:00
Guido Curcio
b0ed2b6f14 button instead of IconButton for password change 2022-12-20 09:03:33 -03:00
Guido Curcio
3e951eab4f fix: don't hide the OAuth methods section; remove the active chip. 2022-12-20 08:42:13 -03:00
Guido Curcio
cd7a198715 fix: fix showing minutes instead of month; correct format. 2022-12-20 08:36:36 -03:00
Guido Curcio
7c4d05a25e fix: loading states, remove sign-in methods, fix default role, move handlers to specific component. 2022-12-20 07:15:43 -03:00
Pierre-Louis Mercereau
32c0632526 docs: correct changeset 2022-12-20 10:28:09 +01:00
Pierre-Louis Mercereau
19cca7f45d chore: changeset 2022-12-20 10:22:00 +01:00
Pierre-Louis Mercereau
191580a819 Merge branch 'main' into chore/deprecate-axios 2022-12-20 10:12:29 +01:00
Pilou
4a57861354 Merge pull request #1396 from nhost/integrations-iaubsd98hasd
Integrations and README updates
2022-12-20 10:11:48 +01:00
Pierre-Louis Mercereau
d96b817476 refactor: simplify 2022-12-20 10:04:19 +01:00
renovate[bot]
db6db8d860 chore(deps): update dependency @types/node to v18 2022-12-20 08:35:37 +00:00
Pierre-Louis Mercereau
fe405ba123 fix: docgen and tsconfig 2022-12-20 09:33:53 +01:00
Pilou
972a5f652f Merge pull request #1281 from nhost/renovate/typescript-4.x
chore(deps): update dependency typescript to v4.9.4
2022-12-20 09:32:34 +01:00
Pierre-Louis Mercereau
7a2c140524 Merge branch 'main' into chore/deprecate-axios 2022-12-20 09:26:41 +01:00
Pierre-Louis Mercereau
3d717d68a9 Merge branch 'main' into integrations-iaubsd98hasd 2022-12-20 09:22:29 +01:00
Pierre-Louis Mercereau
13b6a47bef fix: ignore duplicate type 2022-12-20 09:13:50 +01:00
Pierre-Louis Mercereau
2a6caa47bd chore: deprecate axios 2022-12-20 09:00:59 +01:00
Szilárd Dóró
dbbfaef451 Merge pull request #1373 from nhost/feat/rule-group-editor
feat(dashboard): Rule Group Editor
2022-12-19 11:52:16 +01:00
Szilárd Dóró
b4c07f1723 Merge branch 'main' into feat/rule-group-editor 2022-12-19 11:51:18 +01:00
Szilárd Dóró
e983eb53d9 chore(dashboard): remove "is" prefix 2022-12-19 11:09:18 +01:00
Szilárd Dóró
49867fdcf7 fix(dashboard): add missing inputValue for freeSolo input 2022-12-19 11:07:50 +01:00
Szilárd Dóró
cc428d73ee fix(dashboard): async initialization 2022-12-19 11:02:16 +01:00
Pilou
940a1db0fc Merge branch 'main' into integrations-iaubsd98hasd 2022-12-19 11:00:35 +01:00
Pilou
d70f29a408 Merge pull request #1400 from nhost/contributors-readme-action-PL6rn2c5lN
docs(contributor): contributors readme action update
2022-12-19 10:58:17 +01:00
Pilou
52cb055520 Merge pull request #1402 from nhost/contributors-readme-action-IrWb62uoZo
docs(contributor): contributors readme action update
2022-12-19 10:57:46 +01:00
github-actions[bot]
8c406237a2 docs(contributor): contrib-readme-action has updated readme 2022-12-19 09:57:07 +00:00
Pilou
d036e282e5 Merge pull request #1395 from nhost/fix-asu9hda0sd
revert code to previously working
2022-12-19 10:56:53 +01:00
Szilárd Dóró
39530cd8e8 feat(dashboard): support Hasura array operators 2022-12-19 10:16:36 +01:00
github-actions[bot]
8b58627608 docs(contributor): contrib-readme-action has updated readme 2022-12-19 08:06:33 +00:00
Calvin Lang
c4e2d87e5c Removed accept headers * in axios requests 2022-12-19 16:06:26 +08:00
Johan Eliasson
66b0378d38 Merge pull request #1397 from muttenzer/docs/event-trigger-env-var
Replace deprecated "NHOST_BACKEND_URL" in event trigger example
2022-12-19 09:06:09 +01:00
Timo M
22d7a36247 update hasura event trigger video using the correct env var 2022-12-17 17:48:36 +01:00
Timo M
7d2eb2de66 update webhook url format image 2022-12-17 17:45:09 +01:00
Timo M
4bba002c30 replace NHOST_BACKEND_URL with NHOST_FUNCTIONS_URL env var 2022-12-17 17:43:42 +01:00
Johan Eliasson
881a3344d4 update lock file 2022-12-17 11:00:45 +01:00
Johan Eliasson
d1562d33fb update 2022-12-17 10:57:06 +01:00
Johan Eliasson
21ced66f22 moved apollo and react-apollo to integrations/ folder 2022-12-17 10:50:13 +01:00
Johan Eliasson
2a2d86904d revert to previously working 2022-12-17 10:46:12 +01:00
Johan Eliasson
82d46f716b Merge pull request #1381 from nhost/docs-ui8asud9
docs update
2022-12-17 10:42:33 +01:00
Johan Eliasson
5e26810868 using authentication everywhere 2022-12-17 10:36:39 +01:00
Johan Eliasson
d590258371 correct query 2022-12-17 10:34:06 +01:00
Johan Eliasson
697ef57cb8 Update docs/docs/authentication/users.mdx
Co-authored-by: Guido Curcio <guidomaurocurcio@gmail.com>
2022-12-17 10:29:28 +01:00
Johan Eliasson
93002dc8c3 Update docs/docs/authentication/users.mdx
Co-authored-by: Guido Curcio <guidomaurocurcio@gmail.com>
2022-12-17 10:29:09 +01:00
Guido Curcio
357e0933ff pass roles with payload; handle adding and removing roles from user 2022-12-17 03:18:53 -03:00
Johan Eliasson
baa1937d06 Merge pull request #1228 from nhost/react-urql
urql
2022-12-16 22:11:29 +01:00
Johan Eliasson
03e5662df9 Merge branch 'main' into react-urql 2022-12-16 22:10:54 +01:00
Pilou
e0711bdfc8 Merge pull request #1372 from kematzy/docs-update-vue-quickstart
Update of Vue Quickstart documentation
2022-12-16 22:05:55 +01:00
Johan Eliasson
caca27fde3 Merge branch 'main' into docs-ui8asud9 2022-12-16 21:55:55 +01:00
Johan Eliasson
b0d51033c6 typo fix 2022-12-16 21:43:02 +01:00
Johan Eliasson
088f9394fc Update docs/docs/graphql/permissions.mdx
Co-authored-by: Siarhei Lipchyk <contact@siarhei.dev>
2022-12-16 21:42:07 +01:00
Szilárd Dóró
a91361f971 feat(dashboard): add support for Hasura permission object converter 2022-12-16 21:10:04 +01:00
Pilou
a4f5be6ab9 Merge pull request #1390 from nhost/fix/strict-mode
fix: React 18 strict mode and other improvements
2022-12-16 19:09:55 +01:00
Szilárd Dóró
dbbccbf1cd fix(dashboard): mobile responsive design 2022-12-16 13:50:22 +01:00
Guido Curcio
7d8f82b99d user.lastSeen fix 2022-12-16 09:36:22 -03:00
Szilárd Dóró
a70dc7b352 chore(dashboard): improve rule group editor design 2022-12-16 12:57:03 +01:00
Pierre-Louis Mercereau
54df0df42b fix: 🐛 React 18 strict mode and other improvements 2022-12-16 12:08:04 +01:00
Szilárd Dóró
0bbb2598fd feat(dashboard): finalize Hasura permission converter 2022-12-16 11:01:25 +01:00
Guido Curcio
e10480b761 missing refetch when deleting user. 2022-12-15 15:02:05 -03:00
Guido Curcio
1343abbe50 comments & types 2022-12-15 12:33:04 -03:00
Szilárd Dóró
4ba34cc827 feat(dashboard): working on Hasura converters 2022-12-15 16:00:11 +01:00
Guido Curcio
fa37546139 handle banning users 2022-12-15 11:22:55 -03:00
Szilárd Dóró
42ece48ce3 feat(dashboard): added support for text truncation 2022-12-15 15:05:24 +01:00
Guido Curcio
112526a984 render email & password only if email and no social oauth. 2022-12-15 11:04:40 -03:00
Szilárd Dóró
f97ab31f69 feat(dashboard): added support for _c* operators 2022-12-15 10:21:09 +01:00
Johan Eliasson
21dc1ecd6b image didn't exist 2022-12-15 10:01:24 +01:00
Szilárd Dóró
c11adbe3e2 chore(dashboard): improve initialization 2022-12-15 09:58:27 +01:00
Johan Eliasson
6078e9c207 new sys env vars 2022-12-15 09:22:00 +01:00
Johan Eliasson
450582dc43 update 2022-12-15 09:12:11 +01:00
Johan Eliasson
ddec0e1be1 update 2022-12-15 09:09:31 +01:00
Johan Eliasson
698154b24b permissions updated 2022-12-15 09:05:42 +01:00
Johan Eliasson
9c87b0f67b updates 2022-12-14 20:48:43 +01:00
Pilou
85afe3d216 Merge pull request #1368 from nhost/refactor/less-peer-dependencies
refactor: less peer dependencies
2022-12-14 19:19:09 +01:00
Pierre-Louis Mercereau
0773e5215f Merge branch 'main' into refactor/less-peer-dependencies 2022-12-14 19:01:31 +01:00
Pierre-Louis Mercereau
10f25fcc4e chore: remove needless paths 2022-12-14 18:59:57 +01:00
Johan Eliasson
2ee90d6ea3 removed unused files 2022-12-14 18:16:33 +01:00
Johan Eliasson
5db5323a1d updates 2022-12-14 18:14:40 +01:00
Guido Curcio
0c74806245 fix showing limit in pagination rather than total users. 2022-12-14 13:31:47 -03:00
Guido Curcio
5df84d7f50 fix remoteAppUser type 2022-12-14 13:08:39 -03:00
Guido Curcio
d58de8fcf8 join totalUsers query with main users query 2022-12-14 12:55:49 -03:00
Szilárd Dóró
ed93d4b583 feat(dashboard): add onInitialized to autocomplete 2022-12-14 16:15:40 +01:00
Szilárd Dóró
1ec1953eaa chore(dashboard): improve default value error tolerance 2022-12-14 15:41:17 +01:00
Szilárd Dóró
63e9c3933e chore(dashboard): improved column autocomplete value management 2022-12-14 15:33:29 +01:00
Guido Curcio
85674c4d90 handle users providers other than email (e.g. github); handle removed providers from a user. 2022-12-14 11:32:28 -03:00
Guido Curcio
5a1d3b9bfc handle anonymous users 2022-12-14 11:06:35 -03:00
Guido Curcio
942570ed29 show table header on not found search strings 2022-12-14 10:33:29 -03:00
Szilárd Dóró
1b1620f633 chore(dashboard): cleaned up default value functionality 2022-12-14 14:24:41 +01:00
Johan Eliasson
e8e8d661e1 small exchange refactor 2022-12-14 13:50:44 +01:00
Johan Eliasson
ba08ec7f5c comments 2022-12-14 13:44:39 +01:00
Johan Eliasson
fb0c98c21d moved react-urql to integrations 2022-12-14 13:37:20 +01:00
Johan Eliasson
c9f575c40c database settings update 2022-12-14 13:24:25 +01:00
Johan Eliasson
6c8bed7ecc Update packages/react-urql/src/provider.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2022-12-14 13:23:37 +01:00
Guido Curcio
3058eee48f 25 limit, loading screen 2022-12-14 09:21:14 -03:00
Szilárd Dóró
d5a712f7ef feat(dashboard): add support for default values 2022-12-14 13:07:11 +01:00
Johan Eliasson
83422f5ee6 updated README 2022-12-14 09:42:01 +01:00
Johan Eliasson
51909a6a8f remove 2022-12-14 09:34:05 +01:00
Johan Eliasson
2f30797556 update 2022-12-14 09:28:35 +01:00
Johan Eliasson
0b8f7d1661 Merge branch 'main' into react-urql 2022-12-14 09:27:54 +01:00
Johan Eliasson
52ee9d84b6 demo img 2022-12-14 09:11:17 +01:00
Szilárd Dóró
d9612b28b0 chore(dashboard): update pnpm-lock.yaml 2022-12-14 09:07:56 +01:00
Szilárd Dóró
0034791493 Merge remote-tracking branch 'origin/main' into feat/rule-group-editor 2022-12-14 09:06:15 +01:00
Johan Eliasson
80fed14a6b updated 2022-12-14 09:03:30 +01:00
Szilárd Dóró
d457ada435 Merge pull request #1380 from nhost/changeset-release/main
chore: update versions
2022-12-14 08:57:35 +01:00
github-actions[bot]
b41e5a9df5 chore: update versions 2022-12-14 07:46:43 +00:00
Szilárd Dóró
0c8ace1bd4 Merge pull request #1379 from nhost/fix/dashboard-urls
fix(dashboard): use correct service URLs
2022-12-14 08:45:12 +01:00
Johan Eliasson
3f800a068b update 2022-12-13 22:31:42 +01:00
Szilárd Dóró
7d490fe569 fix(dashboard): use correct GraphQL slug and admin secret 2022-12-13 21:34:11 +01:00
Szilárd Dóró
d6527122db fix(dashboard): use correct service URLs 2022-12-13 21:21:12 +01:00
Johan Eliasson
3211140dec remove changeset for now 2022-12-13 21:10:41 +01:00
Johan Eliasson
469352cd81 merge 2022-12-13 21:09:44 +01:00
Szilárd Dóró
88400f6b7c chore(dashboard): add delay to queries 2022-12-13 17:41:16 +01:00
Szilárd Dóró
f8c8a06d71 feat(dashboard): permission variable queries 2022-12-13 17:24:14 +01:00
Szilárd Dóró
2f819865bc chore(dashboard): move handlers to mocks folder 2022-12-13 16:32:01 +01:00
Szilárd Dóró
3888f3041f chore(dashboard): improve operator selectors 2022-12-13 16:15:01 +01:00
Guido Curcio
bacb1b9720 fix number of pages going to 0 on nextPageClick 2022-12-13 11:53:42 -03:00
Guido Curcio
e119e4fc18 styles on userBody 2022-12-13 11:19:23 -03:00
Pierre-Louis Mercereau
569c4004f6 Merge branch 'main' into refactor/less-peer-dependencies 2022-12-13 14:10:31 +01:00
Guido Curcio
b1fe2be963 users body loader, pagination elemnts; input fix for pages > 9 2022-12-13 09:07:36 -03:00
Szilárd Dóró
872e50b635 chore(dashboard): improve Chip in autocomplete 2022-12-13 13:01:44 +01:00
Guido Curcio
9b6e8ab3bc slotProps for pagination component 2022-12-13 07:50:43 -03:00
Szilárd Dóró
fe0742e278 chore(dashboard): update pnpm-lock.yaml 2022-12-13 10:15:29 +01:00
Szilárd Dóró
ded57d3b24 Merge remote-tracking branch 'origin/main' into feat/rule-group-editor 2022-12-13 10:13:03 +01:00
Szilárd Dóró
c30abaea22 chore(dashboard): add reference to storybook to the README 2022-12-13 10:11:31 +01:00
Guido Curcio
d2c4b7cad1 custom styles for pagination in users tab 2022-12-13 02:26:32 -03:00
Guido Curcio
59d737696a onChangePage handler for Pagination 2022-12-13 02:13:57 -03:00
Kematzy
22de3214f1 Minor typo & formatting changes to standardise 2022-12-13 11:52:08 +08:00
Kematzy
cf880f992f Update Vue Quickstart docs 2022-12-13 11:50:51 +08:00
Pilou
aee4cdcb72 Update .changeset/modern-days-eat.md
Co-authored-by: Johan Eliasson <johan@nhost.io>
2022-12-12 20:20:03 +01:00
Szilárd Dóró
87af60cc03 feat(dashboard): add experimental multiselect autocomplete 2022-12-12 17:30:09 +01:00
Pierre-Louis Mercereau
3d8dd39995 fix: dashboard tests 2022-12-12 17:00:41 +01:00
Pierre-Louis Mercereau
65687beecc chore: rewrite the pr so it fits with Nuno's mind 2022-12-12 16:41:16 +01:00
Szilárd Dóró
62aa859737 feat(dashboard): support for boolean input field 2022-12-12 15:53:50 +01:00
Guido Curcio
35cd76e562 UsersBodyProps types 2022-12-12 11:16:56 -03:00
Guido Curcio
266bbe837d type users, add comments UsersBodyProps, memo limit & offset. 2022-12-12 11:15:31 -03:00
Guido Curcio
caf785a938 feat: Pagination common component. 2022-12-12 10:48:06 -03:00
Szilárd Dóró
9bc3e755df feat(dashboard): add dynamic rule group editor input type 2022-12-12 14:43:22 +01:00
Szilárd Dóró
638a7ac11d feat(dashboard): finalize relationship discovery 2022-12-12 14:19:02 +01:00
Szilárd Dóró
4e49c8db50 fix(dashboard): correct column autocomplete input value 2022-12-12 11:28:49 +01:00
Szilárd Dóró
210f65b4db feat(dashboard): added column autocomplete to rule group editor 2022-12-12 10:50:15 +01:00
Szilárd Dóró
1b6482126f feat(dashboard): add relationship discovery 2022-12-12 10:10:39 +01:00
Guido Curcio
96f9c1a55d handle pagination when searching for users 2022-12-12 06:08:35 -03:00
Guido Curcio
731460b20d handle pagination in users' table 2022-12-12 06:05:22 -03:00
Guido Curcio
1537d46b1d handle states of search: case insensitive query, preserve search query. 2022-12-12 04:29:12 -03:00
Guido Curcio
632def158d render activated providers from users. 2022-12-12 02:38:53 -03:00
Guido Curcio
39271a67e2 render all available roles from the app, check roles form user. 2022-12-12 02:11:43 -03:00
Guido Curcio
9e25c4f386 render verified checkbox as helpertext except when errors; disable prop on phoneNumberVerified when no phoneNumber. 2022-12-12 01:26:48 -03:00
Guido Curcio
dd58a4ac7f handle mutation of displayName, email, and avatarUrl; render avatar url if not default=blank. 2022-12-12 01:14:04 -03:00
Guido Curcio
b9c3567baa update gql query with locale, lastSeen, emailVerified, and phoneNumberVerified 2022-12-12 00:56:00 -03:00
Guido Curcio
108937789a EditUserForm: roles & sign-in methods styles. 2022-12-12 00:20:59 -03:00
Guido Curcio
e651745a7e no users found; default & allowed roles 2022-12-11 23:15:29 -03:00
Szilárd Dóró
699debb2b8 fix(dashboard): correct group rendering 2022-12-09 16:12:05 +01:00
Szilárd Dóró
3e08dc7f8c feat(dashboard): improve navigation in popover 2022-12-09 14:26:46 +01:00
Szilárd Dóró
6928b48781 fix(dashboard): custom autocomplete props 2022-12-09 14:14:58 +01:00
Szilárd Dóró
02886350ff chore(dashboard): use custom autocomplete for column selector 2022-12-09 14:11:32 +01:00
Szilárd Dóró
b3672f8246 feat(dashboard): column selector metadata support 2022-12-09 12:09:43 +01:00
Guido Curcio
6091b4a8e8 handle delete users & refetch main users query 2022-12-09 06:46:01 -03:00
Guido Curcio
82ddcbd180 handle search strings, static UsersBodyHeader, nullish render on no users 2022-12-09 03:48:34 -03:00
Guido Curcio
8aa7aafa3b add form footer to EditUserForm 2022-12-09 03:10:23 -03:00
Guido Curcio
183cb4b26a EditUserPasswordForm: handle change password edits, remote gql client, toast on edit. 2022-12-09 03:00:20 -03:00
Guido Curcio
3a7377c6e2 EditUserForm: add avatar with letters, change password button & modal. 2022-12-08 23:15:05 -03:00
Guido Curcio
1529f58c33 fetch remote project users on page load 2022-12-08 23:00:26 -03:00
Guido Curcio
95af5421d1 UsersBody: render first two letters of displayname if no avatar 2022-12-08 22:51:11 -03:00
Guido Curcio
feb39404db first section of user's drawer 2022-12-08 22:26:54 -03:00
Guido Curcio
15b3100c63 pass on data from previously fetched users table 2022-12-08 12:17:22 -03:00
Guido Curcio
f7ef7d106d open drawer on edit user 2022-12-08 11:18:11 -03:00
Szilárd Dóró
0dbbcc5595 chore(dashboard): improve column autocomplete 2022-12-08 15:16:13 +01:00
Szilárd Dóró
816f8d069d fix(dashboard): pnpm-lock file 2022-12-08 14:20:28 +01:00
Guido Curcio
72c31622cd render user's table with dropdown trigger 2022-12-08 09:25:07 -03:00
Guido Curcio
6959461e3f handle creating users (CreateUserForm) 2022-12-08 08:40:13 -03:00
Guido Curcio
103472ac77 empty state for new users page 2022-12-08 07:57:42 -03:00
Szilárd Dóró
2ebf99ff8f chore(dashboard): improved column autocomplete 2022-12-08 09:32:51 +01:00
renovate[bot]
c13e492bbf chore(deps): update dependency typescript to v4.9.4 2022-12-08 01:41:29 +00:00
Szilárd Dóró
63476a2351 feat(dashboard): start work on column autocomplete 2022-12-07 16:49:55 +01:00
Szilárd Dóró
782252c059 chore(dashboard): improved Rule Group editor 2022-12-07 14:42:20 +01:00
Szilárd Dóró
e86978a1ff chore(dashboard): upgrade storybook
- chore(dashboard): improve mobile layout of the rule group editor
2022-12-07 13:32:05 +01:00
Szilárd Dóró
84cfd11953 chore(dashboard): extend rule group editor story 2022-12-07 13:12:13 +01:00
Szilárd Dóró
9a43e136f6 fix(dashboard): fixed rule group editor form structure 2022-12-07 11:22:50 +01:00
Szilárd Dóró
e9cff26fa0 chore(dashboard): improve rule group editor layout 2022-12-07 10:11:54 +01:00
Szilárd Dóró
3d32bca2b3 fix(dashboard): fix rule group editor delete button 2022-12-06 16:11:08 +01:00
Szilárd Dóró
4021feaf38 feat(dashboard): rule editor implementation 2022-12-06 14:02:07 +01:00
Szilárd Dóró
306ec74356 chore(dashboard): update import paths 2022-12-06 10:16:42 +01:00
Johan Eliasson
4b2d2a4f55 merge 2022-12-03 13:36:02 +01:00
Johan Eliasson
eba2bd05b8 update 2022-12-03 13:35:04 +01:00
Johan Eliasson
84a1b28261 Create breezy-donuts-try.md 2022-12-03 13:34:20 +01:00
Johan Eliasson
ff3b9df41e cleanup 2022-12-03 13:32:06 +01:00
Szilárd Dóró
f1272947dd chore(deps): update @types/react and @types/react-dom 2022-12-02 10:17:32 +01:00
Johan Eliasson
e5041bfd30 Merge branch 'main' into react-urql 2022-12-02 09:59:38 +01:00
Johan Eliasson
3d7cc74feb env vars 2022-12-02 09:42:38 +01:00
Johan Eliasson
d007202783 update 2022-12-02 09:40:26 +01:00
Johan Eliasson
38e92a705d works outside strict mode now 2022-12-02 09:38:16 +01:00
Johan Eliasson
74cc63833a package updates 2022-12-01 22:05:28 +01:00
Johan Eliasson
425320bbb5 lockfile update 2022-12-01 21:31:57 +01:00
Johan Eliasson
499352ad8a Merge branch 'main' into react-urql 2022-12-01 21:30:53 +01:00
Johan Eliasson
2e30371086 Merge branch 'main' into react-urql 2022-11-28 17:04:51 +01:00
Johan Eliasson
66659bb293 update 2022-11-27 21:31:14 +01:00
Johan Eliasson
579d4f3170 update 2022-11-27 21:14:10 +01:00
Johan Eliasson
bb56548603 urql 2022-11-27 21:12:07 +01:00
640 changed files with 18787 additions and 9436 deletions

View File

@@ -2,9 +2,22 @@
"$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json",
"changelog": "@changesets/cli/changelog", "changelog": "@changesets/cli/changelog",
"commit": false, "commit": false,
"linked": [], "linked": [
[
"@nhost/nextjs",
"@nhost/react",
"@nhost/vue",
"@nhost/nhost-js",
"@nhost/hasura-auth-js",
"@nhost/hasura-storage-js"
],
[
"@nhost/react-apollo",
"@nhost/apollo"
]
],
"access": "restricted", "access": "restricted",
"baseBranch": "main", "baseBranch": "main",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
"ignore": [] "ignore": []
} }

View File

@@ -35,8 +35,11 @@ runs:
fi fi
- name: Install Nhost CLI - name: Install Nhost CLI
if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }} if: ${{ steps.check-nhost-cli.outputs.installed == 'false' }}
shell: bash uses: nick-fields/retry@v2
run: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }} with:
timeout_minutes: 3
max_attempts: 3
command: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
- name: Set custom configuration - name: Set custom configuration
if: ${{ inputs.config }} if: ${{ inputs.config }}
shell: bash shell: bash

7
.github/labeler.yml vendored
View File

@@ -12,11 +12,14 @@ examples:
sdk: sdk:
- packages/**/* - packages/**/*
integrations:
- integrations/**/*
react: react:
- '{packages,examples}/*react*/**/*' - '{packages,examples,integrations}/*react*/**/*'
nextjs: nextjs:
- '{packages,examples}/*next*/**/*' - '{packages,examples}/*next*/**/*'
vue: vue:
- '{packages,examples}/*vue*/**/*' - '{packages,examples,integrations}/*vue*/**/*'

View File

@@ -8,9 +8,16 @@
}, },
"ignoreDeps": [ "ignoreDeps": [
"pnpm", "pnpm",
"node" "node",
"@types/node"
], ],
"labels": [ "labels": [
"dependencies" "dependencies"
],
"enabledManagers": [
"npm",
"dockerfile",
"docker-compose",
"github-actions"
] ]
} }

View File

@@ -38,7 +38,7 @@ jobs:
BUILD: 'all' BUILD: 'all'
- name: Check if the pnpm lockfile changed - name: Check if the pnpm lockfile changed
id: changed-lockfile id: changed-lockfile
uses: tj-actions/changed-files@v34 uses: tj-actions/changed-files@v35
with: with:
files: pnpm-lock.yaml files: pnpm-lock.yaml
# * Determine a pnpm filter argument for packages that have been modified. # * Determine a pnpm filter argument for packages that have been modified.

View File

@@ -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 }}

View File

@@ -21,6 +21,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GH_PAT }}
# * Install Node and dependencies. Package downloads will be cached for the next jobs. # * Install Node and dependencies. Package downloads will be cached for the next jobs.
- name: Install Node and dependencies - name: Install Node and dependencies
uses: ./.github/actions/install-dependencies uses: ./.github/actions/install-dependencies
@@ -61,8 +62,28 @@ jobs:
${{ github.event.pull_request.title }} ${{ github.event.pull_request.title }}
EOF EOF
- uses: stefanzweifel/git-auto-commit-action@v4 - name: Create Pull Request
if: steps.bumps.outputs.result != '' id: cpr
uses: peter-evans/create-pull-request@v4
with: with:
commit_message: ${{ github.event.pull_request.title }} token: ${{ secrets.GH_PAT }}
branch: main 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
View File

@@ -101,14 +101,17 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
## Nhost Clients ## Nhost Clients
- [JavaScript/TypeScript SDK](https://docs.nhost.io/reference/javascript) - [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript)
- [Dart and Flutter SDK](https://github.com/nhost/nhost-dart) - [Dart and Flutter](https://github.com/nhost/nhost-dart)
- [Nhost React](https://docs.nhost.io/reference/react) - [React](https://docs.nhost.io/reference/react)
- [Nhost Next.js](https://docs.nhost.io/reference/nextjs) - [Next.js](https://docs.nhost.io/reference/nextjs)
- [Nhost Vue](https://docs.nhost.io/reference/vue) - [Vue](https://docs.nhost.io/reference/vue)
## Integrations ## 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) - [Stripe GraphQL API](./integrations/stripe-graphql-js#nhoststripe-graphql-js)
- [Google Translation GraphQL API](./integrations/google-translation#nhostgoogle-translation) - [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. 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"> <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_badge_black_Q1_2022.svg" alt="ROSS Index - Fastest Growing Open-Source Startups in Q1 2022 | Runa Capital" width="260" height="56" /> <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> </a>
### How to contribute ### How to contribute
@@ -141,437 +144,8 @@ Here are some ways of contributing to making Nhost better:
### Contributors ### Contributors
<!-- readme: contributors -start --> <a href="https://github.com/nhost/nhost/graphs/contributors">
<table> <p align="center">
<tr> <img width="720" src="https://contrib.rocks/image?repo=nhost/nhost" alt="A table of avatars from the project's contributors" />
<td align="center"> </p>
<a href="https://github.com/plmercereau"> </a>
<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 -->

View File

@@ -40,7 +40,7 @@
], ],
"paths": { "paths": {
"@nhost/apollo": [ "@nhost/apollo": [
"../packages/apollo/src/index.ts" "../integrations/apollo/src/index.ts"
], ],
"@nhost/docgen": [ "@nhost/docgen": [
"../packages/docgen/src/index.ts" "../packages/docgen/src/index.ts"
@@ -61,10 +61,7 @@
"../packages/react/src/index.ts" "../packages/react/src/index.ts"
], ],
"@nhost/react-apollo": [ "@nhost/react-apollo": [
"../packages/react-apollo/src/index.ts" "../integrations/react-apollo/src/index.ts"
],
"@nhost/react-auth": [
"../packages/react-auth/src/index.ts"
], ],
"@nhost/vue": [ "@nhost/vue": [
"../packages/vue/src/index.ts" "../packages/vue/src/index.ts"

View File

@@ -1,3 +1,4 @@
import replace from '@rollup/plugin-replace'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
@@ -18,7 +19,9 @@ export default defineConfig({
tsconfigPaths(), tsconfigPaths(),
dts({ dts({
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'], exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
entryRoot: 'src' entryRoot: 'src',
// Was defaulting to true until version 1.7
skipDiagnostics: true
}) })
], ],
test: { test: {
@@ -41,6 +44,12 @@ export default defineConfig({
}, },
rollupOptions: { rollupOptions: {
external: (id) => deps.some((dep) => id.startsWith(dep)), external: (id) => deps.some((dep) => id.startsWith(dep)),
plugins: [
replace({
preventAssignment: true,
'exports.hasOwnProperty(': 'Object.prototype.hasOwnProperty.call(exports,'
})
],
output: { output: {
globals: { globals: {
graphql: 'graphql', graphql: 'graphql',

View File

@@ -6,6 +6,7 @@ module.exports = {
'@storybook/addon-links', '@storybook/addon-links',
'@storybook/addon-essentials', '@storybook/addon-essentials',
'@storybook/addon-interactions', '@storybook/addon-interactions',
'storybook-addon-next-router',
{ {
/** /**
* Fix Storybook issue with PostCSS@8 * 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',
}),
}; };

View File

@@ -2,9 +2,25 @@ import '@fontsource/inter';
import '@fontsource/inter/500.css'; import '@fontsource/inter/500.css';
import '@fontsource/inter/700.css'; import '@fontsource/inter/700.css';
import { CssBaseline, ThemeProvider } from '@mui/material'; 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'; import defaultTheme from '../src/theme/default';
global.Buffer = Buffer;
initialize({ onUnhandledRequest: 'bypass' });
const queryClient = new QueryClient();
export const parameters = { export const parameters = {
nextRouter: {
Provider: RouterContext.Provider,
isReady: true,
},
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },
controls: { controls: {
matchers: { matchers: {
@@ -14,11 +30,25 @@ export const parameters = {
}, },
}; };
export const withMuiTheme = (Story) => ( export const decorators = [
<ThemeProvider theme={defaultTheme}> (Story) => (
<CssBaseline /> <ThemeProvider theme={defaultTheme}>
<Story /> <CssBaseline />
</ThemeProvider> <Story />
); </ThemeProvider>
),
export const decorators = [withMuiTheme]; (Story) => (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
),
(Story) => (
<NhostApolloProvider
fetchPolicy="cache-first"
graphqlUrl="http://localhost:1337/v1/graphql"
>
<Story />
</NhostApolloProvider>
),
mswDecorator,
];

View File

@@ -1,5 +1,42 @@
# @nhost/dashboard # @nhost/dashboard
## 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 ## 0.7.10
### Patch Changes ### Patch Changes

View File

@@ -37,6 +37,14 @@ NEXT_PUBLIC_ENV=dev
NEXT_PUBLIC_NHOST_PLATFORM=false 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 ### Full list of environment variables
| Name | Description | | Name | Description |

View File

@@ -66,6 +66,11 @@ module.exports = withBundleAnalyzer({
destination: '/:workspaceSlug/:appSlug/settings/environment-variables', destination: '/:workspaceSlug/:appSlug/settings/environment-variables',
permanent: true, permanent: true,
}, },
{
source: '/:workspaceSlug/:appSlug/users/:userId',
destination: '/:workspaceSlug/:appSlug/users?userId=:userId',
permanent: true,
},
]; ];
}, },
}); });

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/dashboard", "name": "@nhost/dashboard",
"version": "0.7.10", "version": "0.8.0",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -13,11 +13,11 @@
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only", "codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost dev -d", "nhost:dev": "nhost dev -d",
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.", "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" "build-storybook": "build-storybook"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.6.2", "@apollo/client": "^3.7.3",
"@codemirror/language": "^6.3.0", "@codemirror/language": "^6.3.0",
"@emotion/cache": "^11.10.5", "@emotion/cache": "^11.10.5",
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.5",
@@ -35,8 +35,6 @@
"@mui/system": "^5.10.14", "@mui/system": "^5.10.14",
"@mui/x-date-pickers": "^5.0.8", "@mui/x-date-pickers": "^5.0.8",
"@nhost/nextjs": "workspace:*", "@nhost/nextjs": "workspace:*",
"@nhost/nhost-js": "workspace:*",
"@nhost/react": "workspace:*",
"@nhost/react-apollo": "workspace:*", "@nhost/react-apollo": "workspace:*",
"@segment/snippet": "^4.15.3", "@segment/snippet": "^4.15.3",
"@stripe/react-stripe-js": "^1.10.0", "@stripe/react-stripe-js": "^1.10.0",
@@ -90,21 +88,21 @@
"@graphql-codegen/typescript-operations": "^2.5.1", "@graphql-codegen/typescript-operations": "^2.5.1",
"@graphql-codegen/typescript-react-apollo": "^3.3.1", "@graphql-codegen/typescript-react-apollo": "^3.3.1",
"@next/bundle-analyzer": "^12.3.1", "@next/bundle-analyzer": "^12.3.1",
"@storybook/addon-actions": "^6.5.13", "@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.13", "@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-interactions": "^6.5.13", "@storybook/addon-interactions": "^6.5.14",
"@storybook/addon-links": "^6.5.13", "@storybook/addon-links": "^6.5.14",
"@storybook/addon-postcss": "^2.0.0", "@storybook/addon-postcss": "^2.0.0",
"@storybook/builder-webpack5": "^6.5.13", "@storybook/builder-webpack5": "^6.5.14",
"@storybook/manager-webpack5": "^6.5.13", "@storybook/manager-webpack5": "^6.5.14",
"@storybook/react": "^6.5.13", "@storybook/react": "^6.5.14",
"@storybook/testing-library": "^0.0.13", "@storybook/testing-library": "^0.0.13",
"@testing-library/dom": "^8.19.0", "@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/node": "^18.11.9", "@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29", "@types/pluralize": "^0.0.29",
"@types/react": "18.0.25", "@types/react": "18.0.25",
"@types/react-dom": "18.0.9", "@types/react-dom": "18.0.9",
@@ -113,8 +111,8 @@
"@types/validator": "^13.7.10", "@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0", "@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0", "@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^3.0.0",
"@vitest/coverage-c8": "^0.25.2", "@vitest/coverage-c8": "^0.26.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0", "babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-console": "^6.9.4",
@@ -131,19 +129,21 @@
"jsdom": "^20.0.3", "jsdom": "^20.0.3",
"lint-staged": ">=13", "lint-staged": ">=13",
"msw": "^0.49.0", "msw": "^0.49.0",
"msw-storybook-addon": "^1.6.3",
"postcss": "^8.4.19", "postcss": "^8.4.19",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.2.0", "prettier-plugin-organize-imports": "^3.2.0",
"prettier-plugin-tailwindcss": "^0.2.0", "prettier-plugin-tailwindcss": "^0.2.0",
"react-date-fns-hooks": "^0.9.4", "react-date-fns-hooks": "^0.9.4",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2",
"storybook-addon-next-router": "^4.0.1",
"tailwindcss": "^3.1.2", "tailwindcss": "^3.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths-webpack-plugin": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.4", "typescript": "^4.8.4",
"vite": "^3.2.4", "vite": "^4.0.2",
"vite-tsconfig-paths": "^3.6.0", "vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.25.2", "vitest": "^0.26.2",
"webpack": "^5.75.0" "webpack": "^5.75.0"
}, },
"browserslist": { "browserslist": {
@@ -157,5 +157,8 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"msw": {
"workerDirectory": "public"
} }
} }

View 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)
}

View File

@@ -15,7 +15,7 @@ import {
useInsertFeatureFlagMutation, useInsertFeatureFlagMutation,
useUpdateApplicationMutation, useUpdateApplicationMutation,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import { useUserEmail } from '@nhost/react'; import { useUserEmail } from '@nhost/nextjs';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
/** /**

View File

@@ -11,7 +11,7 @@ import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce'; import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache'; import { updateOwnCache } from '@/utils/updateOwnCache';
import { useUserData } from '@nhost/react'; import { useUserData } from '@nhost/nextjs';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import { RemoveApplicationModal } from './RemoveApplicationModal'; import { RemoveApplicationModal } from './RemoveApplicationModal';

View File

@@ -5,7 +5,7 @@ import { Modal } from '@/ui/Modal';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown'; import { Dropdown } from '@/ui/v2/Dropdown';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import { useUserData } from '@nhost/react'; import { useUserData } from '@nhost/nextjs';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import ApplicationInfo from './ApplicationInfo'; import ApplicationInfo from './ApplicationInfo';

View File

@@ -32,9 +32,12 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
useState<ConnectGithubModalState>('CONNECTING'); useState<ConnectGithubModalState>('CONNECTING');
const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null); const [selectedRepoId, setSelectedRepoId] = useState<string | null>(null);
const { data, loading, error } = useGetGithubRepositoriesQuery({ const { data, loading, error, startPolling } =
pollInterval: 2000, useGetGithubRepositoriesQuery();
});
useEffect(() => {
startPolling(2000);
}, [startPolling]);
const handleSelectAnotherRepository = () => { const handleSelectAnotherRepository = () => {
setSelectedRepoId(null); setSelectedRepoId(null);

View File

@@ -1,12 +1,16 @@
import { ConnectionDetail } from '@/components/applications/ConnectionDetail'; import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
import { LoadingScreen } from '@/components/common/LoadingScreen'; import { LoadingScreen } from '@/components/common/LoadingScreen';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon'; import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
import Link from '@/ui/v2/Link'; import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import generateAppServiceUrl, {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
} from '@/utils/common/generateAppServiceUrl';
import { LOCAL_HASURA_URL } from '@/utils/env'; import { LOCAL_HASURA_URL } from '@/utils/env';
import { generateAppServiceUrl } from '@/utils/helpers';
import Image from 'next/image'; import Image from 'next/image';
interface HasuraDataProps { interface HasuraDataProps {
@@ -15,6 +19,7 @@ interface HasuraDataProps {
export function HasuraData({ close }: HasuraDataProps) { export function HasuraData({ close }: HasuraDataProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const isPlatform = useIsPlatform();
if ( if (
!currentApplication?.subdomain || !currentApplication?.subdomain ||
@@ -24,13 +29,15 @@ export function HasuraData({ close }: HasuraDataProps) {
} }
const hasuraUrl = const hasuraUrl =
process.env.NEXT_PUBLIC_ENV === 'dev' process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
? LOCAL_HASURA_URL ? `${LOCAL_HASURA_URL}/console`
: `${generateAppServiceUrl( : generateAppServiceUrl(
currentApplication?.subdomain, currentApplication?.subdomain,
currentApplication?.region.awsName, currentApplication?.region.awsName,
'hasura', 'hasura',
)}/console`; defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
);
return ( return (
<div className="mx-auto w-full max-w-md px-6 py-4 text-left"> <div className="mx-auto w-full max-w-md px-6 py-4 text-left">

View File

@@ -13,14 +13,17 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null); const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
const { data } = useGetFunctionLogQuery({ const { data, startPolling } = useGetFunctionLogQuery({
variables: { variables: {
subdomain: currentApplication.subdomain, subdomain: currentApplication.subdomain,
functionPaths: [functionName?.split('/').slice(1, 3).join('/')], functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
}, },
pollInterval: 3000,
}); });
useEffect(() => {
startPolling(3000);
}, [startPolling]);
useEffect(() => { useEffect(() => {
if (!data || data.getFunctionLogs.length === 0) { if (!data || data.getFunctionLogs.length === 0) {
return; return;

View File

@@ -2,7 +2,7 @@ import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLCl
import ActivityIndicator from '@/ui/v2/ActivityIndicator'; import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Option from '@/ui/v2/Option'; import Option from '@/ui/v2/Option';
import Select from '@/ui/v2/Select'; 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 { useRemoteAppGetUsersCustomQuery } from '@/utils/__generated__/graphql';
import { DEFAULT_ROLES } from './utils'; import { DEFAULT_ROLES } from './utils';
@@ -57,7 +57,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
return; return;
} }
const user: RemoteAppGetUsersQuery['users'][number] = users.find( const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
({ id }) => id === userId, ({ id }) => id === userId,
); );

View File

@@ -37,20 +37,27 @@ function ControlledAutocomplete(
}: ControlledAutocompleteProps<AutocompleteOption>, }: ControlledAutocompleteProps<AutocompleteOption>,
ref: ForwardedRef<HTMLInputElement>, ref: ForwardedRef<HTMLInputElement>,
) { ) {
const { setValue } = useFormContext(); const form = useFormContext();
const { field } = useController({ const { field } = useController({
...controllerProps, ...(controllerProps || {}),
name: controllerProps?.name || name || '', name: controllerProps?.name || name || '',
control: controllerProps?.control || control, control: controllerProps?.control || control,
}); });
if (!form) {
throw new Error('ControlledAutocomplete must be used in a FormContext.');
}
const { setValue } = form || {};
return ( return (
<Autocomplete <Autocomplete
{...props} {...props}
{...field} {...field}
inputValue={typeof field.value === 'string' ? field.value : undefined}
ref={mergeRefs([field.ref, ref])} ref={mergeRefs([field.ref, ref])}
onChange={(event, options, reason, details) => { onChange={(event, options, reason, details) => {
setValue(controllerProps?.name || name, options); setValue?.(controllerProps?.name || name, options);
if (props.onChange) { if (props.onChange) {
props.onChange(event, options, reason, details); props.onChange(event, options, reason, details);

View File

@@ -2,11 +2,11 @@ import DataGridBody from '@/components/common/DataGridBody';
import DataGridFrame from '@/components/common/DataGridFrame'; import DataGridFrame from '@/components/common/DataGridFrame';
import type { DataGridHeaderProps } from '@/components/common/DataGridHeader'; import type { DataGridHeaderProps } from '@/components/common/DataGridHeader';
import DataGridHeader 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 { DataGridProvider } from '@/context/DataGridContext';
import type { UseDataGridOptions } from '@/hooks/useDataGrid'; import type { UseDataGridOptions } from '@/hooks/useDataGrid';
import useDataGrid 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 ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { ForwardedRef } from 'react'; import type { ForwardedRef } from 'react';
import { forwardRef, useEffect, useRef } from 'react'; import { forwardRef, useEffect, useRef } from 'react';

View File

@@ -1,7 +1,7 @@
import type { DataGridProps } from '@/components/common/DataGrid'; import type { DataGridProps } from '@/components/common/DataGrid';
import DataGridCell from '@/components/common/DataGridCell'; import DataGridCell from '@/components/common/DataGridCell';
import useDataGridConfig from '@/hooks/useDataGridConfig'; 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 Button from '@/ui/v2/Button';
import PlusIcon from '@/ui/v2/icons/PlusIcon'; import PlusIcon from '@/ui/v2/icons/PlusIcon';
import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react'; import type { DetailedHTMLProps, HTMLProps, KeyboardEvent } from 'react';
@@ -35,15 +35,15 @@ function InsertPlaceholderTableRow({
}: InsertPlaceholderTableRowProps) { }: InsertPlaceholderTableRowProps) {
return ( return (
<div <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} {...props}
> >
<Button <Button
onClick={onInsertRow} onClick={onInsertRow}
variant="borderless" variant="borderless"
color="secondary" 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" 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="h-4 w-4 text-greyscaleGrey" />} startIcon={<PlusIcon className="w-4 h-4 text-greyscaleGrey" />}
> >
Insert New Row Insert New Row
</Button> </Button>
@@ -181,7 +181,7 @@ export default function DataGridBody<T extends object>({
return ( return (
<div {...getTableBodyProps()} ref={bodyRef} {...props}> <div {...getTableBodyProps()} ref={bodyRef} {...props}>
{rows.length === 0 && !loading && ( {rows.length === 0 && !loading && (
<div className="flex flex-nowrap pr-5"> <div className="flex pr-5 flex-nowrap">
{onInsertRow ? ( {onInsertRow ? (
<InsertPlaceholderTableRow <InsertPlaceholderTableRow
style={{ style={{
@@ -279,7 +279,7 @@ export default function DataGridBody<T extends object>({
})} })}
{allowInsertColumn && ( {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> </div>

View File

@@ -3,7 +3,7 @@ import type {
ColumnType, ColumnType,
DataBrowserGridCell, DataBrowserGridCell,
DataBrowserGridCellProps, DataBrowserGridCellProps,
} from '@/types/data-browser'; } from '@/types/dataBrowser';
import Tooltip, { useTooltip } from '@/ui/v2/Tooltip'; import Tooltip, { useTooltip } from '@/ui/v2/Tooltip';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
import type { import type {

View File

@@ -1,6 +1,6 @@
import type { DataGridProps } from '@/components/common/DataGrid'; import type { DataGridProps } from '@/components/common/DataGrid';
import useDataGridConfig from '@/hooks/useDataGridConfig'; 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 Button from '@/ui/v2/Button';
import Divider from '@/ui/v2/Divider'; import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown'; import { Dropdown } from '@/ui/v2/Dropdown';

View File

@@ -15,10 +15,14 @@ export type DialogType =
| 'EDIT_FOREIGN_KEY' | 'EDIT_FOREIGN_KEY'
| 'CREATE_ROLE' | 'CREATE_ROLE'
| 'EDIT_ROLE' | 'EDIT_ROLE'
| 'CREATE_USER'
| 'CREATE_PERMISSION_VARIABLE' | 'CREATE_PERMISSION_VARIABLE'
| 'EDIT_PERMISSION_VARIABLE' | 'EDIT_PERMISSION_VARIABLE'
| 'CREATE_ENVIRONMENT_VARIABLE' | 'CREATE_ENVIRONMENT_VARIABLE'
| 'EDIT_ENVIRONMENT_VARIABLE'; | 'EDIT_ENVIRONMENT_VARIABLE'
| 'EDIT_USER'
| 'EDIT_USER_PASSWORD'
| 'EDIT_JWT_SECRET';
export interface DialogConfig<TPayload = unknown> { export interface DialogConfig<TPayload = unknown> {
/** /**

View File

@@ -1,24 +1,36 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary'; import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import CreateForeignKeyForm from '@/components/data-browser/CreateForeignKeyForm'; import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
import EditForeignKeyForm from '@/components/data-browser/EditForeignKeyForm'; import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm'; import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm'; import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm'; import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm'; import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm'; import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
import EditRoleForm from '@/components/settings/roles/EditRoleForm'; 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 ActivityIndicator from '@/ui/v2/ActivityIndicator';
import AlertDialog from '@/ui/v2/AlertDialog'; import AlertDialog from '@/ui/v2/AlertDialog';
import { BaseDialog } from '@/ui/v2/Dialog'; import { BaseDialog } from '@/ui/v2/Dialog';
import Drawer from '@/ui/v2/Drawer'; import Drawer from '@/ui/v2/Drawer';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
import type { import type {
BaseSyntheticEvent, BaseSyntheticEvent,
DetailedHTMLProps, DetailedHTMLProps,
HTMLProps, HTMLProps,
PropsWithChildren, PropsWithChildren,
} from 'react'; } 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 { twMerge } from 'tailwind-merge';
import type { DialogConfig, DialogType } from './DialogContext'; import type { DialogConfig, DialogType } from './DialogContext';
import DialogContext from './DialogContext'; import DialogContext from './DialogContext';
@@ -49,31 +61,33 @@ function LoadingComponent({
} }
const CreateRecordForm = dynamic( const CreateRecordForm = dynamic(
() => import('@/components/data-browser/CreateRecordForm'), () => import('@/components/dataBrowser/CreateRecordForm'),
{ ssr: false, loading: () => LoadingComponent() }, { ssr: false, loading: () => LoadingComponent() },
); );
const CreateColumnForm = dynamic( const CreateColumnForm = dynamic(
() => import('@/components/data-browser/CreateColumnForm'), () => import('@/components/dataBrowser/CreateColumnForm'),
{ ssr: false, loading: () => LoadingComponent() }, { ssr: false, loading: () => LoadingComponent() },
); );
const EditColumnForm = dynamic( const EditColumnForm = dynamic(
() => import('@/components/data-browser/EditColumnForm'), () => import('@/components/dataBrowser/EditColumnForm'),
{ ssr: false, loading: () => LoadingComponent() }, { ssr: false, loading: () => LoadingComponent() },
); );
const CreateTableForm = dynamic( const CreateTableForm = dynamic(
() => import('@/components/data-browser/CreateTableForm'), () => import('@/components/dataBrowser/CreateTableForm'),
{ ssr: false, loading: () => LoadingComponent() }, { ssr: false, loading: () => LoadingComponent() },
); );
const EditTableForm = dynamic( const EditTableForm = dynamic(
() => import('@/components/data-browser/EditTableForm'), () => import('@/components/dataBrowser/EditTableForm'),
{ ssr: false, loading: () => LoadingComponent() }, { ssr: false, loading: () => LoadingComponent() },
); );
function DialogProvider({ children }: PropsWithChildren<unknown>) { function DialogProvider({ children }: PropsWithChildren<unknown>) {
const router = useRouter();
const [ const [
{ {
open: dialogOpen, open: dialogOpen,
@@ -161,26 +175,29 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
alertDialogDispatch({ type: 'CLEAR_ALERT_CONTENT' }); alertDialogDispatch({ type: 'CLEAR_ALERT_CONTENT' });
} }
function openDirtyConfirmation(config?: Partial<DialogConfig<string>>) { const openDirtyConfirmation = useCallback(
const { props, ...restConfig } = config || {}; (config?: Partial<DialogConfig<string>>) => {
const { props, ...restConfig } = config || {};
openAlertDialog({ setShowDirtyConfirmation(true);
...config, openAlertDialog({
title: 'Unsaved changes', ...config,
payload: title: 'Unsaved changes',
'You have unsaved local changes. Are you sure you want to discard them?', payload:
props: { 'You have unsaved local changes. Are you sure you want to discard them?',
...props, props: {
primaryButtonText: 'Discard', ...props,
primaryButtonColor: 'error', primaryButtonText: 'Discard',
}, primaryButtonColor: 'error',
...restConfig, },
}); ...restConfig,
} });
},
[],
);
function closeDrawerWithDirtyGuard(event?: BaseSyntheticEvent) { function closeDrawerWithDirtyGuard(event?: BaseSyntheticEvent) {
if (isDrawerDirty.current && event?.type !== 'submit') { if (isDrawerDirty.current && event?.type !== 'submit') {
setShowDirtyConfirmation(true);
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } }); openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
return; return;
} }
@@ -190,7 +207,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
function closeDialogWithDirtyGuard(event?: BaseSyntheticEvent) { function closeDialogWithDirtyGuard(event?: BaseSyntheticEvent) {
if (isDialogDirty.current && event?.type !== 'submit') { if (isDialogDirty.current && event?.type !== 'submit') {
setShowDirtyConfirmation(true);
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } }); openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
return; return;
} }
@@ -248,6 +264,32 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
onCancel: closeDrawerWithDirtyGuard, 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 ( return (
<DialogContext.Provider value={contextValue}> <DialogContext.Provider value={contextValue}>
<AlertDialog <AlertDialog
@@ -315,6 +357,10 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
<EditRoleForm {...sharedDialogProps} /> <EditRoleForm {...sharedDialogProps} />
)} )}
{activeDialogType === 'CREATE_USER' && (
<CreateUserForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && ( {activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
<CreatePermissionVariableForm {...sharedDialogProps} /> <CreatePermissionVariableForm {...sharedDialogProps} />
)} )}
@@ -330,6 +376,17 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && ( {activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
<EditEnvironmentVariableForm {...sharedDialogProps} /> <EditEnvironmentVariableForm {...sharedDialogProps} />
)} )}
{activeDialogType === 'EDIT_USER_PASSWORD' && (
<EditUserPasswordForm
{...sharedDialogProps}
user={sharedDialogProps?.user}
/>
)}
{activeDialogType === 'EDIT_JWT_SECRET' && (
<EditJwtSecretForm {...sharedDialogProps} />
)}
</RetryableErrorBoundary> </RetryableErrorBoundary>
</BaseDialog> </BaseDialog>
@@ -375,6 +432,10 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
schema={drawerPayload?.schema} schema={drawerPayload?.schema}
/> />
)} )}
{activeDrawerType === 'EDIT_USER' && (
<EditUserForm {...sharedDrawerProps} {...drawerPayload} />
)}
</RetryableErrorBoundary> </RetryableErrorBoundary>
</Drawer> </Drawer>

View 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>
);
}

View File

@@ -0,0 +1,3 @@
export * from './Pagination';
export { default } from './Pagination';

View File

@@ -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',
},
},
};

View File

@@ -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 { forwardRef } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
const ReadOnlyToggle = forwardRef( export interface ReadOnlyToggleProps
( extends DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement> {
{ checked }: { checked: boolean | null }, /**
ref: ForwardedRef<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 <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} ref={ref}
> >
<span <span
className={twMerge( className={twMerge(
'box-border inline-grid h-3 w-5 items-center rounded-full px-0.5', '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 === false && 'border-1 border-greyscaleDark',
checked === null && 'border-1 border-greyscaleDark', checked === null && 'border-1 border-greyscaleDark',
)} )}
> >
<span <span
className={twMerge( className={twMerge(
'inline rounded-full', 'inline-block rounded-full',
checked === true && 'h-2 w-2 bg-white', checked === true && 'h-2 w-2 bg-white',
checked === false && 'h-2 w-2 bg-greyscaleDark', 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>
<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> </span>
), );
); }
ReadOnlyToggle.displayName = 'NhostReadOnlyToggle'; ReadOnlyToggle.displayName = 'NhostReadOnlyToggle';
export default ReadOnlyToggle; export default forwardRef(ReadOnlyToggle);

View File

@@ -3,7 +3,7 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import type { ColumnType, DatabaseColumn } from '@/types/data-browser'; import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import { OptionBase } from '@/ui/v2/Option'; import { OptionBase } from '@/ui/v2/Option';

View File

@@ -1,6 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import type { BaseForeignKeyFormValues } from '@/components/data-browser/BaseForeignKeyForm'; import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
import type { DatabaseColumn } from '@/types/data-browser'; import type { DatabaseColumn } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon'; import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
import LinkIcon from '@/ui/v2/icons/LinkIcon'; import LinkIcon from '@/ui/v2/icons/LinkIcon';

View File

@@ -2,7 +2,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery'; import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/data-browser'; import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Divider from '@/ui/v2/Divider'; import Divider from '@/ui/v2/Divider';
import Option from '@/ui/v2/Option'; import Option from '@/ui/v2/Option';

View File

@@ -1,6 +1,6 @@
import type { ControlledSelectProps } from '@/components/common/ControlledSelect'; import type { ControlledSelectProps } from '@/components/common/ControlledSelect';
import ControlledSelect 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 Option from '@/ui/v2/Option';
import type { ForwardedRef, PropsWithoutRef } from 'react'; import type { ForwardedRef, PropsWithoutRef } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';

View File

@@ -1,5 +1,5 @@
import ControlledSelect 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 Option from '@/ui/v2/Option';
import { useFormContext, useFormState, useWatch } from 'react-hook-form'; import { useFormContext, useFormState, useWatch } from 'react-hook-form';
import type { BaseForeignKeyFormValues } from './BaseForeignKeyForm'; import type { BaseForeignKeyFormValues } from './BaseForeignKeyForm';

View File

@@ -1,10 +1,10 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import DatabaseRecordInputGroup from '@/components/data-browser/DatabaseRecordInputGroup'; import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
import type { import type {
ColumnInsertOptions, ColumnInsertOptions,
DataBrowserGridColumn, DataBrowserGridColumn,
} from '@/types/data-browser'; } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';

View File

@@ -1,7 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import { baseColumnValidationSchema } from '@/components/data-browser/BaseColumnForm'; import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
import type { DatabaseTable, ForeignKeyRelation } from '@/types/data-browser'; import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import { useEffect } from 'react'; import { useEffect } from 'react';

View File

@@ -1,7 +1,7 @@
import ControlledAutocomplete from '@/components/common/ControlledAutocomplete'; import ControlledAutocomplete from '@/components/common/ControlledAutocomplete';
import ControlledCheckbox from '@/components/common/ControlledCheckbox'; import ControlledCheckbox from '@/components/common/ControlledCheckbox';
import InlineCode from '@/components/common/InlineCode'; 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 { ButtonProps } from '@/ui/v2/Button';
import type { CheckboxProps } from '@/ui/v2/Checkbox'; import type { CheckboxProps } from '@/ui/v2/Checkbox';
import IconButton from '@/ui/v2/IconButton'; import IconButton from '@/ui/v2/IconButton';

View File

@@ -1,4 +1,4 @@
import type { ForeignKeyRelation } from '@/types/data-browser'; import type { ForeignKeyRelation } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon'; import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
import LinkIcon from '@/ui/v2/icons/LinkIcon'; import LinkIcon from '@/ui/v2/icons/LinkIcon';

View File

@@ -1,6 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import type { BaseForeignKeyFormValues } from '@/components/data-browser/BaseForeignKeyForm'; import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/data-browser'; import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import PlusIcon from '@/ui/v2/icons/PlusIcon'; import PlusIcon from '@/ui/v2/icons/PlusIcon';
import InputLabel from '@/ui/v2/InputLabel'; import InputLabel from '@/ui/v2/InputLabel';

View File

@@ -1,5 +1,5 @@
import ControlledSelect from '@/components/common/ControlledSelect'; 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 Option from '@/ui/v2/Option';
import { identityTypes } from '@/utils/dataBrowser/postgresqlConstants'; import { identityTypes } from '@/utils/dataBrowser/postgresqlConstants';
import { useMemo } from 'react'; import { useMemo } from 'react';

View File

@@ -1,5 +1,5 @@
import ControlledSelect from '@/components/common/ControlledSelect'; 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 Option from '@/ui/v2/Option';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useFormState, useWatch } from 'react-hook-form'; import { useFormState, useWatch } from 'react-hook-form';

View File

@@ -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;

View File

@@ -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.

View File

@@ -0,0 +1,382 @@
import InlineCode from '@/components/common/InlineCode';
import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
import { AutocompletePopper } from '@/ui/v2/Autocomplete';
import IconButton from '@/ui/v2/IconButton';
import ArrowLeftIcon from '@/ui/v2/icons/ArrowLeftIcon';
import type { InputProps } from '@/ui/v2/Input';
import Input from '@/ui/v2/Input';
import List from '@/ui/v2/List';
import { OptionBase } from '@/ui/v2/Option';
import { OptionGroupBase } from '@/ui/v2/OptionGroup';
import Text from '@/ui/v2/Text';
import getTruncatedText from '@/utils/common/getTruncatedText';
import type { AutocompleteGroupedOption } from '@mui/base/AutocompleteUnstyled';
import { useAutocomplete } from '@mui/base/AutocompleteUnstyled';
import type { AutocompleteRenderGroupParams } from '@mui/material/Autocomplete';
import { autocompleteClasses } from '@mui/material/Autocomplete';
import type {
ForwardedRef,
HTMLAttributes,
PropsWithoutRef,
SyntheticEvent,
} from 'react';
import { forwardRef, useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import type { UseAsyncInitialValueOptions } from './useAsyncInitialValue';
import useAsyncInitialValue from './useAsyncInitialValue';
import type { UseColumnGroupsOptions } from './useColumnGroups';
import useColumnGroups from './useColumnGroups';
export interface ColumnAutocompleteProps
extends Omit<PropsWithoutRef<InputProps>, 'onChange'> {
/**
* Schema where the `table` is located.
*/
schema: string;
/**
* Table to get the columns from.
*/
table: string;
/**
* Function to be called when the value changes.
*/
onChange?: (
event: SyntheticEvent,
value: {
value: string;
columnMetadata?: Record<string, any>;
disableReset?: boolean;
},
) => void;
/**
* Function to be called when the input is asynchronously initialized.
*/
onInitialized?: UseAsyncInitialValueOptions['onInitialized'];
/**
* Class name to be applied to the root element.
*/
rootClassName?: string;
/**
* Determines if the autocomplete should allow relationships.
*/
disableRelationships?: UseColumnGroupsOptions['disableRelationships'];
}
function renderGroup(params: AutocompleteRenderGroupParams) {
return (
<li key={params.key}>
<OptionGroupBase>{params.group}</OptionGroupBase>
<List>{params.children}</List>
</li>
);
}
function renderOption(
option: AutocompleteOption<string>,
optionProps: HTMLAttributes<HTMLLIElement>,
) {
return (
<OptionBase
{...optionProps}
className="grid grid-flow-col items-baseline justify-start justify-items-start gap-1.5"
>
<span>{option.label}</span>
{option.group === 'columns' && (
<InlineCode>{option.metadata?.type || option.value}</InlineCode>
)}
</OptionBase>
);
}
function ColumnAutocomplete(
{
rootClassName,
schema: defaultSchema,
table: defaultTable,
value: externalValue,
disableRelationships,
onChange,
onInitialized,
...props
}: ColumnAutocompleteProps,
ref: ForwardedRef<HTMLInputElement>,
) {
const [open, setOpen] = useState(false);
const [activeRelationship, setActiveRelationship] = useState<any>();
const selectedSchema = activeRelationship?.schema || defaultSchema;
const selectedTable = activeRelationship?.table || defaultTable;
const {
data: tableData,
status: tableStatus,
error: tableError,
isFetching: isTableFetching,
} = useTableQuery([`default.${selectedSchema}.${selectedTable}`], {
schema: selectedSchema,
table: selectedTable,
queryOptions: { refetchOnWindowFocus: false },
});
const {
data: metadata,
status: metadataStatus,
error: metadataError,
isFetching: isMetadataFetching,
} = useMetadataQuery([`default.metadata`], {
queryOptions: { refetchOnWindowFocus: false },
});
const {
initialized,
inputValue,
setInputValue,
selectedColumn,
setSelectedColumn,
selectedRelationships,
setSelectedRelationships,
relationshipDotNotation,
activeRelationship: asyncActiveRelationship,
} = useAsyncInitialValue({
selectedSchema,
selectedTable,
initialValue: externalValue as string,
isTableLoading: tableStatus === 'loading' || isTableFetching,
isMetadataLoading: metadataStatus === 'loading' || isMetadataFetching,
tableData,
metadata,
onInitialized,
});
useEffect(() => {
setActiveRelationship(asyncActiveRelationship);
}, [asyncActiveRelationship]);
function isOptionEqualToValue(
option: AutocompleteOption,
value: NonNullable<string | AutocompleteOption>,
) {
if (!value) {
return false;
}
if (typeof value === 'string') {
return option.value === value;
}
return option.value === value.value && option.custom === value.custom;
}
function handleChange(
event: SyntheticEvent,
value: NonNullable<string | AutocompleteOption>,
) {
if (typeof value === 'string' || Array.isArray(value) || !value) {
return;
}
if ('group' in value && value.group === 'columns') {
setSelectedColumn(value);
setOpen(false);
setInputValue(value.value);
onChange?.(event, {
value:
selectedRelationships.length > 0
? [relationshipDotNotation, value.value].join('.')
: value.value,
columnMetadata: value.metadata,
});
return;
}
setInputValue('');
setSelectedColumn(null);
setSelectedRelationships((currentRelationships) => [
...currentRelationships,
value.metadata?.target,
]);
}
const options = useColumnGroups({
selectedSchema,
selectedTable,
tableData,
metadata,
disableRelationships,
});
const {
popupOpen,
anchorEl,
setAnchorEl,
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions,
} = useAutocomplete({
open,
inputValue,
options,
id: props?.name,
openOnFocus: true,
disableCloseOnSelect: true,
value: selectedColumn,
onClose: () => setOpen(false),
groupBy: (option) => option.group,
isOptionEqualToValue,
onChange: handleChange,
});
return (
<>
<div {...getRootProps()} className={rootClassName}>
<Input
{...props}
ref={ref}
fullWidth
slotProps={{
...(props.slotProps || {}),
label: getInputLabelProps(),
input: { ...(props.slotProps?.input || {}), ref: setAnchorEl },
inputRoot: {
...getInputProps(),
className: twMerge(
Boolean(selectedColumn) || Boolean(relationshipDotNotation)
? '!pl-0'
: null,
props.slotProps?.inputRoot?.className,
),
},
}}
onFocus={() => setOpen(true)}
onClick={() => setOpen(true)}
error={Boolean(tableError || metadataError)}
helperText={String(tableError || metadataError || '')}
onChange={(event) => setInputValue(event.target.value)}
value={inputValue}
startAdornment={
selectedColumn || relationshipDotNotation ? (
<Text className="!ml-2 lg:max-w-[200px] flex-shrink-0 truncate">
<span className="text-greyscaleGrey">{defaultTable}</span>.
{relationshipDotNotation && (
<>
<span className="hidden lg:inline">
{getTruncatedText(relationshipDotNotation, 15, 'start')}.
</span>
<span className="inline lg:hidden">
{relationshipDotNotation}.
</span>
</>
)}
</Text>
) : null
}
endAdornment={
tableStatus === 'loading' ||
metadataStatus === 'loading' ||
!initialized ? (
<ActivityIndicator className="mr-2" delay={500} />
) : null
}
/>
</div>
<AutocompletePopper
onMouseDown={(event) => event.preventDefault()}
modifiers={[{ name: 'offset', options: { offset: [0, 10] } }]}
placement="bottom-start"
open={popupOpen}
anchorEl={anchorEl}
style={{ width: anchorEl?.parentElement?.clientWidth }}
>
<div className={autocompleteClasses.paper}>
<div className="px-3 py-2.5 border-b-1 border-greyscale-100 grid grid-flow-col gap-2 justify-start items-center">
{selectedRelationships.length > 0 && (
<IconButton
variant="borderless"
color="secondary"
onClick={(event) => {
event.stopPropagation();
setInputValue('');
setSelectedColumn(null);
setSelectedRelationships((activeRelationships) =>
activeRelationships.slice(0, -1),
);
}}
>
<ArrowLeftIcon className="w-4 h-4" />
</IconButton>
)}
<Text className="truncate direction-rtl text-left">
<span className="!text-greyscaleMedium">{defaultTable}</span>
{relationshipDotNotation && (
<>
<span className="hidden lg:inline">
.{getTruncatedText(relationshipDotNotation, 20, 'start')}
</span>
<span className="inline lg:hidden">
.{relationshipDotNotation}
</span>
</>
)}
</Text>
</div>
{(tableStatus === 'loading' ||
metadataStatus === 'loading' ||
!initialized) && (
<div className="p-2">
<ActivityIndicator label="Loading..." />
</div>
)}
{groupedOptions.length > 0 && (
<List
{...getListboxProps()}
className={autocompleteClasses.listbox}
>
{(
groupedOptions as AutocompleteGroupedOption<
typeof options[number]
>[]
).map((optionGroup) =>
renderGroup({
key: `${optionGroup.key}`,
group: optionGroup.group,
children: optionGroup.options.map((option, index) =>
renderOption(
option,
getOptionProps({
option,
index: optionGroup.index + index,
}),
),
),
}),
)}
</List>
)}
{groupedOptions.length === 0 && Boolean(anchorEl) && (
<Text className={autocompleteClasses.noOptions}>No options</Text>
)}
</div>
</AutocompletePopper>
</>
);
}
export default forwardRef(ColumnAutocomplete);

View File

@@ -0,0 +1,2 @@
export * from './ColumnAutocomplete';
export { default } from './ColumnAutocomplete';

View File

@@ -0,0 +1,263 @@
import type { FetchMetadataReturnType } from '@/hooks/dataBrowser/useMetadataQuery';
import type { FetchTableReturnType } from '@/hooks/dataBrowser/useTableQuery';
import type { HasuraMetadataTable } from '@/types/dataBrowser';
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
import { useEffect, useState } from 'react';
export interface UseAsyncInitialValueOptions {
/**
* Selected schema to be used to determine the initial value.
*/
selectedSchema?: string;
/**
* Selected table to be used to determine the initial value.
*/
selectedTable?: string;
/**
* Initial value to be used before the async value is loaded.
*/
initialValue?: string;
/**
* Determines whether or not the table data is loading.
*/
isTableLoading?: boolean;
/**
* Determines whether or not the metadata is loading.
*/
isMetadataLoading?: boolean;
/**
* Table data to be used to determine the initial value.
*/
tableData?: FetchTableReturnType;
/**
* Metadata to be used to determine the initial value.
*/
metadata?: FetchMetadataReturnType;
/**
* Function to be called when the input is asynchronously initialized.
*/
onInitialized?: (value: {
value: string;
columnMetadata?: Record<string, any>;
}) => void;
}
export default function useAsyncInitialValue({
selectedSchema,
selectedTable,
initialValue,
isTableLoading,
isMetadataLoading,
tableData,
metadata,
onInitialized,
}: UseAsyncInitialValueOptions) {
const currentTablePath = `${selectedSchema}.${selectedTable}`;
const [inputValue, setInputValue] = useState('');
const [initialized, setInitialized] = useState(false);
// We might not going to have the most up-to-date table data because the
// relationship is loaded asynchronously, so we need to make sure we don't
// look for the column in a stale table when initializing
const [asyncTablePath, setAsyncTablePath] = useState(currentTablePath);
const [remainingColumnPath, setRemainingColumnPath] = useState(
initialValue?.split('.') || [],
);
const [selectedRelationships, setSelectedRelationships] = useState<
{ schema: string; table: string; name: string }[]
>([]);
const relationshipDotNotation =
initialized && selectedRelationships?.length > 0
? selectedRelationships.map((relationship) => relationship.name).join('.')
: '';
const [selectedColumn, setSelectedColumn] =
useState<AutocompleteOption>(null);
const activeRelationship =
selectedRelationships[selectedRelationships.length - 1];
useEffect(() => {
if (remainingColumnPath?.length > 0 || initialized) {
return;
}
setInitialized(true);
if (!selectedColumn) {
return;
}
onInitialized?.({
value:
selectedRelationships.length > 0
? [relationshipDotNotation, selectedColumn.value].join('.')
: selectedColumn.value,
columnMetadata: selectedColumn.metadata,
});
}, [
initialized,
onInitialized,
relationshipDotNotation,
remainingColumnPath?.length,
selectedColumn,
selectedRelationships.length,
]);
useEffect(() => {
if (
remainingColumnPath?.length !== 1 ||
isTableLoading ||
!tableData?.columns ||
asyncTablePath !== currentTablePath
) {
return;
}
const [activeColumn] = remainingColumnPath;
// If there is a single column in the path, it means that we can look for it
// in the table columns
if (
!tableData?.columns.some((column) => column.column_name === activeColumn)
) {
setRemainingColumnPath([]);
return;
}
setSelectedColumn({
value: activeColumn,
label: activeColumn,
group: 'columns',
metadata: tableData.columns.find(
(column) => column.column_name === activeColumn,
),
});
setRemainingColumnPath((columnPath) => columnPath.slice(1));
setInputValue(activeColumn);
}, [
remainingColumnPath,
isTableLoading,
tableData?.columns,
asyncTablePath,
currentTablePath,
]);
useEffect(() => {
if (
remainingColumnPath.length < 2 ||
isTableLoading ||
isMetadataLoading ||
!tableData?.columns
) {
return;
}
const metadataMap = metadata.tables.reduce(
(map, metadataTable) =>
map.set(
`${metadataTable.table.schema}.${metadataTable.table.name}`,
metadataTable,
),
new Map<string, HasuraMetadataTable>(),
);
const [nextPath] = remainingColumnPath.slice(
0,
remainingColumnPath.length - 1,
);
const tableMetadata = metadataMap.get(`${selectedSchema}.${selectedTable}`);
const currentRelationship = [
...(tableMetadata.object_relationships || []),
...(tableMetadata.array_relationships || []),
].find(({ name }) => name === nextPath);
if (!currentRelationship) {
setRemainingColumnPath([]);
return;
}
const { foreign_key_constraint_on: metadataConstraint } =
currentRelationship.using || {};
// In some cases the metadata already contains the schema and table name
if (typeof metadataConstraint !== 'string') {
setAsyncTablePath(
`${metadataConstraint.table.schema || 'public'}.${
metadataConstraint.table.name
}`,
);
setSelectedRelationships((currentRelationships) => [
...currentRelationships,
{
schema: metadataConstraint.table.schema || 'public',
table: metadataConstraint.table.name,
name: nextPath,
},
]);
setRemainingColumnPath((columnPath) => columnPath.slice(1));
return;
}
const foreignKeyRelation = tableData?.foreignKeyRelations?.find(
({ columnName }) => {
const { foreign_key_constraint_on } = currentRelationship.using || {};
if (!foreign_key_constraint_on) {
return false;
}
if (typeof foreign_key_constraint_on === 'string') {
return foreign_key_constraint_on === columnName;
}
return foreign_key_constraint_on.column === columnName;
},
);
if (!foreignKeyRelation) {
setRemainingColumnPath([]);
return;
}
setAsyncTablePath(
`${foreignKeyRelation.referencedSchema || 'public'}.${
foreignKeyRelation.referencedTable
}`,
);
setSelectedRelationships((currentRelationships) => [
...currentRelationships,
{
schema: foreignKeyRelation.referencedSchema || 'public',
table: foreignKeyRelation.referencedTable,
name: nextPath,
},
]);
setRemainingColumnPath((columnPath) => columnPath.slice(1));
}, [
selectedSchema,
selectedTable,
metadata?.tables,
tableData?.columns,
tableData?.foreignKeyRelations,
remainingColumnPath,
isTableLoading,
isMetadataLoading,
]);
return {
initialized,
inputValue,
setInputValue,
activeRelationship,
selectedRelationships: initialized ? selectedRelationships : [],
selectedColumn: initialized ? selectedColumn : null,
setSelectedRelationships,
setSelectedColumn,
relationshipDotNotation,
};
}

View File

@@ -0,0 +1,102 @@
import type { FetchMetadataReturnType } from '@/hooks/dataBrowser/useMetadataQuery';
import type { FetchTableReturnType } from '@/hooks/dataBrowser/useTableQuery';
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
export interface UseColumnGroupsOptions {
/**
* Selected schema to be used to determines the column groups.
*/
selectedSchema?: string;
/**
* Selected table to be used to determine the column groups.
*/
selectedTable?: string;
/**
* Table data to be used to determine the column groups.
*/
tableData?: FetchTableReturnType;
/**
* Metadata to be used to determine the column groups.
*/
metadata?: FetchMetadataReturnType;
/**
* Determines whether or not to disable column groups.
*/
disableRelationships?: boolean;
}
export default function useColumnGroups({
selectedTable,
selectedSchema,
tableData,
metadata,
disableRelationships,
}: UseColumnGroupsOptions) {
const { columns, foreignKeyRelations } = tableData || {};
const columnTargetMap = foreignKeyRelations?.reduce(
(map, currentRelation) =>
map.set(currentRelation.columnName, {
schema: currentRelation.referencedSchema || 'public',
table: currentRelation.referencedTable,
}),
new Map<string, { schema: string; table: string }>(),
);
const columnOptions: AutocompleteOption[] =
columns?.map((column) => ({
label: column.column_name,
value: column.column_name,
group: 'columns',
metadata: column,
})) || [];
if (disableRelationships) {
return columnOptions;
}
const { object_relationships, array_relationships } =
metadata?.tables?.find(
({ table: metadataTable }) =>
metadataTable.name === selectedTable &&
metadataTable.schema === selectedSchema,
) || {};
const objectAndArrayRelationships = [
...(object_relationships || []),
...(array_relationships || []),
].map((relationship) => {
const { foreign_key_constraint_on } = relationship?.using || {};
if (typeof foreign_key_constraint_on === 'string') {
return {
schema: selectedSchema,
table: selectedTable,
column: foreign_key_constraint_on,
name: relationship.name,
};
}
return {
schema: foreign_key_constraint_on.table.schema,
table: foreign_key_constraint_on.table.name,
column: foreign_key_constraint_on.column,
name: relationship.name,
};
});
return [
...columnOptions,
...objectAndArrayRelationships.map((relationship) => ({
label: relationship.name,
value: relationship.name,
group: 'relationships',
metadata: {
target: {
...(columnTargetMap?.get(relationship.column) || {}),
name: relationship.name,
},
},
})),
];
}

View File

@@ -1,10 +1,10 @@
import type { import type {
BaseColumnFormProps, BaseColumnFormProps,
BaseColumnFormValues, BaseColumnFormValues,
} from '@/components/data-browser/BaseColumnForm'; } from '@/components/dataBrowser/BaseColumnForm';
import BaseColumnForm, { import BaseColumnForm, {
baseColumnValidationSchema, baseColumnValidationSchema,
} from '@/components/data-browser/BaseColumnForm'; } from '@/components/dataBrowser/BaseColumnForm';
import useCreateColumnMutation from '@/hooks/dataBrowser/useCreateColumnMutation'; import useCreateColumnMutation from '@/hooks/dataBrowser/useCreateColumnMutation';
import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation'; import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';

View File

@@ -1,11 +1,11 @@
import type { import type {
BaseForeignKeyFormProps, BaseForeignKeyFormProps,
BaseForeignKeyFormValues, BaseForeignKeyFormValues,
} from '@/components/data-browser/BaseForeignKeyForm'; } from '@/components/dataBrowser/BaseForeignKeyForm';
import { import {
BaseForeignKeyForm, BaseForeignKeyForm,
baseForeignKeyValidationSchema, baseForeignKeyValidationSchema,
} from '@/components/data-browser/BaseForeignKeyForm'; } from '@/components/dataBrowser/BaseForeignKeyForm';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';

View File

@@ -1,7 +1,7 @@
import type { BaseRecordFormProps } from '@/components/data-browser/BaseRecordForm'; import type { BaseRecordFormProps } from '@/components/dataBrowser/BaseRecordForm';
import BaseRecordForm from '@/components/data-browser/BaseRecordForm'; import BaseRecordForm from '@/components/dataBrowser/BaseRecordForm';
import useCreateRecordMutation from '@/hooks/dataBrowser/useCreateRecordMutation'; 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 { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { createDynamicValidationSchema } from '@/utils/dataBrowser/validationSchemaHelpers'; import { createDynamicValidationSchema } from '@/utils/dataBrowser/validationSchemaHelpers';

View File

@@ -1,14 +1,14 @@
import type { import type {
BaseTableFormProps, BaseTableFormProps,
BaseTableFormValues, BaseTableFormValues,
} from '@/components/data-browser/BaseTableForm'; } from '@/components/dataBrowser/BaseTableForm';
import BaseTableForm, { import BaseTableForm, {
baseTableValidationSchema, baseTableValidationSchema,
} from '@/components/data-browser/BaseTableForm'; } from '@/components/dataBrowser/BaseTableForm';
import useCreateTableMutation from '@/hooks/dataBrowser/useCreateTableMutation'; import useCreateTableMutation from '@/hooks/dataBrowser/useCreateTableMutation';
import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation'; import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
import useTrackTableMutation from '@/hooks/dataBrowser/useTrackTableMutation'; 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 { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';

View File

@@ -6,8 +6,8 @@ import DataGridNumericCell from '@/components/common/DataGridNumericCell';
import DataGridTextCell from '@/components/common/DataGridTextCell'; import DataGridTextCell from '@/components/common/DataGridTextCell';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import DataBrowserEmptyState from '@/components/data-browser/DataBrowserEmptyState'; import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
import DataBrowserGridControls from '@/components/data-browser/DataBrowserGridControls'; import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
import useDeleteColumnWithToastMutation from '@/hooks/dataBrowser/useDeleteColumnMutation/useDeleteColumnWithToastMutation'; import useDeleteColumnWithToastMutation from '@/hooks/dataBrowser/useDeleteColumnMutation/useDeleteColumnWithToastMutation';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery'; import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import type { UpdateRecordVariables } from '@/hooks/dataBrowser/useUpdateRecordMutation'; import type { UpdateRecordVariables } from '@/hooks/dataBrowser/useUpdateRecordMutation';
@@ -17,7 +17,7 @@ import useTablePath from '@/hooks/useTablePath';
import type { import type {
DataBrowserGridColumn, DataBrowserGridColumn,
NormalizedQueryDataRow, NormalizedQueryDataRow,
} from '@/types/data-browser'; } from '@/types/dataBrowser';
import KeyIcon from '@/ui/v2/icons/KeyIcon'; import KeyIcon from '@/ui/v2/icons/KeyIcon';
import normalizeDefaultValue from '@/utils/dataBrowser/normalizeDefaultValue'; import normalizeDefaultValue from '@/utils/dataBrowser/normalizeDefaultValue';
import { import {

View File

@@ -4,7 +4,7 @@ import { useDialog } from '@/components/common/DialogProvider';
import useDeleteRecordMutation from '@/hooks/dataBrowser/useDeleteRecordMutation'; import useDeleteRecordMutation from '@/hooks/dataBrowser/useDeleteRecordMutation';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import useDataGridConfig from '@/hooks/useDataGridConfig'; import useDataGridConfig from '@/hooks/useDataGridConfig';
import type { DataBrowserGridColumn } from '@/types/data-browser'; import type { DataBrowserGridColumn } from '@/types/dataBrowser';
import Chip from '@/ui/Chip'; import Chip from '@/ui/Chip';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown'; import { Dropdown } from '@/ui/v2/Dropdown';

View File

@@ -1,5 +1,5 @@
import type { DataBrowserSidebarProps } from '@/components/data-browser/DataBrowserSidebar'; import type { DataBrowserSidebarProps } from '@/components/dataBrowser/DataBrowserSidebar';
import DataBrowserSidebar from '@/components/data-browser/DataBrowserSidebar'; import DataBrowserSidebar from '@/components/dataBrowser/DataBrowserSidebar';
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout'; import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
import ProjectLayout from '@/components/layout/ProjectLayout'; import ProjectLayout from '@/components/layout/ProjectLayout';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';

View File

@@ -1,6 +1,6 @@
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import ReadOnlyToggle from '@/components/common/ReadOnlyToggle'; 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 KeyIcon from '@/ui/v2/icons/KeyIcon';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import Option from '@/ui/v2/Option'; import Option from '@/ui/v2/Option';
@@ -204,7 +204,7 @@ export default function DatabaseRecordInputGroup({
multiline={isMultiline} multiline={isMultiline}
rows={5} rows={5}
autoFocus={index === 0 && autoFocusFirstInput} autoFocus={index === 0 && autoFocusFirstInput}
componentsProps={{ slotProps={{
label: commonLabelProps, label: commonLabelProps,
inputRoot: { step: 1 }, inputRoot: { step: 1 },
}} }}

View File

@@ -1,13 +1,13 @@
import type { import type {
BaseColumnFormProps, BaseColumnFormProps,
BaseColumnFormValues, BaseColumnFormValues,
} from '@/components/data-browser/BaseColumnForm'; } from '@/components/dataBrowser/BaseColumnForm';
import BaseColumnForm, { import BaseColumnForm, {
baseColumnValidationSchema, baseColumnValidationSchema,
} from '@/components/data-browser/BaseColumnForm'; } from '@/components/dataBrowser/BaseColumnForm';
import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation'; import useTrackForeignKeyRelationMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
import useUpdateColumnMutation from '@/hooks/dataBrowser/useUpdateColumnMutation'; 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 { Alert } from '@/ui/Alert';
import type { AutocompleteOption } from '@/ui/v2/Autocomplete'; import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';

View File

@@ -1,12 +1,12 @@
import type { import type {
BaseForeignKeyFormProps, BaseForeignKeyFormProps,
BaseForeignKeyFormValues, BaseForeignKeyFormValues,
} from '@/components/data-browser/BaseForeignKeyForm'; } from '@/components/dataBrowser/BaseForeignKeyForm';
import { import {
BaseForeignKeyForm, BaseForeignKeyForm,
baseForeignKeyValidationSchema, baseForeignKeyValidationSchema,
} from '@/components/data-browser/BaseForeignKeyForm'; } from '@/components/dataBrowser/BaseForeignKeyForm';
import type { ForeignKeyRelation } from '@/types/data-browser'; import type { ForeignKeyRelation } from '@/types/dataBrowser';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';

View File

@@ -1,17 +1,17 @@
import type { import type {
BaseTableFormProps, BaseTableFormProps,
BaseTableFormValues, BaseTableFormValues,
} from '@/components/data-browser/BaseTableForm'; } from '@/components/dataBrowser/BaseTableForm';
import BaseTableForm, { import BaseTableForm, {
baseTableValidationSchema, baseTableValidationSchema,
} from '@/components/data-browser/BaseTableForm'; } from '@/components/dataBrowser/BaseTableForm';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery'; import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation'; import useTrackForeignKeyRelationsMutation from '@/hooks/dataBrowser/useTrackForeignKeyRelationsMutation';
import useUpdateTableMutation from '@/hooks/dataBrowser/useUpdateTableMutation'; import useUpdateTableMutation from '@/hooks/dataBrowser/useUpdateTableMutation';
import type { import type {
DatabaseTable, DatabaseTable,
NormalizedQueryDataRow, NormalizedQueryDataRow,
} from '@/types/data-browser'; } from '@/types/dataBrowser';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';
import ActivityIndicator from '@/ui/v2/ActivityIndicator'; import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';

View File

@@ -0,0 +1,208 @@
import ControlledSelect from '@/components/common/ControlledSelect';
import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
import type { HasuraOperator } from '@/types/dataBrowser';
import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import { useRouter } from 'next/router';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { useState } from 'react';
import { useController, useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import RuleRemoveButton from './RuleRemoveButton';
import RuleValueInput from './RuleValueInput';
export interface RuleEditorRowProps
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
/**
* Name of the parent group editor.
*/
name: string;
/**
* Index of the rule.
*/
index: number;
/**
* Function to be called when the remove button is clicked.
*/
onRemove?: VoidFunction;
/**
* List of operators to be disabled for the rule editor.
*
* @default []
*/
disabledOperators?: HasuraOperator[];
}
const commonOperators: {
value: HasuraOperator;
label?: string;
helperText?: string;
}[] = [
{ value: '_eq', helperText: 'equal' },
{ value: '_neq', helperText: 'not equal' },
{ value: '_in_hasura', label: '_in', helperText: 'in (X-Hasura-)' },
{ value: '_in', helperText: 'in (array)' },
{ value: '_nin_hasura', label: '_nin', helperText: 'not in (X-Hasura-)' },
{ value: '_nin', helperText: 'not in (array)' },
{ value: '_gt', helperText: 'greater than' },
{ value: '_lt', helperText: 'lower than' },
{ value: '_gte', helperText: 'greater than or equal' },
{ value: '_lte', helperText: 'lower than or equal' },
{ value: '_ceq', helperText: 'equal to column' },
{ value: '_cne', helperText: 'not equal to column' },
{ value: '_cgt', helperText: 'greater than column' },
{ value: '_clt', helperText: 'lower than column' },
{ value: '_cgte', helperText: 'greater than or equal to column' },
{ value: '_clte', helperText: 'lower than or equal to column' },
{ value: '_is_null', helperText: 'null' },
];
const textSpecificOperators: typeof commonOperators = [
{ value: '_like', helperText: 'like' },
{ value: '_nlike', helperText: 'not like' },
{ value: '_ilike', helperText: 'like (case-insensitive)' },
{ value: '_nilike', helperText: 'not like (case-insensitive)' },
{ value: '_similar', helperText: 'similar' },
{ value: '_nsimilar', helperText: 'not similar' },
{ value: '_regex', helperText: 'matches regex' },
{ value: '_nregex', helperText: `doesn't match regex` },
{ value: '_iregex', helperText: 'matches case-insensitive regex' },
{ value: '_niregex', helperText: `doesn't match case-insensitive regex` },
];
function renderOption({
value,
label,
helperText,
}: typeof commonOperators[number]) {
return (
<Option key={value} value={value} className="grid grid-flow-col gap-2">
<Text component="span" className="inline-block w-16">
{label || value}
</Text>
{helperText && (
<Text component="span" className="!text-greyscaleGrey">
{helperText}
</Text>
)}
</Option>
);
}
export default function RuleEditorRow({
name,
index,
onRemove,
className,
disabledOperators = [],
...props
}: RuleEditorRowProps) {
const {
query: { schemaSlug, tableSlug },
} = useRouter();
const { control, setValue } = useFormContext();
const rowName = `${name}.rules.${index}`;
const [selectedTablePath, setSelectedTablePath] = useState<string>('');
const [selectedColumnType, setSelectedColumnType] = useState<string>('');
const { field: autocompleteField } = useController({
name: `${rowName}.column`,
control,
});
const disabledOperatorMap = disabledOperators.reduce(
(map, currentOperator) => map.set(currentOperator, true),
new Map<string, boolean>(),
);
const availableOperators = [
...commonOperators.filter(({ value }) => !disabledOperatorMap.has(value)),
...(selectedColumnType === 'text'
? textSpecificOperators.filter(
({ value }) => !disabledOperatorMap.get(value),
)
: []),
];
return (
<div
className={twMerge(
'flex lg:flex-row flex-col items-stretch lg:max-h-10 flex-1 space-y-1 lg:space-y-0',
className,
)}
{...props}
>
<ColumnAutocomplete
{...autocompleteField}
schema={schemaSlug as string}
table={tableSlug as string}
rootClassName="lg:flex-grow-0 lg:flex-shrink-0 lg:flex-[320px] h-10"
slotProps={{ input: { className: 'bg-white lg:!rounded-r-none' } }}
fullWidth
onChange={(_event, { value, columnMetadata, disableReset }) => {
setSelectedTablePath(
`${columnMetadata.table_schema}.${columnMetadata.table_name}`,
);
setSelectedColumnType(columnMetadata?.udt_name);
setValue(`${rowName}.column`, value, { shouldDirty: true });
if (disableReset) {
return;
}
setValue(`${rowName}.operator`, '_eq', { shouldDirty: true });
setValue(`${rowName}.value`, '', { shouldDirty: true });
}}
onInitialized={({ value, columnMetadata }) => {
setSelectedTablePath(
`${columnMetadata.table_schema}.${columnMetadata.table_name}`,
);
setSelectedColumnType(columnMetadata?.udt_name);
setValue(`${rowName}.column`, value, { shouldDirty: true });
}}
/>
<ControlledSelect
name={`${rowName}.operator`}
className="lg:flex-grow-0 lg:flex-shrink-0 lg:flex-[140px] h-10"
slotProps={{ root: { className: 'bg-white lg:!rounded-none' } }}
fullWidth
onChange={(_event, value: HasuraOperator) => {
if (!['_in', '_nin', '_in_hasura', '_nin_hasura'].includes(value)) {
return;
}
if (value === '_in_hasura' || value === '_nin_hasura') {
setValue(`${rowName}.value`, null, { shouldDirty: true });
return;
}
setValue(`${rowName}.value`, [], { shouldDirty: true });
}}
renderValue={(option) => {
if (!option?.value) {
return <span />;
}
if (option.value === '_in_hasura') {
return <span>_in</span>;
}
if (option.value === '_nin_hasura') {
return <span>_nin</span>;
}
return <span>{option.value}</span>;
}}
>
{availableOperators.map(renderOption)}
</ControlledSelect>
<RuleValueInput selectedTablePath={selectedTablePath} name={rowName} />
<RuleRemoveButton onRemove={onRemove} name={name} />
</div>
);
}

View File

@@ -0,0 +1,58 @@
import ControlledSelect from '@/components/common/ControlledSelect';
import type { RuleGroup } from '@/types/dataBrowser';
import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { useWatch } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface RuleGroupControlsProps
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
/**
* Name of the rule group to control.
*/
name: string;
/**
* Determines whether or not select should be shown or just a label with the
* operator name.
*/
showSelect?: boolean;
}
const operatorDictionary: Record<RuleGroup['operator'], string> = {
_and: 'and',
_or: 'or',
};
export default function RuleGroupControls({
name,
showSelect,
className,
...props
}: RuleGroupControlsProps) {
const currentOperator: RuleGroup['operator'] = useWatch({
name: `${name}.operator`,
});
return (
<div
className={twMerge('grid grid-flow-row gap-2 content-start', className)}
{...props}
>
{showSelect ? (
<ControlledSelect
name={`${name}.operator`}
slotProps={{ root: { className: 'bg-white' } }}
fullWidth
>
<Option value="_and">and</Option>
<Option value="_or">or</Option>
</ControlledSelect>
) : (
<Text className="p-2 !font-medium">
{operatorDictionary[currentOperator]}
</Text>
)}
</div>
);
}

View File

@@ -0,0 +1,91 @@
import Form from '@/components/common/Form';
import type { RuleGroup } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type { RuleGroupEditorProps } from './RuleGroupEditor';
import RuleGroupEditor from './RuleGroupEditor';
export default {
title: 'Data Browser / RuleGroupEditor',
component: RuleGroupEditor,
parameters: {
docs: {
source: {
type: 'code',
},
},
},
} as ComponentMeta<typeof RuleGroupEditor>;
const defaultParameters = {
nextRouter: {
path: '/[workspaceSlug]/[appSlug]/database/browser/[dataSourceSlug]/[schemaSlug]/[tableSlug]',
asPath: '/workspace/app/database/browser/default/public/users',
query: {
workspaceSlug: 'workspace',
appSlug: 'app',
dataSourceSlug: 'default',
schemaSlug: 'public',
tableSlug: 'books',
},
},
msw: {
handlers: [tableQuery, hasuraMetadataQuery, customClaimsQuery],
},
};
const Template: ComponentStory<typeof RuleGroupEditor> = function Template(
args: RuleGroupEditorProps,
) {
const [submittedValues, setSubmittedValues] = useState<string>();
const form = useForm<{ ruleGroupEditor: RuleGroup }>({
defaultValues: {
ruleGroupEditor: {
operator: '_and',
rules: [{ column: '', operator: '_eq', value: '' }],
groups: [],
},
},
reValidateMode: 'onSubmit',
});
function handleSubmit(values: { ruleGroupEditor: RuleGroup }) {
setSubmittedValues(JSON.stringify(values, null, 2));
}
// note: Storybook passes `onRemove` as a prop, but we don't want to use it
return (
<div className="grid grid-flow-row gap-2">
<FormProvider {...form}>
<Form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
<RuleGroupEditor {...args} name="ruleGroupEditor" onRemove={null} />
<Button type="submit" className="justify-self-start">
Submit
</Button>
</Form>
</FormProvider>
<Text component="pre" className="!font-mono !text-gray-700">
{submittedValues || 'The form has not been submitted yet.'}
</Text>
</div>
);
};
export const Default = Template.bind({});
Default.args = {};
Default.parameters = defaultParameters;
export const DisabledOperators = Template.bind({});
DisabledOperators.args = {
disabledOperators: ['_in_hasura', '_nin_hasura', '_is_null'],
};
DisabledOperators.parameters = defaultParameters;

View File

@@ -0,0 +1,193 @@
import type { Rule, RuleGroup } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import Text from '@/ui/v2/Text';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import type { RuleEditorRowProps } from './RuleEditorRow';
import RuleEditorRow from './RuleEditorRow';
import RuleGroupControls from './RuleGroupControls';
export interface RuleGroupEditorProps
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>,
Pick<RuleEditorRowProps, 'disabledOperators'> {
/**
* Name of the group editor.
*/
name: string;
/**
* Function to be called when the remove button is clicked.
*/
onRemove?: VoidFunction;
/**
* Determines whether or not remove should be disabled for the rule group.
*/
disableRemove?: boolean;
/**
* Group editor depth.
*
* @default 0
*/
depth?: number;
/**
* Maximum depth of the group editor.
*
* @default 7
*/
maxDepth?: number;
}
export default function RuleGroupEditor({
onRemove,
name,
className,
disableRemove,
disabledOperators = [],
depth = 0,
maxDepth = 7,
...props
}: RuleGroupEditorProps) {
const form = useFormContext();
const { control } = form;
// Note: Reason for the type cast to `never`
// https://github.com/react-hook-form/react-hook-form/issues/4055#issuecomment-950145092
const {
fields: rules,
append: appendRule,
remove: removeRule,
} = useFieldArray({
control,
name: `${name}.rules`,
} as never);
// Note: Reason for the type cast to `never`
// https://github.com/react-hook-form/react-hook-form/issues/4055#issuecomment-950145092
const {
fields: groups,
append: appendGroup,
remove: removeGroup,
} = useFieldArray({
control,
name: `${name}.groups`,
} as never);
if (!form) {
throw new Error('RuleGroupEditor must be used in a FormContext.');
}
return (
<div
className={twMerge(
'rounded-lg px-2',
depth === 0 && 'bg-greyscale-50',
depth === 1 && 'bg-greyscale-100',
depth === 2 && 'bg-greyscale-200',
depth === 3 && 'bg-greyscale-300',
depth === 4 && 'bg-greyscale-400',
depth === 5 && 'bg-greyscale-500',
depth >= 6 && 'bg-greyscale-600',
className,
)}
{...props}
>
<div className="flex flex-col flex-auto space-y-4 lg:space-y-2 py-4">
{(rules as (Rule & { id: string })[]).map((rule, ruleIndex) => (
<div className="flex flex-row flex-auto" key={rule.id}>
<div className="flex-[70px] flex-grow-0 flex-shrink-0 mr-2">
{ruleIndex === 0 && (
<Text className="p-2 !font-medium">Where</Text>
)}
{ruleIndex > 0 && (
<RuleGroupControls name={name} showSelect={ruleIndex === 1} />
)}
</div>
<RuleEditorRow
name={name}
index={ruleIndex}
onRemove={() => removeRule(ruleIndex)}
className="flex-auto"
disabledOperators={disabledOperators}
/>
</div>
))}
{(groups as (RuleGroup & { id: string })[]).map(
(ruleGroup, ruleGroupIndex) => (
<div
className="flex flex-row flex-auto items-start mt-2"
key={ruleGroup.id}
>
<div className="flex-[70px] flex-grow-0 flex-shrink-0 mr-2">
{rules.length === 0 && ruleGroupIndex === 0 && (
<Text className="p-2 !font-medium">Where</Text>
)}
<RuleGroupControls
name={name}
showSelect={
(rules.length === 0 && ruleGroupIndex === 1) ||
(rules.length === 1 && ruleGroupIndex === 0)
}
/>
</div>
<RuleGroupEditor
onRemove={() => removeGroup(ruleGroupIndex)}
disableRemove={rules.length === 0 && groups.length === 1}
disabledOperators={disabledOperators}
name={`${name}.groups.${ruleGroupIndex}`}
className="flex-auto"
depth={depth + 1}
/>
</div>
),
)}
</div>
<div className="grid grid-flow-row lg:grid-flow-col lg:justify-between gap-2 pb-2">
<div className="grid grid-flow-row lg:grid-flow-col gap-2 lg:justify-start">
<Button
startIcon={<PlusIcon />}
variant="borderless"
onClick={() =>
appendRule({ column: '', operator: '_eq', value: '' })
}
>
New Rule
</Button>
<Button
startIcon={<PlusIcon />}
variant="borderless"
onClick={() =>
appendGroup({
operator: '_and',
rules: [{ column: '', operator: '_eq', value: '' }],
groups: [],
})
}
disabled={depth >= maxDepth - 1}
>
New Group
</Button>
</div>
{onRemove && (
<Button
variant="borderless"
color="secondary"
onClick={onRemove}
disabled={disableRemove}
>
Delete Group
</Button>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import type { Rule, RuleGroup } from '@/types/dataBrowser';
import type { ButtonProps } from '@/ui/v2/Button';
import Button from '@/ui/v2/Button';
import XIcon from '@/ui/v2/icons/XIcon';
import { useWatch } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface RuleRemoveButtonProps extends ButtonProps {
/**
* Name of the parent group editor.
*/
name: string;
/**
* Function to be called when the remove button is clicked.
*/
onRemove?: VoidFunction;
}
function RuleRemoveButton({
name,
onRemove,
className,
...props
}: RuleRemoveButtonProps) {
const rules: Rule[] = useWatch({ name: `${name}.rules` });
const groups: RuleGroup[] = useWatch({ name: `${name}.groups` });
return (
<Button
variant="outlined"
color="secondary"
className={twMerge(
'!bg-white lg:!rounded-l-none lg:flex-grow-0 lg:flex-shrink-0 lg:flex-[40px] !min-w-0 h-10',
className,
)}
disabled={rules.length === 1 && groups.length === 0}
aria-label="Remove Rule"
{...props}
onClick={onRemove}
>
<XIcon className="!w-4 !h-4" />
</Button>
);
}
export default RuleRemoveButton;

View File

@@ -0,0 +1,177 @@
import ControlledAutocomplete from '@/components/common/ControlledAutocomplete';
import ControlledSelect from '@/components/common/ControlledSelect';
import ReadOnlyToggle from '@/components/common/ReadOnlyToggle';
import type { ColumnAutocompleteProps } from '@/components/dataBrowser/ColumnAutocomplete';
import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { HasuraOperator } from '@/types/dataBrowser';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Option from '@/ui/v2/Option';
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
import { useRouter } from 'next/router';
import { useController, useFormContext, useWatch } from 'react-hook-form';
export interface RuleValueInputProps {
/**
* Name of the parent group editor.
*/
name: string;
/**
* Path of the table selected through the column input.
*/
selectedTablePath?: string;
}
function ColumnSelectorInput({
name,
selectedTablePath,
schema,
table,
...props
}: ColumnAutocompleteProps & { selectedTablePath: string }) {
const { setValue, control } = useFormContext();
const { field } = useController({
name,
control,
});
return (
<ColumnAutocomplete
{...props}
{...field}
value={
// this array can either be ['$', 'columnName'] or ['columnName']
Array.isArray(field.value) ? field.value.slice(-1)[0] : field.value
}
schema={schema}
table={table}
disableRelationships
rootClassName="flex-auto"
slotProps={{
input: { className: 'lg:!rounded-none !bg-white !z-10' },
}}
onChange={(_event, { value }) => {
if (selectedTablePath === `${schema}.${table}`) {
setValue(name, [value], { shouldDirty: true });
return;
}
// For more information, see https://github.com/hasura/graphql-engine/issues/3459#issuecomment-1085666541
setValue(name, ['$', value], { shouldDirty: true });
}}
/>
);
}
export default function RuleValueInput({
name,
selectedTablePath,
}: RuleValueInputProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { setValue } = useFormContext();
const inputName = `${name}.value`;
const operator: HasuraOperator = useWatch({ name: `${name}.operator` });
const isHasuraInput = operator === '_in_hasura' || operator === '_nin_hasura';
const {
query: { schemaSlug, tableSlug },
} = useRouter();
const { data, loading, error } = useGetAppCustomClaimsQuery({
variables: { id: currentApplication?.id },
skip: !isHasuraInput,
});
if (operator === '_is_null') {
return (
<ControlledSelect
name={inputName}
className="flex-auto"
fullWidth
slotProps={{ root: { className: 'bg-white lg:!rounded-none h-10' } }}
>
<Option value="true">
<ReadOnlyToggle
checked
slotProps={{ label: { className: '!text-sm' } }}
/>
</Option>
<Option value="false">
<ReadOnlyToggle
checked={false}
slotProps={{ label: { className: '!text-sm' } }}
/>
</Option>
</ControlledSelect>
);
}
if (operator === '_in' || operator === '_nin') {
return (
<ControlledAutocomplete
name={inputName}
multiple
freeSolo
limitTags={5}
className="flex-auto"
slotProps={{ input: { className: 'lg:!rounded-none !bg-white !z-10' } }}
options={[]}
fullWidth
filterSelectedOptions
/>
);
}
if (['_ceq', '_cne', '_cgt', '_clt', '_cgte', '_clte'].includes(operator)) {
return (
<ColumnSelectorInput
selectedTablePath={selectedTablePath}
schema={schemaSlug as string}
table={tableSlug as string}
name={inputName}
/>
);
}
const availableHasuraPermissionVariables = !loading
? getPermissionVariablesArray(data?.app?.authJwtCustomClaims).map(
({ key }) => ({
value: `X-Hasura-${key}`,
label: `X-Hasura-${key}`,
}),
)
: [];
return (
<ControlledAutocomplete
freeSolo={!isHasuraInput}
name={inputName}
className="flex-auto"
slotProps={{
input: { className: 'lg:!rounded-none !bg-white' },
formControl: { className: '!bg-transparent' },
}}
fullWidth
loading={loading}
loadingText={<ActivityIndicator label="Loading..." />}
error={!!error}
helperText={error?.message}
options={
isHasuraInput
? availableHasuraPermissionVariables
: [{ value: 'X-Hasura-User-Id', label: 'X-Hasura-User-Id' }]
}
onChange={(_event, _value, reason, details) => {
if (
reason !== 'selectOption' &&
details.option.value !== 'X-Hasura-User-Id'
) {
return;
}
setValue(inputName, details.option.value, { shouldDirty: true });
}}
/>
);
}

View File

@@ -0,0 +1,2 @@
export * from './RuleGroupEditor';
export { default } from './RuleGroupEditor';

View File

@@ -9,6 +9,7 @@ import { updateOwnCache } from '@/utils/updateOwnCache';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useUserData } from '@nhost/nextjs'; import { useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react';
export function InviteAnnounce() { export function InviteAnnounce() {
const user = useUserData(); const user = useUserData();
@@ -21,15 +22,18 @@ export function InviteAnnounce() {
useSubmitState(); useSubmitState();
// @FIX: We probably don't want to poll every ten seconds for possible invites. (We can change later depending on how it works in production.) Maybe just on the workspace page? // @FIX: We probably don't want to poll every ten seconds for possible invites. (We can change later depending on how it works in production.) Maybe just on the workspace page?
const { data, loading, error, refetch } = const { data, loading, error, refetch, startPolling } =
useGetWorkspaceMemberInvitesToManageQuery({ useGetWorkspaceMemberInvitesToManageQuery({
variables: { variables: {
userId: user?.id, userId: user?.id,
}, },
pollInterval: 15000,
skip: !isPlatform, skip: !isPlatform,
}); });
useEffect(() => {
startPolling(15000);
}, [startPolling]);
if (loading) { if (loading) {
return null; return null;
} }

View File

@@ -61,10 +61,8 @@ function LogsTimePicker({
value={format(selectedDate, 'HH:mm:ss')} value={format(selectedDate, 'HH:mm:ss')}
style={{ width: '135px' }} style={{ width: '135px' }}
id="time-picker" id="time-picker"
componentsProps={{ slotProps={{
formControl: { formControl: { className: 'grid grid-flow-col gap-x-3' },
className: 'grid grid-flow-col gap-x-3',
},
label: { sx: { fontSize: '14px' } }, label: { sx: { fontSize: '14px' } },
}} }}
onChange={handleTimePicking} onChange={handleTimePicking}

View File

@@ -15,7 +15,7 @@ import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatab
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema'; import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/react'; import { useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
@@ -117,7 +117,7 @@ export default function ResetDatabasePasswordSettings() {
error={Boolean(errors?.databasePassword)} error={Boolean(errors?.databasePassword)}
fullWidth fullWidth
hideEmptyHelperText hideEmptyHelperText
componentsProps={{ slotProps={{
input: { className: 'lg:w-1/2' }, input: { className: 'lg:w-1/2' },
helperText: { component: 'div' }, helperText: { component: 'div' },
}} }}

View File

@@ -0,0 +1,164 @@
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
refetchGetAppInjectedVariablesQuery,
useUpdateApplicationMutation,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
export interface EditJwtSecretFormProps {
/**
* Initial JWT secret.
*/
jwtSecret: string;
/**
* Determines whether the form is disabled.
*/
disabled?: boolean;
/**
* Submit button text.
*
* @default 'Save'
*/
submitButtonText?: string;
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => void;
/**
* Function to be called when the operation is cancelled.
*/
onCancel?: VoidFunction;
}
export interface EditJwtSecretFormValues {
/**
* JWT secret.
*/
jwtSecret: string;
}
const validationSchema = Yup.object().shape({
jwtSecret: Yup.string()
.nullable()
.required('This field is required.')
.test('isJson', 'This is not a valid JSON.', (value) => {
try {
JSON.parse(value);
return true;
} catch (error) {
return false;
}
}),
});
export default function EditJwtSecretForm({
disabled,
jwtSecret,
onSubmit,
onCancel,
submitButtonText = 'Save',
}: EditJwtSecretFormProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApplication] = useUpdateApplicationMutation({
refetchQueries: [
refetchGetAppInjectedVariablesQuery({ id: currentApplication?.id }),
],
});
const { onDirtyStateChange } = useDialog();
const form = useForm<EditJwtSecretFormValues>({
defaultValues: {
jwtSecret,
},
resolver: yupResolver(validationSchema),
});
const {
register,
formState: { dirtyFields, isSubmitting, errors },
} = form;
const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => {
onDirtyStateChange(isDirty, 'dialog');
}, [isDirty, onDirtyStateChange]);
async function handleSubmit(values: EditJwtSecretFormValues) {
const updateAppPromise = updateApplication({
variables: {
appId: currentApplication?.id,
app: {
hasuraGraphqlJwtSecret: values.jwtSecret,
},
},
});
await toast.promise(
updateAppPromise,
{
loading: 'Updating JWT secret...',
success: 'JWT secret has been updated successfully.',
error: 'An error occurred while updating the JWT secret.',
},
toastStyleProps,
);
onSubmit?.();
}
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex flex-auto flex-col content-between overflow-hidden pb-4"
>
<div className="px-6 overflow-y-auto flex-auto">
<Input
{...register('jwtSecret')}
error={Boolean(errors.jwtSecret?.message)}
helperText={errors.jwtSecret?.message}
autoFocus={!disabled}
disabled={disabled}
aria-label="JWT Secret"
multiline
minRows={4}
fullWidth
hideEmptyHelperText
slotProps={{ inputRoot: { className: 'font-mono !text-sm' } }}
/>
</div>
<div className="grid flex-shrink-0 grid-flow-row gap-2 px-6 pt-4">
{!disabled && (
<Button
loading={isSubmitting}
disabled={isSubmitting}
type="submit"
>
{submitButtonText}
</Button>
)}
<Button
variant="outlined"
color="secondary"
onClick={onCancel}
tabIndex={isDirty ? -1 : 0}
autoFocus={disabled}
>
{disabled ? 'Close' : 'Cancel'}
</Button>
</div>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1,2 @@
export * from './EditJwtSecretForm';
export { default } from './EditJwtSecretForm';

View File

@@ -1,6 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import SettingsContainer from '@/components/settings/SettingsContainer'; import SettingsContainer from '@/components/settings/SettingsContainer';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useAppClient } from '@/hooks/useAppClient'; import { useAppClient } from '@/hooks/useAppClient';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator'; import ActivityIndicator from '@/ui/v2/ActivityIndicator';
@@ -9,12 +10,15 @@ import Divider from '@/ui/v2/Divider';
import IconButton from '@/ui/v2/IconButton'; import IconButton from '@/ui/v2/IconButton';
import EyeIcon from '@/ui/v2/icons/EyeIcon'; import EyeIcon from '@/ui/v2/icons/EyeIcon';
import EyeOffIcon from '@/ui/v2/icons/EyeOffIcon'; import EyeOffIcon from '@/ui/v2/icons/EyeOffIcon';
import Input from '@/ui/v2/Input';
import List from '@/ui/v2/List'; import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem'; import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import generateAppServiceUrl, {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
} from '@/utils/common/generateAppServiceUrl';
import { LOCAL_HASURA_URL } from '@/utils/env'; import { LOCAL_HASURA_URL } from '@/utils/env';
import { generateAppServiceUrl, generateRemoteAppUrl } from '@/utils/helpers'; import { generateRemoteAppUrl } from '@/utils/helpers';
import { useGetAppInjectedVariablesQuery } from '@/utils/__generated__/graphql'; import { useGetAppInjectedVariablesQuery } from '@/utils/__generated__/graphql';
import { Fragment, useState } from 'react'; import { Fragment, useState } from 'react';
@@ -22,13 +26,14 @@ export default function SystemEnvironmentVariableSettings() {
const [showAdminSecret, setShowAdminSecret] = useState(false); const [showAdminSecret, setShowAdminSecret] = useState(false);
const [showWebhookSecret, setShowWebhookSecret] = useState(false); const [showWebhookSecret, setShowWebhookSecret] = useState(false);
const { openAlertDialog } = useDialog(); const { openDialog } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const { data, loading, error } = useGetAppInjectedVariablesQuery({ const { data, loading, error } = useGetAppInjectedVariablesQuery({
variables: { id: currentApplication?.id }, variables: { id: currentApplication?.id },
}); });
const isPlatform = useIsPlatform();
const appClient = useAppClient({ start: false }); const appClient = useAppClient();
if (loading) { if (loading) {
return ( return (
@@ -43,30 +48,39 @@ export default function SystemEnvironmentVariableSettings() {
throw error; throw error;
} }
function showJwtSecret() { function showViewJwtSecretModal() {
openAlertDialog({ openDialog('EDIT_JWT_SECRET', {
title: 'Auth JWT Secret', title: (
payload: ( <span className="grid grid-flow-row">
<div className="grid grid-flow-row gap-2"> <span>Auth JWT Secret</span>
<Text variant="subtitle2">
<Text variant="subtitle1" component="span">
This is the key used for generating JWTs. It&apos;s HMAC-SHA-based This is the key used for generating JWTs. It&apos;s HMAC-SHA-based
and the same as configured in Hasura. and the same as configured in Hasura.
</Text> </Text>
</span>
<Input
defaultValue={data?.app?.hasuraGraphqlJwtSecret}
disabled
fullWidth
multiline
minRows={5}
hideEmptyHelperText
inputProps={{ className: 'font-mono' }}
/>
</div>
), ),
props: { payload: {
hidePrimaryAction: true, disabled: true,
secondaryButtonText: 'Close', jwtSecret: data?.app?.hasuraGraphqlJwtSecret,
},
});
}
function showEditJwtSecretModal() {
openDialog('EDIT_JWT_SECRET', {
title: (
<span className="grid grid-flow-row">
<span>Edit JWT Secret</span>
<Text variant="subtitle1" component="span">
You can add your custom JWT secret here. Hasura will use it to
validate the identity of your users.
</Text>
</span>
),
payload: {
jwtSecret: data?.app?.hasuraGraphqlJwtSecret,
}, },
}); });
} }
@@ -81,13 +95,15 @@ export default function SystemEnvironmentVariableSettings() {
{ {
key: 'NHOST_HASURA_URL', key: 'NHOST_HASURA_URL',
value: value:
process.env.NEXT_PUBLIC_ENV === 'dev' process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
? LOCAL_HASURA_URL ? `${LOCAL_HASURA_URL}/console`
: `${generateAppServiceUrl( : generateAppServiceUrl(
currentApplication.subdomain, currentApplication?.subdomain,
currentApplication.region.awsName, currentApplication?.region.awsName,
'hasura', 'hasura',
)}/console`, defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
),
}, },
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url }, { key: 'NHOST_AUTH_URL', value: appClient.auth.url },
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url }, { key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
@@ -199,14 +215,25 @@ export default function SystemEnvironmentVariableSettings() {
<ListItem.Root className="px-4 grid grid-cols-2 lg:grid-cols-3 justify-start"> <ListItem.Root className="px-4 grid grid-cols-2 lg:grid-cols-3 justify-start">
<ListItem.Text>NHOST_JWT_SECRET</ListItem.Text> <ListItem.Text>NHOST_JWT_SECRET</ListItem.Text>
<Button <div className="grid grid-flow-row md:grid-flow-col gap-1.5 justify-center text-center lg:text-left lg:justify-start items-center lg:col-span-2">
variant="borderless" <Button
onClick={showJwtSecret} variant="borderless"
size="small" onClick={showViewJwtSecretModal}
className="justify-self-start" size="small"
> >
Show JWT Secret Show JWT Secret
</Button> </Button>
<Text component="span">or</Text>
<Button
variant="borderless"
onClick={showEditJwtSecretModal}
size="small"
>
Edit JWT Secret
</Button>
</div>
</ListItem.Root> </ListItem.Root>
</List> </List>
</SettingsContainer> </SettingsContainer>

View File

@@ -14,7 +14,7 @@ import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem'; import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import Tooltip from '@/ui/v2/Tooltip'; import Tooltip from '@/ui/v2/Tooltip';
import getPermissionVariables from '@/utils/settings/getPermissionVariablesArray'; import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
import { toastStyleProps } from '@/utils/settings/settingsConstants'; import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { import {
useGetAppCustomClaimsQuery, useGetAppCustomClaimsQuery,
@@ -125,7 +125,7 @@ export default function PermissionVariableSettings() {
}); });
} }
const availablePermissionVariables = getPermissionVariables( const availablePermissionVariables = getPermissionVariablesArray(
data?.app?.authJwtCustomClaims, data?.app?.authJwtCustomClaims,
); );

View File

@@ -1,5 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
@@ -55,11 +56,20 @@ export default function BaseRoleForm({
}, [isDirty, onDirtyStateChange]); }, [isDirty, onDirtyStateChange]);
return ( return (
<div className="grid grid-flow-row gap-2 px-6 pb-6"> <div className="grid grid-flow-row gap-3 px-6 pb-6">
<Text variant="subtitle1" component="span"> <Text variant="subtitle1" component="span">
Enter the name for the role below. Enter the name for the role below.
</Text> </Text>
{submitButtonText !== 'Create' && (
<Alert severity="warning" className="text-left">
<span className="text-left">
<strong>Note:</strong> Changing the name of the role will lose the
associated permissions with that role.
</span>
</Alert>
)}
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4"> <Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
<Input <Input
{...register('name')} {...register('name')}

Some files were not shown because too many files have changed in this diff Show More