Compare commits

...

172 Commits

Author SHA1 Message Date
Szilárd Dóró
21fb316655 Merge pull request #1554 from nhost/changeset-release/main
chore: update versions
2023-01-30 15:39:36 +01:00
github-actions[bot]
9c4f350508 chore: update versions 2023-01-30 11:18:24 +00:00
Szilárd Dóró
eb3ba21afc Merge pull request #1556 from nhost/renovate-changesets
chore: create changesest from Renovate bumps
2023-01-30 12:16:59 +01:00
Szilárd Dóró
a84ac62999 Merge pull request #1546 from nhost/roles-iagsd9ahsd
fix(dashboard): allowed roles
2023-01-30 11:38:27 +01:00
Szilárd Dóró
f32bfed9a9 Merge pull request #1558 from nhost/fix/pnpm-lock
fix pnpm-lock.yaml
2023-01-30 10:49:40 +01:00
Szilárd Dóró
55632506e4 fix pnpm-lock.yaml 2023-01-30 10:49:21 +01:00
szilarddoro
e146d32e69 chore(deps): update dependency @types/react to v18.0.27 2023-01-30 09:44:29 +00:00
Szilárd Dóró
df8a13997c Merge pull request #1487 from nhost/renovate/jsdom-21.x
chore(deps): update dependency jsdom to v21
2023-01-30 10:43:48 +01:00
Szilárd Dóró
c488fd4c0c Merge pull request #1533 from nhost/renovate/react-18.x
chore(deps): update dependency @types/react to v18.0.27
2023-01-30 10:43:20 +01:00
Johan Eliasson
a6e8569822 Merge pull request #1544 from nhost/docs-auth-uba9sdasd
docs(auth): improvements
2023-01-30 10:37:56 +01:00
Johan Eliasson
a8023c9a3f Merge pull request #1552 from jonorossi/patch-1
docs: fix link to sign-in with security keys
2023-01-30 10:37:32 +01:00
Johan Eliasson
59347fcd4b added changeset 2023-01-30 10:18:05 +01:00
Johan Eliasson
5b65cac91e added changeset 2023-01-30 10:16:46 +01:00
Johan Eliasson
331ae02e2d revert 2023-01-30 10:15:34 +01:00
Johan Eliasson
3b8b3be393 Update docs/docs/authentication/sign-in-methods/3-security-keys.mdx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-30 10:12:30 +01:00
Johan Eliasson
9fb4d82d86 Apply suggestions from code review
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-30 10:12:13 +01:00
Szilárd Dóró
37d836b9b6 Merge pull request #1553 from nhost/chore/enhanced-feedback
feat(dashboard): include project info in feedback
2023-01-30 10:08:50 +01:00
Szilárd Dóró
3c1996b13b Merge pull request #1543 from nhost/changeset-release/main
chore: update versions
2023-01-30 10:01:21 +01:00
github-actions[bot]
e3d90fd5d2 chore: update versions 2023-01-30 08:58:53 +00:00
Szilárd Dóró
af016e1caa Merge pull request #1548 from nhost/remove-functions-section
fix(dashboard): removed functions section
2023-01-30 09:57:31 +01:00
Szilárd Dóró
963f9b5e85 feat(dashboard): include project info in feedback 2023-01-30 09:56:56 +01:00
Szilárd Dóró
ed4c780115 fix(dashboard): lint error 2023-01-30 09:40:51 +01:00
Jonathon Rossi
260997c6fe docs: fix link to sign-in with security keys 2023-01-30 12:25:29 +10:00
Johan Eliasson
941f0f5755 removed unused code 2023-01-28 20:17:43 +01:00
Johan Eliasson
75344b2bc0 remove 2023-01-28 18:02:57 +01:00
Johan Eliasson
65da426e8b update 2023-01-28 17:46:18 +01:00
Johan Eliasson
a711727e94 wording 2023-01-28 14:13:14 +01:00
Johan Eliasson
8e8f6fd9c9 updates 2023-01-28 09:41:15 +01:00
Johan Eliasson
a1c2e5d8ee docs updates 2023-01-27 21:42:40 +01:00
Johan Eliasson
5c276ae844 improved docs for auth 2023-01-27 20:16:58 +01:00
Johan Eliasson
f34702f3c5 Merge pull request #1541 from nhost/docs-graphql-integrations
stripe graphql api updates
2023-01-27 19:35:31 +01:00
Johan Eliasson
6cb70eee01 Update docs/docs/graphql/remote-schemas/stripe.mdx
Co-authored-by: Guido Curcio <guidomaurocurcio@gmail.com>
2023-01-27 16:12:43 +01:00
Johan Eliasson
9395c9687f update 2023-01-27 16:03:31 +01:00
Johan Eliasson
eb1eb934a4 update 2023-01-27 10:36:56 +01:00
Johan Eliasson
c62fed2c9a update 2023-01-27 09:16:24 +01:00
Johan Eliasson
16fe1a47da fixed broken links 2023-01-27 09:13:42 +01:00
Johan Eliasson
0f04e8b8b8 added example response 2023-01-27 09:07:38 +01:00
Johan Eliasson
e6dad4d696 added changeset 2023-01-27 08:57:30 +01:00
Johan Eliasson
bcb3b79add updates 2023-01-27 08:54:23 +01:00
Szilárd Dóró
fe658231b4 Merge pull request #1538 from nhost/changeset-release/main
chore: update versions
2023-01-23 10:37:11 +01:00
github-actions[bot]
a1188b7d98 chore: update versions 2023-01-23 09:10:50 +00:00
Szilárd Dóró
cd4bdc581d Merge pull request #1537 from nhost/fix/local-auth-page
fix(dashboard): don't break Auth page in local mode
2023-01-23 10:09:26 +01:00
Szilárd Dóró
4e2f8ccd52 fix(dashboard): don't break Auth page in local mode 2023-01-23 09:30:11 +01:00
Szilárd Dóró
8a6d8c7534 Merge pull request #1534 from nhost/changeset-release/main
chore: update versions
2023-01-19 08:17:58 +01:00
github-actions[bot]
fa75409f09 chore: update versions 2023-01-18 20:37:34 +00:00
Szilárd Dóró
74662052ae Merge pull request #1531 from nhost/fix/allowed-emails-and-domains
fix(dashboard): enable toggle when settings are filled in
2023-01-18 21:36:20 +01:00
renovate[bot]
2de904c865 chore(deps): update dependency @types/react to v18.0.27 2023-01-18 20:06:28 +00:00
Szilárd Dóró
37ab5fe878 trigger build 2023-01-18 17:50:28 +01:00
Szilárd Dóró
be9af96fa7 fix(dashboard): remove values if toggle is disabled 2023-01-18 16:42:44 +01:00
Szilárd Dóró
31abbe5f30 fix(dashboard): enable toggle when settings are filled in 2023-01-18 15:18:39 +01:00
Szilárd Dóró
268b461d5b Merge pull request #1529 from nhost/changeset-release/main
chore: update versions
2023-01-18 10:44:26 +01:00
github-actions[bot]
58af592cfa chore: update versions 2023-01-18 09:04:47 +00:00
Szilárd Dóró
0e9d623c69 Merge pull request #1527 from nhost/fix/permission-editor-array-input
fix(dashboard): don't throw validation error for valid permission rules
2023-01-18 10:03:35 +01:00
Szilárd Dóró
412a290646 Merge pull request #1528 from nhost/chore/lower-storage-page-limit
chore(dashboard): list fewer images per page on the Storage page
2023-01-18 09:35:15 +01:00
Szilárd Dóró
123add38a4 fix(dashboard): fetch images from correct URL 2023-01-17 16:43:34 +01:00
Szilárd Dóró
5bdd31ad36 chore(dashboard): list fewer images per page on the Storage page 2023-01-17 16:41:14 +01:00
Szilárd Dóró
5121851c8b fix(dashboard): don't throw validation error for valid permission rules 2023-01-17 16:35:29 +01:00
Szilárd Dóró
8ca1f92491 Merge pull request #1525 from nhost/changeset-release/main
chore: update versions
2023-01-17 14:20:25 +01:00
github-actions[bot]
5535b9085b chore: update versions 2023-01-17 10:20:52 +00:00
Szilárd Dóró
bc51122b25 Merge pull request #1522 from nhost/fix/retrigger-deployment-status
fix(dashboard): correct redeployment button
2023-01-17 11:19:38 +01:00
Szilárd Dóró
b060e5e550 fix(dashboard): restore deployment timer 2023-01-17 09:27:28 +01:00
Szilárd Dóró
6a906b22e2 fix(dashboard): show deployment duration 2023-01-17 09:04:43 +01:00
Pilou
860c9d1be4 Merge pull request #1523 from akd-io/patch-2
Docs: Fix npm install command
2023-01-16 18:33:44 +01:00
Anders Kjær Damgaard
9eec3e58f5 Fix npm install command
Was missing a space
2023-01-16 15:42:56 +01:00
Johan Eliasson
4e01a43e94 Merge pull request #1431 from nhost/example-updates
codegen example updates
2023-01-16 14:31:27 +01:00
Szilárd Dóró
c126b20dcf chore(dashboard): add changeset 2023-01-16 13:38:00 +01:00
Szilárd Dóró
b727a24a5f fix(dashboard): restore "Redeploy" button behavior 2023-01-16 12:37:32 +01:00
Szilárd Dóró
ecadd7e1b9 fix(dashboard): don't show redeploy button when deployment is in progress 2023-01-16 11:52:58 +01:00
Johan Eliasson
2d661174a8 update 2023-01-16 10:01:02 +01:00
Johan Eliasson
fcb3e5192f Merge branch 'main' into example-updates 2023-01-16 10:00:28 +01:00
Szilárd Dóró
66fdc63f38 Merge pull request #1516 from nhost/renovate/rimraf-4.x
chore(deps): update dependency rimraf to v4
2023-01-13 10:25:34 +01:00
renovate[bot]
fa37cb6171 chore(deps): update dependency rimraf to v4 2023-01-13 02:39:43 +00:00
Szilárd Dóró
c1bea1294d Merge pull request #1512 from nhost/changeset-release/main
chore: update versions
2023-01-12 15:46:08 +01:00
github-actions[bot]
8af2f6e9dd chore: update versions 2023-01-12 11:43:39 +00:00
Szilárd Dóró
e3d0b96917 Merge pull request #1503 from nhost/feat/retrigger-deployments
feat(dashboard): Retrigger Deployments
2023-01-12 12:41:55 +01:00
Szilárd Dóró
43705b992d Merge pull request #1509 from nhost/changeset-release/main
chore: update versions
2023-01-12 12:41:41 +01:00
github-actions[bot]
2e999e8715 chore: update versions 2023-01-12 10:14:41 +00:00
Pilou
0370696d5c Merge pull request #1511 from nhost/chore/unlink-packages
chore(changeset): stop linking packages
2023-01-12 11:12:44 +01:00
Pierre-Louis Mercereau
f62131d55a chore(changeset): stop linking packages 2023-01-12 10:59:57 +01:00
Szilárd Dóró
36c3519cf8 chore(dashboard): retrigger deployments 2023-01-12 10:18:28 +01:00
Szilárd Dóró
86d077ac00 Merge pull request #1508 from nhost/renovate-changesets
chore: create changesest from Renovate bumps
2023-01-12 10:10:35 +01:00
renovate[bot]
a21aa05b5a chore(deps): update dependency jsdom to v21 2023-01-12 08:53:01 +00:00
szilarddoro
200e9f774c chore(deps): update dependency @types/react-dom to v18.0.10 2023-01-12 08:49:58 +00:00
Szilárd Dóró
9b52e9bf13 Merge branch 'main' into feat/retrigger-deployments 2023-01-12 09:49:46 +01:00
Szilárd Dóró
bc1235de3b Merge pull request #1433 from nhost/renovate/react-dom-18.x
chore(deps): update dependency @types/react-dom to v18.0.10
2023-01-12 09:48:10 +01:00
Szilárd Dóró
fce58ebaea remove changeset, CI generates it 2023-01-12 09:47:53 +01:00
Szilárd Dóró
452e281120 chore(dashboard): add changeset 2023-01-12 09:47:04 +01:00
Szilárd Dóró
9a338e54c9 Merge pull request #1492 from nhost/renovate/vitest-monorepo
chore(deps): update vitest monorepo to ^0.27.0
2023-01-12 09:45:14 +01:00
Szilárd Dóró
baeebf980d Merge pull request #1507 from nhost/changeset-release/main
chore: update versions
2023-01-12 09:27:25 +01:00
github-actions[bot]
ac92c6ee61 chore: update versions 2023-01-12 01:38:20 +00:00
Guido Curcio
1ddaf680c0 Merge pull request #1471 from nhost/fix(dashboard)/workspace-creation-redirection-delete 2023-01-11 22:36:50 -03:00
Guido Curcio
c6e6194d8e mutating -> updating for signaling changes in course. 2023-01-11 09:05:43 -03:00
Pilou
83deea8b45 Merge pull request #1501 from nhost/chore/exclude-functions-from-workspace
chore: exclude functions from workspace
2023-01-11 11:42:07 +01:00
Szilárd Dóró
07c8d90053 fix(dashboard): lint errors 2023-01-11 11:13:37 +01:00
Pierre-Louis Mercereau
acbaabcf85 chore: update lockfile 2023-01-11 10:44:36 +01:00
Szilárd Dóró
a2621e40a4 feat(dashboard): unified list items for deployments
fixed the way the latest scheduled or pending deployment is tracked
2023-01-11 10:37:10 +01:00
Pierre-Louis Mercereau
3534501f37 chore: force using turborepo v1 2023-01-11 10:31:10 +01:00
Pierre-Louis Mercereau
27bc23cbbc chore: exclude functions from workspace 2023-01-11 10:15:47 +01:00
Szilárd Dóró
61120a137a feat(dashboard): add redeployment support to overview 2023-01-11 09:43:06 +01:00
Szilárd Dóró
faea8feb2e Merge branch 'main' into feat/retrigger-deployments 2023-01-11 08:56:05 +01:00
Szilárd Dóró
6450223558 Merge pull request #1498 from nhost/changeset-release/main
chore: update versions
2023-01-11 08:48:36 +01:00
Guido Curcio
a62a85a777 add comments to effects and router changes. 2023-01-11 02:07:15 -03:00
Guido Curcio
ae24f83953 fix changing application name redirect to 404, fix 404 flash when changing workspace name. 2023-01-11 01:33:28 -03:00
Guido Curcio
fc60d7a782 2023-01-10 19:14:00 -03:00
Guido Curcio
6be8a998df 2023-01-10 19:13:09 -03:00
Guido Curcio
ea091f6251 2023-01-10 19:11:02 -03:00
Guido Curcio
8175c052f7 2023-01-10 18:50:45 -03:00
github-actions[bot]
e6605a6ed0 chore: update versions 2023-01-10 16:43:20 +00:00
Szilárd Dóró
1cba0e6492 Merge pull request #1497 from nhost/fix/database-ui-hasura-metadata
fix(dashboard): don't break the table creation process
2023-01-10 17:41:38 +01:00
Szilárd Dóró
179c90fcdb fix(dashboard): update inline snapshot 2023-01-10 17:13:29 +01:00
Szilárd Dóró
552e31a4f0 feat(dashboard): initial redeploy button code 2023-01-10 17:12:14 +01:00
Szilárd Dóró
85f0f943a1 chore(dashboard): add changeset 2023-01-10 16:04:15 +01:00
Szilárd Dóró
c4c23fde31 fix(dashboard): don't break table creation
don't break table creation when referencing a table that is not in the `public` schema
2023-01-10 15:39:05 +01:00
Szilárd Dóró
e0b94c3e90 Merge pull request #1493 from nhost/changeset-release/main
chore: update versions
2023-01-10 09:33:58 +01:00
github-actions[bot]
113d638532 chore: update versions 2023-01-09 17:20:13 +00:00
Pilou
d87448916f Merge pull request #1486 from nhost/chore/bump-start-server-and-test
chore(deps): bump start-server-and-test
2023-01-09 18:17:47 +01:00
Pilou
af4292658c Merge pull request #1495 from nhost/fix/export-types
Export commonly used types
2023-01-09 18:17:01 +01:00
Pilou
f735bcd2ea Merge pull request #1485 from nhost/fix/explicit-types
fix: 🐛 add explicit types to React hooks and Vue composables
2023-01-09 18:00:48 +01:00
Pierre-Louis Mercereau
66fb74af86 Merge branch 'main' into fix/explicit-types 2023-01-09 16:30:20 +01:00
Pierre-Louis Mercereau
791eac30bb Merge branch 'main' into chore/bump-start-server-and-test 2023-01-09 16:29:57 +01:00
Pierre-Louis Mercereau
da4ad889d7 Merge branch 'main' into fix/export-types 2023-01-09 16:27:31 +01:00
Pilou
9ef111760c Merge pull request #1490 from nhost/test/correct-forgot-password
test: correct forgot-password test
2023-01-09 16:26:57 +01:00
Pierre-Louis Mercereau
c2706c7d97 chore: export commonly used types 2023-01-09 16:26:01 +01:00
Pilou
683b8768c4 Merge pull request #1482 from nhost/chore/access-token-cookie
chore: store the session in a cookie to avoid refetching the jwt on every ssr call
2023-01-09 16:11:39 +01:00
renovate[bot]
6d9df237a8 chore(deps): update vitest monorepo to ^0.27.0 2023-01-09 13:34:33 +00:00
Szilárd Dóró
220ae37aa7 Merge pull request #1491 from nhost/changeset-release/main
chore: update versions
2023-01-09 14:26:56 +01:00
Pierre-Louis Mercereau
d0d94d9239 chore: use email+password sign-up 2023-01-09 13:27:28 +01:00
Pierre-Louis Mercereau
aed3d1f147 chore: wrap 2023-01-09 11:59:49 +01:00
github-actions[bot]
d07bf08e45 chore: update versions 2023-01-09 10:46:27 +00:00
Szilárd Dóró
f2183250d2 Merge pull request #1470 from nhost/fix(dashboard)/sign-out
fix(dashboard): Resetting the cache when signing out.
2023-01-09 11:44:48 +01:00
Pierre-Louis Mercereau
d2bb5ecfae refactor: unnest code blocks 2023-01-09 11:39:33 +01:00
Pierre-Louis Mercereau
02d0db0cf0 revert: remove line 2023-01-09 11:20:28 +01:00
Pierre-Louis Mercereau
441005d5c3 chore: another attempt 2023-01-09 10:59:38 +01:00
Pierre-Louis Mercereau
eea8708549 chore: visit 2023-01-09 10:31:44 +01:00
Szilárd Dóró
5f3f9390aa chore(dashboard): updated changeset 2023-01-09 09:42:36 +01:00
Pierre-Louis Mercereau
1c5b0560ed chore: 10 attempts 2023-01-09 09:38:19 +01:00
Pierre-Louis Mercereau
1bfdf21b99 test: correct forgot-password test 2023-01-09 09:33:40 +01:00
Johan Eliasson
c4561cae38 redirect 2023-01-07 10:20:29 +01:00
Pierre-Louis Mercereau
efd522a38a chore: update changesets 2023-01-06 16:53:21 +01:00
Pierre-Louis Mercereau
55c35fa9c5 chore(deps): bump start-server-and-test 2023-01-06 16:45:06 +01:00
Pierre-Louis Mercereau
d42c27ae99 fix: 🐛 add explicit types to React hooks and Vue composables 2023-01-06 13:56:19 +01:00
Pierre-Louis Mercereau
927be4a2c9 chore: store the session in a cookie 2023-01-06 10:47:09 +01:00
Guido Curcio
e44352abbd typo in CreateWorkspaceFormProps Save -> Create 2023-01-05 23:48:35 -03:00
Guido Curcio
f9289f3c32 Merge branch 'fix(dashboard)/workspace-creation-redirection-delete' of https://github.com/nhost/nhost into fix(dashboard)/workspace-creation-redirection-delete 2023-01-05 23:47:01 -03:00
Guido Curcio
8ff06e5637 disable create workspace button if error on input. 2023-01-05 23:45:32 -03:00
Guido Curcio
49e4633bca handle when workspace name is already taken. 2023-01-05 23:41:23 -03:00
Guido Curcio
7ae7a7206c Update dashboard/src/components/home/CreateWorkspaceForm/CreateWorkspaceForm.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-05 23:31:02 -03:00
Guido Curcio
43d7e7babf Update dashboard/src/components/workspace/WorkspaceSection.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-05 23:30:54 -03:00
Guido Curcio
463a51ce7c Update dashboard/src/components/home/CreateWorkspaceForm/CreateWorkspaceForm.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-05 23:30:47 -03:00
Guido Curcio
86e9d9d47f Update dashboard/src/components/home/CreateWorkspaceForm/CreateWorkspaceForm.tsx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-01-05 23:30:42 -03:00
Guido Curcio
f99b72cd7c useUserData instead of nhost.auth.getUser() 2023-01-05 23:29:42 -03:00
Guido Curcio
0dc2f3ff29 remove unused file 2023-01-05 23:24:11 -03:00
Guido Curcio
d0f8081101 new changeset 2023-01-05 23:16:09 -03:00
Guido Curcio
84ebfb79d0 reorder calls when signing out 2023-01-05 23:14:35 -03:00
Pilou
3c78d0ef46 Merge pull request #1476 from nhost/test/reset-password
test: add forgot password test
2023-01-05 17:07:47 +01:00
Pierre-Louis Mercereau
ad28bf2166 test: add forgot password test 2023-01-05 10:59:52 +01:00
Guido Curcio
dbd3ded515 add patch changeset for dashboard. 2023-01-04 17:41:20 -03:00
Guido Curcio
5399fac211 remove AddWorkspace.tsx file and imports. 2023-01-04 17:39:01 -03:00
Guido Curcio
52e3127a34 fix(dashboard): workspaces creation, new form, correct redirects. 2023-01-04 17:34:35 -03:00
Johan Eliasson
599387934c update 2022-12-31 09:37:45 +01:00
Johan Eliasson
04cea41111 removed package 2022-12-31 08:20:02 +01:00
Johan Eliasson
dc3723306d updated lock file 2022-12-30 11:38:48 +01:00
Johan Eliasson
d7fa572ab6 Merge branch 'main' into example-updates 2022-12-30 11:38:08 +01:00
renovate[bot]
a529b654bc chore(deps): update dependency @types/react-dom to v18.0.10 2022-12-26 17:31:33 +00:00
Johan Eliasson
c21118257f updated lock file 2022-12-25 21:43:27 +01:00
Johan Eliasson
4712b7ff68 updated readme files 2022-12-25 21:40:13 +01:00
Johan Eliasson
4f305a8985 update 2022-12-25 21:33:12 +01:00
Johan Eliasson
cd7d133ba3 updated README 2022-12-25 21:32:30 +01:00
Johan Eliasson
2927a9ac31 move 2022-12-25 21:31:00 +01:00
Johan Eliasson
695eaa77ca update 2022-12-25 21:29:18 +01:00
Johan Eliasson
a29d21e194 react apollo updated 2022-12-25 15:21:52 +01:00
Johan Eliasson
cd20bd4ef2 urql fixes + apollo metadata updates 2022-12-25 14:57:11 +01:00
357 changed files with 13153 additions and 5174 deletions

View File

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

View File

@@ -38,7 +38,7 @@ runs:
uses: nick-fields/retry@v2
with:
timeout_minutes: 3
max_attempts: 3
max_attempts: 10
command: bash <(curl --silent -L https://raw.githubusercontent.com/nhost/cli/main/get.sh) ${{ inputs.version }}
- name: Set custom configuration
if: ${{ inputs.config }}

View File

@@ -1,5 +1,87 @@
# @nhost/dashboard
## 0.10.1
### Patch Changes
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
- 59347fcd: correct allowed role name
- 5b65cac9: updated authentication documentation
- 963f9b5e: feat(dashboard): include project info in feedback
## 0.10.0
### Minor Changes
- ed4c7801: chore(dashboard): remove Functions section
## 0.9.10
### Patch Changes
- 4e2f8ccd: fix(dashboard): don't break Auth page in local mode
## 0.9.9
### Patch Changes
- 31abbe5f: fix(dashboard): enable toggle when settings are filled in
## 0.9.8
### Patch Changes
- 5bdd31ad: chore(dashboard): list fewer images per page on the Storage page
- 5121851c: fix(dashboard): don't throw validation error for valid permission rules
## 0.9.7
### Patch Changes
- c126b20d: fix(dashboard): correct redeployment button
## 0.9.6
### Patch Changes
- 36c3519c: feat(dashboard): retrigger deployments
## 0.9.5
### Patch Changes
- 200e9f77: chore(deps): update dependency @types/react-dom to v18.0.10
- Updated dependencies [200e9f77]
- @nhost/nextjs@1.13.2
- @nhost/react-apollo@4.13.2
## 0.9.4
### Patch Changes
- dbd3ded5: fix(dashboard): workspaces creation, new form, correct redirects.
## 0.9.3
### Patch Changes
- 85f0f943: fix(dashboard): don't break the table creation process
## 0.9.2
### Patch Changes
- Updated dependencies [d42c27ae]
- Updated dependencies [927be4a2]
- @nhost/nextjs@1.13.1
- @nhost/react-apollo@4.13.1
## 0.9.1
### Patch Changes
- d0f80811: fix(dashboard): don't show error when signing out the user
## 0.9.0
### Minor Changes

View File

@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
RUN yarn global add turbo
RUN yarn global add turbo@1
COPY . .
RUN turbo prune --scope="@nhost/dashboard" --docker

View File

@@ -1,6 +1,6 @@
# Nhost Dashboard
This is the Nhost Dashboard, a web application that allows you to manage your Nhost project.
This is the Nhost Dashboard, a web application that allows you to manage your Nhost projects.
To get started, you need to have an Nhost project. If you don't have one, you can [create a project here](https://app.nhost.io).
```bash

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.9.0",
"version": "0.10.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -8,7 +8,7 @@
"build": "next build --no-lint",
"analyze": "ANALYZE=true pnpm build --no-lint",
"start": "next start",
"lint": "next lint --max-warnings 3",
"lint": "next lint --max-warnings 2",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost dev -d",
@@ -104,15 +104,15 @@
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/react": "18.0.25",
"@types/react-dom": "18.0.9",
"@types/react": "18.0.27",
"@types/react-dom": "18.0.10",
"@types/react-table": "^7.7.12",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-react": "^3.0.0",
"@vitest/coverage-c8": "^0.26.0",
"@vitest/coverage-c8": "^0.27.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -126,7 +126,7 @@
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^20.0.3",
"jsdom": "^21.0.0",
"lint-staged": ">=13",
"msw": "^0.49.0",
"msw-storybook-addon": "^1.6.3",
@@ -143,7 +143,7 @@
"typescript": "^4.8.4",
"vite": "^4.0.2",
"vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.26.2",
"vitest": "^0.27.0",
"webpack": "^5.75.0"
},
"browserslist": {

View File

@@ -1,21 +1,14 @@
import type { DeploymentRowFragment } from '@/generated/graphql';
import { useGetDeploymentsSubSubscription } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import DelayedLoading from '@/ui/DelayedLoading';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import { getLastLiveDeployment } from '@/utils/helpers';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import {
differenceInSeconds,
formatDistanceToNowStrict,
parseISO,
} from 'date-fns';
useGetDeploymentsSubSubscription,
useLatestLiveDeploymentSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/generated/graphql';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import List from '@/ui/v2/List';
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/solid';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
type AppDeploymentsProps = {
appId: string;
@@ -66,146 +59,8 @@ function NextPrevPageLink(props: NextPrevPageLinkProps) {
);
}
type AppDeploymentDurationProps = {
startedAt: string;
endedAt: string;
};
export function AppDeploymentDuration({
startedAt,
endedAt,
}: AppDeploymentDurationProps) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
let interval: NodeJS.Timeout;
if (!endedAt) {
interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [endedAt]);
const totalDurationInSeconds = differenceInSeconds(
endedAt ? parseISO(endedAt) : currentTime,
parseISO(startedAt),
);
if (totalDurationInSeconds > 1200) {
return <div>20+m</div>;
}
const durationMins = Math.floor(totalDurationInSeconds / 60);
const durationSecs = totalDurationInSeconds % 60;
return (
<div
style={{
fontVariantNumeric: 'tabular-nums',
}}
className="self-center font-display text-sm+ text-greyscaleDark"
>
{durationMins}m {durationSecs}s
</div>
);
}
type AppDeploymentRowProps = {
deployment: DeploymentRowFragment;
isDeploymentLive: boolean;
};
export function AppDeploymentRow({
deployment,
isDeploymentLive,
}: AppDeploymentRowProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { commitMessage } = deployment;
return (
<div className="flex flex-row items-center px-2 py-4">
<div className="mr-2 flex items-center justify-center">
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8"
/>
</div>
<div className="mx-4 w-full">
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
passHref
>
<a
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="max-w-md truncate text-sm+ font-normal text-greyscaleDark">
{commitMessage?.trim() || (
<span className="pr-1 font-normal italic">
No commit message
</span>
)}
</div>
<div className="text-sm+ text-greyscaleGrey">
{formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
)}
</div>
</a>
</Link>
</div>
<div className="flex flex-row">
{isDeploymentLive && (
<div className="flex self-center align-middle">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-28 self-center text-right font-mono text-sm- font-medium">
<a
className="font-mono font-medium text-greyscaleDark"
target="_blank"
rel="noreferrer"
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
>
{deployment.commitSHA.substring(0, 7)}
</a>
</div>
<div className="mx-4 w-28 text-right">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<div className="mx-3 self-center">
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
</div>
<div className="self-center">
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
passHref
>
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</Link>
</div>
</div>
</div>
);
}
export default function AppDeployments(props: AppDeploymentsProps) {
const { appId } = props;
const [idOfLiveDeployment, setIdOfLiveDeployment] = useState('');
const router = useRouter();
@@ -216,36 +71,59 @@ export default function AppDeployments(props: AppDeploymentsProps) {
const limit = 10;
const offset = (page - 1) * limit;
// @TODO: Should query for all deployments, then subscribe to new ones.
const { data, loading, error } = useGetDeploymentsSubSubscription({
variables: {
id: appId,
limit,
offset,
},
const {
data: deploymentPageData,
loading: deploymentPageLoading,
error,
} = useGetDeploymentsSubSubscription({
variables: { id: appId, limit, offset },
});
useEffect(() => {
if (!data) {
return;
}
const { data: latestDeploymentData, loading: latestDeploymentLoading } =
useGetDeploymentsSubSubscription({
variables: { id: appId, limit: 1, offset: 0 },
});
if (page === 1) {
setIdOfLiveDeployment(getLastLiveDeployment(data.deployments));
}
}, [data, idOfLiveDeployment, loading, page]);
const {
data: latestLiveDeploymentData,
loading: latestLiveDeploymentLoading,
} = useLatestLiveDeploymentSubSubscription({ variables: { appId } });
const {
data: scheduledOrPendingDeploymentsData,
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({ variables: { appId } });
const loading =
deploymentPageLoading ||
scheduledOrPendingDeploymentsLoading ||
latestDeploymentLoading ||
latestLiveDeploymentLoading;
if (loading) {
return <DelayedLoading delay={500} className="mt-12" />;
return (
<ActivityIndicator
delay={500}
className="mt-12"
label="Loading deployments..."
/>
);
}
if (error) {
throw error;
}
const nrOfDeployments = data.deployments.length;
const { deployments } = deploymentPageData || {};
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData || {};
const latestDeployment = latestDeploymentData?.deployments[0];
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
const nrOfDeployments = deployments?.length || 0;
const nextAllowed = !(nrOfDeployments < limit);
const liveDeploymentId = latestLiveDeployment?.id || '';
return (
<div className="mt-6">
@@ -253,15 +131,17 @@ export default function AppDeployments(props: AppDeploymentsProps) {
<p className="text-sm text-greyscaleGrey">No deployments yet.</p>
) : (
<div>
<div className="mt-3 divide-y-1 border-t border-b">
{data.deployments.map((deployment) => (
<AppDeploymentRow
deployment={deployment}
<List className="mt-3 divide-y-1 border-t border-b">
{deployments.map((deployment) => (
<DeploymentListItem
key={deployment.id}
isDeploymentLive={idOfLiveDeployment === deployment.id}
deployment={deployment}
isLive={liveDeploymentId === deployment.id}
showRedeploy={latestDeployment.id === deployment.id}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
/>
))}
</div>
</List>
<div className="mt-8 flex w-full justify-center">
<div className="flex items-center">
<NextPrevPageLink

View File

@@ -1,119 +0,0 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { inputErrorMessages } from '@/utils/getErrorMessage';
import { slugifyString } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { useUpdateWorkspaceMutation } from '@/utils/__generated__/graphql';
import router from 'next/router';
import type { ChangeEvent } from 'react';
import React, { useState } from 'react';
type ChangeWorkspaceNameProps = {
close: VoidFunction;
};
export default function ChangeWorkspaceName({
close,
}: ChangeWorkspaceNameProps) {
const { currentWorkspace } = useCurrentWorkspaceAndApplication();
const [newWorkspaceName, setNewWorkspaceName] = useState(
currentWorkspace.name,
);
const [workspaceError, setWorkspaceError] = useState<string>('');
const [updateWorkspace, { loading: mutationLoading, error: mutationError }] =
useUpdateWorkspaceMutation({
refetchQueries: [],
});
function handleChange(event: ChangeEvent<HTMLInputElement>) {
inputErrorMessages(
event.target.value,
setNewWorkspaceName,
setWorkspaceError,
'Workspace',
);
}
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const name = newWorkspaceName;
const slug = slugifyString(name);
if (slug.length < 4 || slug.length > 32) {
setWorkspaceError('Slug should be within 4 and 32 characters.');
return;
}
try {
await updateWorkspace({
variables: {
id: currentWorkspace.id,
workspace: {
name,
slug,
},
},
});
close();
triggerToast('Workspace name changed');
} catch (error) {
await discordAnnounce(
`Error trying to remove workspace: ${currentWorkspace.id} - ${error.message}`,
);
}
await router.push(slug);
}
return (
<div className="w-modal px-6 py-6 text-left">
<div className="flex flex-col">
<Text variant="h3" component="h2">
Change Workspace Name
</Text>
<form onSubmit={handleSubmit}>
<div className="mt-4 grid grid-flow-row gap-2">
<Input
id="workspaceName"
label="New Workspace Name"
onChange={handleChange}
value={newWorkspaceName}
placeholder="New workspace name"
fullWidth
autoFocus
autoComplete="off"
helperText={`https://app.nhost.io/${slugifyString(
newWorkspaceName || '',
)}`}
/>
{workspaceError && <Alert severity="error">{workspaceError}</Alert>}
{mutationError && (
<Alert severity="error">{mutationError.toString()}</Alert>
)}
</div>
<div className="mt-6 grid grid-flow-row gap-2">
<Button
type="submit"
disabled={mutationLoading || !!workspaceError}
>
Save Changes
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Close
</Button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -1,5 +0,0 @@
export type FunctionLog = {
name: string;
language: string;
logs: { date: string; message: string; createdAt: string }[];
};

View File

@@ -1,30 +0,0 @@
import { Text } from '@/ui/Text';
import { ChevronRightIcon } from '@heroicons/react/solid';
import { formatDistance } from 'date-fns';
export interface FunctionLogDataEntryProps {
time: string;
nav: string;
}
export function FunctionLogDataEntry({ time, nav }: FunctionLogDataEntryProps) {
return (
<a href={`#${nav}`}>
<div className="flex cursor-pointer flex-row place-content-between border-t py-3">
<Text
color="greyscaleDark"
variant="body"
className="flex font-medium"
size="tiny"
>
{formatDistance(new Date(time), new Date(), {
addSuffix: true,
})}
</Text>
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center text-greyscaleDark" />
</div>
</a>
);
}
export default FunctionLogDataEntry;

View File

@@ -1,52 +0,0 @@
import { Text } from '@/ui/Text';
import { FunctionLogDataEntry } from './FunctionLogDataEntry';
export interface FunctionLogHistoryProps {
logs?: Log[];
}
type Log = {
createdAt: string;
date: any;
message: string;
};
export function FunctionLogHistory({ logs }: FunctionLogHistoryProps) {
return (
<div className=" mx-auto max-w-6xl pt-10">
<div className="flex flex-row place-content-between">
<div className="flex">
<Text size="large" className="font-medium" color="greyscaleDark">
Log History
</Text>
</div>
</div>
<div className="mt-5 flex flex-col">
<div className="flex flex-row">
<Text className="font-semibold" size="normal" color="greyscaleDark">
Time
</Text>
</div>
<div className="flex flex-col">
{logs ? (
<div>
{logs.slice(0, 4).map((log: Log) => (
<FunctionLogDataEntry
time={log.createdAt}
nav={`#-${log.date}`}
key={`${log.date}-${log.message.slice(66)}`}
/>
))}
</div>
) : (
<div className="pt-1 pl-0.5 font-mono text-xs text-greyscaleDark">
No log history.
</div>
)}
</div>
</div>
</div>
);
}
export default FunctionLogHistory;

View File

@@ -1,89 +0,0 @@
import { normalizeToIndividualFunctionsWithLogs } from '@/components/applications/functions/normalizeToIndividualFunctionsWithLogs';
import terminalTheme from '@/data/terminalTheme';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetFunctionLogQuery } from '@/utils/__generated__/graphql';
import { useEffect, useState } from 'react';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
import { FunctionLogHistory } from './FunctionLogHistory';
SyntaxHighlighter.registerLanguage('json', json);
export function FunctionsLogsTerminalPage({ functionName }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
const { data, startPolling } = useGetFunctionLogQuery({
variables: {
subdomain: currentApplication.subdomain,
functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
},
});
useEffect(() => {
startPolling(3000);
}, [startPolling]);
useEffect(() => {
if (!data || data.getFunctionLogs.length === 0) {
return;
}
setNormalizedFunctionData(
normalizeToIndividualFunctionsWithLogs(data.getFunctionLogs)[0],
);
}, [data]);
if (
!data ||
data.getFunctionLogs.length === 0 ||
!normalizedFunctionData ||
normalizedFunctionData.logs.length === 0
) {
return (
<div className="w-full rounded-lg text-white">
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
<div className="font-mono text-xs text-grey">
There are no stored logs yet. Try calling your function for logs to
appear.
</div>
</div>
<FunctionLogHistory />
</div>
);
}
return (
<div className="w-full rounded-lg text-white">
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
{normalizedFunctionData.logs.map((log) => (
<div
key={`${log.date}-${log.message.slice(66)}`}
className=" flex text-sm"
>
<div id={`#-${log.date}`}>
<pre className="inline">
<span className="mr-4 text-greyscaleGrey">{log.date}</span>{' '}
<span className="">
{' '}
<SyntaxHighlighter
style={terminalTheme}
customStyle={{
display: 'inline',
}}
className="inline-flex"
language="json"
>
{log.message}
</SyntaxHighlighter>
</span>
</pre>
</div>
</div>
))}
</div>
<FunctionLogHistory logs={normalizedFunctionData.logs} />
</div>
);
}
export default FunctionsLogsTerminalPage;

View File

@@ -1,5 +0,0 @@
export type FunctionResponseLog = {
functionPath: string;
createdAt: string;
message: string;
};

View File

@@ -1,46 +0,0 @@
import { Button } from '@/ui/Button';
import Loading from '@/ui/Loading';
import { Text } from '@/ui/Text';
import Image from 'next/image';
export function FunctionsNotDeployed() {
return (
<div className="mx-auto mt-12 max-w-2xl text-center">
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/terminal-text.svg"
alt="Terminal with a green dot"
width={72}
height={72}
/>
</div>
<Text className="mt-4 font-medium" size="large" color="dark">
Functions Logs
</Text>
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
Once you deploy a function, you can view the logs here.
</Text>
<div className="mt-1.5 flex text-center">
<Button
Component="a"
transparent
color="blue"
className="mx-auto cursor-pointer font-medium"
href="https://docs.nhost.io/platform/serverless-functions"
target="_blank"
rel="noreferrer"
>
Read more
</Button>
</div>
<div className="mt-24 flex flex-col text-center">
<Loading />
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
Awaiting new requests
</Text>
</div>
</div>
);
}
export default FunctionsNotDeployed;

View File

@@ -1,79 +0,0 @@
export type FinalFunction = {
folder: string;
funcs: Func[];
nestedLevel: number;
parentFolder?: string;
};
export type Func = {
name: string;
id: string;
lang: string;
functionName: string;
route?: string;
path?: string;
createdAt?: string;
updatedAt?: string;
createdWithCommitSha?: string;
formattedCreatedAt?: string;
formattedUpdatedAt?: string;
};
export const normalizeFunctionMetadata = (functions): FinalFunction[] => {
const finalFunctions: FinalFunction[] = [
{ folder: 'functions', funcs: [], nestedLevel: 0 },
];
const topLevelFunctionsFolder = finalFunctions[0].funcs;
functions.forEach((func) => {
const nestedLevel = func.path?.split('/').length;
const newFuncToAdd = {
...func,
name: func.path?.split('/')[nestedLevel - 1],
lang: func.path?.split('.')[1],
// formattedCreatedAt: `${format(
// parseISO(func.createdAt),
// 'yyyy-MM-dd HH:mm:ss',
// )}`,
// formattedUpdatedAt: `${formatDistanceToNowStrict(
// parseISO(func.updatedAt),
// {
// addSuffix: true,
// },
// )}`,
};
if (nestedLevel === 2) {
topLevelFunctionsFolder.push(newFuncToAdd);
} else if (nestedLevel > 2) {
const nameOfTheFolder = func.path?.split('/')[nestedLevel - 2];
const nameOfParentFolder = func.path?.split('/')[nestedLevel - 3];
const checkForFolderExistence = finalFunctions.find(
(functionFolder) => functionFolder.folder === nameOfTheFolder,
);
if (!checkForFolderExistence) {
finalFunctions.push({
folder: nameOfTheFolder,
funcs: [newFuncToAdd],
nestedLevel: nestedLevel - 2,
parentFolder: nameOfParentFolder,
});
} else {
checkForFolderExistence.funcs.push(newFuncToAdd);
}
}
});
// Sort folders by putting the subfolder next to their parent folder, even though they share the same place in the array
// except for the nestedLevel prop. A future change to this would be to make folders have subfolders, which is easier
// understand, but would require a change in the UI.
// @TODO: Change to have elements have subfolders inside the object?
finalFunctions.sort((a, b) => {
if (a.folder === b.parentFolder) {
return -1;
}
return 1;
});
return finalFunctions;
};

View File

@@ -1,39 +0,0 @@
import { format, parseISO } from 'date-fns';
import type { FunctionLog } from './FunctionLog';
import type { FunctionResponseLog } from './FunctionResponseLog';
export const normalizeToIndividualFunctionsWithLogs = (
functionLogs: FunctionResponseLog[],
) => {
const arrayOfFunctions: FunctionLog[] = [];
const sortedFunctions = [...functionLogs].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
sortedFunctions.forEach((functionLog) => {
const funcName = functionLog.functionPath;
const logMessage = {
createdAt: functionLog.createdAt,
date: `${format(parseISO(functionLog.createdAt), 'yyyy-MM-dd HH:mm:ss')}`,
message: functionLog.message,
};
const newFunc = {
name: funcName,
language: functionLog.functionPath.split('.')[1],
logs: [logMessage],
};
// If the function is already in the array of functions to log, just add the new log message to the existing object...
if (arrayOfFunctions.some((obj) => obj.name === funcName)) {
const index = arrayOfFunctions.findIndex((obj) => obj.name === funcName);
const currentFunction = arrayOfFunctions[index];
currentFunction.logs.push(logMessage);
} else {
// If the function is not in the array of functions, add it with the log message to it.
arrayOfFunctions.push(newFunc);
}
});
return arrayOfFunctions;
};
export default normalizeToIndividualFunctionsWithLogs;

View File

@@ -34,13 +34,13 @@ export function Repo({ repo, setSelectedRepoId }: RepoProps) {
const [updateApp, { loading, error }] = useUpdateAppMutation({
refetchQueries: [
refetchGetAppByWorkspaceAndNameQuery({
workspace: currentWorkspace.slug,
slug: currentApplication.slug,
workspace: currentWorkspace?.slug,
slug: currentApplication?.slug,
}),
],
});
const { githubRepository } = currentApplication;
const { githubRepository } = currentApplication || {};
const isThisRepositoryAlreadyConnected =
githubRepository?.fullName && githubRepository.fullName === repo.fullName;

View File

@@ -52,9 +52,9 @@ function ControlledAutocomplete(
return (
<Autocomplete
inputValue={typeof field.value === 'string' ? field.value : undefined}
{...props}
{...field}
inputValue={typeof field.value === 'string' ? field.value : undefined}
ref={mergeRefs([field.ref, ref])}
onChange={(event, options, reason, details) => {
setValue?.(controllerProps?.name || name, options, {

View File

@@ -6,6 +6,7 @@ import { createContext } from 'react';
* Available dialog types.
*/
export type DialogType =
| 'EDIT_WORKSPACE_NAME'
| 'CREATE_RECORD'
| 'CREATE_COLUMN'
| 'EDIT_COLUMN'

View File

@@ -1,6 +1,7 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
@@ -366,6 +367,10 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
<RetryableErrorBoundary
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
>
{activeDialogType === 'EDIT_WORKSPACE_NAME' && (
<EditWorkspaceNameForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
<CreateForeignKeyForm {...sharedDialogProps} />
)}

View File

@@ -1,13 +1,11 @@
import { ChangePasswordModal } from '@/components/applications/ChangePasswordModal';
import { useWorkspaceContext } from '@/context/workspace-context';
import { useUserDataContext } from '@/context/workspace1-context';
import { Avatar } from '@/ui/Avatar';
import { Modal } from '@/ui/Modal';
import Button from '@/ui/v2/Button';
import { Dropdown, useDropdown } from '@/ui/v2/Dropdown';
import Text from '@/ui/v2/Text';
import { emptyWorkspace } from '@/utils/helpers';
import { nhost } from '@/utils/nhost';
import { useApolloClient } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import { useRouter } from 'next/router';
@@ -22,9 +20,8 @@ function AccountMenuContent({
}: AccountMenuContentProps) {
const user = useUserData();
const router = useRouter();
const client = useApolloClient();
const [clicked, setClicked] = useState(false);
const { setWorkspaceContext } = useWorkspaceContext();
const { setUserContext } = useUserDataContext();
const { handleClose } = useDropdown();
return (
@@ -34,10 +31,9 @@ function AccountMenuContent({
color="secondary"
className="absolute top-6 right-4 grid grid-flow-col items-center gap-1 self-start font-medium"
onClick={async () => {
setWorkspaceContext(emptyWorkspace());
setUserContext({ workspaces: [] });
nhost.auth.signOut();
router.push('/signin');
await nhost.auth.signOut();
await client.resetStore();
}}
aria-label="Sign Out"
>

View File

@@ -118,6 +118,7 @@ export default function BaseColumnForm({
variant="inline"
className="col-span-8 py-3"
autoFocus
autoComplete="off"
/>
<ControlledAutocomplete
@@ -272,6 +273,7 @@ export default function BaseColumnForm({
error={Boolean(errors.comment)}
variant="inline"
className="col-span-8 py-3"
autoComplete="off"
/>
</section>
</div>

View File

@@ -88,6 +88,7 @@ function NameInput() {
error={Boolean(errors.name)}
variant="inline"
className="col-span-8 py-3"
autoComplete="off"
autoFocus
/>
);

View File

@@ -70,6 +70,7 @@ function NameInput({ index }: FieldArrayInputProps) {
}
},
})}
autoComplete="off"
aria-label="Name"
placeholder="Enter name"
hideEmptyHelperText

View File

@@ -13,6 +13,7 @@ import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
import { useTheme } from '@mui/material';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import PermissionSettingsSection from './PermissionSettingsSection';
@@ -41,6 +42,7 @@ export default function ColumnPresetsSection({
table,
disabled,
}: ColumnPresetSectionProps) {
const theme = useTheme();
const {
data: tableData,
status: tableStatus,
@@ -131,7 +133,12 @@ export default function ColumnPresetsSection({
freeSolo
fullWidth
disableClearable={false}
clearIcon={<XIcon />}
clearIcon={
<XIcon
className="w-4 h-4 mt-px"
sx={{ color: theme.palette.text.primary }}
/>
}
autoSelect
autoHighlight={false}
error={Boolean(

View File

@@ -4,7 +4,17 @@ import * as Yup from 'yup';
const ruleSchema = Yup.object().shape({
column: Yup.string().nullable().required('Please select a column.'),
operator: Yup.string().nullable().required('Please select an operator.'),
value: Yup.string().nullable().required('Please enter a value.'),
value: Yup.mixed()
.test(
'isArray',
'Please enter a valid value.',
(value) =>
typeof value === 'string' ||
(Array.isArray(value) &&
value.every((item) => typeof item === 'string')),
)
.nullable()
.required('Please enter a value.'),
});
const ruleGroupSchema = Yup.object().shape({

View File

@@ -0,0 +1,60 @@
import { differenceInSeconds, parseISO } from 'date-fns';
import { useEffect, useState } from 'react';
export interface AppDeploymentDurationProps {
/**
* Start date of the deployment.
*/
startedAt: string;
/**
* End date of the deployment.
*/
endedAt?: string;
}
export default function AppDeploymentDuration({
startedAt,
endedAt,
}: AppDeploymentDurationProps) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
let interval: NodeJS.Timeout;
if (!endedAt) {
interval = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
}
return () => {
clearInterval(interval);
};
}, [endedAt]);
const totalDurationInSeconds = differenceInSeconds(
endedAt ? parseISO(endedAt) : currentTime,
parseISO(startedAt),
);
if (totalDurationInSeconds > 1200) {
return <div>20+m</div>;
}
const durationMins = Math.floor(totalDurationInSeconds / 60);
const durationSecs = totalDurationInSeconds % 60;
return (
<div
style={{ fontVariantNumeric: 'tabular-nums' }}
className="self-center font-display text-sm+ text-greyscaleDark"
>
{Number.isNaN(durationMins) || Number.isNaN(durationSecs) ? (
<span>0m 0s</span>
) : (
<span>
{durationMins}m {durationSecs}s
</span>
)}
</div>
);
}

View File

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

View File

@@ -0,0 +1,161 @@
import NavLink from '@/components/common/NavLink';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import Button from '@/ui/v2/Button';
import ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import { ListItem } from '@/ui/v2/ListItem';
import Tooltip from '@/ui/v2/Tooltip';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DeploymentListItemProps {
/**
* Deployment data.
*/
deployment: DeploymentRowFragment;
/**
* Determines whether or not the deployment is live.
*/
isLive?: boolean;
/**
* Determines whether or not the redeploy button should be shown for the
* deployment.
*/
showRedeploy?: boolean;
/**
* Determines whether or not the redeploy button is disabled.
*/
disableRedeploy?: boolean;
}
export default function DeploymentListItem({
deployment,
isLive,
showRedeploy,
disableRedeploy,
}: DeploymentListItemProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const relativeDateOfDeployment = deployment.deploymentStartedAt
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
addSuffix: true,
})
: '';
const [insertDeployment, { loading }] = useInsertDeploymentMutation();
const { commitMessage } = deployment;
return (
<ListItem.Root>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<ListItem.Avatar>
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8 shrink-0"
/>
</ListItem.Avatar>
<ListItem.Text
primary={
commitMessage?.trim() || (
<span className="truncate pr-1 font-normal italic">
No commit message
</span>
)
}
secondary={relativeDateOfDeployment}
/>
</div>
<div className="grid grid-flow-col gap-2 items-center">
{showRedeploy && (
<Tooltip
title="Deployments cannot be re-triggered when a deployment is in progress."
hasDisabledChildren={disableRedeploy || loading}
disableHoverListener={!disableRedeploy}
>
<Button
disabled={disableRedeploy || loading}
size="small"
color="secondary"
variant="outlined"
onClick={async (event) => {
event.stopPropagation();
event.preventDefault();
const insertDeploymentPromise = insertDeployment({
variables: {
object: {
appId: currentApplication?.id,
commitMessage: deployment.commitMessage,
commitSHA: deployment.commitSHA,
commitUserAvatarUrl: deployment.commitUserAvatarUrl,
commitUserName: deployment.commitUserName,
deploymentStatus: 'SCHEDULED',
},
},
});
await toast.promise(
insertDeploymentPromise,
{
loading: 'Scheduling deployment...',
success: 'Deployment has been scheduled successfully.',
error: 'An error occurred when scheduling deployment.',
},
toastStyleProps,
);
}}
startIcon={
<ArrowCounterclockwiseIcon className={twMerge('w-4 h-4')} />
}
className="rounded-full py-1 px-2 text-xs"
>
Redeploy
</Button>
</Tooltip>
)}
{isLive && (
<div className="w-12 flex justify-end">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-16 text-right font-mono text-sm- font-medium">
{deployment.commitSHA.substring(0, 7)}
</div>
<div className="w-[80px] text-right font-mono text-sm- font-medium">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
<ChevronRightIcon className="h-4 w-4" />
</div>
</ListItem.Button>
</ListItem.Root>
);
}

View File

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

View File

@@ -37,7 +37,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
parseInt(router.query.page as string, 10) - 1 || 0,
);
const [sortBy, setSortBy] = useState<SortingRule<StoredFile>[]>();
const limit = 25;
const limit = 10;
const emptyStateMessage = searchString
? 'No search results found.'
: 'No files are uploaded yet.';

View File

@@ -0,0 +1,239 @@
import Form from '@/components/common/Form';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import {
refetchGetOneUserQuery,
useInsertWorkspaceMutation,
useUpdateWorkspaceMutation,
} from '@/utils/__generated__/graphql';
import { slugifyString } from '@/utils/helpers';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
export interface EditWorkspaceNameFormProps {
/**
* The current workspace name if this is an edit operation.
*/
currentWorkspaceName?: string;
/**
* The current workspace name id if this is an edit operation.
*/
currentWorkspaceId?: string;
/**
* Determines whether the form is disabled.
*/
disabled?: boolean;
/**
* Submit button text.
*
* @default 'Create'
*/
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 EditWorkspaceNameFormValues {
/**
* New workspace name.
*/
newWorkspaceName: string;
}
const validationSchema = Yup.object().shape({
newWorkspaceName: Yup.string()
.required('Workspace name is required.')
.min(4, 'The new Workspace name must be at least 4 characters.')
.max(32, "The new Workspace name can't be longer than 32 characters.")
.test(
'canBeSlugified',
`This field should be at least 4 characters and can't be longer than 32 characters.`,
(value) => {
const slug = slugifyString(value);
if (slug.length < 4 || slug.length > 32) {
return false;
}
return true;
},
),
});
export default function EditWorkspaceName({
disabled,
onSubmit,
onCancel,
currentWorkspaceName,
currentWorkspaceId,
submitButtonText = 'Create',
}: EditWorkspaceNameFormProps) {
const currentUser = useUserData();
const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
const [updateWorkspaceName] = useUpdateWorkspaceMutation({
refetchQueries: [
refetchGetOneUserQuery({
userId: currentUser.id,
}),
],
awaitRefetchQueries: true,
ignoreResults: true,
});
const router = useRouter();
const form = useForm<EditWorkspaceNameFormValues>({
defaultValues: {
newWorkspaceName: currentWorkspaceName || '',
},
resolver: yupResolver(validationSchema),
});
const {
register,
formState: { dirtyFields, isSubmitting, errors },
} = form;
const isDirty = Object.keys(dirtyFields).length > 0;
async function handleSubmit({
newWorkspaceName,
}: EditWorkspaceNameFormValues) {
const slug = slugifyString(newWorkspaceName);
try {
if (currentWorkspaceId) {
// In this bit of code we spread the props of the current path (e.g. /workspace/...) and add one key-value pair: `mutating: true`.
// We want to indicate that the currently we're in the process of running a mutation state that will affect the routing behaviour of the website
// i.e. redirecting to 404 if there's no workspace/project with that slug.
await router.replace({
pathname: router.pathname,
query: { ...router.query, updating: true },
});
await toast.promise(
updateWorkspaceName({
variables: {
id: currentWorkspaceId,
workspace: {
name: newWorkspaceName,
slug,
},
},
}),
{
loading: 'Updating workspace name...',
success: 'Workspace name has been updated successfully.',
error: 'An error occurred while updating the workspace name.',
},
toastStyleProps,
);
} else {
await toast.promise(
insertWorkspace({
variables: {
workspace: {
name: newWorkspaceName,
companyName: newWorkspaceName,
email: currentUser.email,
slug,
workspaceMembers: {
data: [
{
userId: currentUser.id,
type: 'owner',
},
],
},
},
},
}),
{
loading: 'Creating new workspace...',
success: 'The new workspace has been created successfully.',
error: 'An error occurred while creating the new workspace.',
},
toastStyleProps,
);
}
} catch (error) {
if (error.message?.includes('duplicate key value')) {
form.setError(
'newWorkspaceName',
{
type: 'manual',
message: 'This workspace name is already taken.',
},
{
shouldFocus: false,
},
);
}
return;
}
await client.refetchQueries({
include: ['getOneUser'],
});
await router.push(slug);
onSubmit?.();
}
return (
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex flex-col content-between flex-auto pt-2 pb-6 overflow-hidden"
>
<div className="flex-auto px-6 overflow-y-auto">
<Input
{...register('newWorkspaceName')}
error={Boolean(errors.newWorkspaceName?.message)}
label="Name"
helperText={errors.newWorkspaceName?.message}
autoFocus={!disabled}
disabled={disabled}
fullWidth
hideEmptyHelperText
placeholder='e.g. "My Workspace"'
/>
</div>
<div className="grid flex-shrink-0 grid-flow-row gap-2 px-6 pt-4">
{!disabled && (
<Button
loading={isSubmitting}
disabled={
isSubmitting || Boolean(errors.newWorkspaceName?.message)
}
type="submit"
>
{currentWorkspaceName ? 'Save' : 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,3 @@
export * from './EditWorkspaceNameForm';
export { default } from './EditWorkspaceNameForm';

View File

@@ -1,22 +1,33 @@
import { useInsertFeedbackOneMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { nhost } from '@/utils/nhost';
import { useUserData } from '@nhost/nextjs';
import * as React from 'react';
export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [insertFeedback, { loading }] = useInsertFeedbackOneMutation();
const user = nhost.auth.getUser();
const user = useUserData();
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const feedbackWithProjectInfo = [
currentApplication && `Project ID: ${currentApplication.id}`,
typeof window !== 'undefined' && `URL: ${window.location.href}`,
feedback,
]
.filter(Boolean)
.join('\n\n');
try {
await insertFeedback({
variables: {
feedback: {
feedback,
feedback: feedbackWithProjectInfo,
},
},
});

View File

@@ -4,11 +4,8 @@ import { InviteAnnounce } from '@/components/home/InviteAnnounce';
import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
import BaseLayout from '@/components/layout/BaseLayout';
import Container from '@/components/layout/Container';
import AddWorkspace from '@/components/workspace/AddWorkspace';
import { useUI } from '@/context/UIContext';
import useIsHealthy from '@/hooks/common/useIsHealthy';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { Modal } from '@/ui';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
@@ -39,7 +36,6 @@ export default function AuthenticatedLayout({
}: AuthenticatedLayoutProps) {
const router = useRouter();
const isPlatform = useIsPlatform();
const { newWorkspace, closeSection } = useUI();
const { isAuthenticated, isLoading } = useAuthenticationStatus();
const isHealthy = useIsHealthy();
@@ -85,7 +81,7 @@ export default function AuthenticatedLayout({
<BaseLayout {...props}>
<Header className="flex max-h-[59px] flex-auto" />
<Container className="my-12 grid max-w-md grid-flow-row justify-center gap-2 text-center">
<Container className="grid justify-center max-w-md grid-flow-row gap-2 my-12 text-center">
<div className="mx-auto">
<Image
src="/terminal-text.svg"
@@ -123,13 +119,7 @@ export default function AuthenticatedLayout({
}
return (
<BaseLayout className="flex h-full flex-col" {...props}>
<Modal
showModal={newWorkspace}
close={closeSection}
Component={AddWorkspace}
/>
<BaseLayout className="flex flex-col h-full" {...props}>
<Header className="flex max-h-[59px] flex-auto" />
<InviteAnnounce />

View File

@@ -1,25 +1,21 @@
import { AppDeploymentDuration } from '@/components/applications/AppDeployments';
import { EditRepositorySettings } from '@/components/applications/github/EditRepositorySettings';
import useGitHubModal from '@/components/applications/github/useGitHubModal';
import { useDialog } from '@/components/common/DialogProvider';
import NavLink from '@/components/common/NavLink';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import GithubIcon from '@/components/icons/GithubIcon';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Status, { StatusEnum } from '@/ui/Status';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import type { ListItemRootProps } from '@/ui/v2/ListItem';
import { ListItem } from '@/ui/v2/ListItem';
import List from '@/ui/v2/List';
import Text from '@/ui/v2/Text';
import { getLastLiveDeployment } from '@/utils/helpers';
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
import { useGetDeploymentsSubSubscription } from '@/utils/__generated__/graphql';
import {
useGetDeploymentsSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
function OverviewDeploymentsTopBar() {
@@ -54,92 +50,6 @@ function OverviewDeploymentsTopBar() {
);
}
interface OverviewDeployProps extends ListItemRootProps {
/**
* Deployment metadata to display.
*/
deployment: DeploymentRowFragment;
/**
* Determines to show a status badge showing the live status of a deployment reflecting the latest state of the application.
*/
isDeploymentLive: boolean;
}
function OverviewDeployment({
deployment,
isDeploymentLive,
className,
}: OverviewDeployProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const relativeDateOfDeployment = formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
);
const { commitMessage } = deployment;
return (
<ListItem.Root className={twMerge('grid grid-flow-row', className)}>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<div>
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="h-8 w-8"
/>
</div>
<div className="grid grid-flow-row truncate text-sm+ font-medium">
<Text className="inline cursor-pointer truncate font-medium leading-snug text-greyscaleDark">
{commitMessage?.trim() || (
<span className="truncate pr-1 font-normal italic">
No commit message
</span>
)}
</Text>
<Text className="text-sm font-normal leading-[1.375rem] text-greyscaleGrey">
{relativeDateOfDeployment}
</Text>
</div>
</div>
<div className="grid grid-flow-col items-center self-center">
{isDeploymentLive && (
<div className="flex self-center align-middle">
<Status status={StatusEnum.Live}>Live</Status>
</div>
)}
<div className="w-20 self-center text-right align-middle font-mono text-sm- font-medium">
{deployment.commitSHA.substring(0, 7)}
</div>
<div className="w-20 self-center text-right align-middle font-mono text-sm-">
<AppDeploymentDuration
startedAt={deployment.deploymentStartedAt}
endedAt={deployment.deploymentEndedAt}
/>
</div>
<div className="mx-3 self-center">
<StatusCircle
status={deployment.deploymentStatus as DeploymentStatus}
/>
</div>
<div className="self-center">
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</div>
</ListItem.Button>
</ListItem.Root>
);
}
interface OverviewDeploymentsProps {
projectId: string;
githubRepository: { fullName: string };
@@ -159,9 +69,18 @@ function OverviewDeployments({
},
});
if (loading) {
const {
data: scheduledOrPendingDeploymentsData,
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({
variables: {
appId: projectId,
},
});
if (loading || scheduledOrPendingDeploymentsLoading) {
return (
<div style={{ height: '240px' }}>
<div className="h-60">
<ActivityIndicator label="Loading deployments..." />
</div>
);
@@ -218,22 +137,22 @@ function OverviewDeployments({
);
}
const getLastLiveDeploymentId = getLastLiveDeployment(deployments);
const liveDeploymentId = getLastLiveDeployment(deployments);
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData;
return (
<div className="rounded-x-lg flex flex-col divide-y-1 divide-gray-200 rounded-lg border border-veryLightGray">
{deployments.map((deployment) => {
const isDeploymentLive = deployment.id === getLastLiveDeploymentId;
return (
<OverviewDeployment
key={deployment.id}
deployment={deployment}
isDeploymentLive={isDeploymentLive}
/>
);
})}
</div>
<List className="rounded-x-lg flex flex-col divide-y-1 divide-gray-200 rounded-lg border border-veryLightGray">
{deployments.map((deployment, index) => (
<DeploymentListItem
key={deployment.id}
deployment={deployment}
isLive={deployment.id === liveDeploymentId}
showRedeploy={index === 0}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
/>
))}
</List>
);
}

View File

@@ -5,12 +5,16 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface AllowedEmailSettingsFormValues {
/**
* Determines whether or not the allowed email settings are enabled.
*/
enabled: boolean;
/**
* Set of email that are allowed to be used for project's users authentication.
*/
@@ -25,7 +29,6 @@ export interface AllowedEmailSettingsFormValues {
export default function AllowedEmailDomainsSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
@@ -36,12 +39,30 @@ export default function AllowedEmailDomainsSettings() {
const form = useForm<AllowedEmailSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled:
Boolean(data?.app?.authAccessControlAllowedEmails) ||
Boolean(data?.app?.authAccessControlAllowedEmailDomains),
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
authAccessControlAllowedEmailDomains:
data?.app?.authAccessControlAllowedEmailDomains,
},
});
const { register, formState, setValue, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (
!data.app?.authAccessControlAllowedEmails &&
!data.app?.authAccessControlAllowedEmailDomains
) {
return;
}
setValue('enabled', true, { shouldDirty: false });
}, [data.app, setValue]);
if (loading) {
return (
<ActivityIndicator
@@ -56,8 +77,6 @@ export default function AllowedEmailDomainsSettings() {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: AllowedEmailSettingsFormValues,
) => {
@@ -65,7 +84,12 @@ export default function AllowedEmailDomainsSettings() {
variables: {
id: currentApplication.id,
app: {
...values,
authAccessControlAllowedEmails: values.enabled
? values.authAccessControlAllowedEmails
: '',
authAccessControlAllowedEmailDomains: values.enabled
? values.authAccessControlAllowedEmailDomains
: '',
},
},
});
@@ -89,13 +113,17 @@ export default function AllowedEmailDomainsSettings() {
<SettingsContainer
title="Allowed Emails and Domains"
description="Allow specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
slotProps={{
submitButton: {
disabled: !formState.isValid || !isDirty,
loading: formState.isSubmitting,
},
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains"
enabled={enabled}
onEnabledChange={setEnabled}
onEnabledChange={(switchEnabled) =>
setValue('enabled', switchEnabled, { shouldDirty: true })
}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',

View File

@@ -84,7 +84,7 @@ export default function AllowedRedirectURLsSettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls"
className="grid grid-flow-row px-4 lg:grid-cols-5"
>
<Input

View File

@@ -5,12 +5,16 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface BlockedEmailFormValues {
/**
* Determines whether or not the blocked email settings are enabled.
*/
enabled: boolean;
/**
* Set of emails that are blocked from registering to the user's project.
*/
@@ -24,7 +28,6 @@ export interface BlockedEmailFormValues {
export default function BlockedEmailSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
@@ -35,12 +38,30 @@ export default function BlockedEmailSettings() {
const form = useForm<BlockedEmailFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled:
Boolean(data?.app?.authAccessControlBlockedEmails) ||
Boolean(data?.app?.authAccessControlBlockedEmailDomains),
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
authAccessControlBlockedEmailDomains:
data?.app?.authAccessControlBlockedEmailDomains,
},
});
const { register, formState, setValue, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (
!data.app?.authAccessControlBlockedEmails &&
!data.app?.authAccessControlBlockedEmailDomains
) {
return;
}
setValue('enabled', true, { shouldDirty: false });
}, [data.app, setValue]);
if (loading) {
return (
<ActivityIndicator
@@ -55,8 +76,6 @@ export default function BlockedEmailSettings() {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: BlockedEmailFormValues,
) => {
@@ -64,7 +83,12 @@ export default function BlockedEmailSettings() {
variables: {
id: currentApplication.id,
app: {
...values,
authAccessControlBlockedEmails: values.enabled
? values.authAccessControlBlockedEmails
: '',
authAccessControlBlockedEmailDomains: values.enabled
? values.authAccessControlBlockedEmailDomains
: '',
},
},
});
@@ -88,13 +112,17 @@ export default function BlockedEmailSettings() {
<SettingsContainer
title="Blocked Emails and Domains"
description="Block specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
slotProps={{
submitButton: {
disabled: !formState.isValid || !isDirty,
loading: formState.isSubmitting,
},
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains"
enabled={enabled}
onEnabledChange={setEnabled}
onEnabledChange={(switchEnabled) =>
setValue('enabled', switchEnabled, { shouldDirty: true })
}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',

View File

@@ -82,7 +82,7 @@ export default function ClientURLSettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#client-url"
className="grid grid-flow-row lg:grid-cols-5"
>
<Input

View File

@@ -89,7 +89,7 @@ export default function DisableNewUsersSettings() {
<SettingsContainer
title="Disable New Users"
description="If set, newly registered users are disabled and wont be able to sign in."
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#disable-new-users"
switchId="authDisableNewUsers"
showSwitch
enabled={authDisableNewUsers}

View File

@@ -110,7 +110,7 @@ export default function GravatarSettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#gravatar"
switchId="authGravatarEnabled"
showSwitch
enabled={authGravatarEnabled}

View File

@@ -99,7 +99,7 @@ export default function MFASettings() {
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication"
switchId="authMfaEnabled"
enabled={authMfaEnabled}
showSwitch

View File

@@ -58,10 +58,10 @@ export default function BaseRoleForm({
return (
<div className="grid grid-flow-row gap-3 px-6 pb-6">
<Text variant="subtitle1" component="span">
Enter the name for the role below.
Enter the name for the allowed role below.
</Text>
{submitButtonText !== 'Create' && (
{submitButtonText !== 'Add' && (
<Alert severity="warning" className="text-left">
<span className="text-left">
<strong>Note:</strong> Changing the name of the role will lose the

View File

@@ -85,11 +85,7 @@ export default function CreateRoleForm({
return (
<FormProvider {...form}>
<BaseRoleForm
submitButtonText="Create"
onSubmit={handleSubmit}
{...props}
/>
<BaseRoleForm submitButtonText="Add" onSubmit={handleSubmit} {...props} />
</FormProvider>
);
}

View File

@@ -8,18 +8,18 @@ import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import DotsVerticalIcon from '@/ui/v2/icons/DotsVerticalIcon';
import LockIcon from '@/ui/v2/icons/LockIcon';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import {
useGetRolesQuery,
useUpdateAppMutation
} from '@/utils/__generated__/graphql';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
useGetRolesQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { Fragment } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
@@ -98,9 +98,9 @@ export default function RoleSettings() {
await toast.promise(
updateAppPromise,
{
loading: 'Deleting role...',
success: 'Role has been deleted successfully.',
error: 'An error occurred while trying to delete the role.',
loading: 'Deleting allowed role...',
success: 'Allowed Role has been deleted successfully.',
error: 'An error occurred while trying to delete the allowed role.',
},
toastStyleProps,
);
@@ -108,7 +108,7 @@ export default function RoleSettings() {
function handleOpenCreator() {
openDialog('CREATE_ROLE', {
title: 'Create Role',
title: 'Create Allowed Role',
props: {
titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' },
@@ -118,7 +118,7 @@ export default function RoleSettings() {
function handleOpenEditor(originalRole: Role) {
openDialog('EDIT_ROLE', {
title: 'Edit Role',
title: 'Edit Allowed Role',
payload: { originalRole },
props: {
titleProps: { className: '!pb-0' },
@@ -129,12 +129,11 @@ export default function RoleSettings() {
function handleConfirmDelete(originalRole: Role) {
openAlertDialog({
title: 'Delete Role',
title: 'Delete Allowed Role',
payload: (
<Text>
Are you sure you want to delete the &quot;
<strong>{originalRole.name}</strong>&quot; role? This cannot be
undone.
Are you sure you want to delete the allowed role &quot;
<strong>{originalRole.name}</strong>&quot;?.
</Text>
),
props: {
@@ -145,13 +144,15 @@ export default function RoleSettings() {
});
}
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
const availableAllowedRoles = getUserRoles(
data?.app?.authUserDefaultAllowedRoles,
);
return (
<SettingsContainer
title="Roles"
description="Roles are used to control access to your application."
docsLink="https://docs.nhost.io/authentication/users#roles"
title="Allowed Roles"
description="Allowed roles are roles users get automatically when they sign up."
docsLink="https://docs.nhost.io/authentication/users#allowed-roles"
rootClassName="gap-0"
className="px-0 my-2"
slotProps={{ submitButton: { className: 'invisible' } }}
@@ -162,7 +163,7 @@ export default function RoleSettings() {
<div className="grid grid-flow-row gap-2">
<List>
{availableRoles.map((role, index) => (
{availableAllowedRoles.map((role, index) => (
<Fragment key={role.name}>
<ListItem.Root
className="px-4"
@@ -249,7 +250,9 @@ export default function RoleSettings() {
<Divider
component="li"
className={twMerge(
index === availableRoles.length - 1 ? '!mt-4' : '!my-4',
index === availableAllowedRoles.length - 1
? '!mt-4'
: '!my-4',
)}
/>
</Fragment>
@@ -262,7 +265,7 @@ export default function RoleSettings() {
startIcon={<PlusIcon />}
onClick={handleOpenCreator}
>
Create Role
Create Allowed Role
</Button>
</div>
</SettingsContainer>

View File

@@ -80,7 +80,7 @@ export default function MagicLinkSettings() {
<Form onSubmit={handleMagicLinkSettingsUpdate}>
<SettingsContainer
title="Magic Link"
description="Allow users to sign in with a magic link."
description="Allow users to sign in with a Magic Link."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,

View File

@@ -90,7 +90,7 @@ export default function SMSSettings() {
<FormProvider {...form}>
<Form onSubmit={handleSMSSettingsChange}>
<SettingsContainer
title="SMS"
title="Phone Number (SMS)"
description="Allow users to sign in with Phone Number (SMS)."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,

View File

@@ -1,9 +1,11 @@
import clsx from 'clsx';
import { twMerge } from 'tailwind-merge';
export type DeploymentStatus =
| 'DEPLOYING'
| 'DEPLOYED'
| 'FAILED'
| 'PENDING'
| 'SCHEDULED'
| undefined
| null;
@@ -12,32 +14,30 @@ type StatusCircleProps = {
className?: string;
};
export function StatusCircle(props: StatusCircleProps) {
const { status, className } = props;
export function StatusCircle({ status, className }: StatusCircleProps) {
const baseClasses = 'w-1.5 h-1.5 rounded-full';
if (!status) {
const classes = clsx(baseClasses, 'bg-gray-300', className);
return <div className={classes} />;
}
if (status === 'DEPLOYING') {
const classes = clsx(baseClasses, 'bg-yellow-300', className);
return <div className={classes} />;
if (status === 'DEPLOYING' || status === 'PENDING') {
return (
<div
className={twMerge(
baseClasses,
'bg-yellow-300 animate-pulse',
className,
)}
/>
);
}
if (status === 'DEPLOYED') {
const classes = clsx(baseClasses, 'bg-green-300', className);
return <div className={classes} />;
return <div className={twMerge(baseClasses, 'bg-green-300', className)} />;
}
if (status === 'FAILED') {
const classes = clsx(baseClasses, 'bg-red', className);
return <div className={classes} />;
return <div className={twMerge(baseClasses, 'bg-red', className)} />;
}
return null;
return <div className={twMerge(baseClasses, 'bg-gray-300', className)} />;
}
export default StatusCircle;

View File

@@ -0,0 +1,17 @@
import { styled } from '@mui/material';
import type { ListItemAvatarProps as MaterialListItemAvatarProps } from '@mui/material/ListItemAvatar';
import MaterialListItemAvatar from '@mui/material/ListItemAvatar';
export interface ListItemAvatarProps extends MaterialListItemAvatarProps {}
const StyledListItemAvatar = styled(MaterialListItemAvatar)({
minWidth: 0,
});
function ListItemAvatar({ children, ...props }: ListItemAvatarProps) {
return <StyledListItemAvatar {...props}>{children}</StyledListItemAvatar>;
}
ListItemAvatar.displayName = 'NhostListItemAvatar';
export default ListItemAvatar;

View File

@@ -1,8 +1,11 @@
import ListItemAvatar from './ListItemAvatar';
import ListItemButton from './ListItemButton';
import ListItemIcon from './ListItemIcon';
import ListItemRoot from './ListItemRoot';
import ListItemText from './ListItemText';
export * from './ListItemAvatar';
export { default as ListItemAvatar } from './ListItemAvatar';
export * from './ListItemButton';
export { default as ListItemButton } from './ListItemButton';
export * from './ListItemIcon';
@@ -13,6 +16,7 @@ export * from './ListItemText';
export { default as ListItemText } from './ListItemText';
export const ListItem = {
Avatar: ListItemAvatar,
Root: ListItemRoot,
Button: ListItemButton,
Icon: ListItemIcon,

View File

@@ -0,0 +1,34 @@
import type { IconProps } from '@/ui/v2/icons';
import SvgIcon from '@mui/material/SvgIcon';
function ArrowCounterclockwiseIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="A counterclockwise arrow"
{...props}
>
<path
d="M4.99 6.232h-3v-3"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M4.11 11.89a5.5 5.5 0 1 0 0-7.78L1.99 6.233"
stroke="currentColor"
fill="none"
strokeWidth="1.5"
strokeLinejoin="round"
/>
</SvgIcon>
);
}
ArrowCounterclockwiseIcon.displayName = 'NhostArrowCounterclockwiseIcon';
export default ArrowCounterclockwiseIcon;

View File

@@ -0,0 +1 @@
export { default } from './ArrowCounterclockwiseIcon';

View File

@@ -67,8 +67,8 @@ export default function CreateUserForm({
} = form;
const baseAuthUrl = generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
currentApplication?.subdomain,
currentApplication?.region?.awsName,
'auth',
);

View File

@@ -8,18 +8,18 @@ import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputLabel from '@/ui/v2/InputLabel';
import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
useGetRolesQuery,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { Avatar } from '@mui/material';
import { format } from 'date-fns';
@@ -137,7 +137,7 @@ export default function EditUserForm({
}
const { data: dataRoles } = useGetRolesQuery({
variables: { id: currentApplication.id },
variables: { id: currentApplication?.id },
});
const allAvailableProjectRoles = getUserRoles(
@@ -206,11 +206,7 @@ export default function EditUserForm({
</div>
<div>
<Dropdown.Root>
<Dropdown.Trigger
autoFocus={false}
asChild
className="gap-2"
>
<Dropdown.Trigger autoFocus={false} asChild className="gap-2">
<Button variant="outlined" color="secondary">
Actions
</Button>

View File

@@ -7,12 +7,14 @@ import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import DotsHorizontalIcon from '@/ui/v2/icons/DotsHorizontalIcon';
import TrashIcon from '@/ui/v2/icons/TrashIcon';
import UserIcon from '@/ui/v2/icons/UserIcon';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
import {
useDeleteRemoteAppUserRolesMutation,
@@ -21,9 +23,6 @@ import {
useRemoteAppDeleteUserMutation,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { ApolloQueryResult } from '@apollo/client';
import { Avatar } from '@mui/material';
import { formatDistance } from 'date-fns';
@@ -77,7 +76,7 @@ export default function UsersBody({
* in the drawer form.
*/
const { data: dataRoles } = useGetRolesQuery({
variables: { id: currentApplication.id },
variables: { id: currentApplication?.id },
});
const allAvailableProjectRoles = useMemo(

View File

@@ -1,159 +0,0 @@
import { useUI } from '@/context/UIContext';
import { useInsertWorkspaceMutation } from '@/generated/graphql';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { getErrorMessage, inputErrorMessages } from '@/utils/getErrorMessage';
import { slugifyString } from '@/utils/helpers';
import { nhost } from '@/utils/nhost';
import { triggerToast } from '@/utils/toast';
import router from 'next/router';
import React, { useState } from 'react';
import slugify from 'slugify';
function AddNewWorkspaceForm({
closeSection: externalCloseSection,
}: {
closeSection: VoidFunction;
}) {
const [workspace, setWorkspace] = useState('');
const { closeSection } = useUI();
const [workspaceError, setWorkspaceError] = useState<string>('');
const [loadingAddWorkspace, setLoadingAddWorkspace] = useState(false);
const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
const slug = slugify(workspace, { lower: true, strict: true });
const user = nhost.auth.getUser();
if (!user) {
return <div>No user..</div>;
}
const userId = user.id;
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
setWorkspaceError('');
setLoadingAddWorkspace(true);
if (
!inputErrorMessages(
workspace,
setWorkspace,
setWorkspaceError,
'Workspace',
)
) {
return;
}
if (slug.length < 4 || slug.length > 32) {
setWorkspaceError('Slug should be within 4 and 32 characters.');
setLoadingAddWorkspace(false);
return;
}
const currentUser = nhost.auth.getUser();
if (!currentUser) {
triggerToast('User is not signed in');
setLoadingAddWorkspace(false);
return;
}
try {
await insertWorkspace({
variables: {
workspace: {
name: workspace,
companyName: workspace,
email: user.email,
slug,
workspaceMembers: {
data: [
{
userId,
type: 'owner',
},
],
},
},
},
});
await client.refetchQueries({ include: ['getOneUser'] });
router.push(`/${slug}`);
setLoadingAddWorkspace(false);
closeSection();
} catch (error: any) {
setWorkspaceError(getErrorMessage(error, 'workspace'));
setLoadingAddWorkspace(false);
}
}
return (
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-4">
<Input
type="text"
placeholder="Your new workspace"
name="workspace"
id="workspace"
label="Workspace"
fullWidth
autoFocus
helperText={`https://app.nhost.io/${slugifyString(workspace)}`}
onChange={(event) => {
setWorkspace(event.target.value);
setWorkspaceError('');
}}
/>
{workspaceError && <Alert severity="error">{workspaceError}</Alert>}
<div className="grid grid-flow-col justify-between gap-2">
<Button
variant="outlined"
color="secondary"
onClick={(e) => {
e.preventDefault();
externalCloseSection();
}}
>
Cancel
</Button>
<Button
type="submit"
disabled={!!workspaceError}
loading={loadingAddWorkspace}
>
Create Workspace
</Button>
</div>
</form>
);
}
export default function AddWorkspace() {
const { closeSection } = useUI();
const user = nhost.auth.getUser();
if (!user) {
return <div>No user..</div>;
}
return (
<div className="grid w-modal grid-flow-row gap-2 px-6 py-6 text-left">
<div className="grid w-full grid-flow-row gap-1">
<Text variant="h3" component="h2">
New Workspace
</Text>
<Text variant="subtitle2">
Invite team members to workspaces to work collaboratively.
</Text>
</div>
<AddNewWorkspaceForm closeSection={closeSection} />
</div>
);
}

View File

@@ -1,14 +1,16 @@
import { Avatar } from '@/ui/Avatar';
import Text from '@/ui/v2/Text';
import { nhost } from '@/utils/nhost';
import { useGetWorkspacesQuery } from '@/utils/__generated__/graphql';
import { nhost } from '@/utils/nhost';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect } from 'react';
export default function SidebarWorkspaces() {
const user = nhost.auth.getUser();
const { data, loading, startPolling, stopPolling } = useGetWorkspacesQuery();
const { data, loading, startPolling, stopPolling } = useGetWorkspacesQuery({
fetchPolicy: 'cache-and-network',
});
useEffect(() => {
startPolling(1000);
@@ -28,7 +30,7 @@ export default function SidebarWorkspaces() {
<div className="mt-3 mb-4 space-y-2">
<div className="flex flex-row">
<svg
className="ml-1 h-4 w-4 animate-spin self-center text-dark"
className="self-center w-4 h-4 ml-1 animate-spin text-dark"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -47,7 +49,7 @@ export default function SidebarWorkspaces() {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
<Text size="tiny" className="ml-2 self-center" color="greyscaleGrey">
<Text size="tiny" className="self-center ml-2" color="greyscaleGrey">
Creating first workspace...
</Text>
</div>
@@ -66,12 +68,12 @@ export default function SidebarWorkspaces() {
>
{name === 'Default Workspace' && creatorUserId === user.id ? (
<Avatar
className="h-8 w-8 self-center rounded-full"
className="self-center w-8 h-8 rounded-full"
name={user?.displayName}
avatarUrl={user?.avatarUrl}
/>
) : (
<div className="inline-block h-8 w-8 overflow-hidden rounded-lg">
<div className="inline-block w-8 h-8 overflow-hidden rounded-lg">
<Image
src="/logos/new.svg"
alt="Nhost Logo"

View File

@@ -1,4 +1,4 @@
import ChangeWorkspaceName from '@/components/applications/ChangeWorkspaceName';
import { useDialog } from '@/components/common/DialogProvider';
import RemoveWorkspaceModal from '@/components/workspace/RemoveWorkspaceModal';
import { useUI } from '@/context/UIContext';
import { useGetWorkspace } from '@/hooks/use-GetWorkspace';
@@ -13,7 +13,6 @@ import { copy } from '@/utils/copy';
import { nhost } from '@/utils/nhost';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useState } from 'react';
export default function WorkspaceHeader() {
const { currentWorkspace } = useCurrentWorkspaceAndApplication();
@@ -21,14 +20,14 @@ export default function WorkspaceHeader() {
query: { workspaceSlug },
} = useRouter();
const [changeWorkspaceNameModal, setChangeWorkspaceNameModal] =
useState(false);
const {
openDeleteWorkspaceModal,
closeDeleteWorkspaceModal,
deleteWorkspaceModal,
} = useUI();
const { openDialog } = useDialog();
const { data } = useGetWorkspace(workspaceSlug);
const workspace = data?.workspaces[0];
@@ -45,11 +44,6 @@ export default function WorkspaceHeader() {
return (
<div className="mx-auto flex max-w-3xl flex-col">
<Modal
showModal={changeWorkspaceNameModal}
close={() => setChangeWorkspaceNameModal(!changeWorkspaceNameModal)}
Component={ChangeWorkspaceName}
/>
<Modal
showModal={deleteWorkspaceModal}
close={closeDeleteWorkspaceModal}
@@ -112,9 +106,23 @@ export default function WorkspaceHeader() {
>
<Dropdown.Item
className="py-2"
onClick={() =>
setChangeWorkspaceNameModal(!changeWorkspaceNameModal)
}
onClick={() => {
openDialog('EDIT_WORKSPACE_NAME', {
title: (
<span className="grid grid-flow-row">
<span>Change Workspace Name</span>
<Text variant="subtitle1" component="span">
Changing the workspace name will also affect the URL
of the workspace.
</Text>
</span>
),
payload: {
currentWorkspaceName: currentWorkspace.name,
currentWorkspaceId: currentWorkspace.id,
},
});
}}
>
Change workspace name
</Dropdown.Item>

View File

@@ -1,11 +1,12 @@
import { useDialog } from '@/components/common/DialogProvider';
import { SidebarTitle } from '@/components/home/SidebarTitle';
import { useUI } from '@/context/UIContext';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
import SidebarWorkspaces from './SidebarWorkspaces';
export function WorkspaceSection() {
const { openSection } = useUI();
const { openDialog } = useDialog();
return (
<>
@@ -15,7 +16,19 @@ export function WorkspaceSection() {
<Button
variant="borderless"
color="secondary"
onClick={openSection}
onClick={() => {
openDialog('EDIT_WORKSPACE_NAME', {
title: (
<span className="grid grid-flow-row">
<span>New Workspace</span>
<Text variant="subtitle1" component="span">
Invite team members to workspaces to work collaboratively.
</Text>
</span>
),
});
}}
startIcon={<PlusCircleIcon />}
>
New Workspace

View File

@@ -1,114 +0,0 @@
const terminalTheme = {
hljs: {
display: 'block',
background: '#F4F7F9',
color: '#21324B',
},
'hljs-tag': {
color: '#9C73DF',
},
'hljs-keyword': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-selector-tag': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-literal': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-strong': {
color: '#9C73DF',
},
'hljs-name': {
color: '#9C73DF',
},
'hljs-code': {
color: '#66d9ef',
},
'hljs-class .hljs-title': {
color: 'red',
},
'hljs-attribute': {
color: '#bf79db',
},
'hljs-symbol': {
color: '#bf79db',
},
'hljs-regexp': {
color: '#bf79db',
},
'hljs-link': {
color: '#bf79db',
},
'hljs-string': {
color: '#B7A590',
},
'hljs-bullet': {
color: '#B7A590',
},
'hljs-subst': {
color: '#B7A590',
},
'hljs-title': {
color: '#B7A590',
fontWeight: 'bold',
},
'hljs-section': {
color: '#B7A590',
fontWeight: 'bold',
},
'hljs-emphasis': {
color: '#3ECF8E',
},
'hljs-type': {
color: '#3ECF8E',
fontWeight: 'bold',
},
'hljs-built_in': {
color: '#3ECF8E',
},
'hljs-builtin-name': {
color: '#3ECF8E',
},
'hljs-selector-attr': {
color: '#3ECF8E',
},
'hljs-selector-pseudo': {
color: '#3ECF8E',
},
'hljs-addition': {
color: '#3ECF8E',
},
'hljs-variable': {
color: '#3ECF8E',
},
'hljs-template-tag': {
color: '#3ECF8E',
},
'hljs-template-variable': {
color: '#3ECF8E',
},
'hljs-comment': {
color: '#21324B',
},
'hljs-quote': {
color: '#75715e',
},
'hljs-deletion': {
color: '#75715e',
},
'hljs-meta': {
color: '#75715e',
},
'hljs-doctag': {
fontWeight: 'bold',
},
'hljs-selector-id': {
fontWeight: 'bold',
},
};
export default terminalTheme;

View File

@@ -20,6 +20,34 @@ query getDeployments($id: uuid!, $limit: Int!, $offset: Int!) {
}
}
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(
where: {
deploymentStatus: { _in: ["PENDING", "SCHEDULED"] }
appId: { _eq: $appId }
}
) {
...DeploymentRow
}
}
subscription LatestLiveDeploymentSub($appId: uuid!) {
deployments(
where: { deploymentStatus: { _eq: "DEPLOYED" }, appId: { _eq: $appId } }
order_by: { deploymentEndedAt: desc }
limit: 1
offset: 0
) {
...DeploymentRow
}
}
mutation InsertDeployment($object: deployments_insert_input!) {
insertDeployment(object: $object) {
...DeploymentRow
}
}
subscription getDeploymentsSub($id: uuid!, $limit: Int!, $offset: Int!) {
deployments(
where: { appId: { _eq: $id } }

View File

@@ -6,7 +6,6 @@ import FileTextIcon from '@/ui/v2/icons/FileTextIcon';
import GraphQLIcon from '@/ui/v2/icons/GraphQLIcon';
import HasuraIcon from '@/ui/v2/icons/HasuraIcon';
import HomeIcon from '@/ui/v2/icons/HomeIcon';
import LambdaIcon from '@/ui/v2/icons/LambdaIcon';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import StorageIcon from '@/ui/v2/icons/StorageIcon';
import UserIcon from '@/ui/v2/icons/UserIcon';
@@ -54,13 +53,6 @@ export default function useProjectRoutes() {
const isPlatform = useIsPlatform();
const nhostRoutes: ProjectRoute[] = [
{
relativePath: '/functions',
exact: false,
label: 'Functions',
icon: <LambdaIcon />,
disabled: !isPlatform,
},
{
relativePath: '/deployments',
exact: false,

View File

@@ -237,7 +237,10 @@ test('should drop existing relationships and prepare a new one-to-many relations
"cascade": false,
"relationship": "books",
"source": "default",
"table": "authors",
"table": {
"name": "authors",
"schema": "public",
},
},
"type": "pg_drop_relationship",
}

View File

@@ -152,7 +152,12 @@ export default async function prepareTrackForeignKeyRelationsMetadata({
type: 'pg_drop_relationship',
args: {
source: dataSource,
table: foreignKeyRelation.referencedTable,
table: foreignKeyRelation.referencedSchema
? {
name: foreignKeyRelation.referencedTable,
schema: foreignKeyRelation.referencedSchema,
}
: foreignKeyRelation.referencedTable,
relationship: oneToManyRelationshipName,
cascade: false,
},

View File

@@ -184,7 +184,7 @@ export default function prepareUpdateTableQuery({
);
return [
...args,
...updatedArgs,
...prepareUpdateForeignKeyConstraintQuery({
...baseVariables,
originalForeignKeyRelation,

View File

@@ -70,7 +70,7 @@ export default function useFiles({
currentApplication.subdomain,
currentApplication.region.awsName,
'storage',
)}/${file.id}`;
)}/files/${file.id}`;
const fetchParams = new URLSearchParams();

View File

@@ -10,7 +10,7 @@ export default function useNotFoundRedirect() {
useCurrentWorkspaceAndApplication();
const router = useRouter();
const {
query: { workspaceSlug, appSlug },
query: { workspaceSlug, appSlug, updating },
} = useRouter();
const notIn404Already = router.pathname !== '/404';
@@ -25,6 +25,15 @@ export default function useNotFoundRedirect() {
const inSettingsDatabasePage = router.pathname.includes('/settings/database');
useEffect(() => {
// This code is checking if the URL has a query of the form `?updating=true`
// If it does (`updating` is true) this useEffect will immediately exit without executing
// any further statements (e.g. the page will show a loader until `updating` is false).
// This is to prevent the user from being redirected to the 404 page while we are updating
// either the workspace slug or application slug.
if (updating) {
return;
}
if (noResolvedWorkspace && notIn404Already) {
router.push('/404');
}
@@ -37,6 +46,7 @@ export default function useNotFoundRedirect() {
router.push('/404');
}
}, [
updating,
currentApplication,
currentWorkspace,
noResolvedApplication,

View File

@@ -1,4 +1,4 @@
import { AppDeploymentDuration } from '@/components/applications/AppDeployments';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useDeploymentSubSubscription } from '@/generated/graphql';
@@ -8,6 +8,7 @@ import DelayedLoading from '@/ui/DelayedLoading';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import { Text } from '@/ui/Text';
import Link from '@/ui/v2/Link';
import { format, formatDistanceToNowStrict, parseISO } from 'date-fns';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
@@ -50,6 +51,12 @@ export default function DeploymentDetailsPage() {
);
}
const relativeDateOfDeployment = deployment.deploymentStartedAt
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
addSuffix: true,
})
: '';
return (
<Container>
<div className="flex justify-between">
@@ -104,24 +111,20 @@ export default function DeploymentDetailsPage() {
{deployment.commitMessage}
</div>
<div className="text-sm+ text-greyscaleGrey">
{formatDistanceToNowStrict(
parseISO(deployment.deploymentStartedAt),
{
addSuffix: true,
},
)}
{relativeDateOfDeployment}
</div>
</div>
</div>
<div className=" flex items-center">
<a
className="self-center font-mono text-sm- font-medium text-greyscaleDark"
<Link
className="self-center font-mono text-sm- font-medium"
target="_blank"
rel="noreferrer"
href={`https://github.com/${currentApplication.githubRepository?.fullName}/commit/${deployment.commitSHA}`}
underline="hover"
>
{deployment.commitSHA.substring(0, 7)}
</a>
</Link>
<div className="w-20 text-right">
<AppDeploymentDuration
@@ -133,6 +136,10 @@ export default function DeploymentDetailsPage() {
</div>
<div>
<div className="rounded-lg bg-verydark p-4 text-sm- text-white">
{deployment.deploymentLogs.length === 0 && (
<span className="font-mono">No message.</span>
)}
{deployment.deploymentLogs.map((log) => (
<div key={log.id} className="flex font-mono">
<div className=" mr-2 flex-shrink-0">

View File

@@ -1,269 +0,0 @@
import ConnectGithubModal from '@/components/applications/ConnectGithubModal';
import { FunctionsNotDeployed } from '@/components/applications/functions/FunctionsNotDeployed';
import { normalizeFunctionMetadata } from '@/components/applications/functions/normalizeFunctionMetadata';
import { EditRepositorySettings } from '@/components/applications/github/EditRepositorySettings';
import useGitHubModal from '@/components/applications/github/useGitHubModal';
import Folder from '@/components/icons/Folder';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useWorkspaceContext } from '@/context/workspace-context';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Button } from '@/ui/Button';
import DelayedLoading from '@/ui/DelayedLoading';
import { Modal } from '@/ui/Modal';
import Status, { StatusEnum } from '@/ui/Status';
import { Text } from '@/ui/Text';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { useGetAppFunctionsMetadataQuery } from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import clsx from 'clsx';
import Image from 'next/image';
import Link from 'next/link';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
function FunctionsNoRepo() {
const [githubModal, setGithubModal] = useState(false);
const [githubRepoModal, setGithubRepoModal] = useState(false);
const { openGitHubModal } = useGitHubModal();
return (
<>
<Modal showModal={githubModal} close={() => setGithubModal(!githubModal)}>
<ConnectGithubModal close={() => setGithubModal(false)} />
</Modal>
<Modal
showModal={githubRepoModal}
close={() => setGithubRepoModal(!githubRepoModal)}
>
<EditRepositorySettings
openConnectGithubModal={() => setGithubModal(true)}
close={() => setGithubRepoModal(false)}
handleSelectAnotherRepository={openGitHubModal}
/>
</Modal>
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/assets/githubRepo.svg"
width={72}
height={72}
alt="GitHub Logo"
/>
</div>
<Text className="mt-4 font-medium" size="large" color="dark">
Function Logs
</Text>
<div className="flex">
<div className="mx-auto flex flex-row self-center text-center">
<Text size="normal" color="greyscaleDark" className="mt-1">
To deploy serverless functions, you need to connect your project to
version control.
</Text>
</div>
</div>
<div className="mt-3 flex text-center">
<Button
transparent
color="blue"
className="mx-auto font-medium"
onClick={() => setGithubModal(true)}
>
Connect your Project to GitHub
</Button>
</div>
</>
);
}
export default function FunctionsPage() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { workspaceContext } = useWorkspaceContext();
const { data, loading, error } = useGetAppFunctionsMetadataQuery({
variables: { id: currentApplication?.id },
});
const [normalizedFunctions, setNormalizedFunctions] = useState(null);
useEffect(() => {
if (!data) {
return;
}
if (data.app.metadataFunctions) {
setNormalizedFunctions(
normalizeFunctionMetadata(data.app.metadataFunctions),
);
}
}, [data]);
if (!workspaceContext.repository) {
return (
<Container className="mt-12 max-w-3xl text-center antialiased">
<FunctionsNoRepo />
</Container>
);
}
if (loading) {
return (
<Container>
<DelayedLoading delay={500} className="mt-12" />
</Container>
);
}
if (!data || normalizedFunctions === null) {
return (
<Container>
<FunctionsNotDeployed />
</Container>
);
}
if (error) {
return <Container>Error</Container>;
}
return (
<Container>
<div className="mt-2">
{normalizedFunctions?.map((folder) => (
<div key={folder.folder}>
<div
className={clsx(
'flex flex-row pt-8 pb-2 align-middle',
folder.nestedLevel < 2 && 'ml-6',
folder.nestedLevel >= 2 && 'ml-12',
)}
>
<div className={clsx('flex w-full')}>
{folder.nestedLevel > 0 && (
<Folder className="self-center align-middle text-greyscaleGrey" />
)}
<Text
color="greyscaleDark"
variant="body"
className={clsx(
'font-medium',
folder.nestedLevel > 0 && 'ml-2',
)}
size="tiny"
>
{folder.folder}/
</Text>
</div>
{folder.nestedLevel === 0 ? (
<div className="flex w-full flex-row">
<div className="flex w-52">
<Text
color="greyscaleDark"
variant="body"
className="font-medium"
size="tiny"
>
Created At
</Text>
</div>
<div className="flex w-16 self-end">
<Text
color="greyscaleDark"
variant="body"
className="font-medium"
size="tiny"
>
Status
</Text>
</div>
</div>
) : null}
</div>
<div
className={clsx(
'border-t py-1',
folder.nestedLevel < 2 && 'ml-6',
folder.nestedLevel >= 2 && 'ml-12',
)}
>
{folder.funcs.map((func) => (
<Link
key={func.id}
href={{
pathname:
'/[workspaceSlug]/[appSlug]/functions/[functionId]',
query: {
workspaceSlug: currentWorkspace.slug,
appSlug: currentApplication.slug,
functionId: func.functionName,
},
}}
passHref
>
<a
href="[workspaceSlug]/[appSlug]/functions/[functionId]"
className={clsx(
'flex cursor-pointer flex-row border-b py-2.5',
folder.nestedLevel && 'ml-0',
)}
>
<div className="flex w-full flex-row items-center">
<Image
src={`/assets/functions/${func.lang}.svg`}
alt={`Logo of ${func.lang}`}
width={16}
height={16}
/>
<Text
color="greyscaleDark"
variant="body"
className="pl-2 font-medium"
size="small"
>
{func.name}
</Text>
</div>
<div className="flex w-full flex-row">
<div className={clsx('flex w-52 self-center')}>
<Text
color="greyscaleDark"
variant="body"
className=""
size="tiny"
>
{func.formattedCreatedAt || '-'}
</Text>
</div>
<div className="flex w-16 self-center">
<Status status={StatusEnum.Live}>Live</Status>
<ChevronRightIcon className="middl ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</div>
</a>
</Link>
))}
</div>
</div>
))}
</div>
<div className="mx-auto mt-10 max-w-6xl">
<div className="text-center">
<Text size="tiny" color="greyscaleDark" className="font-medium">
Base URL for function endpoints is{' '}
{generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}
</Text>
</div>
</div>
</Container>
);
}
FunctionsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -1,142 +0,0 @@
import { FunctionsLogsTerminalPage } from '@/components/applications/functions/FunctionLogsTerminalFromPage';
import type { Func } from '@/components/applications/functions/normalizeFunctionMetadata';
import { normalizeFunctionMetadata } from '@/components/applications/functions/normalizeFunctionMetadata';
import { LoadingScreen } from '@/components/common/LoadingScreen';
import Help from '@/components/icons/Help';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetAllUserWorkspacesAndApplications } from '@/hooks/useGetAllUserWorkspacesAndApplications';
import { Text } from '@/ui/Text';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { yieldFunction } from '@/utils/helpers';
import { useGetAppFunctionsMetadataQuery } from '@/utils/__generated__/graphql';
import Image from 'next/image';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
export default function FunctionDetailsPage() {
const { currentApplication, currentWorkspace } =
useCurrentWorkspaceAndApplication();
useGetAllUserWorkspacesAndApplications(false);
const { data, loading, error } = useGetAppFunctionsMetadataQuery({
variables: { id: currentApplication?.id },
});
const [currentFunction, setCurrentFunction] = useState<Func | null>(null);
const router = useRouter();
// currentFunction will be null until we get data back from remote and we set it to be the function we're looking for.
useEffect(() => {
if (!data) {
return;
}
const appFunctions = normalizeFunctionMetadata(data?.app.metadataFunctions);
setCurrentFunction(yieldFunction(appFunctions, router));
}, [data, router]);
if (!currentApplication || !currentWorkspace || loading) {
return <LoadingScreen />;
}
if (error) {
throw new Error(
error.message ||
'An unexpected error has ocurred. Please try again later.',
);
}
if (!currentFunction) {
return (
<Container>
<h1 className="text-4xl font-semibold text-greyscaleDark">Not found</h1>
<p className="text-sm text-greyscaleGrey">
This function does not exist.
</p>
</Container>
);
}
return (
<>
<Container>
<div className="flex place-content-between">
<div className="flex flex-row items-center py-1">
<Image
src={`/assets/functions/${
currentFunction.name.split('.')[1]
}.svg`}
alt={`Logo of ${currentFunction.name.split('.')[1]}`}
width={40}
height={40}
/>
<div className="flex flex-col">
<Text
color="greyscaleDark"
variant="body"
className="ml-2 font-medium"
size="big"
>
{currentFunction.name}
</Text>
<a
className="ml-2 text-xs font-medium text-greyscaleGrey"
href={`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}${currentFunction?.route}`}
target="_blank"
rel="noreferrer"
>
{`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}${currentFunction?.route}`}
</a>
</div>
</div>
</div>
</Container>
<Container className="pt-10">
<div className="flex flex-row place-content-between">
<div className="flex">
<Text size="large" className="font-medium" color="greyscaleDark">
Log
</Text>
</div>
<div className="flex">
<Text
size="tiny"
className="self-center font-medium"
color="greyscaleDark"
>
Awaiting new requests
</Text>
<a
href="https://docs.nhost.io/platform/serverless-functions"
target="_blank"
rel="noreferrer"
>
<Help className="h-7 w-7" />
</a>
</div>
</div>
<div className="mt-5">
<FunctionsLogsTerminalPage functionName={currentFunction?.path} />
</div>
</Container>
</>
);
}
FunctionDetailsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -9,8 +9,8 @@ import {
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import CheckIcon from '@/ui/v2/icons/CheckIcon';
import Input from '@/ui/v2/Input';
import CheckIcon from '@/ui/v2/icons/CheckIcon';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { slugifyString } from '@/utils/helpers';
import { updateOwnCache } from '@/utils/updateOwnCache';
@@ -53,6 +53,7 @@ export default function SettingsGeneralPage() {
const [deleteApplication] = useDeleteApplicationMutation({
variables: { appId: currentApplication?.id },
});
const { currentWorkspace } = useCurrentWorkspaceAndApplication();
const router = useRouter();
const form = useForm<ProjectNameValidationSchema>({
@@ -69,6 +70,14 @@ export default function SettingsGeneralPage() {
const { register, formState } = form;
const handleProjectNameChange = async (data: ProjectNameValidationSchema) => {
// In this bit of code we spread the props of the current path (e.g. /workspace/...) and add one key-value pair: `updating: true`.
// We want to indicate that the currently we're in the process of running a mutation state that will affect the routing behaviour of the website
// i.e. redirecting to 404 if there's no workspace/project with that slug.
await router.replace({
pathname: router.pathname,
query: { ...router.query, updating: true },
});
const newProjectSlug = slugifyString(data.name);
if (newProjectSlug.length < 1 || newProjectSlug.length > 32) {
@@ -100,8 +109,13 @@ export default function SettingsGeneralPage() {
toastStyleProps,
);
try {
await client.refetchQueries({ include: ['getOneUser'] });
await client.refetchQueries({
include: ['getOneUser'],
});
form.reset(undefined, { keepValues: true, keepDirty: false });
await router.push(
`/${currentWorkspace.slug}/${newProjectSlug}/settings/general`,
);
} catch (error) {
await discordAnnounce(
error.message || 'Error while trying to update application cache',

View File

@@ -17215,6 +17215,27 @@ export type GetDeploymentsQueryVariables = Exact<{
export type GetDeploymentsQuery = { __typename?: 'query_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type ScheduledOrPendingDeploymentsSubSubscriptionVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type ScheduledOrPendingDeploymentsSubSubscription = { __typename?: 'subscription_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type LatestLiveDeploymentSubSubscriptionVariables = Exact<{
appId: Scalars['uuid'];
}>;
export type LatestLiveDeploymentSubSubscription = { __typename?: 'subscription_root', deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null }> };
export type InsertDeploymentMutationVariables = Exact<{
object: Deployments_Insert_Input;
}>;
export type InsertDeploymentMutation = { __typename?: 'mutation_root', insertDeployment?: { __typename?: 'deployments', id: any, commitSHA: string, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, deploymentStatus?: string | null, commitUserName?: string | null, commitUserAvatarUrl?: string | null, commitMessage?: string | null } | null };
export type GetDeploymentsSubSubscriptionVariables = Exact<{
id: Scalars['uuid'];
limit: Scalars['Int'];
@@ -19147,6 +19168,106 @@ export type GetDeploymentsQueryResult = Apollo.QueryResult<GetDeploymentsQuery,
export function refetchGetDeploymentsQuery(variables: GetDeploymentsQueryVariables) {
return { query: GetDeploymentsDocument, variables: variables }
}
export const ScheduledOrPendingDeploymentsSubDocument = gql`
subscription ScheduledOrPendingDeploymentsSub($appId: uuid!) {
deployments(
where: {deploymentStatus: {_in: ["PENDING", "SCHEDULED"]}, appId: {_eq: $appId}}
) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
/**
* __useScheduledOrPendingDeploymentsSubSubscription__
*
* To run a query within a React component, call `useScheduledOrPendingDeploymentsSubSubscription` and pass it any options that fit your needs.
* When your component renders, `useScheduledOrPendingDeploymentsSubSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useScheduledOrPendingDeploymentsSubSubscription({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useScheduledOrPendingDeploymentsSubSubscription(baseOptions: Apollo.SubscriptionHookOptions<ScheduledOrPendingDeploymentsSubSubscription, ScheduledOrPendingDeploymentsSubSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<ScheduledOrPendingDeploymentsSubSubscription, ScheduledOrPendingDeploymentsSubSubscriptionVariables>(ScheduledOrPendingDeploymentsSubDocument, options);
}
export type ScheduledOrPendingDeploymentsSubSubscriptionHookResult = ReturnType<typeof useScheduledOrPendingDeploymentsSubSubscription>;
export type ScheduledOrPendingDeploymentsSubSubscriptionResult = Apollo.SubscriptionResult<ScheduledOrPendingDeploymentsSubSubscription>;
export const LatestLiveDeploymentSubDocument = gql`
subscription LatestLiveDeploymentSub($appId: uuid!) {
deployments(
where: {deploymentStatus: {_eq: "DEPLOYED"}, appId: {_eq: $appId}}
order_by: {deploymentEndedAt: desc}
limit: 1
offset: 0
) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
/**
* __useLatestLiveDeploymentSubSubscription__
*
* To run a query within a React component, call `useLatestLiveDeploymentSubSubscription` and pass it any options that fit your needs.
* When your component renders, `useLatestLiveDeploymentSubSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLatestLiveDeploymentSubSubscription({
* variables: {
* appId: // value for 'appId'
* },
* });
*/
export function useLatestLiveDeploymentSubSubscription(baseOptions: Apollo.SubscriptionHookOptions<LatestLiveDeploymentSubSubscription, LatestLiveDeploymentSubSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<LatestLiveDeploymentSubSubscription, LatestLiveDeploymentSubSubscriptionVariables>(LatestLiveDeploymentSubDocument, options);
}
export type LatestLiveDeploymentSubSubscriptionHookResult = ReturnType<typeof useLatestLiveDeploymentSubSubscription>;
export type LatestLiveDeploymentSubSubscriptionResult = Apollo.SubscriptionResult<LatestLiveDeploymentSubSubscription>;
export const InsertDeploymentDocument = gql`
mutation InsertDeployment($object: deployments_insert_input!) {
insertDeployment(object: $object) {
...DeploymentRow
}
}
${DeploymentRowFragmentDoc}`;
export type InsertDeploymentMutationFn = Apollo.MutationFunction<InsertDeploymentMutation, InsertDeploymentMutationVariables>;
/**
* __useInsertDeploymentMutation__
*
* To run a mutation, you first call `useInsertDeploymentMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useInsertDeploymentMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [insertDeploymentMutation, { data, loading, error }] = useInsertDeploymentMutation({
* variables: {
* object: // value for 'object'
* },
* });
*/
export function useInsertDeploymentMutation(baseOptions?: Apollo.MutationHookOptions<InsertDeploymentMutation, InsertDeploymentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<InsertDeploymentMutation, InsertDeploymentMutationVariables>(InsertDeploymentDocument, options);
}
export type InsertDeploymentMutationHookResult = ReturnType<typeof useInsertDeploymentMutation>;
export type InsertDeploymentMutationResult = Apollo.MutationResult<InsertDeploymentMutation>;
export type InsertDeploymentMutationOptions = Apollo.BaseMutationOptions<InsertDeploymentMutation, InsertDeploymentMutationVariables>;
export const GetDeploymentsSubDocument = gql`
subscription getDeploymentsSub($id: uuid!, $limit: Int!, $offset: Int!) {
deployments(

View File

@@ -1,10 +1,5 @@
import type {
FinalFunction,
Func,
} from '@/components/applications/functions/normalizeFunctionMetadata';
import features from '@/data/features.json';
import { ApplicationStatus } from '@/types/application';
import type { NextRouter } from 'next/router';
import slugify from 'slugify';
import { LOCAL_BACKEND_URL } from './env';
import type { DeploymentRowFragment } from './__generated__/graphql';
@@ -89,26 +84,6 @@ export function emptyWorkspace() {
};
}
export function yieldFunction(
functionsToSearch: FinalFunction[],
router: NextRouter,
): Func {
let functionToReturn: Func = null;
functionsToSearch.forEach((currentFolder) => {
currentFolder.funcs.forEach((currentFunction) => {
if (
!functionToReturn &&
currentFunction.functionName === router.query.functionId
) {
functionToReturn = currentFunction;
}
});
});
return functionToReturn;
}
/**
* Converts the state number of the application to its string equivalent.
* @param appStatus The current state of the application.

View File

@@ -13,6 +13,9 @@ module.exports = {
...defaultTheme.screens,
},
extend: {
animation: {
'spin-reverse': 'spin 1.5s linear infinite reverse',
},
colors: {
primary: '#0052cd',
'primary-light': '#ebf3ff',

View File

@@ -1,5 +1,24 @@
# @nhost/docs
## 0.0.12
### Patch Changes
- e146d32e: chore(deps): update dependency @types/react to v18.0.27
- 5b65cac9: updated authentication documentation
## 0.0.11
### Patch Changes
- e6dad4d6: Added remote schemas
## 0.0.10
### Patch Changes
- 200e9f77: chore(deps): update dependency @types/react-dom to v18.0.10
## 0.0.9
### Patch Changes

View File

@@ -3,7 +3,7 @@ title: Email Templates
sidebar_position: 4
---
Nhost Authentication sends out transactional emails as part of the authentication service. These emails can be modified using email templates.
Nhost Auth sends out transactional emails as part of the authentication service. These emails can be modified using email templates.
The following email templates are available:
@@ -70,8 +70,6 @@ As you see, the format is:
nhost/emails/{two-letter-language-code}/{email-template}/[subject.txt, body.html]
```
Default templates for English (`en`) and French (`fr`) are automatically generated when the project is initialized with the [CLI](/cli).
## Languages
The user's language is what decides what template to send. The user's language is stored in the `auth.users` table in the `locale` column. This `locale` column contains a two-letter language code in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format.

View File

@@ -22,3 +22,65 @@ Nhost Authentication lets you authenticate users using different sign-in methods
- [Spotify](/authentication/sign-in-with-spotify)
- [Twitch](/authentication/sign-in-with-twitch)
- [WorkOS](/authentication/sign-in-with-workos)
## Client URL
Client URL is the URL of your frontend application. The Client URL is used to redirect the user after interacting with any authentication operation, like signing in or resetting their password.
## Allowed Redirect URLs
Allowed Redirect URLs are the URLs of your frontend application that are allowed to redirect the user after interacting with any authentication operation, like signing in or resetting their password. This is useful if you have multiple frontend applications that are using the same Nhost backend or if you want to redirect the user to a specific URL after interacting with an authentication operation.
As an example, for a staging project, you can set the Client URL to `https://staging.example.com` and Allowed Redirect URLs to `https://*.vercel.app`. This way, the user can be redirected to any Vercel deployment of your frontend application.
## Allowed Emails and Domains
Allowed Emails and Domains are used to restrict the sign-up an sign-in process to specific email addresses and domains.
If both allowed emails and allowed domains are set a user can only sign up if their email address matches one of the allowed emails or one of the allowed domains.
## Blocked Emails and Domains
Blocked Emails and Domains are used to block specific email addresses and domains from signing up and singin in.
Note that even if a user's email address matches any allowed email or domain, they will still be blocked if their email address matches any blocked email or domain.
## Multi-factor Authentication
By enabling Multi-factor Authentication (MFA), you can allow users to verify their identity using a second factor during the sign-in process. We currently support Authenticator Apps (TOTP) for MFA.
Once MFA is enabled, a user can enable MFA for their account by scanning a QR code with their Authenticator App. After that, they will be prompted to enter a code generated by their Authenticator App during the sign-in process.
We'll be adding more support in our SDKs and documentation around MFA soon.
## Gravatar
If Gravatar is enabled, Nhost Auth will use the user's email address to fetch their Gravatar profile picture. If the user doesn't have a Gravatar profile picture, a default image will be used.
There are two options for Gravatars:
### Default Image
If the user doesn't have a Gravatar profile picture, a default image will be used. You can choose between the following options:
- `404`: Do not load any image if none is associated with the email hash, instead return an HTTP 404 (File Not Found) response.
- `mp`: (mystery-person) a simple, cartoon-style silhouetted outline of a person (does not vary by email hash).
- `identicon`: a geometric pattern based on an email hash.
- `monsterid`: a generated 'monster' with different colors, faces, etc.
- `wavatar`: generated faces with differing features and backgrounds.
- `retro`: awesome generated, 8-bit arcade-style pixelated faces.
- `robohash`: a generated robot with different colors, faces, etc.
- `blank`: a transparent PNG image.
### Rating
Gravatar images are rated by default. You can choose between the following options:
- `g`: suitable for display on all websites with any audience type.
- `pg`: may contain rude gestures, provocatively dressed individuals, lesser swear words or mild violence.
- `r`: may contain such things as harsh profanity, intense violence, nudity, or hard drug use.
- `x`: may contain hardcore sexual imagery or extremely disturbing violence.
## Disable New Users
If set, newly registered users are disabled and won't be able to sign in. This is useful if you want to manually approve new users before they can sign in.

View File

@@ -5,13 +5,11 @@ slug: /authentication/sign-in-with-email-and-password
image: /img/og/sign-in-with-email-and-password.png
---
Follow this guide to sign in users with email and password.
The email and password sign-in method is enabled by default for all Nhost projects.
The Email and Password sign-in method is always enabled for all Nhost projects.
## Sign Up
Users must first sign up to be able to sign in with Email and Password.
Users must first sign up to be able to sign in.
**Example:** Sign up users using the [Nhost JavaScript client](/reference/javascript).
@@ -26,7 +24,7 @@ If you've turned on email verification in your project's **Authentication Settin
## Sign In
Once a user has been signed up (and optionally verified), you can sign them in.
After the user has successfully signed up, they can sign in.
**Example:** Sign in users using the [Nhost JavaScript client](/reference/javascript).
@@ -37,8 +35,10 @@ await nhost.auth.signIn({
})
```
## Verified Emails
## Email Verification
You can decide if only verified emails should be able to sign in or not. Modify the **Only allow users with verified emails to sign in.** setting in the **Authentication Settings** section under **Users** in your Nhost project.
If you want to require users to verify their email before they can sign in, you can enable this under **Settings -> Sign-In Methods -> Email and Password** by checking the **Require Verified Emails** checkbox.
An email-verification email is automatically sent to the user during sign-up if your project only allows to sign in users with verified emails. You can also manually send the verification email to the user using [`nhost.auth.sendVerificationEmail()`](/reference/javascript/auth/send-verification-email).
If **Require Verified Emails** is enabled, users automatically get a verification email when they sign up. The user must click the verification link in the email before they can sign in. It's possible to edit the ["email-verify" email template](/authentication/email-templates).
It's possible to manually send a verification email to the user using [`nhost.auth.sendVerificationEmail()`](/reference/javascript/auth/send-verification-email).

View File

@@ -5,15 +5,15 @@ slug: /authentication/sign-in-with-magic-link
image: /img/og/sign-in-with-magic-link.png
---
Follow this guide to sign in users with Magic Link, also called passwordless email.
Nhost allows you to sign in users with a Magic Link, which is a way to sign in users so they don't have to remember a password.
The Magic Link sign-in method enables you to sign in users using an email address, without requiring a password.
When users sign in using this sign-in method, they'll enter their email address and then receive an email with a (magic) link. When the user clicks on the (magic) link, they get automatically signed in to your app.
## Setup
The sign-in method is called Magic Link because the user gets "magically" signed in without having to enter a password.
Enable the Magic Link sign-in method in the Nhost dashboard under **Users** -> **Authentication Settings** -> **Magic Link**.
## Configuration
![Magic Link Setup with Nhost](/img/authentication/magic-link/magic-link-setup.png)
Enable the Magic Link sign-in method in the Nhost dashboard under **Settings -> Sign-In Methods -> Magic Link**.
## Sign In
@@ -30,4 +30,10 @@ nhost.auth.signIn({
})
```
If you want to change the email for your magic link emails, you can do so by changing the [email templates](/authentication/email-templates).
There is no sign up method for Magic Link. Users will be automatically created when they sign in for the first time.
Users who have signed up with email and password can also sign in with Magic Link.
## Email
It's possible to edit the ["signin-passwordless" email template](/authentication/email-templates).

View File

@@ -7,11 +7,11 @@ image: /img/og/sign-in-with-phone-number-sms.png
Follow this guide to sign in users with a phone number (SMS).
## Setup
## Configuration
You need a [Twilio account](https://www.twilio.com/try-twilio) to use this feature because all SMS are sent through Twilio.
Enable the Phone Number (SMS) sign-in method in the Nhost dashboard under **Users** -> **Authentication Settings** -> **Passwordless SMS**.
Enable the Phone Number (SMS) sign-in method in the Nhost dashboard under **Settings -> Sign-In Methods -> Phone Number (SMS)**.
You need to insert the following settings in the Nhost dashboard from Twilio:
@@ -19,10 +19,6 @@ You need to insert the following settings in the Nhost dashboard from Twilio:
- Auth Token
- Messaging Service SID (or a Twilio phone number)
<video width="99%" autoPlay muted loop controls="true" style={{ marginBottom: '15px' }}>
<source src="/videos/enable-sms-sign-in.mp4" type="video/mp4" />
</video>
## Sign In
To sign in users with a phone number is a two-step process:
@@ -44,7 +40,7 @@ await nhost.auth.signIn({
})
```
The first time a user signs in using a phone number, the user is created. That means you don't need to sign up the user before signin in the user.
The first time a user signs in using a phone number, the user is created. That means you don't need to sign up users before signing in users.
:::info

View File

@@ -16,81 +16,105 @@ Examples of security keys:
You can read more about this feature in our [blog post](https://nhost.io/blog/webauthn-sign-in-method)
## Setup
## Configuration
Enable the Security Key sign-in method in the Nhost dashboard under **Users** -> **Authentication Settings** -> **Security Keys**.
Enable the Security Key sign-in method in the Nhost dashboard under **Settings -> Sign-In Methods -> Security Keys**.
You need to make sure you also set a valid client URL under **Users** -> **Authentication Settings** -> **Client URL**.
<video width="99%" autoPlay muted loop controls="true" style={{ marginBottom: '15px' }}>
<source src="/videos/enable-security-keys-sign-in.mp4" type="video/mp4" />
</video>
You need to make sure you also set a valid client URL under **Settings -> Authentication -> Client URL**.
## Sign Up
Signing up with a security key uses the same method as signing up with an email and a password. Instead of a `password` parameter, you need to set the `securityKey` parameter to `true`:
Users must use an email address to sign up with a security key.
Here's an example of how to sign up a user with a security key with our [JavaScript SDK](/reference/javascript):
**Example:**: Sign up with a security key:
```tsx
const { error, session } = await nhost.auth.signUp({
email: 'joe@example.com',
securityKey: true
})
// Something unexpected happened, for instance, the user canceled the process
if (error) {
// Something unexpected happened, for instance, the user canceled their registration
console.log(error)
} else if (session) {
// Sign up is complete!
console.log(session.user)
} else {
return
}
// if there is no error and no session, the user needs to verify their email address.
if (!session) {
console.log(
'You need to verify your email address by clicking the link in the email we sent you.'
)
return
}
//Sign-up is complete!
console.log(session.user)
```
## Sign In
Once a user added a security key, they can use it to sign in:
Once a user signed up with a security key, and verfied their email if needed, they can use it to sign in.
**Example:** Sign in with a security key:
```tsx
const { error, session } = await nhost.auth.signIn({
email,
email: 'joe@example.com',
securityKey: true
})
if (session) {
// User is signed in
} else {
// Something unexpected happened, for instance, the user canceled the process
if (error) {
console.log(error)
return
}
if (!session) {
// Something unexpected happened
console.log(error)
return
}
// User is signed in
```
## Add a Security Key
Any signed-in user with a valid email can add a security key when the feature is enabled. For instance, someone who signed up with an email and a password can add a security key and thus use it for their later sign-in!
Any signed-in user with a verified email can add a security key to their user account. Once a security key is added, the user can use it their email and the security key to sign in.
Users can use multiple devices to sign in to their account. They can add as many security keys as they like.
It's possible to add multiple security keys to a user account.
**Example:** Add a security key to a user account:
```tsx
const { key, error } = await nhost.auth.addSecurityKey()
if (key) {
// Successfully added a new security key
console.log(key.id)
} else {
// Somethine unexpected happened
// Something unexpected happened
if (error) {
console.log(error)
return
}
// Successfully added a new security key
console.log(key.id)
```
A nickname can be added for each security key to make them easy to identify:
A nickname can be associated with each security key to make it easier to manage security keys in the future.
**Example:** Add a security key with a nickname:
```tsx
await nhost.auth.addSecurityKey('my macbook')
await nhost.auth.addSecurityKey('iPhone')
```
## List or Remove Security Keys
To list and to remove security keys can be achieved over GraphQL after setting the correct Hasura permissions to the `auth.security_keys` table:
To list and remove security keys, use GraphQL and set permissions on the `auth.security_keys` table:
**Example:** Get all security keys for a user:
```graphql
query securityKeys($userId: uuid!) {
@@ -99,6 +123,11 @@ query securityKeys($userId: uuid!) {
nickname
}
}
```
**Example:** Remove a security key:
```graphql
mutation removeSecurityKey($id: uuid!) {
deleteAuthUserSecurityKey(id: $id) {
id

View File

@@ -9,7 +9,7 @@ Nhost Authentication supports the following sign-in methods:
- [Email and Password](/authentication/sign-in-with-email-and-password)
- [Magic Link](/authentication/sign-in-with-magic-link)
- [Phone Number (SMS)](/authentication/sign-in-with-phone-number-sms)
- [Security Keys (WebAuthn)](/authentication/sign-in-with-phone-number-sms)
- [Security Keys (WebAuthn)](/authentication/sign-in-with-security-keys)
- [Apple](/authentication/sign-in-with-apple)
- [Discord](/authentication/sign-in-with-discord)
- [Facebook](/authentication/sign-in-with-facebook)

View File

@@ -1,20 +1,18 @@
---
title: Social Providers Configuration
sidebar_label: Social Providers Configuration
title: Social Providers
sidebar_label: Social Providers
sidebar_position: 10
---
## Enabling Social Sign-In Provider
To start with social sign-in, select your project in Nhost Dashboard and go to **Users** → **Authentication Settings**.
You need to set the Client ID and Client Secret for each provider that you want to enable.
To start with social sign-in, select your project in Nhost Dashboard and go to **Settings -> Sign-In Methods**.
## Implementing sign-in experience
Use the [Nhost JavaScript SDK](/reference/javascript) and the `signIn()` method to implement social sign-in for your project.
Here's an example of how to implement sign-in with GitHub:
**Example**: Sign in a user with [GitHub](/authentication/sign-in-with-github).
```js
nhost.auth.signIn({
@@ -22,19 +20,21 @@ nhost.auth.signIn({
})
```
Users are redirected to your Nhost project's **client URL** by default. By default, your Nhost project's client URL is set to `http://localhost:3000`. You can change the value of your client URL in the Nhost console by going to **Users** → **Authentication Settings** → **Client URL**.
During the sign-in flow, the user is redirected to the provider's website to authenticate. After the user authenticates, they are redirected back to your Nhost project's [**Client URL**](/authentication#client-url) by default. You can change where the user gets redirected to after authentication by passing the `redirectTo` option.
Here is an example of how to redirect to another host or path:
**Example:** Redirect the user to `https://staging.example.com/welcome` after they complete the sign-in flow.
```js
nhost.auth.signIn({
provider: '<provider>'
provider: 'github'
options: {
redirectTo: "<host>/<slug>" // Example: "https://example.com/dashboard"
redirectTo: "https://staging.example.com/welcome",
},
})
```
In the example above, it's important to note that the `redirectTo` URL must be part of the [Allowed Redirect URLs](/authentication#allowed-redirect-urls) of your Nhost project.
## Provider OAuth scopes
Scopes are a mechanism in OAuth to allow or limit an application's access to a user's account.

View File

@@ -5,35 +5,7 @@ sidebar_position: 1
image: /img/og/users.png
---
Users are stored in the database in the `auth.users` table.
## Get User Information using GraphQL
**Example:** Get all users.
```graphql
query {
users {
id
displayName
email
metadata
}
}
```
**Example:** Get a single user.
```graphql
query {
user(id: "<user-id>") {
id
displayName
email
metadata
}
}
```
Users are stored in the `auth.users` table in the [database](/database).
## Creating Users
@@ -133,6 +105,34 @@ await nhost.auth.signUp({
})
```
## Get User Information using GraphQL
**Example:** Get all users.
```graphql
query {
users {
id
displayName
email
metadata
}
}
```
**Example:** Get a single user.
```graphql
query {
user(id: "<user-id>") {
id
displayName
email
metadata
}
}
```
## Import Users
If you have users in a different system, you can import them into Nhost. When importing users you should insert the users directly into the database instead of using the authentication endpoints (`/signup/email-password`) to avoid sending unnecessary transactional emails.

View File

@@ -0,0 +1,4 @@
{
"label": "Remote Schemas",
"position": 11
}

View File

@@ -0,0 +1,252 @@
---
title: Stripe GraphQL API
sidebar_label: Stripe
sidebar_position: 2
image: /img/og/graphql.png
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
This package creates a Stripe GraphQL API, allowing for interaction with data at Stripe.
Here's an example of how to use the Stripe GraphQL API to get a list of invoices for a specific Stripe customer:
<Tabs >
<TabItem value="request" label="Request" default>
```graphql
query {
stripe {
customer(id: "cus_xxx") {
id
name
invoices {
data {
id
created
paid
hostedInvoiceUrl
}
}
}
}
}
```
</TabItem>
<TabItem value="response" label="Response">
```json
{
"data": {
"stripe": {
"customer": {
"id": "cus_xxx",
"name": "joe@example.com",
"invoices": {
"data": [
{
"id": "in_1MUmwnCCF9wuB4xxxxxxxx",
"created": 1674806769,
"paid": true,
"hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_xxxxxxx/test_YWNjdF8xS25xV1lDQ0Y5d3VCNGZYLF9ORkhWxxxxxxxxxxxx?s=ap"
}
]
}
}
}
}
}
```
</TabItem>
</Tabs>
It's recommended to add the Stripe GraphQL API as a [Remote Schema in Hasura](https://hasura.io/docs/latest/remote-schemas/index/) and connect data from your database with data in Stripe. By doing so, it's possible to request data from your database and Stripe in a single GraphQL query.
Here's an example of how to use the Stripe GraphQL API to get a list of invoices for a specific Stripe customer. Note that the user data is fetched from your database and the Stripe customer data is fetched from Stripe:
```graphql
query {
users {
# User in your database
id
displayName
userData {
stripeCustomerId # Customer's Stripe Customer Id
stripeCustomer {
# Data from Stripe
id
name
paymentMethods {
id
card {
brand
last4
}
}
}
}
}
}
```
## Get Started
Install the package:
<Tabs groupId="package-manager">
<TabItem value="npm" label="npm" default>
```bash
npm install @nhost/stripe-graphql-js
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn install @nhost/stripe-graphql-js
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @nhost/stripe-graphql-js
```
</TabItem>
</Tabs>
## Serverless Function
Create a new [Serverless Function](/serverless-functions): `functions/graphql/stripe.ts`:
```ts
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const server = createStripeGraphQLServer()
export default server
```
> You can run the Stripe GraphQL API in any Node.js environment because it's built using [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga).
## Stripe Secret Key
Add `STRIPE_SECRET_KEY` as an environment variable.
If you're using Nhost, add `STRIPE_SECRET_KEY` to `.env.development` like this:
```
STRIPE_SECRET_KEY=sk_test_***
```
And add the production key (`sk_live_***`) to [environment variables](/platform/environment-variables) in the Nhost dashboard.
Learn more about [Stripe API keys](https://stripe.com/docs/keys#obtain-api-keys).
## Start Nhost
```
nhost up
```
Learn more about the [Nhost CLI](/cli).
## Test
Test the Stripe GraphQL API in the browser:
[http://localhost:1337/v1/functions/graphql/stripe](http://localhost:1337/v1/functions/graphql/stripe)
## Remote Schema
Add the Stripe GraphQL API as a Remote Schema in Hasura.
**URL**
```
{{NHOST_FUNCTIONS_URL}}/graphql/stripe
```
**Headers**
```
x-nhost-webhook-secret: NHOST_WEBHOOK_SECRET (From env var)
```
> The `NHOST_WEBHOOK_SECRET` is used to verify that the request is coming from Nhost. The environment variable is a [system environment variable](/platform/environment-variables#system-environment-variables) and is always available.
![Hasura Remote Schema](/img/graphql/remote-schemas/stripe/remote-schema.png)
## Permissions
Here's a minimal example without any custom permissions. Only requests using the `x-hasura-admin-secret` header will work:
```js
const server = createStripeGraphQLServer()
```
For more granular permissions, you can pass an `isAllowed` function to the `createStripeGraphQLServer`. The `isAllowed` function takes a `stripeCustomerId` and [`context`](#context) as parameters and runs every time the GraphQL server makes a request to Stripe to get or modify data for a specific Stripe customer.
Here is an example of an `isAllowed` function:
```ts
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const isAllowed = (stripeCustomerId: string, context: Context) => {
const { isAdmin, userClaims } = context
// allow all requests if they have a valid `x-hasura-admin-secret`
if (isAdmin) {
return true
}
// get user id
const userId = userClaims['x-hasura-user-id']
// check if the user is signed in
if (!userId) {
return false
}
// get more user information from the database
const { user } = await gqlSDK.getUser({
id: userId
})
if (!user) {
return false
}
// check if the user is part of a workspace with the `stripeCustomerId`
return user.workspaceMembers.some((workspaceMember) => {
return workspaceMember.workspace.stripeCustomerId === stripeCustomerId
})
}
const server = createStripeGraphQLServer({ isAllowed })
export default server
```
### Context
The `context` object contains:
- `userClaims` - verified JWT claims from the user's access token.
- `isAdmin` - `true` if the request was made using a valid `x-hasura-admin-secret` header.
- `request` - [Fetch API Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request) that represents the incoming HTTP request in platform-independent way. It can be useful for accessing headers to authenticate a user
- `query` - the DocumentNode that was parsed from the GraphQL query string
- `operationName` - the operation name selected from the incoming query
- `variables` - the variables that were defined in the query
- `extensions` - the extensions that were received from the client
Read more about the [default context from GraphQL Yoga](https://www.the-guild.dev/graphql/yoga-server/docs/features/context#default-context).
## Source Code
The source code is available on [GitHub](https://github.com/nhost/nhost/tree/main/integrations/stripe-graphql-js).

View File

@@ -103,7 +103,7 @@ You can install the Nhost Next.js SDK with:
<TabItem value="npm" label="npm" default>
```bash
npm install@nhost/nextjs graphql
npm install @nhost/nextjs graphql
```
</TabItem>

View File

@@ -11,9 +11,9 @@ With Nhost, you can deploy Serverless Functions to execute custom code. Each Ser
Serverless functions can be used to handle [event triggers](/database/event-triggers), form submissions, integrations (e.g. Stripe, Slack, etc), and more.
## Creating a Serverless Function
## Create a Serverless Function
Every `.js` (JavaScript) and `.ts` (TypeScript) file in the `functions/` folder of your Nhost project is its own Serverless Function.
Every `.ts` (TypeScript) and `.js` (JavaScript) file in the `functions/` folder of your Nhost project is its own Serverless Function.
<Tabs groupId="language">
<TabItem value="ts" label="TypeScript" default>

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.0.9",
"version": "0.0.12",
"private": true,
"scripts": {
"docusaurus": "docusaurus",

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -1,5 +1,14 @@
# @nhost-examples/codegen-react-apollo
## 0.1.5
### Patch Changes
- 200e9f77: chore(deps): update dependency @types/react-dom to v18.0.10
- Updated dependencies [200e9f77]
- @nhost/react@1.13.2
- @nhost/react-apollo@4.13.2
## 0.1.4
### Patch Changes

View File

@@ -1,11 +1,12 @@
# GraphQL Code Generator Example with React and Apollo Client
This is an example repo for how to use GraphQL Code Generator together with:
Todo app to show how to use:
- [TypeScript](https://www.typescriptlang.org/)
- [Nhost](https://nhost.io/)
- [React](https://reactjs.org/)
- [TypeScript](https://www.typescriptlang.org/)
- [GraphQL Code Generator](https://the-guild.dev/graphql/codegen)
- [Apollo Client](https://www.apollographql.com/docs/react/)
- [Nhost](http://nhost.io/)
This repo is a reference repo for the blog post: [How to use GraphQL Code Generator with React and Apollo](https://nhost.io/blog/how-to-use-graphql-code-generator-with-react-and-apollo).
@@ -13,42 +14,42 @@ This repo is a reference repo for the blog post: [How to use GraphQL Code Genera
1. Clone the repository
```
```sh
git clone https://github.com/nhost/nhost
cd nhost
```
2. Install and build dependencies
```
```sh
pnpm install
pnpm build
pnpm run build
```
3. Go to the Codegen React Apollo example folder
3. Go to the example folder
```
cd examples/codegen-react-apollo
```sh
cd examples/codegen-react-urql
```
4. Terminal 1: Start Nhost
> Make sure you have the [Nhost CLI installed](https://docs.nhost.io/platform/cli).
```sh
nhost up
nhost up -d
```
5. Terminal 2: Run GraphQL Codegen
5. Terminal 2: Start the React application
```sh
pnpm run dev
```
## GraphQL Code Generators
To re-run the GraphQL Code Generators, run the following:
```
pnpm codegen -w
```
> `-w` runs [codegen in watch mode](https://www.the-guild.dev/graphql/codegen/docs/getting-started/development-workflow#watch-mode).
6. Terminal 3: Start the React application
```sh
pnpm dev
```

View File

@@ -2,13 +2,3 @@ schema:
- http://localhost:1337/v1/graphql:
headers:
x-hasura-admin-secret: nhost-admin-secret
documents:
- 'src/**/*.graphql'
generates:
src/utils/__generated__/graphql.ts:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
config:
withRefetchFn: true

View File

@@ -9,6 +9,6 @@
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,14 +1,25 @@
metadata_directory: metadata
services:
hasura:
image: hasura/graphql-engine:v2.15.2
environment:
hasura_graphql_enable_remote_schema_permissions: false
image: hasura/graphql-engine:v2.15.2
minio:
environment:
minio_root_password: minioaccesskey123123
minio_root_user: minioaccesskey123123
postgres:
environment:
postgres_password: postgres
postgres_user: postgres
auth:
image: nhost/hasura-auth:0.16.2
image: nhost/hasura-auth:0.16.1
storage:
image: nhost/hasura-storage:0.3.0
auth:
webauthn:
enabled: true
rp_name: URQL
access_control:
email:
allowed_email_domains: ''
@@ -18,13 +29,13 @@ auth:
url:
allowed_redirect_urls: ''
anonymous_users_enabled: false
client_url: http://localhost:3000
client_url: http://localhost:5173
disable_new_users: false
email:
enabled: false
passwordless:
enabled: false
signin_email_verified_required: true
enabled: true
signin_email_verified_required: false
template_fetch_url: ''
gravatar:
default: ''
@@ -117,11 +128,7 @@ auth:
secure: false
sender: hasura-auth@example.com
user: user
token:
access:
expires_in: 900
refresh:
expires_in: 43200
access_token_expires_in: 315
user:
allowed_roles: user,me
default_allowed_roles: user,me

View File

@@ -9,6 +9,6 @@
connection_lifetime: 600
idle_timeout: 180
max_connections: 50
retries: 1
retries: 20
use_prepared_statements: true
tables: "!include default/tables/tables.yaml"

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