Compare commits

...

298 Commits

Author SHA1 Message Date
Szilárd Dóró
8857314e22 Merge pull request #1237 from nhost/changeset-release/main
chore: update versions
2022-11-29 20:57:22 +01:00
github-actions[bot]
85f1c4a98e chore: update versions 2022-11-29 19:33:07 +00:00
Pilou
efa6b5755d Merge pull request #1248 from nhost/chore/remove-sort-lint-rule
chore: remove eslint plugin simple-import-sort
2022-11-29 20:31:24 +01:00
Pierre-Louis Mercereau
2b19416787 chore: remove eslint plugin simple-import-sort 2022-11-29 20:28:13 +01:00
Szilárd Dóró
4e5d43f300 Merge pull request #1245 from nhost/chore/settings-roles-and-permissions-refactor
chore(dashboard): refactor Roles and Permissions settings sections
2022-11-29 17:00:12 +01:00
Szilárd Dóró
db342f453e chore(dashboard): add changesets 2022-11-29 16:15:45 +01:00
Szilárd Dóró
54386a3b56 chore(dashboard): simplify env var dialog props 2022-11-29 16:14:20 +01:00
Pilou
ff40b99f84 Merge pull request #1242 from nhost/fix/set-access-token
fix: 🐛 Distribute the access token to all the sub-clients
2022-11-29 16:08:00 +01:00
Szilárd Dóró
33f8f1d78a Merge remote-tracking branch 'origin/main' into chore/settings-roles-and-permissions-refactor 2022-11-29 16:07:19 +01:00
Szilárd Dóró
c50fe47ab4 Merge pull request #1215 from nhost/feat/settings-environment-variables
feat(dashboard): add Environment Variables page
2022-11-29 16:04:09 +01:00
Pilou
0580f832c8 Merge pull request #1239 from nhost/chore/version-bumps
chore: 🤖 bump to axios 1.2.0
2022-11-29 15:59:38 +01:00
Szilárd Dóró
7d1eb099c0 fix(dashboard): correct system environment variables 2022-11-29 15:53:11 +01:00
Pierre-Louis Mercereau
e15322296b chore: lint 2022-11-29 15:52:25 +01:00
Pierre-Louis Mercereau
91a2bf905b refactor: simplify 2022-11-29 15:37:31 +01:00
Szilárd Dóró
0f9393fe27 fix(dashboard): change system env vars layout 2022-11-29 15:10:37 +01:00
Szilárd Dóró
aebb822549 feat(dashboard): extend system variables 2022-11-29 15:05:58 +01:00
Pierre-Louis Mercereau
1e2be6fadf Merge branch 'main' into chore/version-bumps 2022-11-29 13:41:00 +01:00
Pilou
aafbf5173d Merge pull request #1240 from nhost/test/nhost-js
test: 💍 `nhost.graphql.request` as an authenticated user
2022-11-29 13:00:46 +01:00
Pierre-Louis Mercereau
01e13e2f8c chore: 🤖 lint 2022-11-29 12:52:34 +01:00
Pierre-Louis Mercereau
4364647501 fix: 🐛 accept any encoding 2022-11-29 12:16:35 +01:00
Pierre-Louis Mercereau
ef117c284e fix: 🐛 Distribute the access token to all the sub-clients 2022-11-29 11:49:59 +01:00
Szilárd Dóró
3f919c0a80 chore(dashboard): refactor system variable layout 2022-11-29 11:30:31 +01:00
Pierre-Louis Mercereau
49e447e7b7 test: 💍 nhost.graphql.request as an authenticated user 2022-11-29 11:04:56 +01:00
Pierre-Louis Mercereau
66b4f3d0be chore: 🤖 bump to axios 1.2.0 2022-11-29 10:29:51 +01:00
Szilárd Dóró
aa7fdafe8b chore(dashboard): split Permission Variable dialog 2022-11-28 21:45:15 +01:00
Pilou
7d6de3b289 Merge pull request #1234 from nhost/chore/bump-hasura-auth
chore: 🤖 bump hasura-auth version
2022-11-28 19:51:10 +01:00
Pilou
57e41f77a9 Merge pull request #1238 from nhost/ci/turbo-team
ci: hardcode turbo team
2022-11-28 19:36:48 +01:00
Pierre-Louis Mercereau
f5c2a0ef4f ci: hardcode turbo team 2022-11-28 18:38:03 +01:00
Szilárd Dóró
d52bc8cca5 chore(dashboard): refactor role delete and default 2022-11-28 16:58:28 +01:00
Pilou
04a3e4c965 Merge pull request #1236 from nhost/refactor/state-snapshots
refactor: use state snapshots
2022-11-28 16:52:27 +01:00
Szilárd Dóró
853c0c5775 chore(dashboard): split RoleForm into multiple forms 2022-11-28 16:44:02 +01:00
Pierre-Louis Mercereau
2e6923dc73 refactor: use state snapshots 2022-11-28 16:41:53 +01:00
Pierre-Louis Mercereau
7d6d70d0c7 chore: 🤖 bump hasura-auth version 2022-11-28 15:59:04 +01:00
Szilárd Dóró
7a2100cc17 fix(dashboard): do not try to autofocus a disable input 2022-11-28 15:19:42 +01:00
Szilárd Dóró
5d55f3fa60 chore(dashboard): remove async form loading 2022-11-28 15:13:16 +01:00
Szilárd Dóró
8b0c44a93c chore(dashboard): rename env var forms
- chore(dashboard): do not convert env vars to uppercase by default
2022-11-28 14:59:27 +01:00
Szilárd Dóró
e0cc7cce0a chore(dashboard): update env var dialog subtitle 2022-11-28 13:40:25 +01:00
Szilárd Dóró
6e7d5e0dd4 fix(dashboard): change env var name placeholder 2022-11-28 13:19:17 +01:00
Szilárd Dóró
54c143ebf6 fix(dashboard): allow lowercase letters, convert variable to uppercase 2022-11-28 13:18:16 +01:00
Szilárd Dóró
8b9fa0b150 feat(dashboard): env var validation 2022-11-28 13:05:42 +01:00
Szilárd Dóró
c3bb79e1dd chore(dashboard): refactor env var forms 2022-11-28 11:15:38 +01:00
Szilárd Dóró
128d21e4ec Merge branch 'main' into feat/settings-environment-variables 2022-11-28 09:21:56 +01:00
Szilárd Dóró
40e503c356 Merge pull request #1227 from nhost/fix/vercel-deployment-token
fix(changesets): add Vercel deployment token to CI
2022-11-28 08:28:33 +01:00
Szilárd Dóró
d007e0ade8 chore(changesets): do not create dedicated env var for deploy token 2022-11-28 08:26:11 +01:00
Pilou
fa32513ba7 Merge pull request #1231 from nhost/docs/docker-compose-dashboard
docs: run hasura console from the cli to run the dashboard
2022-11-27 22:50:14 +01:00
Pierre-Louis Mercereau
8893d9e010 docs: run hasura console from the cli to run the dashboard 2022-11-27 21:37:37 +01:00
Szilárd Dóró
81d2fd865c fix(changesets): add Vercel deployment token to CI 2022-11-27 19:51:07 +01:00
Szilárd Dóró
fe3c462099 Merge pull request #1217 from nhost/fix/vercel-pipeline
fix(changesets): add missing `pnpm` command, pre-build project
2022-11-26 11:49:08 +01:00
Szilárd Dóró
f8b082cb02 chore(changesets): split Vercel CLI command 2022-11-25 16:59:56 +01:00
Szilárd Dóró
0c748e6ee6 feat(dashboard): add env var management 2022-11-25 16:57:22 +01:00
Pilou
e2c4ca85b3 Merge pull request #1219 from nhost/docs/docker-compose-dashboard
docs(docker-compose): add the dashboard to the docker-compose example
2022-11-25 16:38:54 +01:00
Szilárd Dóró
0165b998c2 chore(changesets): create separate step for Vercel 2022-11-25 16:33:09 +01:00
Pierre-Louis Mercereau
5d970cc229 feat(docs): add the dashboard to the docker-compose example 2022-11-25 16:14:46 +01:00
Szilárd Dóró
7167170663 feat(dashboard): add env var management dialog 2022-11-25 15:47:34 +01:00
Szilárd Dóró
0f77de2dd0 feat(dashboard): add menu to env vars 2022-11-25 15:13:54 +01:00
Szilárd Dóró
6ae91e48d1 feat(dashboard): list env vars 2022-11-25 14:52:47 +01:00
Szilárd Dóró
69db1594cc fix(changesets): add missing pnpm command, pre-build project 2022-11-25 14:36:54 +01:00
Szilárd Dóró
158cf0da49 Merge pull request #1216 from nhost/changeset-release/main
chore: update versions
2022-11-25 14:12:10 +01:00
github-actions[bot]
7992fc3baa chore: update versions 2022-11-25 13:09:54 +00:00
Szilárd Dóró
85d9596956 Merge branch 'main' into feat/settings-environment-variables 2022-11-25 14:08:46 +01:00
Szilárd Dóró
16d383516e Merge pull request #1206 from nhost/feat/settings-roles-and-permissions
feat(dashboard): Roles and Permissions
2022-11-25 14:08:13 +01:00
Szilárd Dóró
2ca193ccf3 chore(dashboard): improve inline secrets 2022-11-25 14:07:44 +01:00
Szilárd Dóró
ab8e12003d feat(dashboard): show JWT secret 2022-11-25 14:02:48 +01:00
Szilárd Dóró
29cdf6b125 Merge pull request #1214 from nhost/elitan-patch-3
Update serverless-functions.mdx
2022-11-25 13:51:24 +01:00
Szilárd Dóró
41cc3dc5d0 feat(dashboard): settings page for Environment Variables 2022-11-25 13:50:08 +01:00
Johan Eliasson
6b67c9996a Update serverless-functions.mdx 2022-11-25 12:50:39 +01:00
Szilárd Dóró
23274dee41 chore(docs): add changeset 2022-11-25 11:59:42 +01:00
Szilárd Dóró
a5b55c2667 chore(docs): update permission variables image 2022-11-25 11:58:46 +01:00
Szilárd Dóró
1263676eb3 fix(dashboard): permission variable validation 2022-11-25 11:49:46 +01:00
Szilárd Dóró
b1b647ad96 fix(dashboard): reset default role on role removal 2022-11-25 11:34:51 +01:00
Szilárd Dóró
21bbaf5e95 feat(dashboard): add docs link to roles section 2022-11-25 10:56:14 +01:00
Szilárd Dóró
eef9c91403 feat(dashboard): add support for default roles
- remove unnecessary helper labels from roles and permissions
2022-11-25 10:55:20 +01:00
Johan Eliasson
1742cb444d Merge pull request #1212 from nhost/szilarddoro-patch-1
chore(docs): add WorkOS to Authentication page
2022-11-25 10:49:45 +01:00
Szilárd Dóró
c4f374d7f3 Merge remote-tracking branch 'origin/main' into feat/settings-roles-and-permissions 2022-11-25 09:36:57 +01:00
Szilárd Dóró
369ec13070 chore(docs): add WorkOS to Authentication page 2022-11-25 09:36:21 +01:00
Szilárd Dóró
101129eef2 Update dashboard/src/components/settings/permissions/PermissionVariableSettings/PermissionVariableSettings.tsx
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2022-11-25 09:33:33 +01:00
Szilárd Dóró
228fda0364 Merge pull request #1210 from nhost/fix/vercel-deployment 2022-11-25 09:05:57 +01:00
Szilárd Dóró
74085c67a2 fix(dashboard): correct production deployment 2022-11-24 22:31:11 +01:00
Szilárd Dóró
a273725419 Merge pull request #1209 from nhost/fix/precommit-hook
fix(docgen): prevent docgen from breaking the pre-commit hook
2022-11-24 21:07:40 +01:00
Pierre-Louis Mercereau
c5240f8d74 docs: ✏️ remove irrelevant workaround to fixed docgen git hook 2022-11-24 20:53:17 +01:00
Szilárd Dóró
4490068257 chore(docgen): copy binary to node_modules
execute `pnpm i` to copy the `docgen` binary to every package
2022-11-24 20:41:59 +01:00
Szilárd Dóró
3601de3f85 Merge pull request #1208 from nhost/changeset-release/main
chore: update versions
2022-11-24 20:20:45 +01:00
github-actions[bot]
ac9404610b chore: update versions 2022-11-24 19:09:19 +00:00
Szilárd Dóró
63570db57c Merge pull request #1207 from nhost/contributors-readme-action-MBHUTcRQyD
contributors readme action update
2022-11-24 20:08:33 +01:00
github-actions[bot]
538ed78f5a contrib-readme-action has updated readme 2022-11-24 19:07:02 +00:00
Szilárd Dóró
b1a31ecb00 Merge pull request #1181 from nhost/fix/dashboard-custom-local-ports
feat(dashboard): make backend port configurable
2022-11-24 20:06:44 +01:00
Szilárd Dóró
3d151c448c chore(precommit): extend precommit hook with docgen build 2022-11-24 19:42:23 +01:00
Siarhei Lipchyk
bac8ace434 Add support for custom ports via placeholders 2022-11-24 19:03:02 +01:00
Szilárd Dóró
fdd417ed25 feat(dashboard): add router cancellation to permission form
- chore(dashboard): update terminology
2022-11-24 18:03:04 +01:00
Pilou
a402fc17de Merge pull request #1192 from nhost/changeset-release/main
chore: update versions
2022-11-24 17:21:07 +01:00
Szilárd Dóró
4416ceb9cf chore(dashboard): cleanup unused files 2022-11-24 17:00:35 +01:00
Szilárd Dóró
4762ebf61e fix(dashboard): correct original value for role editing 2022-11-24 16:54:01 +01:00
Szilárd Dóró
73e28b5831 feat(dashboard): simplify role management form 2022-11-24 16:52:26 +01:00
Szilárd Dóró
2a7dc5060f feat(dashboard): add support for permission variable management 2022-11-24 16:25:14 +01:00
Szilárd Dóró
9b8ede40a9 feat(dashboard): prevent invalid characters for variables 2022-11-24 15:50:14 +01:00
Szilárd Dóró
f005c20d99 feat(dashboard): add modals for permission variable management 2022-11-24 15:23:30 +01:00
Szilárd Dóró
4adfd613b6 feat(dashboard): support variable listing
- chore(dashboard): rename RolesSettings to RoleSettings
2022-11-24 15:01:51 +01:00
Szilárd Dóró
b6da82c8e3 fix(dashboard): wrong tooltip appearance 2022-11-24 14:25:49 +01:00
Szilárd Dóró
816456edc4 fix(dashboard): remove unnecessary manual focus 2022-11-24 14:24:07 +01:00
Szilárd Dóró
deaf0e86d4 fix(dashboard): changed role form's logic 2022-11-24 14:16:02 +01:00
Szilárd Dóró
23f8206f18 feat(dashboard): add support for role deletion 2022-11-24 12:46:41 +01:00
Szilárd Dóró
9dde4d7988 feat(dashboard): add support for role editing 2022-11-24 12:31:48 +01:00
Szilárd Dóró
26385b9cf9 fix(dashboard): fix ESLint working directories 2022-11-24 12:17:50 +01:00
Szilárd Dóró
6d318206ef feat(dashboard): finalize Create Role modal
- fix(dashboard): lint errors
- chore(dashboard): reduce max allowed linter warnings
2022-11-24 12:11:41 +01:00
Szilárd Dóró
4d727b78a1 feat(dashboard): updated Roles and Permissions page
- created modal for role creation
2022-11-24 11:46:56 +01:00
github-actions[bot]
de0a125e98 chore: update versions 2022-11-24 09:38:59 +00:00
Pilou
ea1ad29031 Merge pull request #1200 from nhost/plmercereau-patch-storage-tag
Update docker-compose.yaml
2022-11-24 10:37:25 +01:00
Szilárd Dóró
3da40e5712 Merge pull request #1205 from nhost/fix/dashboard-meta
fix(dashboard): update terminology
2022-11-24 10:35:48 +01:00
Szilárd Dóró
b9087a4add fix(dashboard): console / dashboard terminology 2022-11-24 09:54:44 +01:00
Szilárd Dóró
1b7a6d0252 fix(dashboard): update terminology 2022-11-24 09:23:08 +01:00
Szilárd Dóró
1417d3e794 Merge pull request #1203 from nhost/contributors-readme-action-FFIU1CawPP
contributors readme action update
2022-11-24 08:55:41 +01:00
github-actions[bot]
e187923858 contrib-readme-action has updated readme 2022-11-24 07:52:33 +00:00
Johan Eliasson
8a60ed4074 Merge pull request #1199 from nhost/functions-more-examples
examples(serverless-functions): smtp + async/await
2022-11-24 08:52:19 +01:00
Pilou
d7d11a44a7 Update docker-compose.yaml
There's no latest tag on storage
2022-11-23 22:04:40 +01:00
Johan Eliasson
062e4691cd added examples 2022-11-23 21:53:25 +01:00
Pilou
a95d49fa2c Merge pull request #1197 from nhost/docs/custom-claims-singleton-array
docs: custom claims and singleton arrays
2022-11-23 19:06:34 +01:00
Johan Eliasson
d14fc96899 Merge pull request #1036 from ejkkan/feat/add-charges-to-stripe-package
feat(stripe-graphql-js): add charges, payment intents and connected accounts
2022-11-23 19:03:37 +01:00
Johan Eliasson
93db718254 Create blue-ghosts-accept.md 2022-11-23 19:03:12 +01:00
Pierre-Louis Mercereau
c367bd58b9 docs: custom claims and singleton arrays 2022-11-23 19:01:29 +01:00
Pilou
0bfed4d9e1 Merge pull request #1196 from nhost/plmercereau-patch-1
Update docker-compose.yaml
2022-11-23 18:43:36 +01:00
Pilou
1f3aecd379 Merge pull request #1193 from nhost/chore/bump-service-versions
ci: 🎡 bump services versions and trigger CI
2022-11-23 18:43:20 +01:00
Pilou
42306ea3bb Update docker-compose.yaml 2022-11-23 18:19:41 +01:00
Pilou
1b12a175f6 Update docker-compose.yaml 2022-11-23 18:18:06 +01:00
Szilárd Dóró
32060aaea0 Merge pull request #1195 from nhost/chore/dashboard-version
chore(dashboard): add changeset
2022-11-23 16:00:18 +01:00
Szilárd Dóró
f94cace5f2 Merge pull request #1194 from nhost/feat/dashboard-vercel-deployment
feat(dashboard): add Vercel deployment
2022-11-23 15:22:09 +01:00
Szilárd Dóró
5de965d9a5 chore(dashboard): add changeset 2022-11-23 15:15:08 +01:00
Szilárd Dóró
e10b3adc11 chore(changesets): incorporate Vercel deployment into publish process 2022-11-23 15:08:21 +01:00
Szilárd Dóró
457db76b06 Merge pull request #1191 from nhost/fix/sign-in-methods-order
fix(dashboard): alphabetic ordering of providers
2022-11-23 15:04:26 +01:00
Szilárd Dóró
1e952a026e chore(changesets): update publish step's name 2022-11-23 15:01:55 +01:00
Szilárd Dóró
2f4c040789 feat(dashboard): add Vercel deployment 2022-11-23 14:33:00 +01:00
Pierre-Louis Mercereau
74648752b4 ci: use correct hasura version 2022-11-23 13:12:32 +01:00
Szilárd Dóró
09d218a3fe fix(dashboard): sign-in method phrasing 2022-11-23 13:12:24 +01:00
Szilárd Dóró
2e8938dbb0 fix(dashboard): add missing Twilio icon 2022-11-23 13:00:12 +01:00
Pierre-Louis Mercereau
ec60d03536 ci: 🎡 bump services versions and trigger CI 2022-11-23 12:57:04 +01:00
Johan Eliasson
2f3767552f Merge pull request #1189 from nhost/docs-workos
docs(workos): WorkOS Docs
2022-11-23 12:14:57 +01:00
Szilárd Dóró
bc401c0dd2 fix(dashboard): alphabetic ordering of providers
- fixes #1188
- fix checkbox font size on the Settings page
2022-11-23 12:12:39 +01:00
Pilou
2907ecb7ff Merge pull request #1179 from nhost/changeset-release/main
chore: update versions
2022-11-23 11:24:55 +01:00
Pierre-Louis Mercereau
05d7f5207f chore: no major bump of peer dependencies 2022-11-23 11:22:55 +01:00
github-actions[bot]
07a053ee80 chore: update versions 2022-11-23 09:58:39 +00:00
Szilárd Dóró
2145243b19 chore(dashboard): update environment variables 2022-11-23 10:56:47 +01:00
Pilou
61e4414a8f Merge pull request #1190 from nhost/changeset-react-components
react components changeset added
2022-11-23 10:55:04 +01:00
Johan Eliasson
4601d84e0e changeset added 2022-11-23 10:29:19 +01:00
Johan Eliasson
ca012d790c Create tidy-teachers-flow.md 2022-11-23 10:27:23 +01:00
Johan Eliasson
aeda14ef53 docs link 2022-11-23 10:25:18 +01:00
Johan Eliasson
3fa5e2005a updates 2022-11-23 10:23:40 +01:00
Johan Eliasson
4dd2e99159 Merge pull request #1187 from nhost/docs-git-8g712asd
docs(git): git docs updates
2022-11-23 08:27:17 +01:00
Johan Eliasson
282c6c6d24 git docs update 2022-11-23 08:16:55 +01:00
Johan Eliasson
beadd84adb moving files 2022-11-23 07:52:57 +01:00
Johan Eliasson
f8f55d2b99 Merge branch 'main' into feat/add-charges-to-stripe-package 2022-11-23 07:49:17 +01:00
Johan Eliasson
03a98d4f3a config updates 2022-11-23 07:36:31 +01:00
Johan Eliasson
8ed8e04ab6 merge 2022-11-23 07:31:37 +01:00
Erik Magnusson
587efd4551 re-applied isAdmin checks for connected account field types 2022-11-23 08:15:44 +02:00
Johan Eliasson
c78227b085 Merge pull request #1185 from nhost/docs/node-16
docs: update developer's guide
2022-11-23 07:06:10 +01:00
Pierre-Louis Mercereau
d87e520307 docs: lessons learned from Sheena and Chris 2022-11-22 21:44:48 +01:00
Pilou
bbed04e4da Merge pull request #1158 from nhost/fix/use-user-roles
fix: 🐛 make `useUserRoles` reactive
2022-11-22 21:35:06 +01:00
Pilou
273afc9740 Merge pull request #1184 from nhost/contributors-readme-action-h-P6q9XKTD
contributors readme action update
2022-11-22 21:32:07 +01:00
github-actions[bot]
f4083aa4b3 contrib-readme-action has updated readme 2022-11-22 20:12:23 +00:00
Pilou
ddd2641726 Merge pull request #1183 from massless/min-version-node
Update minimum version of node
2022-11-22 21:12:04 +01:00
Chris Wetherell
4658aeb31e Update minimum version of node 2022-11-22 12:08:47 -08:00
Johan Eliasson
cc8e5fe4a9 Merge pull request #1180 from nhost/react-components-iy8gasd9
React Auth components: SignedIn and SignedOut
2022-11-22 19:54:19 +01:00
Szilárd Dóró
85c897c717 chore(docs): update source code references 2022-11-22 18:32:48 +01:00
Szilárd Dóró
c99e5552e6 chore(react): simplify component signature 2022-11-22 18:31:39 +01:00
Szilárd Dóró
97a2520ea1 feat(docgen): add support for components 2022-11-22 18:24:23 +01:00
Johan Eliasson
964af2912b inline docs 2022-11-22 17:04:39 +01:00
Szilárd Dóró
a48dd5bf74 chore(dashboard): add changeset 2022-11-22 14:32:53 +01:00
Szilárd Dóró
ef53df5cb3 feat(dashboard): make backend port configurable
- additionally: improve the way Hasura service ports are. configured through environment variables
2022-11-22 14:32:13 +01:00
Johan Eliasson
afea682a8c merge 2022-11-22 14:09:38 +01:00
Pierre-Louis Mercereau
fefa2baa2e refactor: readability 2022-11-22 13:28:09 +01:00
Pilou
f09b3cfd24 Merge pull request #1178 from nhost/ci/freeze-pnpm-version
ci: set explicit pnpm version in the dashboard dockerfile
2022-11-22 13:20:35 +01:00
Pilou
dd3b2c41f1 Merge pull request #1171 from nhost/chore/vue-file-upload-changeset
chore: add changeset to vue, and correct inline documentation
2022-11-22 13:20:21 +01:00
Szilárd Dóró
aaced20f31 Merge pull request #1173 from nhost/fix/dashboard-provider-redirect-url
fix(dashboard): correct redirect URL input opacity
2022-11-22 13:15:35 +01:00
Pierre-Louis Mercereau
3e91c19e13 ci: set explicit pnpm version in the dashboard dockerfile 2022-11-22 13:11:00 +01:00
Pilou
abe0edcacb Merge pull request #1169 from nhost/changeset-release/main
chore: update versions
2022-11-22 12:37:10 +01:00
github-actions[bot]
f8dae56bda chore: update versions 2022-11-22 10:06:23 +00:00
Pilou
9133726dbe Merge pull request #1176 from nhost/chore/correct-changeset
chore: correct changeset from major to patch
2022-11-22 11:03:17 +01:00
Pierre-Louis Mercereau
7eed617034 chore: correct changeset from major to patch 2022-11-22 10:49:00 +01:00
Johan Eliasson
d4fd4ec3e9 signedin and signedout 2022-11-22 10:48:24 +01:00
Pilou
19a0288861 Merge pull request #1172 from nhost/chore/nextjs-13
chore: nextjs 13
2022-11-22 10:48:10 +01:00
Pilou
da975387ac Merge pull request #1175 from nhost/contributors-readme-action-AN1AN0O3NW
contributors readme action update
2022-11-22 10:42:46 +01:00
github-actions[bot]
e46c77e409 contrib-readme-action has updated readme 2022-11-22 09:41:42 +00:00
Szilárd Dóró
6c642d86f3 Merge pull request #1174 from nhost/fix/dashboard-codegen
fix(dashboard): remove functions folder reference from codegen config
2022-11-22 10:41:30 +01:00
Pierre-Louis Mercereau
46a77f1ce5 chore: change nextjs 13 patch version 2022-11-22 10:37:53 +01:00
Szilárd Dóró
6053560b5a fix(dashboard): remove functions folder reference from codegen config 2022-11-22 10:26:57 +01:00
Szilárd Dóró
89bd37bc28 chore(dashboard): add changeset 2022-11-22 10:20:52 +01:00
Szilárd Dóró
0df73a41c9 fix(dashboard): redirect URL opacity 2022-11-22 10:20:09 +01:00
Pierre-Louis Mercereau
53bdc294e2 chore: nextjs 13 2022-11-22 09:40:52 +01:00
Pierre-Louis Mercereau
f6d2042adb chore: add changeset to vue, and correct inline documentation 2022-11-22 09:34:47 +01:00
Pilou
ba83475ced Merge pull request #1162 from nhost/fix/dashboard-fix-oauth-callback
fix(dashboard): correct redirect URL for oauth providers
2022-11-22 09:32:25 +01:00
Pilou
dafc581c08 Merge pull request #1057 from chrisli-03/main
add useFileUpload composable for vue
2022-11-22 09:28:07 +01:00
Pilou
c88b77ef43 Merge pull request #1166 from nhost/ci/bump-codecov-action
ci: bump codecov/codecov-action to v3
2022-11-22 09:25:20 +01:00
Pilou
1470592aac Merge pull request #1168 from nhost/ci/bump-pnpm
chore: bump pnpm version to v7.17.0
2022-11-22 09:25:01 +01:00
Szilárd Dóró
4e9a560346 chore(dashboard): update changeset 2022-11-22 09:23:59 +01:00
Szilárd Dóró
766cb61243 chore(dashboard): add changeset 2022-11-22 09:22:43 +01:00
Pilou
7a9370abb2 Merge pull request #1165 from nhost/contributors-readme-action-7MgU1b_FLK
contributors readme action update
2022-11-22 09:19:18 +01:00
Pilou
73368c87a2 Merge pull request #1167 from nhost/contributors-readme-action-QJVpJRh9Hj
contributors readme action update
2022-11-22 09:15:46 +01:00
github-actions[bot]
ef20f1f504 contrib-readme-action has updated readme 2022-11-22 08:13:00 +00:00
Pierre-Louis Mercereau
616a71fc89 chore: bump pnpm version to v7.17.0 2022-11-22 09:12:55 +01:00
Szilárd Dóró
9477e11d4c Merge pull request #1161 from nhost/changeset-release/main
chore: update versions
2022-11-22 09:12:45 +01:00
Pierre-Louis Mercereau
3c0adb4922 ci: bump codecov/codecov-action to v3 2022-11-22 09:01:47 +01:00
github-actions[bot]
c1bfc16ec2 contrib-readme-action has updated readme 2022-11-22 06:59:16 +00:00
Johan Eliasson
1fe86f770c Merge pull request #1164 from alexander-mart/patch-1
Vue docs fix: «client» → «nhost»
2022-11-22 07:59:02 +01:00
Alexander Mart
d5de56256a fix: client → nhost 2022-11-22 12:19:28 +07:00
Nuno Pato
b5e8222b76 Fix redirect url for oauth providers 2022-11-21 23:50:25 -01:00
Pilou
7f15375a9a Merge pull request #1157 from nhost/fix/same-site-cookie
fix: 🐛 Set same-site cookie to `lax`
2022-11-21 23:17:44 +01:00
github-actions[bot]
ffd8660bcc chore: update versions 2022-11-21 19:32:34 +00:00
Szilárd Dóró
9159cf46b1 Merge pull request #1159 from nhost/feat/dashboard-feature-migration 2022-11-21 20:29:54 +01:00
Szilárd Dóró
9211743d9c chore(dashboard): add changeset 2022-11-21 17:32:30 +01:00
Szilárd Dóró
cc6aae3fba chore(ci): do not lint dashboard when linting packages 2022-11-21 17:30:53 +01:00
Szilárd Dóró
a9fbe8e0fc fix(dashboard): linter errors
chore(dashboard): bump dependency versions
2022-11-21 17:28:27 +01:00
Szilárd Dóró
40cbeac221 chore(dashboard): generate pnpm-lock, migrate bugfixes 2022-11-21 17:17:27 +01:00
Szilárd Dóró
df8e31305d feat(dashboard): migrate Settings page features
- migrate some features from the old repo to `nhost/nhost`
2022-11-21 17:01:48 +01:00
Pilou
90af9f2224 Merge pull request #1147 from nhost/ci/optimisation
ci: fine-tune the dashboard release
2022-11-21 16:47:00 +01:00
Szilárd Dóró
037fbdf37a chore(actions): fix workflow names 2022-11-21 16:32:49 +01:00
Pierre-Louis Mercereau
843087cb11 fix: 🐛 make useUserRoles reactive 2022-11-21 15:58:55 +01:00
Pierre-Louis Mercereau
f2aaff0504 fix: 🐛 Set same-site cookie to lax 2022-11-21 15:17:47 +01:00
Pierre-Louis Mercereau
ee2f53a052 Merge branch 'main' into ci/optimisation 2022-11-21 13:34:50 +01:00
Pilou
8f5255172e Merge pull request #1156 from nhost/chore/dashboard-ci
chore(dashboard): parallelize build, tests and lint
2022-11-21 13:31:31 +01:00
Szilárd Dóró
c9de90e027 chore(dashboard): parallelize build, tests and lint 2022-11-21 13:23:58 +01:00
Pierre-Louis Mercereau
3341632f23 Merge branch 'ci/optimisation' of https://github.com/nhost/nhost into ci/optimisation 2022-11-21 13:08:03 +01:00
Pierre-Louis Mercereau
e005a67ab4 ci: enable turborepo everywhere 2022-11-21 13:07:19 +01:00
Pilou
1f4bbf75e0 Update .github/workflows/changesets.yaml
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2022-11-21 09:02:30 +01:00
Pilou
e5934d5dfd Update .github/workflows/changesets.yaml
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2022-11-21 09:02:18 +01:00
Pilou
8b368ba2e8 Merge pull request #1149 from nhost/remove-crm-example
Remove crm example
2022-11-20 11:26:44 +01:00
Johan Eliasson
16017fb8d2 remove crm example 2022-11-20 09:34:09 +01:00
Pierre-Louis Mercereau
effc0aba52 ci: tag inside the release job 2022-11-19 21:02:16 +01:00
Pierre-Louis Mercereau
45a81ca823 chore: add version dependency to publish to use needs.version 2022-11-19 19:32:09 +01:00
Pierre-Louis Mercereau
377e8f8c37 ci: github release 2022-11-19 19:29:21 +01:00
Pierre-Louis Mercereau
977d58a938 ci: fine-tune the dashboard release 2022-11-19 19:25:15 +01:00
Szilárd Dóró
eb7a14cedb Merge pull request #1146 from nhost/changeset-release/main
chore: update versions
2022-11-18 16:42:24 +01:00
github-actions[bot]
5a64bdf30a chore: update versions 2022-11-18 15:27:02 +00:00
Szilárd Dóró
7acf96d65f Merge pull request #1145 from nhost/fix/dashboard-backend-url
fix(dashboard): avoid using `BACKEND_URL` locally
2022-11-18 16:25:28 +01:00
Pilou
57726864fd Merge pull request #1144 from nhost/ci/fix-dashboard-version
ci: fix version
2022-11-18 16:19:07 +01:00
Szilárd Dóró
73da6a67f1 chore(dashboard): add changeset 2022-11-18 15:52:45 +01:00
Szilárd Dóró
48964b82e0 fix(dashboard): avoid using BACKEND_URL locally 2022-11-18 15:51:31 +01:00
Pierre-Louis Mercereau
9d2fdbadc8 ci: remove env cmd 2022-11-18 15:35:13 +01:00
Pierre-Louis Mercereau
6bd874b485 ci: correct env 2022-11-18 15:19:07 +01:00
Pierre-Louis Mercereau
4b36670897 ci: config turborepo 2022-11-18 15:10:50 +01:00
Pierre-Louis Mercereau
947696efc6 ci: fix version 2022-11-18 14:52:01 +01:00
Pilou
a3168a1dae Merge pull request #1143 from nhost/ci/turbo-cache
ci: fix typo
2022-11-18 14:48:52 +01:00
Pierre-Louis Mercereau
29efea2ad8 ci: fix typo 2022-11-18 14:44:05 +01:00
Pilou
390688feb1 Merge pull request #1142 from nhost/ci/turbo-cache
ci: use turborepo cache when building images
2022-11-18 13:48:29 +01:00
Pierre-Louis Mercereau
20e19ec7db ci: use turborepo cache when building images 2022-11-18 13:27:59 +01:00
Szilárd Dóró
b0c58ff351 Merge pull request #1141 from nhost/changeset-release/main
chore: update versions
2022-11-18 12:18:40 +01:00
github-actions[bot]
7b7cc74948 chore: update versions 2022-11-18 11:17:45 +00:00
Szilárd Dóró
1f501c829c Merge pull request #1123 from nhost/dashboard-docker
Dashboard Docker
2022-11-18 12:15:05 +01:00
Szilárd Dóró
8f9993d8ed chore(dashboard): bump graphiql, fix React warning 2022-11-18 10:31:27 +01:00
Pilou
f53b1f5c13 Merge pull request #1140 from nhost/chore/no-gyp
chore: get rid of node-gyp and its bin dependencies
2022-11-18 10:10:16 +01:00
Pierre-Louis Mercereau
1b8dcf237a chore: get rid of node-gyp and its bin dependencies 2022-11-18 10:07:22 +01:00
Szilárd Dóró
fc559d9e29 chore(changesets): fix review comments 2022-11-18 09:16:24 +01:00
Szilárd Dóró
fe61dbb6dc fix(changesets): push Docker image 2022-11-18 09:06:01 +01:00
Pilou
8f90569230 Merge pull request #1135 from nhost/docs/inline-webauthn
docs: inline webauthn examples
2022-11-17 21:33:05 +01:00
Szilárd Dóró
5a11ace8f0 chore(changesets): incorporate publishing in changesets 2022-11-17 15:49:58 +01:00
Szilárd Dóró
c569c5f60c fix(dashboard): don't block render because of health check 2022-11-17 15:18:44 +01:00
Johan Eliasson
fbcef432a3 Merge pull request #1134 from nhost/elitan-patch-3
Update storage.mdx
2022-11-17 14:57:30 +01:00
Szilárd Dóró
44ae629f86 fix(publish): correct workflow 2022-11-17 13:14:58 +01:00
Szilárd Dóró
e030856660 fix(changesets): remove publish step 2022-11-17 10:47:23 +01:00
Szilárd Dóró
db118f9769 chore(actions): extract publish to a separate workflow 2022-11-17 10:35:29 +01:00
Szilárd Dóró
8a48a897a7 feat(changesets): add Docker publish step 2022-11-17 09:41:13 +01:00
Szilárd Dóró
dce91ec7d8 fix(dashboard): correct client for local development 2022-11-17 09:31:47 +01:00
Pierre-Louis Mercereau
0a3383d6c5 chore: typo 2022-11-17 08:14:22 +01:00
Pierre-Louis Mercereau
97b5310c5d docs: inline webauthn examples 2022-11-17 08:10:48 +01:00
Johan Eliasson
5c8c79444a Update storage.mdx 2022-11-16 23:35:08 +01:00
Johan Eliasson
eb3041341d Merge pull request #1115 from nhost/docs-updates-8yg9hjasd
Docs updates
2022-11-16 23:29:16 +01:00
Szilárd Dóró
f57f237e37 chore(dashboard): create changelog 2022-11-16 16:44:54 +01:00
Szilárd Dóró
b1e90e6e2b Merge branch 'main' into dashboard-docker 2022-11-16 16:21:20 +01:00
Szilárd Dóró
1c947b2995 chore(dashboard): add missing env vars to Dockerfile 2022-11-16 12:42:36 +01:00
Johan Eliasson
d00f6ed84e Update docs/docs/platform/multiple-environments.mdx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2022-11-16 12:22:34 +01:00
Johan Eliasson
4c88846d72 Update docs/docs/reference/cli/init.mdx
Co-authored-by: Pilou <24897252+plmercereau@users.noreply.github.com>
2022-11-16 12:21:21 +01:00
Johan Eliasson
97dc689d79 redirect update 2022-11-16 08:32:01 +01:00
Johan Eliasson
311417d679 typo 2022-11-16 08:27:07 +01:00
Johan Eliasson
ce0e1ee7ae redirect update 2022-11-16 08:24:04 +01:00
Johan Eliasson
1cc6841107 redirect update 2022-11-16 08:19:49 +01:00
Johan Eliasson
4b7fff0440 redirect update 2022-11-16 08:16:43 +01:00
Johan Eliasson
47fc7ffc0e testing redirects 2022-11-16 08:05:26 +01:00
Szilárd Dóró
7c5d0d0ec6 fix(dashboard): copy public folder 2022-11-15 18:02:59 +01:00
Szilárd Dóró
4d9c48f524 chore(dashboard): move Dockerfile, update commands 2022-11-15 15:17:19 +01:00
Szilárd Dóró
842e9892c0 chore(dashboard): update Dockerfile 2022-11-15 12:05:18 +01:00
Johan Eliasson
37fee16552 Merge branch 'main' into docs-updates-8yg9hjasd 2022-11-15 10:58:20 +01:00
Johan Eliasson
d056fb4dbd Merge branch 'main' into docs-updates-8yg9hjasd 2022-11-14 11:25:36 +01:00
Johan Eliasson
ed6d9e8a85 Dashboard Docker 2022-11-14 11:11:46 +01:00
Johan Eliasson
7840201e91 cli ref updates 2022-11-14 10:21:40 +01:00
Johan Eliasson
af8891686b updates 2022-11-14 10:11:46 +01:00
Johan Eliasson
13efafb000 multiple environments first draft 2022-11-12 10:01:46 +01:00
Johan Eliasson
719a3ddcf9 crazy update 2022-11-11 22:38:22 +01:00
Johan Eliasson
d11980f078 update 2022-11-11 11:16:26 +01:00
Johan Eliasson
bfbe8733f6 updates 2022-11-11 10:01:02 +01:00
Erik Magnusson
7055ffc37a Merge branch 'feat/add-charges-to-stripe-package' of https://github.com/ejkkan/nhost into feat/add-charges-to-stripe-package 2022-10-20 20:57:56 +02:00
Erik Magnusson
c68ce6d480 removed isAdmin checks for connected accounts related schema fields 2022-10-20 20:57:01 +02:00
Chris
412b1fa8c6 add useFileUpload composable for vue 2022-10-19 21:30:29 -04:00
Erik Magnusson
98a149c8bf Update packages/stripe-graphql-js/src/schema/charge.ts
Co-authored-by: Johan Eliasson <johan@eliasson.me>
2022-10-18 08:48:58 +02:00
Erik Magnusson
ceb558975e Update packages/stripe-graphql-js/src/schema/charge.ts
Co-authored-by: Johan Eliasson <johan@eliasson.me>
2022-10-18 08:48:46 +02:00
Erik Magnusson
7a87321a7e fixd imports and linting 2022-10-14 21:21:25 +02:00
Erik Magnusson
9349766c0a corrected spelling 2022-10-14 21:19:23 +02:00
Erik Magnusson
31655191a3 added connected accounts 2022-10-14 21:05:57 +02:00
Erik Magnusson
e3b91efa84 feat/added charges 2022-10-14 20:13:35 +02:00
Erik Magnusson
cfe736776a feat/added charges 2022-10-14 20:03:19 +02:00
Erik Magnusson
481bf237cc updated naming convention to camel casing 2022-10-14 10:11:02 +02:00
Erik Magnusson
33ce9bf1b9 added payment intents type for customer and invoice objects 2022-10-04 18:10:02 +02:00
490 changed files with 12544 additions and 8455 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
**/node_modules
**/npm-debug.log
**/out
**/dist
**/umd
**/.turbo
**/.nhost
**/coverage
**/.next

View File

@@ -1,11 +1,16 @@
name: Install Node and package dependencies
description: 'Install Node dependencies with pnpm'
inputs:
TURBO_TOKEN:
description: 'Turborepo token'
TURBO_TEAM:
description: 'Turborepo team'
runs:
using: 'composite'
steps:
- uses: pnpm/action-setup@v2.2.4
with:
version: 7.9.1
version: 7.17.0
run_install: false
- name: Get pnpm cache directory
id: pnpm-cache-dir
@@ -31,3 +36,6 @@ runs:
- shell: bash
name: Build packages
run: pnpm build
env:
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}

View File

@@ -5,26 +5,34 @@ on:
branches: [main]
paths-ignore:
- 'docs/**'
- 'dashboard/**'
- 'examples/**'
- 'assets/**'
- '**.md'
- '!.changeset/**'
- 'LICENSE'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost
DASHBOARD_PACKAGE: '@nhost/dashboard'
jobs:
version:
name: Version
runs-on: ubuntu-latest
outputs:
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
dashboardVersion: ${{ steps.dashboard.outputs.dashboardVersion }}
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
# * Install Node and dependencies
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- name: Create PR or Publish release
id: changesets
uses: changesets/action@v1
@@ -36,3 +44,107 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Check Dashboard tag
id: dashboard
if: steps.changesets.outputs.hasChangesets == 'false'
run: |
DASHBOARD_VERSION=$(jq -r .version dashboard/package.json)
GIT_TAG="${{ env.DASHBOARD_PACKAGE}}@$DASHBOARD_VERSION"
if [ -z "$(git tag -l | grep $GIT_TAG)" ]; then
echo "dashboardVersion=$DASHBOARD_VERSION" >> $GITHUB_OUTPUT
fi
test:
needs: version
name: Dashboard
if: needs.version.outputs.dashboardVersion != ''
uses: ./.github/workflows/dashboard.yaml
secrets: inherit
publish-docker:
name: Publish to Docker Hub
runs-on: ubuntu-latest
needs:
- test
- version
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Add git tag
run: |
git tag "${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}"
git push origin --tags
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
nhost/dashboard
tags: |
type=raw,value=latest,enable=true
type=semver,pattern={{version}},value=v${{ needs.version.outputs.dashboardVersion }}
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.version.outputs.dashboardVersion }}
type=semver,pattern={{major}},value=v${{ needs.version.outputs.dashboardVersion }}
type=sha
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v3
timeout-minutes: 60
with:
context: .
file: ./dashboard/Dockerfile
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
TURBO_TOKEN=${{ env.TURBO_TOKEN }}
TURBO_TEAM=${{ env.TURBO_TEAM }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
- name: Create GitHub Release
uses: taiki-e/create-gh-release-action@v1
with:
changelog: dashboard/CHANGELOG.md
token: ${{ secrets.GITHUB_TOKEN }}
prefix: ${{ env.DASHBOARD_PACKAGE }}@
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
- name: Remove tag on failure
if: failure()
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
publish-vercel:
name: Publish to Vercel
runs-on: ubuntu-latest
needs:
- publish-docker
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- name: Setup Vercel CLI
run: pnpm add -g vercel
- name: Trigger a Vercel deployment
env:
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
run: |
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}

View File

@@ -1,14 +1,12 @@
name: 'Dashboard'
on:
push:
branches: [main]
paths:
- 'dashboard/**'
workflow_call:
pull_request:
branches: [main]
types: [opened, synchronize]
paths:
- 'packages/**'
- 'dashboard/**'
env:
@@ -26,17 +24,15 @@ jobs:
- uses: actions/checkout@v3
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- name: Build the application
run: pnpm build:dashboard
- uses: actions/cache@v3.0.11
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
tests:
name: Tests
runs-on: ubuntu-latest
needs: build
env:
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
@@ -45,18 +41,15 @@ jobs:
- uses: actions/checkout@v3
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
- uses: actions/cache@v3.0.11
id: restore-build
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- name: Run tests
run: pnpm test:dashboard
lint:
name: Lint
runs-on: ubuntu-latest
needs: build
env:
NEXT_PUBLIC_ENV: dev
NEXT_TELEMETRY_DISABLED: 1
@@ -65,9 +58,7 @@ jobs:
- uses: actions/checkout@v3
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
- uses: actions/cache@v3.0.11
id: restore-build
with:
path: ./*
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- run: pnpm lint:dashboard

View File

@@ -1,4 +1,4 @@
name: Tests
name: Packages
on:
push:
@@ -33,6 +33,9 @@ jobs:
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
# * In a PR, only include packages that have been modified, and their dependencies
- name: List examples with an e2e script
@@ -61,6 +64,9 @@ jobs:
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
# * Install Nhost CLI if a `nhost/config.yaml` file is found
- name: Install Nhost CLI
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
@@ -92,11 +98,14 @@ jobs:
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
- name: Run unit tests
run: pnpm run test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v3
with:
files: '**/coverage/coverage-final.json'
name: codecov-umbrella
@@ -113,6 +122,9 @@ jobs:
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
- name: Lint
run: pnpm run lint

4
.gitignore vendored
View File

@@ -48,6 +48,10 @@ todo.md
.netlify
.monorepo-example
# Local Vercel folder
.vercel
# Next.js build output
.next
# TypeDoc output

View File

@@ -1,5 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
}
},
"eslint.workingDirectories": ["./dashboard"]
}

View File

@@ -2,6 +2,8 @@
## Requirements
- This repository works with **Node 16**
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
```sh
@@ -97,6 +99,7 @@ You can take a look at the changeset documentation: [How to add a changeset](htt
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
<!-- ## Good practices
- lint
- prettier

View File

@@ -179,14 +179,21 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Grégory D'Angelo</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ejkkan">
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
<br />
<sub><b>Erik Magnusson</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/guicurcio">
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
<br />
<sub><b>Guido Curcio</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/subatuba21">
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
@@ -221,15 +228,15 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Christopher Möller</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/GavanWilhite">
<img src="https://avatars.githubusercontent.com/u/2085119?v=4" width="100;" alt="GavanWilhite"/>
<br />
<sub><b>Gavan Wilhite</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/FuzzyReason">
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
@@ -237,13 +244,6 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Vadim Smirnov</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ejkkan">
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
<br />
<sub><b>Erik Magnusson</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/macmac49">
<img src="https://avatars.githubusercontent.com/u/831190?v=4" width="100;" alt="macmac49"/>
@@ -344,21 +344,28 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Muttenzer</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/alexander-mart">
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
<br />
<sub><b>Alexander Mart</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ahmic">
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
<br />
<sub><b>Amir Ahmic</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/akd-io">
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
<br />
<sub><b>Anders Kjær Damgaard</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/Sonichigo">
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
@@ -366,6 +373,20 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Animesh Pathak</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/chrisli-03">
<img src="https://avatars.githubusercontent.com/u/11177048?v=4" width="100;" alt="chrisli-03"/>
<br />
<sub><b>Chris</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/massless">
<img src="https://avatars.githubusercontent.com/u/44389?v=4" width="100;" alt="massless"/>
<br />
<sub><b>Chris Wetherell</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/rustyb">
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
@@ -379,7 +400,8 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Dago</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/dminkovsky">
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
@@ -400,8 +422,7 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Gaurav Agrawal</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/alveshelio">
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
@@ -422,7 +443,8 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Ikko Ashimine</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/jladuval">
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
@@ -443,8 +465,7 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Lucas Bois</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/MarcelloTheArcane">
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
@@ -465,7 +486,8 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Nirmalya Ghosh</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/quentin-decre">
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
@@ -473,6 +495,13 @@ Here are some ways of contributing to making Nhost better:
<sub><b>Quentin Decré</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/elephant3">
<img src="https://avatars.githubusercontent.com/u/48279149?v=4" width="100;" alt="elephant3"/>
<br />
<sub><b>Siarhei Lipchyk</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/altschuler">
<img src="https://avatars.githubusercontent.com/u/956928?v=4" width="100;" alt="altschuler"/>
@@ -486,8 +515,7 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Tapas Adhikary</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/uulwake">
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
@@ -501,7 +529,8 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Vadim</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/TheRedLancer">
<img src="https://avatars.githubusercontent.com/u/58493767?v=4" width="100;" alt="TheRedLancer"/>

View File

@@ -21,7 +21,7 @@ module.exports = {
'tests/**/*.ts',
'tests/**/*.d.ts'
],
plugins: ['@typescript-eslint', 'simple-import-sort', 'cypress'],
plugins: ['@typescript-eslint', 'cypress'],
extends: ['plugin:cypress/recommended'],
parserOptions: {
ecmaVersion: 2020,
@@ -30,31 +30,6 @@ module.exports = {
rules: {
'react/prop-types': 'off',
'no-use-before-define': 'off',
'simple-import-sort/exports': 'error',
'simple-import-sort/imports': [
'error',
{
groups: [
// Node.js builtins. You could also generate this regex if you use a `.js` config.
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
[
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
],
// Packages
['^\\w'],
// Internal packages.
['^(@|config/)(/*|$)'],
// Side effect imports.
['^\\u0000'],
// Parent imports. Put `..` last.
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
// Other relative imports. Put same-folder imports and `.` last.
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
// Style imports.
['^.+\\.s?css$']
]
}
],
'import/no-anonymous-default-export': [
'error',
{

View File

@@ -1,3 +1,4 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['next', 'airbnb', 'airbnb-typescript', 'airbnb/hooks', 'prettier'],

View File

@@ -6,6 +6,18 @@ module.exports = {
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
{
/**
* Fix Storybook issue with PostCSS@8
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
*/
name: '@storybook/addon-postcss',
options: {
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],
framework: '@storybook/react',
core: {

82
dashboard/CHANGELOG.md Normal file
View File

@@ -0,0 +1,82 @@
# @nhost/dashboard
## 0.7.0
### Minor Changes
- db342f45: chore(dashboard): refactor Roles and Permissions settings sections
- 8b9fa0b1: feat(dashboard): add Environment Variables page
### Patch Changes
- Updated dependencies [66b4f3d0]
- Updated dependencies [2e6923dc]
- Updated dependencies [ef117c28]
- Updated dependencies [aebb8225]
- @nhost/core@0.9.4
- @nhost/nhost-js@1.6.2
- @nhost/nextjs@1.9.1
- @nhost/react@0.15.1
- @nhost/react-apollo@4.9.1
## 0.6.0
### Minor Changes
- eef9c914: feat(dashboard): add Roles and Permissions page
## 0.5.0
### Minor Changes
- a48dd5bf: feat(dashboard): make backend port configurable
## 0.4.3
### Patch Changes
- 5de965d9: fix(dashboard): alphabetic ordering of providers
- b9087a4a: fix(dashboard): console -> dashboard terminology
- ca012d79: docs(workos): WorkOS Docs
## 0.4.2
### Patch Changes
- 89bd37bc: fix(dashboard): correct redirect URL input opacity
- Updated dependencies [4601d84e]
- Updated dependencies [843087cb]
- @nhost/react@0.15.0
- @nhost/nextjs@1.9.0
- @nhost/react-apollo@4.9.0
## 0.4.1
### Patch Changes
- 766cb612: fix(dashboard): correct redirect URL for oauth providers
- Updated dependencies [53bdc294]
- Updated dependencies [f2aaff05]
- @nhost/nextjs@1.8.3
- @nhost/core@0.9.3
- @nhost/react@0.14.3
- @nhost/nhost-js@1.6.1
- @nhost/react-apollo@4.8.3
## 0.4.0
### Minor Changes
- 9211743d: feat(dashboard): migrate Settings page features
## 0.3.0
### Minor Changes
- 73da6a67: fix(dashboard): avoid using BACKEND_URL locally
## 0.2.0
### Minor Changes
- db118f97: feat(dashboard): generate Docker image

55
dashboard/Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
FROM node:16-alpine AS pruner
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope="@nhost/dashboard" --docker
FROM node:16-alpine AS builder
ARG TURBO_TOKEN
ARG TURBO_TEAM
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED 1
ENV NEXT_PUBLIC_ENV dev
ENV NEXT_PUBLIC_NHOST_PLATFORM false
# placeholders for ports, will be replaced on runtime by entrypoint script
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_PORT __NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__
ENV NEXT_PUBLIC_NHOST_HASURA_PORT __NEXT_PUBLIC_NHOST_HASURA_PORT__
ENV NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT __NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__
RUN yarn global add pnpm@7.17.0
COPY .gitignore .gitignore
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-*.yaml .
RUN pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
COPY turbo.json turbo.json
COPY config/ config/
RUN pnpm build:dashboard
FROM node:16-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
COPY --chown=nextjs:nodejs dashboard/docker-entrypoint.sh .
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/next.config.js .
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/package.json .
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/public ./dashboard/public
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/standalone/app ./
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/static ./dashboard/.next/static
ENTRYPOINT ["./docker-entrypoint.sh"]
CMD ["node", "dashboard/server.js"]

View File

@@ -30,31 +30,27 @@ First, you need to run the following command to start your backend locally:
cd <your_nhost_project> && nhost dev
```
Two environment variables are required to connect the Nhost Dashboard to your local backend:
You can connect the Nhost Dashboard to your locally running backend by setting the following environment variables in `.env.development.local`:
- `NEXT_PUBLIC_NHOST_PLATFORM` should be set to `false`, because otherwise the Nhost Dashboard will try to connect to the Nhost platform.
- `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` should be set to `http://localhost:9693` unless Hasura is configured to run on a different port. This is the URL of Hasura's migrations endpoint.
Example:
```
```bash
NEXT_PUBLIC_ENV=dev
NEXT_PUBLIC_NHOST_PLATFORM=false
NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
```
### Full list of environment variables
| Name | Description |
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. |
| `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` | URL of Hasura's migrations endpoint. Used only if local development is enabled. |
| `NEXT_PUBLIC_NHOST_HASURA_URL` | URL of the Hasura Console. Used only when `NEXT_PUBLIC_ENV` is `dev`. |
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | URL of the local backend. This is `http://localhost:1337` by default. |
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
| Name | Description |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. Setting this to `true` turns off local development. |
| `NEXT_PUBLIC_NHOST_LOCAL_MIGRATIONS_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `9693` |
| `NEXT_PUBLIC_NHOST_LOCAL_HASURA_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled and `NEXT_PUBLIC_ENV` is `dev`. Default: `9695` |
| `NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `1337` |
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
## ESLint Rules
@@ -67,6 +63,7 @@ NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
| `import/extensions` | JS / TS files should be imported without file extensions. |
| `react/jsx-filename-extension` | JSX should only appear in `.jsx` and `.tsx` files. |
| `react/jsx-no-bind` | Further investigation must be made on the performance impact of functions directly passed as props to components. |
| `import/order` | Until we have a better auto-formatter, we disable this rule. |
| `import/no-extraneous-dependencies` | `devDependencies` should be excluded from the list of disallowed imports. |
| `curly` | By default it only enforces curly braces for multi-line blocks, but it should be enforced for single-line blocks as well. |
| `no-restricted-exports` | `export { default } from './module'` is used heavily in `@/ui/v2` which is a restricted export by default. |

15
dashboard/docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -e
# read ports from env variables or use defaults
NEXT_PUBLIC_NHOST_MIGRATIONS_PORT="${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT:=9693}"
NEXT_PUBLIC_NHOST_HASURA_PORT="${NEXT_PUBLIC_NHOST_HASURA_PORT:=9695}"
NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT="${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT:=1337}"
# replace placeholders
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__/${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT}/g" {} +
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_HASURA_PORT__/${NEXT_PUBLIC_NHOST_HASURA_PORT}/g" {} +
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__/${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT}/g" {} +
exec "$@"

View File

@@ -13,11 +13,3 @@ generates:
- 'typescript-react-apollo'
config:
withRefetchFn: true
functions/utils/__generated__/graphql-request.ts:
documents:
- 'functions/**/*.graphql'
- 'functions/**/*.gql'
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-graphql-request'

View File

@@ -1,3 +1,4 @@
const path = require('path');
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
@@ -5,6 +6,10 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
module.exports = withBundleAnalyzer({
reactStrictMode: true,
swcMinify: false,
output: 'standalone',
experimental: {
outputFileTracingRoot: path.join(__dirname, '../../'),
},
eslint: {
dirs: ['src'],
},

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.1.0",
"version": "0.7.0",
"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 6",
"lint": "next lint --max-warnings 3",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost dev -d",
@@ -25,25 +25,25 @@
"@emotion/styled": "^11.10.5",
"@fontsource/inter": "^4.5.14",
"@fontsource/roboto-mono": "^4.5.8",
"@graphiql/react": "^0.13.2",
"@graphiql/react": "^0.14.0",
"@graphiql/toolkit": "^0.8.0",
"@headlessui/react": "^1.6.5",
"@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^2.9.10",
"@mui/base": "^5.0.0-alpha.105",
"@mui/material": "^5.10.13",
"@mui/system": "^5.10.13",
"@mui/base": "^5.0.0-alpha.106",
"@mui/material": "^5.10.14",
"@mui/system": "^5.10.14",
"@mui/x-date-pickers": "^5.0.8",
"@nhost/core": "^0.9.1",
"@nhost/nextjs": "^1.8.1",
"@nhost/nhost-js": "^1.5.2",
"@nhost/react": "^0.14.1",
"@nhost/react-apollo": "^4.8.1",
"@nhost/core": "^0.9.4",
"@nhost/nextjs": "^1.9.1",
"@nhost/nhost-js": "^1.6.2",
"@nhost/react": "^0.15.1",
"@nhost/react-apollo": "^4.9.1",
"@segment/snippet": "^4.15.3",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/react-query": "^4.16.1",
"@tanstack/react-table": "^8.5.27",
"@tanstack/react-virtual": "^3.0.0-beta.22",
"@tanstack/react-table": "^8.5.30",
"@tanstack/react-virtual": "^3.0.0-beta.23",
"analytics-node": "^6.2.0",
"axios": "^0.27.2",
"bcryptjs": "^2.4.3",
@@ -51,7 +51,7 @@
"cross-fetch": "^3.1.5",
"date-fns": "^2.29.3",
"generate-password": "^1.7.0",
"graphiql": "^2.0.8",
"graphiql": "^2.1.0",
"graphql": "^16.6.0",
"graphql-request": "^4.3.0",
"graphql-tag": "^2.12.6",
@@ -66,13 +66,14 @@
"randomstring": "^1.2.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.39.3",
"react-hook-form": "^7.39.5",
"react-hot-toast": "^2.4.0",
"react-is": "17.0.2",
"react-loading-skeleton": "^2.2.0",
"react-merge-refs": "^1.1.0",
"react-syntax-highlighter": "^15.4.5",
"react-table": "^7.8.0",
"sharp": "^0.31.2",
"slugify": "^1.6.5",
"smartlook-client": "^6.0.0",
"stripe": "^10.17.0",
@@ -95,6 +96,7 @@
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-interactions": "^6.5.13",
"@storybook/addon-links": "^6.5.13",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
@@ -114,10 +116,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.42.1",
"@typescript-eslint/parser": "^5.42.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-react": "^2.2.0",
"@vitest/coverage-c8": "^0.25.1",
"@vitest/coverage-c8": "^0.25.2",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -125,25 +127,24 @@
"critters": "^0.0.10",
"csstype": "^3.0.10",
"dotenv": "^10.0.0",
"eslint": "^8.27.0",
"eslint": "^8.28.0",
"eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-next": "^13.0.2",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.10",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"express": "^4.18.2",
"express-validator": "^6.14.2",
"jsdom": "^20.0.2",
"jsdom": "^20.0.3",
"lint-staged": ">=13",
"msw": "^0.48.2",
"msw": "^0.49.0",
"postcss": "^8.4.19",
"postmark": "^2.7.8",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.2.0",
"prettier-plugin-tailwind-css": "^1.5.0",
"prettier-plugin-tailwindcss": "^0.1.13",
"react-date-fns-hooks": "^0.9.4",
"react-error-boundary": "^3.1.4",
@@ -151,9 +152,9 @@
"tailwindcss": "^3.1.2",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"typescript": "^4.8.4",
"vite": "^3.2.3",
"vite-tsconfig-paths": "^3.5.2",
"vitest": "^0.25.1",
"vite": "^3.2.4",
"vite-tsconfig-paths": "^3.6.0",
"vitest": "^0.25.2",
"webpack": "^5.75.0"
},
"browserslist": {
@@ -168,4 +169,4 @@
"last 1 safari version"
]
}
}
}

View File

@@ -0,0 +1 @@
<svg width="21" height="21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.08 0c5.578 0 10.08 4.507 10.08 10.09 0 5.584-4.502 10.09-10.08 10.09A10.072 10.072 0 0 1 0 10.09C0 4.507 4.503 0 10.08 0Zm0 2.69a7.375 7.375 0 0 0-7.392 7.4c0 4.104 3.293 7.4 7.392 7.4 4.1 0 7.392-3.296 7.392-7.4 0-4.103-3.293-7.4-7.392-7.4Zm-2.486 7.804c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.083-2.086c0-1.143.94-2.085 2.083-2.085Zm4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.084-2.086c0-1.143.941-2.085 2.084-2.085Zm0-4.978c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 10.483 7.6c0-1.143.941-2.085 2.084-2.085Zm-4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 5.51 7.6c0-1.143.94-2.085 2.083-2.085Z" fill="#F22F46"/></svg>

After

Width:  |  Height:  |  Size: 869 B

View File

@@ -1,161 +0,0 @@
import { useDialog } from '@/components/common/DialogProvider';
import features from '@/data/features.json';
import {
useGetWorkspaceMembersQuery,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { ApplicationStatus } from '@/types/application';
import { Modal } from '@/ui';
import Status, { StatusEnum } from '@/ui/Status';
import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown';
import ChevronDownIcon from '@/ui/v2/icons/ChevronDownIcon';
import Tooltip from '@/ui/v2/Tooltip';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { getCurrentEnvironment, isDevOrStaging } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache';
import { useUserData } from '@nhost/react';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
import { ChangeApplicationName } from './ChangeApplicationName';
import ResetDatabasePasswordForm from './overview/ResetDatabasePasswordForm';
import { RemoveApplicationModal } from './RemoveApplicationModal';
const isK8SPostgresEnabledInCurrentEnvironment = features[
'k8s-postgres'
].enabled.find((e) => e === getCurrentEnvironment());
export function ApplicationMenuItems() {
const { currentApplication, currentWorkspace } =
useCurrentWorkspaceAndApplication();
const [updateApplication, { client }] = useUpdateApplicationMutation();
const { openAlertDialog } = useDialog();
const user = useUserData();
const [changeApplicationNameModal, setChangeApplicationNameModal] =
useState(false);
const [deleteApplicationModal, setDeleteApplicationModal] = useState(false);
const isProjectUsingRDS = currentApplication?.featureFlags?.find(
(feature) => feature.name === 'fleetcontrol_use_rds',
);
async function handleTriggerPausing() {
try {
await updateApplication({
variables: {
appId: currentApplication.id,
app: {
desiredState: ApplicationStatus.Paused,
},
},
});
await updateOwnCache(client);
discordAnnounce(`${currentApplication.name} set to pause.`);
triggerToast(`${currentApplication.name} set to pause.`);
} catch (e) {
triggerToast(`Error trying to pause ${currentApplication.name}`);
}
}
const { data: workspaceData, loading } = useGetWorkspaceMembersQuery({
variables: { workspaceId: currentWorkspace.id },
fetchPolicy: 'cache-first',
});
if (loading) {
return null;
}
const isOwner = workspaceData.workspace.workspaceMembers.some(
(member) => member.user.id === user.id && member.type === 'owner',
);
return (
<>
<Modal
showModal={changeApplicationNameModal}
close={() => setChangeApplicationNameModal(!changeApplicationNameModal)}
Component={ChangeApplicationName}
/>
<Modal
showModal={deleteApplicationModal}
close={() => setDeleteApplicationModal(!deleteApplicationModal)}
Component={RemoveApplicationModal}
/>
<Dropdown.Root>
<Dropdown.Trigger asChild hideChevron>
<Button
endIcon={<ChevronDownIcon className="h-4 w-4" />}
variant="outlined"
color="secondary"
>
Project Settings
</Button>
</Dropdown.Trigger>
<Dropdown.Content
menu
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
className="mt-1"
>
<Dropdown.Item
className="font-display text-sm font-medium text-dark"
onClick={() => setChangeApplicationNameModal(true)}
>
Change Project Name
</Dropdown.Item>
{isDevOrStaging() && (
<Dropdown.Item
className="font-display text-sm font-medium text-dark"
onClick={handleTriggerPausing}
>
<Status status={StatusEnum.Deploying}>Internal</Status>
<span className="ml-2 align-middle">Pause App</span>
</Dropdown.Item>
)}
{isK8SPostgresEnabledInCurrentEnvironment && !isProjectUsingRDS && (
<Dropdown.Item
className="font-display text-sm font-medium text-dark"
onClick={() => {
openAlertDialog({
title: 'Reset Database Password',
payload: <ResetDatabasePasswordForm />,
props: {
hidePrimaryAction: true,
hideSecondaryAction: true,
},
});
}}
>
Reset Database Password
</Dropdown.Item>
)}
<Tooltip
title="Only owners of the workspace can delete apps"
visible={!isOwner}
hasDisabledChildren={!isOwner}
>
<Dropdown.Item
className={twMerge(
'font-display text-sm font-medium text-dark',
!isOwner
? 'cursor-not-allowed text-red text-opacity-70'
: 'font-medium text-red',
)}
onClick={() => setDeleteApplicationModal(true)}
disabled={!isOwner}
>
<span>Delete Project</span>
</Dropdown.Item>
</Tooltip>
</Dropdown.Content>
</Dropdown.Root>
</>
);
}
export default ApplicationMenuItems;

View File

@@ -56,14 +56,14 @@ export function ChangeApplicationName({ close }: any) {
}
return (
<div className="px-6 py-6 text-left w-modal">
<div className="w-modal px-6 py-6 text-left">
<div className="flex flex-col">
<Text variant="h3" component="h2">
Change Project Name
</Text>
<form onSubmit={handleSubmit}>
<div className="grid grid-flow-row gap-2 mt-4">
<div className="mt-4 grid grid-flow-row gap-2">
<Input
label="New Project Name"
id="projectName"
@@ -84,7 +84,7 @@ export function ChangeApplicationName({ close }: any) {
)}
</div>
<div className="grid grid-flow-row gap-2 mt-4">
<div className="mt-4 grid grid-flow-row gap-2">
<Button type="submit" disabled={applicationError}>
Save
</Button>

View File

@@ -30,7 +30,7 @@ function Plan({
return (
<button
type="button"
className="grid items-center justify-between w-full grid-flow-col px-1 my-4"
className="my-4 grid w-full grid-flow-col items-center justify-between px-1"
onClick={setPlan}
tabIndex={-1}
>
@@ -48,7 +48,7 @@ function Plan({
<Text
variant="h3"
component="p"
className="self-center ml-2 font-medium"
className="ml-2 self-center font-medium"
>
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
</Text>
@@ -143,7 +143,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
};
return (
<div className="p-6 text-left w-welcome">
<div className="w-welcome p-6 text-left">
<Modal
showModal={paymentModal}
close={closePaymentModal}
@@ -189,7 +189,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
))}
</div>
<div className="grid grid-flow-row gap-2 mt-6">
<div className="mt-6 grid grid-flow-row gap-2">
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
{!selectedPlan && 'Change Plan'}
{selectedPlan && isDowngrade && 'Downgrade'}

View File

@@ -144,7 +144,7 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
</div>
</div>
<RetryableErrorBoundary>
<div className=" h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
<div className="h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
{githubRepositoriesToDisplay.map((repo) => (
<Repo
key={repo.id}

View File

@@ -1,166 +0,0 @@
import { useWorkspaceContext } from '@/context/workspace-context';
import { Modal } from '@/ui/Modal';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { triggerToast } from '@/utils/toast';
import type { EnvironmentVariableFragment } from '@/utils/__generated__/graphql';
import {
refetchGetEnvironmentVariablesWhereQuery,
useDeleteEnvironmentVariableMutation,
useUpdateEnvironmentVariableMutation,
} from '@/utils/__generated__/graphql';
import React, { useState } from 'react';
type EnvModalProps = {
show: boolean;
close: VoidFunction;
envVar: EnvironmentVariableFragment;
};
export default function EditEnvVarModal({
show,
close,
envVar,
}: EnvModalProps) {
const { workspaceContext } = useWorkspaceContext();
const { appId } = workspaceContext;
const [updateEnvVar, { loading: updateLoading }] =
useUpdateEnvironmentVariableMutation({
refetchQueries: [
refetchGetEnvironmentVariablesWhereQuery({
where: {
appId: {
_eq: appId,
},
},
}),
],
});
const [deleteEnvVar, { loading: deleteLoading }] =
useDeleteEnvironmentVariableMutation({
refetchQueries: [
refetchGetEnvironmentVariablesWhereQuery({
where: {
appId: {
_eq: appId,
},
},
}),
],
});
const [prodValue, setProdValue] = useState(envVar.prodValue || '');
const [devValue, setDevValue] = useState(envVar.devValue || '');
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
e.preventDefault();
try {
await updateEnvVar({
variables: {
id: envVar.id,
environmentVariable: {
prodValue,
devValue,
},
},
});
} catch (error) {
close();
triggerToast('Error updating environment variable');
return;
}
triggerToast(`Environment variable ${envVar.name} updated successfully`);
close();
};
const handleDelete = async () => {
try {
await deleteEnvVar({
variables: {
id: envVar.id,
},
});
} catch (error) {
close();
triggerToast('Error deleting environment variable');
return;
}
triggerToast(`Environment variable ${envVar.name} removed successfully`);
close();
};
return (
<Modal showModal={show} close={close}>
<form onSubmit={handleSubmit}>
<div className="w-modal px-6 py-6 text-left">
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
{envVar.name}
</Text>
<Text variant="subtitle2">
The default value will be available in all environments, unless
you override it. All values are encrypted.
</Text>
<div className="my-2 grid grid-flow-row gap-2">
<Input
id="name"
label="Name"
autoFocus
disabled
autoComplete="off"
defaultValue={envVar.name}
fullWidth
hideEmptyHelperText
/>
<Input
id="prodValue"
label="Production Value"
fullWidth
placeholder="Enter a value"
value={prodValue}
onChange={(event) => setProdValue(event.target.value)}
hideEmptyHelperText
/>
<Input
id="devValue"
label="Development Value"
fullWidth
placeholder="Enter a value"
value={devValue}
onChange={(event) => setDevValue(event.target.value)}
hideEmptyHelperText
/>
</div>
<div className="grid grid-flow-row gap-2">
<Button type="submit" loading={updateLoading}>
Save
</Button>
<Button
variant="outlined"
color="error"
loading={deleteLoading}
onClick={handleDelete}
>
Delete
</Button>
<Button onClick={close} variant="outlined" color="secondary">
Close
</Button>
</div>
</div>
</div>
</form>
</Modal>
);
}

View File

@@ -114,7 +114,7 @@ export function EnableSMSSignIn({ openSMSSettingsModal }: any) {
/>
</div>
</div>
<div className="flex flex-row self-center mt-3 align-middle">
<div className="mt-3 flex flex-row self-center align-middle">
<Text
variant="body"
size="normal"

View File

@@ -1,10 +1,11 @@
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
import { LoadingScreen } from '@/components/common/LoadingScreen';
import ExternalLink from '@/components/icons/ExternalIcon';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import Button from '@/ui/v2/Button';
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
import { LOCAL_HASURA_URL } from '@/utils/env';
import { generateRemoteAppUrl } from '@/utils/helpers';
import Image from 'next/image';
@@ -24,7 +25,7 @@ export function HasuraData({ close }: HasuraDataProps) {
const hasuraUrl =
process.env.NEXT_PUBLIC_ENV === 'dev'
? process.env.NEXT_PUBLIC_NHOST_HASURA_URL || 'http://localhost:9695'
? LOCAL_HASURA_URL
: generateRemoteAppUrl(currentApplication.subdomain);
return (
@@ -66,7 +67,7 @@ export function HasuraData({ close }: HasuraDataProps) {
underline="none"
>
Open Hasura
<ExternalLink className="ml-0.5 h-4 w-4" />
<ArrowSquareOutIcon className="h-4 w-4" />
</Link>
{close && (

View File

@@ -86,7 +86,7 @@ export function EditJWTSecretModal({ close }: any) {
return (
<form
className="px-6 py-4 w-modal"
className="w-modal px-6 py-4"
onSubmit={handleSubmit(handleEditJWTSecret)}
>
<div className="grid grid-flow-row gap-2">
@@ -154,7 +154,7 @@ export function EditJWTSecretModal({ close }: any) {
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
return (
<div className="px-6 py-4 w-modal">
<div className="w-modal px-6 py-4">
<div className="grid grid-flow-row gap-2">
<div className="grid grid-flow-row text-left">
<Text variant="h3" component="h2">
@@ -179,7 +179,7 @@ export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
/>
</div>
<div className="max-w-sm mx-auto text-center">
<div className="mx-auto max-w-sm text-center">
<Text variant="subtitle2">
Already using a third party auth service? <br />
<button

View File

@@ -7,6 +7,7 @@ import { triggerToast } from '@/utils/toast';
import { useDeleteApplicationMutation } from '@/utils/__generated__/graphql';
import router from 'next/router';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface RemoveApplicationModalProps {
/**
@@ -26,6 +27,10 @@ export interface RemoveApplicationModalProps {
* Description of the modal
*/
description?: string;
/**
* Class name to be applied to the modal.
*/
className?: string;
}
export function RemoveApplicationModal({
@@ -33,6 +38,7 @@ export function RemoveApplicationModal({
handler,
title,
description,
className,
}: RemoveApplicationModalProps) {
const [deleteApplication, { client }] = useDeleteApplicationMutation();
const [loadingRemove, setLoadingRemove] = useState(false);
@@ -72,18 +78,14 @@ export function RemoveApplicationModal({
}
return (
<div className="w-modal text-left">
<div className={twMerge('w-full max-w-sm p-6 text-left', className)}>
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
{title || 'Delete Project'}
</Text>
<Text variant="subtitle2">
{description ? (
<div>{description}</div>
) : (
<div>Are you sure you want to delete this app?</div>
)}
{description || 'Are you sure you want to delete this app?'}
</Text>
<Text variant="subtitle2" className="font-bold !text-rose-600">
@@ -98,33 +100,15 @@ export function RemoveApplicationModal({
checked={remove}
onChange={(_event, checked) => setRemove(checked)}
aria-label="Confirm Delete Project #1"
componentsProps={{
formControlLabel: {
componentsProps: {
typography: {
className: '!text-sm+',
},
},
},
}}
/>
<Checkbox
id="accept-2"
label="I understand this action can not be undone"
label="I understand this action cannot be undone"
className="py-2"
checked={remove2}
onChange={(_event, checked) => setRemove2(checked)}
aria-label="Confirm Delete Project #2"
componentsProps={{
formControlLabel: {
componentsProps: {
typography: {
className: '!text-sm+',
},
},
},
}}
/>
</div>

View File

@@ -15,7 +15,7 @@ import { ContainerAllWorkspacesApplications } from './ContainerAllWorkspacesAppl
function ApplicationCreatedAt({ createdAt }: any) {
return (
<Text color="dark" className="self-center text-sm cursor-pointer">
<Text color="dark" className="cursor-pointer self-center text-sm">
created{' '}
{formatDistance(new Date(createdAt), new Date(), {
addSuffix: true,
@@ -30,9 +30,9 @@ function LastSuccesfulDeployment({ deployment }: any) {
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="self-center w-4 h-4 mr-1"
className="mr-1 h-4 w-4 self-center"
/>
<Text color="dark" className="self-center text-sm cursor-pointer">
<Text color="dark" className="cursor-pointer self-center text-sm">
{deployment.commitUserName} deployed{' '}
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
addSuffix: true,
@@ -48,9 +48,9 @@ function CurrentDeployment({ deployment }: any) {
<Avatar
name={deployment.commitUserName}
avatarUrl={deployment.commitUserAvatarUrl}
className="self-center w-4 h-4 mr-1"
className="mr-1 h-4 w-4 self-center"
/>
<Text color="dark" className="self-center text-sm cursor-pointer">
<Text color="dark" className="cursor-pointer self-center text-sm">
{deployment.commitUserName} updated just now
</Text>
</div>
@@ -103,7 +103,7 @@ export function RenderWorkspacesWithApps({
variant="a"
color="greyscaleGrey"
size="normal"
className="mb-3 font-medium cursor-pointer"
className="mb-3 cursor-pointer font-medium"
>
{workspace.name}
</Text>
@@ -138,16 +138,16 @@ export function RenderWorkspacesWithApps({
? app.deployments[0].deploymentStatus === 'DEPLOYING'
: false;
return (
<div key={app.slug} className="py-4 cursor-pointer">
<div key={app.slug} className="cursor-pointer py-4">
<Link href={`${workspace?.slug}/${app.slug}`} passHref>
<a
href={`${workspace?.slug}/${app.slug}`}
className="flex px-2 bg-white rounded-sm place-content-between border-divide"
className="flex place-content-between rounded-sm border-divide bg-white px-2"
>
<div className="flex flex-col self-center w-full">
<div className="flex flex-row w-full place-content-between">
<div className="flex w-full flex-col self-center">
<div className="flex w-full flex-row place-content-between">
<div className="flex flex-row items-center self-center">
<div className="w-10 h-10 overflow-hidden rounded-lg">
<div className="h-10 w-10 overflow-hidden rounded-lg">
<Image
src="/logos/new.svg"
alt="Nhost Logo"
@@ -155,12 +155,12 @@ export function RenderWorkspacesWithApps({
height={40}
/>
</div>
<div className="flex flex-col ml-2 text-left">
<div className="ml-2 flex flex-col text-left">
<div>
<Text
color="dark"
size="normal"
className="self-center font-medium text-left capitalize cursor-pointer"
className="cursor-pointer self-center text-left font-medium capitalize"
>
{app.name}
</Text>
@@ -192,7 +192,7 @@ export function RenderWorkspacesWithApps({
<div className="flex flex-row">
<div className="flex self-center align-middle">
{app.deployments[0] && (
<div className="flex self-center mr-2 align-middle">
<div className="mr-2 flex self-center align-middle">
<StatusCircle
status={
app.deployments[0]

View File

@@ -38,8 +38,8 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
normalizedFunctionData.logs.length === 0
) {
return (
<div className="w-full text-white rounded-lg">
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
<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.
@@ -50,12 +50,12 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
);
}
return (
<div className="w-full text-white rounded-lg">
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
<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 "
className=" flex text-sm"
>
<div id={`#-${log.date}`}>
<pre className="inline">

View File

@@ -24,7 +24,7 @@ export function EditRepositorySettings({
const { currentApplication } = useCurrentWorkspaceAndApplication();
const form = useForm<EditRepositorySettingsFormData>({
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
defaultValues: {
productionBranch: currentApplication.repositoryProductionBranch || 'main',
repoBaseFolder: currentApplication.nhostBaseFolder,

View File

@@ -78,8 +78,8 @@ export function EditRepositorySettingsModal({
return (
<div className="px-1">
<div className="flex flex-col">
<div className="w-8 h-8 mx-auto">
<GithubIcon className="w-8 h-8 text-greyscaleDark" />
<div className="mx-auto h-8 w-8">
<GithubIcon className="h-8 w-8 text-greyscaleDark" />
</div>
<Text
variant="subHeading"
@@ -95,7 +95,7 @@ export function EditRepositorySettingsModal({
variant="body"
color="greyscaleDark"
size="small"
className="font-normal text-center"
className="text-center font-normal"
>
{selectedRepoId
? `We'll deploy changes automatically when you push to the deployment branch. `
@@ -110,7 +110,7 @@ export function EditRepositorySettingsModal({
<div className="">
<RepoAndBranch />
</div>
<div className="flex flex-col mt-2">
<div className="mt-2 flex flex-col">
<Button
type="submit"
color="primary"
@@ -123,7 +123,7 @@ export function EditRepositorySettingsModal({
</Button>
</div>
</form>
<div className="flex flex-col mt-2">
<div className="mt-2 flex flex-col">
<Button
type="button"
variant="outlined"

View File

@@ -1,5 +1,5 @@
import ExternalLink from '@/components/icons/ExternalIcon';
import GithubIcon from '@/components/icons/GithubIcon';
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
@@ -27,7 +27,7 @@ export default function GitHubInstallNhostApplication() {
underline="none"
>
Configure the Nhost application on GitHub{' '}
<ExternalLink className="h-4 w-4" />
<ArrowSquareOutIcon className="h-4 w-4" />
</Link>
</div>
);

View File

@@ -20,19 +20,19 @@ export function GitHubNoRepositoriesAdded({
variant="body"
color="greyscaleDark"
size="tiny"
className="font-normal text-center"
className="text-center font-normal"
>
Check the Nhost app&apos;s settings on your GitHub account, or install
the app on a new account.
</Text>
<div className="py-3 my-2 border-t border-b">
<div className="my-2 border-t border-b py-3">
<div className="flex">
{filteredGitHubAppInstallations.map((githubApp) => (
<div key={githubApp.id} className="flex items-center mr-4">
<div key={githubApp.id} className="mr-4 flex items-center">
<Avatar
avatarUrl={githubApp.accountAvatarUrl as string}
className="w-5 h-5 mr-1"
className="mr-1 h-5 w-5"
/>
{githubApp.accountLogin}
</div>
@@ -45,9 +45,9 @@ export function GitHubNoRepositoriesAdded({
rel="noreferrer noopener"
transparent
type={null}
className="text-xs font-medium cursor-pointer text-blue"
className="cursor-pointer text-xs font-medium text-blue"
>
<PlusSmIcon className="w-4 h-4 mr-1 border rounded-full border-btn" />
<PlusSmIcon className="mr-1 h-4 w-4 rounded-full border border-btn" />
Configure the Nhost application on GitHub.
</Button>
</div>

View File

@@ -1,167 +0,0 @@
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form';
import {
useResetPostgresPasswordMutation,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Text } from '@/ui';
import Button from '@/ui/v2/Button';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputAdornment from '@/ui/v2/InputAdornment';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { generateRandomPassword, schema } from '@/utils/generateRandomPassword';
import { triggerToast } from '@/utils/toast';
import { useUserData } from '@nhost/react';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
export interface ResetDatabasePasswordFormProps {
/**
* The new password to set for the database.
*/
newDatabasePassword: string;
}
export default function ResetDatabasePasswordForm() {
const [passwordError, setPasswordError] = useState('');
const [updateApplication] = useUpdateApplicationMutation();
const form = useForm<ResetDatabasePasswordFormProps>({
reValidateMode: 'onChange',
defaultValues: {
newDatabasePassword: generateRandomPassword(),
},
});
const { setValue, getValues, register } = form;
const { closeAlertDialog } = useDialog();
const [resetPostgresPasswordMutation, { loading }] =
useResetPostgresPasswordMutation();
const user = useUserData();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const handleGenerateRandomPassword = () => {
const newRandomDatabasePassword = generateRandomPassword();
setPasswordError('');
triggerToast('New random database password generated.');
setValue('newDatabasePassword', newRandomDatabasePassword);
};
const handleChangeDatabasePassword = async (
data: ResetDatabasePasswordFormProps,
) => {
try {
await resetPostgresPasswordMutation({
variables: {
appID: currentApplication.id,
newPassword: data.newDatabasePassword,
},
});
await updateApplication({
variables: {
appId: currentApplication.id,
app: {
postgresPassword: data.newDatabasePassword,
},
},
});
closeAlertDialog();
triggerToast(`${currentApplication.name} Database Password changed.`);
} catch (e) {
triggerToast(
`Error trying to change database password for ${currentApplication.name}`,
);
await discordAnnounce(
`Error trying to change database password: ${currentApplication.name} (${user.email}): ${e.message}`,
);
}
};
return (
<FormProvider {...form}>
<Form className="mx-0.5" onSubmit={handleChangeDatabasePassword}>
<Input
{...register('newDatabasePassword')}
name="newDatabasePassword"
id="newDatabasePassword"
autoComplete="new-password"
type="password"
error={Boolean(passwordError)}
helperText={
<>
{passwordError && <div className="pb-2">{passwordError}</div>}
<Text className="font-normal" size="tiny" color="greyscaleDark">
The root postgres password for your database - it must be strong
and hard to guess.{' '}
<Button
onClick={handleGenerateRandomPassword}
className="contents text-xs "
>
<span className="ml-1 font-medium text-greyscaleDark underline underline-offset-2">
Generate a password
</span>
</Button>
</Text>
</>
}
endAdornment={
<InputAdornment position="end" className="absolute right-2">
<Button
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
onClick={() => {
copy(getValues('newDatabasePassword'), 'Postgres password');
}}
variant="borderless"
aria-label="Copy your newly randomly generated password to the clipboard."
>
<CopyIcon className="h-4 w-4" />
</Button>
</InputAdornment>
}
onChange={async (e) => {
if (e.target.value.length === 0) {
setValue('newDatabasePassword', e.target.value);
setPasswordError('Please enter a password');
return;
}
setValue('newDatabasePassword', e.target.value);
setPasswordError('');
try {
await schema.validate({
'Database Password': e.target.value,
});
setPasswordError('');
} catch (validationError) {
setPasswordError(validationError.message);
}
}}
fullWidth
/>
<div className="mt-6 grid grid-flow-col place-content-between py-2">
<Button
color="secondary"
variant="borderless"
onClick={closeAlertDialog}
>
Cancel
</Button>
<Button
color="primary"
type="submit"
disabled={Boolean(passwordError)}
loading={loading}
>
Reset Database Password
</Button>
</div>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -1,70 +0,0 @@
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
// refactor
export interface AppleProviderSettingsFormProps {
authProviderClientId: string;
authProviderTeamId: string;
authProviderKeyId: string;
authProviderClientSecret: string;
handleClientIdChange: (value: string) => void;
handleTeamIdChange: (value: string) => void;
handleKeyIdChange: (value: string) => void;
handleClientSecretChange: (value: string) => void;
}
export default function AppleProviderSettingsForm({
authProviderClientId,
authProviderTeamId,
authProviderKeyId,
authProviderClientSecret,
handleClientIdChange,
handleTeamIdChange,
handleKeyIdChange,
handleClientSecretChange,
}: AppleProviderSettingsFormProps) {
return (
<div className="space-y-3 divide-y-1 divide-divide">
<ProviderSetting
title="Team ID"
desc="Copy from Apple and enter here"
inputPlaceholder="Paste Team ID here"
input
inputValue={authProviderTeamId}
inputOnChange={handleTeamIdChange}
inputType="text"
/>
<ProviderSetting
title="Service ID"
desc="Copy from Apple and enter here"
inputPlaceholder="Paste Service ID here"
input
inputValue={authProviderClientId}
inputOnChange={handleClientIdChange}
inputType="text"
/>
<ProviderSetting
title="Key ID"
desc="Copy from Apple and enter here"
inputPlaceholder="Paste Key ID here"
input
inputValue={authProviderKeyId}
inputOnChange={handleKeyIdChange}
inputType="text"
/>
<ProviderSetting
title="Private Key"
desc="Copy from Apple and enter here"
inputPlaceholder="Paste Private Key here"
input
inputValue={authProviderClientSecret.replace(/\\n/gi, '\n')}
inputOnChange={handleClientSecretChange}
inputType="text"
multiline
/>
</div>
);
}

View File

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

View File

@@ -1,44 +0,0 @@
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
import type { Provider } from '@/types/providers';
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
// refactor
export interface GeneralProviderSettingsFormProps {
provider: Provider;
authProviderClientId: string;
authProviderClientSecret: string;
handleClientIdChange: (value: string) => void;
handleClientSecretChange: (value: string) => void;
}
export default function GeneralProviderSettingsForm({
provider,
authProviderClientId,
authProviderClientSecret,
handleClientIdChange,
handleClientSecretChange,
}: GeneralProviderSettingsFormProps) {
return (
<div className="space-y-3 divide-y-1 divide-divide">
<ProviderSetting
title={`${provider.name} Client ID`}
desc={`Copy from ${provider.name} and enter here`}
inputPlaceholder="Paste Client ID here"
input
inputValue={authProviderClientId}
inputOnChange={handleClientIdChange}
inputType="text"
/>
<ProviderSetting
title={`${provider.name} Client Secret`}
desc={`Copy from ${provider.name} and enter here`}
inputPlaceholder="Paste secret here"
input
inputValue={authProviderClientSecret}
inputOnChange={handleClientSecretChange}
inputType="password"
/>
</div>
);
}

View File

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

View File

@@ -1,20 +0,0 @@
import { capitalize } from '@/utils/helpers';
import Image from 'next/image';
export interface PreviewProps {
provider: string;
}
export function Preview({ provider }: PreviewProps) {
return (
<div className="flex items-center justify-center p-10">
<Image
src={`/assets/social-providers/${provider.toLowerCase()}-preview.svg`}
alt={`${capitalize(provider)} sign in preview`}
className="mx-auto w-full max-w-md"
width={480}
height={267}
/>
</div>
);
}

View File

@@ -1,43 +0,0 @@
import Help from '@/components/icons/Help';
import type { Provider } from '@/types/providers';
import { Text } from '@/ui/Text';
import { resolveProvider } from '@/utils/resolveProvider';
import Image from 'next/image';
import { useRouter } from 'next/router';
type ProviderHeaderProps = {
provider: Provider;
};
export function ProviderHeader({ provider }: ProviderHeaderProps) {
const router = useRouter();
const providerId = router.query.providerId as string;
return (
<div className="flex flex-row items-center space-x-2">
<div className="w-14">
<Image
src={`/assets/${resolveProvider(providerId)}.svg`}
alt={`Logo of ${provider.name}`}
width={56}
height={56}
layout="responsive"
/>
</div>
<div className="flex w-full flex-row place-content-between">
<Text color="dark" className="font-medium capitalize" size="big">
{provider.name}
</Text>
{provider.docsLink && (
<div className="flex flex-col">
<a href={provider.docsLink} target="_blank" rel="noreferrer">
<Help className="h-10 w-10" />
</a>
</div>
)}
</div>
</div>
);
}
export default ProviderHeader;

View File

@@ -1,27 +0,0 @@
import { useGetAppLoginDataQuery } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import { ProviderPage } from './ProviderPage';
export function ProviderPagePreload() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { data, loading, error } = useGetAppLoginDataQuery({
variables: {
id: currentApplication?.id,
},
skip: !currentApplication?.id,
});
if (error) {
throw error;
}
if (loading) {
return <ActivityIndicator delay={500} label="Loading providers..." />;
}
return <ProviderPage app={data?.app} />;
}
export default ProviderPagePreload;

View File

@@ -1,102 +0,0 @@
import { Preview } from '@/components/applications/providers/Preview';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useFormSaver } from '@/hooks/useFormSaver';
import type { Provider } from '@/types/providers';
import { FormSaver } from '@/ui/FormSaver';
import { Text } from '@/ui/Text';
import { Toggle } from '@/ui/Toggle';
import { getDynamicVariables } from '@/utils/getDynamicVariables';
import { triggerToast } from '@/utils/toast';
import { useUpdateAppMutation } from '@/utils/__generated__/graphql';
import { useRouter } from 'next/router';
type ProviderInfoProps = {
provider: Provider;
authProviderEnabled: boolean;
setAuthProviderEnabled: (enabled: boolean) => void;
};
export function ProviderInfo({
provider,
authProviderEnabled,
setAuthProviderEnabled,
}: ProviderInfoProps) {
const router = useRouter();
const providerId = router.query.providerId as string;
const { showFormSaver, setShowFormSaver, submitState } = useFormSaver();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp, { client }] = useUpdateAppMutation();
const { authEnabled } = getDynamicVariables(providerId, {}, true);
const handleFormSubmit = async () => {
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
[authEnabled as string]: authProviderEnabled,
},
},
});
await client.refetchQueries({
include: ['getAppLoginData'],
});
setShowFormSaver(false);
triggerToast('Settings saved');
} catch (error) {
// TODO: Display error to user and use a logging solution
}
};
return (
<div>
{showFormSaver && (
<FormSaver
show={showFormSaver}
onCancel={() => {
setShowFormSaver(false);
setAuthProviderEnabled(false);
}}
onSave={() => {
handleFormSubmit();
}}
loading={submitState.loading}
/>
)}
<div className="mt-8 flex flex-row place-content-between">
<div className=" space-y-3">
<div className="flex flex-col">
<Text
variant="body"
color="greyscaleDark"
className=" font-bold"
size="normal"
>
Let users sign in with
<span className="ml-1 capitalize">{provider.name}</span>
</Text>
</div>
</div>
<div className="self-center">
<Toggle
checked={authProviderEnabled}
onChange={() => {
if (authProviderEnabled) {
setShowFormSaver(true);
}
setAuthProviderEnabled(!authProviderEnabled);
}}
/>
</div>
</div>
{!authProviderEnabled && <Preview provider={providerId} />}
</div>
);
}
export default ProviderInfo;

View File

@@ -1,45 +0,0 @@
import providers from '@/data/providers.json';
import { resolveProvider } from '@/utils/resolveProvider';
import type { GetAppFragment } from '@/utils/__generated__/graphql';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { ProviderHeader } from './ProviderHeader';
import { ProviderInfo } from './ProviderInfo';
import { ProviderSettings } from './ProviderSettings';
type ProviderPageProps = {
app: GetAppFragment;
};
export function ProviderPage({ app }: ProviderPageProps) {
const router = useRouter();
const providerId = router.query.providerId as string;
const providerEnabled = app[`auth${resolveProvider(providerId)}Enabled`];
const [authProviderEnabled, setAuthProviderEnabled] =
useState(providerEnabled);
const provider = providers.find(
(availableProvider) => providerId === availableProvider.name.toLowerCase(),
);
return (
<>
<ProviderHeader provider={provider} />
<ProviderInfo
provider={provider}
authProviderEnabled={authProviderEnabled}
setAuthProviderEnabled={setAuthProviderEnabled}
/>
<ProviderSettings
provider={provider}
app={app}
authProviderEnabled={authProviderEnabled}
/>
</>
);
}
export default ProviderPage;

View File

@@ -1,378 +0,0 @@
import {
ProviderSetting,
ProviderSettingsSave,
} from '@/components/applications/settings/providers';
import type { GetAppFragment } from '@/generated/graphql';
import { useUpdateAppMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useFormSaver } from '@/hooks/useFormSaver';
import type { Provider } from '@/types/providers';
import { Alert } from '@/ui/Alert';
import { FormSaver } from '@/ui/FormSaver';
import Button from '@/ui/v2/Button';
import ChevronDownIcon from '@/ui/v2/icons/ChevronDownIcon';
import ChevronUpIcon from '@/ui/v2/icons/ChevronUpIcon';
import { capitalize, generateRemoteAppUrl } from '@/utils/helpers';
import { resolveProvider } from '@/utils/resolveProvider';
import { triggerToast } from '@/utils/toast';
import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import AppleProviderSettingsForm from './AppleProviderSettingsForm';
import GeneralProviderSettingsForm from './GeneralProviderSettingsForm';
import WorkOsProviderSettingsForm from './WorkOsProviderSettingsForm';
export interface ProviderSettingsProps {
provider: Provider;
app: GetAppFragment;
authProviderEnabled: boolean;
}
// TODO 1: Simplify this component, improve the reusability by redesigning the
// way the component renders the content, because it's hard to create a provider
// specific layout with the current implementation.
// TODO 2: Change the form to use react-hook-form, so that we can avoid passing
// too much props around these components (e.g: passing xy and handleXyChange to
// children would not be necessary at all).
// TODO 3: This is an accessibility improvement, but labels should be connected
// to the inputs.
export function ProviderSettings({
provider,
app,
authProviderEnabled,
}: ProviderSettingsProps) {
const router = useRouter();
const providerId = router.query.providerId as string;
const [hideSettings, setHideSettings] = useState(false);
const [hasSettings, setHasSettings] = useState(false);
const { currentApplication } = useCurrentWorkspaceAndApplication();
const {
authClientId,
authClientSecret,
authTeamId,
authKeyId,
authDefaultDomain,
authDefaultOrganization,
authDefaultConnection,
// TODO: This function should be extracted from this component and also it
// should be checked why values are used from it's return value **inside**
// the function body.
// eslint-disable-next-line @typescript-eslint/no-use-before-define
} = getProviderSpecificVariables(providerId);
const [authProviderClientSecret, setAuthProviderClientSecret] = useState(
app[authClientSecret] || '',
);
const [authProviderClientId, setAuthProviderClientId] = useState(
app[authClientId] || '',
);
const [authProviderTeamId, setAuthProviderTeamId] = useState(
app[authTeamId] || '',
);
const [authProviderKeyId, setAuthProviderKeyId] = useState(
app[authKeyId] || '',
);
const [authProviderDefaultDomain, setAuthProviderDefaultDomain] = useState(
app[authDefaultDomain] || '',
);
const [authProviderDefaultOrganization, setAuthProviderDefaultOrganization] =
useState(app[authDefaultOrganization] || '');
const [authProviderDefaultConnection, setAuthProviderDefaultConnection] =
useState(app[authDefaultConnection] || '');
const [callError, setCallError] = useState({ error: false, message: '' });
const [updateApp, { client, loading }] = useUpdateAppMutation();
const { showFormSaver, setShowFormSaver, submitState } = useFormSaver();
function getProviderSpecificVariables(
targetProvider: string,
{ prefill = true } = {},
) {
if (targetProvider === 'twitter') {
if (!prefill) {
return {
authTwitterEnabled: authProviderEnabled,
authTwitterConsumerKey: authProviderClientId,
authTwitterConsumerSecret: authProviderClientSecret,
};
}
return {
authEnabled: 'authTwitterEnabled',
authClientId: 'authTwitterConsumerKey',
authClientSecret: 'authTwitterConsumerSecret',
};
}
if (targetProvider === 'apple') {
if (!prefill) {
return {
authAppleEnabled: authProviderEnabled,
authAppleClientId: authProviderClientId,
authAppleKeyId: authProviderKeyId,
authAppleTeamId: authProviderTeamId,
authApplePrivateKey: authProviderClientSecret.replace(/\n/gi, '\\n'),
};
}
return {
authEnabled: 'authAppleEnabled',
authClientId: 'authAppleClientId',
authClientSecret: 'authApplePrivateKey',
authTeamId: 'authAppleTeamId',
authKeyId: 'authAppleKeyId',
};
}
if (targetProvider === 'workos') {
if (!prefill) {
return {
authWorkOsEnabled: authProviderEnabled,
authWorkOsClientId: authProviderClientId,
authWorkOsClientSecret: authProviderClientSecret,
authWorkOsDefaultDomain: authProviderDefaultDomain,
authWorkOsDefaultOrganization: authProviderDefaultOrganization,
authWorkOsDefaultConnection: authProviderDefaultConnection,
};
}
return {
authEnabled: 'authWorkOsEnabled',
authClientId: 'authWorkOsClientId',
authClientSecret: 'authWorkOsClientSecret',
authDefaultDomain: 'authWorkOsDefaultDomain',
authDefaultOrganization: 'authWorkOsDefaultOrganization',
authDefaultConnection: 'authWorkOsDefaultConnection',
};
}
const authEnabled = `auth${resolveProvider(providerId)}Enabled`;
const clientId = `auth${resolveProvider(providerId)}ClientId`;
const clientSecret = `auth${resolveProvider(providerId)}ClientSecret`;
if (!prefill) {
return {
[authEnabled]: authProviderEnabled,
[clientId]: authProviderClientId,
[clientSecret]: authProviderClientSecret,
};
}
return {
authEnabled,
authClientId: clientId,
authClientSecret: clientSecret,
};
}
useEffect(() => {
// Gets the particular providerId GQL field.
const { authEnabled } = getProviderSpecificVariables(providerId);
// Checks if the providerId field is enabled on the app that we get from origin.
if (app[authEnabled]) {
setHasSettings(true);
setHideSettings(true);
}
}, [hasSettings, setHasSettings, app]);
useEffect(() => {
const {
authClientId: clientId,
authTeamId: teamId,
authKeyId: keyId,
authClientSecret: clientSecret,
} = getProviderSpecificVariables(providerId);
// This side effect checks if the clientId or secret doesn't equal the app's clientId or secret and shows the form saver, which can be used to save the new changes.
if (
hasSettings &&
(app[clientSecret] !== authProviderClientSecret ||
app[clientId] !== authProviderClientId ||
app[teamId] !== authProviderTeamId ||
app[keyId] !== authProviderKeyId)
) {
setShowFormSaver(true);
}
}, [
hasSettings,
authProviderClientSecret,
authProviderClientId,
authProviderTeamId,
authProviderKeyId,
authClientSecret,
authClientId,
]);
const handleSettingsToggle = (e) => {
e.preventDefault();
setHideSettings(!hideSettings);
};
const handleSubmit = async (e?: React.SyntheticEvent<HTMLFormElement>) => {
if (e) {
e.preventDefault();
}
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
...getProviderSpecificVariables(providerId, { prefill: false }),
},
},
});
} catch (error) {
setCallError({ error: true, message: error.message });
return;
}
await client.refetchQueries({
include: ['getAppLoginData'],
});
setShowFormSaver(false);
triggerToast('Settings saved');
};
const handleClientIdChange = (value: string) => {
setCallError({ error: false, message: '' });
setAuthProviderClientId(value);
};
const handleTeamIdChange = (value: string) => {
setCallError({ error: false, message: '' });
setAuthProviderTeamId(value);
};
const handleKeyIdChange = (value: string) => {
setCallError({ error: false, message: '' });
setAuthProviderKeyId(value);
};
const handleClientSecretChange = (value: string) => {
setCallError({ error: false, message: '' });
setAuthProviderClientSecret(value);
};
return (
<>
{showFormSaver && (
<FormSaver
show={showFormSaver}
onCancel={() => {
setShowFormSaver(false);
}}
onSave={() => {
handleSubmit();
}}
loading={submitState.loading}
/>
)}
<form onSubmit={handleSubmit}>
{authProviderEnabled && (
<div className="mt-8 space-y-3 divide-y-1 divide-divide border-t border-b pb-2">
{!hideSettings && (
<>
{providerId === 'apple' && (
<AppleProviderSettingsForm
authProviderClientId={authProviderClientId}
authProviderTeamId={authProviderTeamId}
authProviderKeyId={authProviderKeyId}
authProviderClientSecret={authProviderClientSecret}
handleClientIdChange={handleClientIdChange}
handleTeamIdChange={handleTeamIdChange}
handleKeyIdChange={handleKeyIdChange}
handleClientSecretChange={handleClientSecretChange}
/>
)}
{providerId !== 'apple' && (
<GeneralProviderSettingsForm
provider={provider}
authProviderClientId={authProviderClientId}
authProviderClientSecret={authProviderClientSecret}
handleClientIdChange={handleClientIdChange}
handleClientSecretChange={handleClientSecretChange}
/>
)}
{providerId === 'workos' && (
<WorkOsProviderSettingsForm
defaultDomain={authProviderDefaultDomain}
defaultOrganization={authProviderDefaultOrganization}
defaultConnection={authProviderDefaultConnection}
handleDefaultDomainChange={setAuthProviderDefaultDomain}
handleDefaultOrganizationChange={
setAuthProviderDefaultOrganization
}
handleDefaultConnectionChange={
setAuthProviderDefaultConnection
}
/>
)}
</>
)}
<ProviderSetting
title={hideSettings ? 'Login button URL' : 'OAuth Callback URL'}
desc={
hideSettings
? `Use this in your frontend`
: `Paste into ${capitalize(providerId)}`
}
inputPlaceholder=""
input={false}
showCopy
link={
hideSettings
? `${generateRemoteAppUrl(
app.subdomain,
)}/v1/auth/signin/provider/${providerId.toLowerCase()}`
: `${generateRemoteAppUrl(
app.subdomain,
)}/v1/auth/signin/provider/${providerId.toLowerCase()}/callback`
}
/>
</div>
)}
{callError.error && (
<Alert severity="error">
{callError.message ||
'Error trying to update login provider settings.'}
</Alert>
)}
{authProviderEnabled && hasSettings && (
<div className="mt-4 px-2">
<Button
variant="borderless"
onClick={handleSettingsToggle}
className="grid grid-flow-col gap-1.5 text-xs"
>
{hideSettings ? (
<>
View Settings
<ChevronDownIcon className="h-4 w-4" />
</>
) : (
<>
Hide Settings
<ChevronUpIcon className="h-4 w-4" />
</>
)}
</Button>
</div>
)}
{authProviderEnabled && !hasSettings && (
<ProviderSettingsSave provider={provider} loading={loading} />
)}
</form>
</>
);
}

View File

@@ -1,55 +0,0 @@
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
// refactor
export interface WorkOsProviderSettingsFormProps {
defaultDomain: string;
defaultOrganization: string;
defaultConnection: string;
handleDefaultDomainChange: (value: string) => void;
handleDefaultOrganizationChange: (value: string) => void;
handleDefaultConnectionChange: (value: string) => void;
}
export default function WorkOsProviderSettingsForm({
defaultDomain,
defaultOrganization,
defaultConnection,
handleDefaultDomainChange,
handleDefaultOrganizationChange,
handleDefaultConnectionChange,
}: WorkOsProviderSettingsFormProps) {
return (
<div className="grid grid-flow-row gap-3 divide-y-1">
<ProviderSetting
title="Default Domain"
desc=""
inputPlaceholder=""
input
inputValue={defaultDomain}
inputOnChange={handleDefaultDomainChange}
inputType="text"
/>
<ProviderSetting
title="Default Organization"
desc=""
inputPlaceholder=""
input
inputValue={defaultOrganization}
inputOnChange={handleDefaultOrganizationChange}
inputType="text"
/>
<ProviderSetting
title="Default Connection"
desc=""
inputPlaceholder=""
input
inputValue={defaultConnection}
inputOnChange={handleDefaultConnectionChange}
inputType="text"
/>
</div>
);
}

View File

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

View File

@@ -1,138 +0,0 @@
import { PermissionSetting } from '@/components/applications/users/PermissionSetting';
import { SettingsSection } from '@/components/applications/users/SettingsSection';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useSubmitState } from '@/hooks/useSubmitState';
import { Alert } from '@/ui/Alert';
import DelayedLoading from '@/ui/DelayedLoading';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import {
useGetAuthSettingsQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useApolloClient } from '@apollo/client';
import toast from 'react-hot-toast';
export function GeneralPermissions() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const client = useApolloClient();
const { submitState, setSubmitState } = useSubmitState();
let toastId: string;
const { loading, data, error } = useGetAuthSettingsQuery({
variables: {
id: currentApplication.id,
},
});
if (loading) {
return <DelayedLoading delay={500} />;
}
if (error) {
throw error;
}
return (
<div className="mx-auto w-full bg-white ">
<SettingsSection
title="General Permissions"
desc="These settings affect all users in your project."
>
{submitState.error && (
<Alert severity="error">{submitState.error.message}</Alert>
)}
<div className="divide-y-1 border-t border-b">
<PermissionSetting
text="Disable New Users"
desc="If set, newly registered users are disabled and won't be able to sign in."
toggle
checked={data.app.authDisableNewUsers}
onChange={async () => {
try {
toastId = showLoadingToast('Saving changes...');
await updateApp({
variables: {
id: currentApplication.id,
app: {
authDisableNewUsers: !data.app.authDisableNewUsers,
},
},
});
await client.refetchQueries({ include: ['getAuthSettings'] });
toast.remove(toastId);
triggerToast(
`Disable new users ${
data.app.authDisableNewUsers ? `Disabled` : `Enabled`
} for ${currentApplication.name}`,
);
} catch (updateError) {
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
if (toastId) {
toast.remove(toastId);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['authDisableNewUsers'],
});
}
}}
/>
<PermissionSetting
text="Allow Anonymous Users"
desc="Enables users to register as an anonymous user."
toggle
checked={data.app.authAnonymousUsersEnabled}
onChange={async () => {
setSubmitState({
loading: true,
error: null,
fieldsWithError: [],
});
try {
toastId = showLoadingToast('Saving changes...');
await updateApp({
variables: {
id: currentApplication.id,
app: {
authAnonymousUsersEnabled:
!data.app.authAnonymousUsersEnabled,
},
},
});
await client.refetchQueries({ include: ['getAuthSettings'] });
toast.remove(toastId);
triggerToast(
`Anonymous users registration ${
data.app.authAnonymousUsersEnabled ? `disabled` : `enabled`
} for ${currentApplication.name}`,
);
} catch (updateError) {
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
if (toastId) {
toast.remove(toastId);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['authAnonymousUsersEnabled'],
});
}
}}
/>
</div>
</SettingsSection>
</div>
);
}
export default GeneralPermissions;

View File

@@ -1,263 +0,0 @@
import { PermissionSetting } from '@/components/applications/users/PermissionSetting';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useSubmitState } from '@/hooks/useSubmitState';
import { Toggle } from '@/ui';
import { Alert } from '@/ui/Alert';
import DelayedLoading from '@/ui/DelayedLoading';
import { Text } from '@/ui/Text';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import {
useGetGravatarSettingsQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useApolloClient } from '@apollo/client';
import { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
export function GravatarSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const client = useApolloClient();
const [currentDefaultGravatar, setCurrentDefaultGravatar] = useState({
id: 'blank',
name: 'blank',
disabled: false,
slug: 'blank',
});
const [currentGravatarRating, setCurrentGravatarRating] = useState({
id: 'g',
name: 'g',
disabled: false,
slug: 'g',
});
const { submitState, setSubmitState } = useSubmitState();
const { loading, data, error } = useGetGravatarSettingsQuery({
variables: {
id: currentApplication.id,
},
});
let toastId: string;
useEffect(() => {
if (!data) {
return;
}
setCurrentDefaultGravatar((previousDefaultGravatar) => ({
...previousDefaultGravatar,
name: data.app.authGravatarDefault,
id: data.app.authGravatarDefault,
slug: data.app.authGravatarDefault,
}));
setCurrentGravatarRating((previousGravatarRating) => ({
...previousGravatarRating,
name: data.app.authGravatarRating,
id: data.app.authGravatarRating,
slug: data.app.authGravatarRating,
}));
}, [data, setCurrentDefaultGravatar, setCurrentGravatarRating]);
if (loading) {
return <DelayedLoading delay={500} />;
}
if (error) {
throw error;
}
return (
<div className="mx-auto w-full font-display">
<div className="flex flex-row place-content-between">
<div className="flex flex-col">
<Text
variant="body"
size="large"
className="font-medium"
color="greyscaleDark"
>
Gravatar Settings
</Text>
<div>
<Text
variant="body"
size="normal"
color="greyscaleDark"
className="mt-1"
>
Enable Gravatars as avatar URL for users.
</Text>
</div>
</div>
<div className="mr-2 flex flex-row">
<Toggle
checked={data.app.authGravatarEnabled}
onChange={async () => {
try {
toastId = showLoadingToast('Saving changes...');
await updateApp({
variables: {
id: currentApplication.id,
app: {
authGravatarEnabled: !data.app.authGravatarEnabled,
},
},
});
await client.refetchQueries({
include: ['getGravatarSettings'],
});
toast.remove(toastId);
triggerToast(
`Gravatars ${
data.app.authGravatarEnabled ? `Disabled` : `Enabled`
} for ${currentApplication.name}`,
);
} catch (updateError) {
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
if (toastId) {
toast.remove(toastId);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['authGravatarEnabled'],
});
}
}}
/>
</div>
</div>
{submitState.error && (
<Alert severity="error" className="mt-4">
{submitState.error.message}
</Alert>
)}
{data.app.authGravatarEnabled && (
<div className="mt-6 mb-12 flex flex-col divide-y-1 divide-divide border-t border-b">
<PermissionSetting
text="AUTH_GRAVATAR_DEFAULT"
options={[
{
id: '404',
name: '404',
},
{
id: 'mp',
name: 'mp',
},
{
id: 'identicon',
name: 'identicon',
},
{
id: 'monsterid',
name: 'monsterid',
},
{
id: 'waatar',
name: 'waatar',
},
{
id: 'retro',
name: 'retro',
},
{
id: 'robohash',
name: 'robohash',
},
{
id: 'blank',
name: 'blank',
},
]}
value={currentDefaultGravatar}
onChange={async (v: { id: string }) => {
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
authGravatarDefault: v.id,
},
},
});
client.refetchQueries({ include: ['getGravatarSettings'] });
triggerToast(
`Changed default gravatar for ${currentApplication.name}`,
);
} catch (updateError) {
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['authGravatarDefault'],
});
}
}}
/>
<PermissionSetting
text="AUTH_GRAVATAR_RATING"
options={[
{
id: 'g',
name: 'g',
},
{
id: 'pg',
name: 'pg',
},
{
id: 'r',
name: 'r',
},
{
id: 'x',
name: 'x',
},
]}
value={currentGravatarRating}
onChange={async (v: { id: string }) => {
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
authGravatarRating: v.id,
},
},
});
client.refetchQueries({ include: ['getGravatarSettings'] });
triggerToast(
`Changed Gravatar rating for ${currentApplication.name}`,
);
} catch (updateError) {
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['authGravatarRating'],
});
}
}}
/>
</div>
)}
</div>
);
}
export default GravatarSettings;

View File

@@ -1,193 +0,0 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useFormSaver } from '@/hooks/useFormSaver';
import { FormSaver, Toggle } from '@/ui';
import { Alert } from '@/ui/Alert';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import {
useGetAuthSettingsQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useApolloClient } from '@apollo/client';
import { useEffect, useRef, useState } from 'react';
import toast from 'react-hot-toast';
export function MultiFactorAuthentication() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [OTPIssuer, setOTPIssuer] = useState('');
const client = useApolloClient();
const { showFormSaver, setShowFormSaver, submitState, setSubmitState } =
useFormSaver();
const toastId = useRef<string>();
const { loading, data, error } = useGetAuthSettingsQuery({
variables: {
id: currentApplication.id,
},
});
useEffect(() => {
if (!data) {
return;
}
if (!data.app.authMfaTotpIssuer) {
return;
}
setOTPIssuer(data.app.authMfaTotpIssuer);
}, [data]);
if (loading) {
return (
<ActivityIndicator
delay={500}
label="Loading settings..."
className="mx-auto"
/>
);
}
if (error) {
throw error;
}
async function handleSaveForm() {
setSubmitState({
loading: true,
error: null,
fieldsWithError: [],
});
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
authMfaTotpIssuer: OTPIssuer,
},
},
});
await client.refetchQueries({ include: ['getAuthSettings'] });
setSubmitState({
loading: false,
error: null,
fieldsWithError: [],
});
setShowFormSaver(false);
triggerToast('All changes saved');
} catch (updateError) {
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['OTPIssuer'],
});
}
}
async function handleToggleMFA() {
try {
toastId.current = showLoadingToast('Saving changes...');
await updateApp({
variables: {
id: currentApplication.id,
app: {
authMfaEnabled: !data.app.authMfaEnabled,
},
},
});
await client.refetchQueries({ include: ['getAuthSettings'] });
if (toastId?.current) {
toast.remove(toastId.current);
}
triggerToast(
`Multi-Factor Authentication ${
data.app.authMfaEnabled ? `Disabled` : `Enabled`
} for ${currentApplication.name}`,
);
} catch (updateError) {
if (toastId?.current) {
toast.remove(toastId.current);
}
if (updateError instanceof Error) {
triggerToast(updateError.message);
}
setSubmitState({
loading: false,
error: updateError,
fieldsWithError: ['authMfaEnabled'],
});
}
}
return (
<div className="grid w-full grid-flow-row gap-4">
{showFormSaver && (
<FormSaver
show={showFormSaver}
onCancel={() => {
setShowFormSaver(false);
}}
onSave={handleSaveForm}
loading={submitState.loading}
/>
)}
<div className="flex flex-row place-content-between">
<div className="grid grid-flow-row gap-1.5">
<Text variant="h3" component="h2">
Multi-Factor Authentication
</Text>
<Text>Enable users to use multi-factor authentication (MFA).</Text>
</div>
<div className="mr-2 flex flex-row">
<Toggle
checked={data.app.authMfaEnabled}
onChange={handleToggleMFA}
/>
</div>
</div>
{submitState.error && (
<Alert severity="error">{submitState.error.message}</Alert>
)}
{data.app.authMfaEnabled && (
<div className="border-t border-b border-gray-200 py-4">
<Input
id="otpIssuer"
label="Name of the One Time Password (OTP) issuer"
onChange={(e) => {
setShowFormSaver(true);
setOTPIssuer(e.target.value);
}}
variant="inline"
value={OTPIssuer}
error={submitState.fieldsWithError?.includes('OTPIssuer')}
placeholder={currentApplication.name}
fullWidth
hideEmptyHelperText
inlineInputProportion="50%"
componentsProps={{
label: { className: 'text-sm+' },
}}
/>
</div>
)}
</div>
);
}
export default MultiFactorAuthentication;

View File

@@ -1,122 +0,0 @@
import Copy from '@/components/icons/Copy';
import type { Provider } from '@/types/providers';
import { Button } from '@/ui/Button';
import CheckBoxes from '@/ui/Checkboxes';
import { Input } from '@/ui/Input';
import { Text } from '@/ui/Text';
import { copy } from '@/utils/copy';
import clsx from 'clsx';
import { useState } from 'react';
// TODO: Instead of a `helpers.tsx`, we should have designated files for these
// components
type ProviderSettingsProps = {
title: string;
desc: string;
inputPlaceholder?: string;
input: boolean;
inputValue?: string;
inputOnChange?: (v: string) => void;
inputType?: 'text' | 'password';
multiline?: boolean;
link?: string;
showCopy?: boolean;
};
export function ProviderSetting({
title,
desc,
inputPlaceholder,
input,
inputValue,
inputOnChange,
inputType,
link,
showCopy = false,
multiline,
}: ProviderSettingsProps) {
return (
<div
className={clsx(
'flex w-full flex-row items-center justify-between px-2 pt-3 pb-1',
)}
>
<div className="flex w-80 flex-col">
<Text
variant="body"
color="greyscaleDark"
size="normal"
className="font-medium capitalize"
>
{title}
</Text>
<Text color="greyscaleDark" size="tiny" className="font-normal">
{desc}
</Text>
</div>
<div className="flex w-full flex-row place-content-between self-center">
{input ? (
<Input
placeholder={inputPlaceholder || ''}
className="h-full w-full"
type={inputType}
value={inputValue}
onChange={inputOnChange}
multiline={multiline}
/>
) : (
<div className="flex flex-row self-center align-middle">
<Text
color="greyscaleDark"
size="tiny"
className="self-center font-normal"
>
{link}
</Text>
</div>
)}
{showCopy && (
<Copy
className="ml-1 mr-4 h-4 w-4 cursor-pointer self-center text-greyscaleDark"
onClick={() => {
copy(link as string, title);
}}
/>
)}
</div>
</div>
);
}
type ProviderSettingsSaveProps = {
provider: Provider;
loading: boolean;
};
export function ProviderSettingsSave({
provider,
loading,
}: ProviderSettingsSaveProps) {
const [confirmed, setConfirmed] = useState(false);
return (
<div className="mt-4 flex w-full flex-row place-content-between px-2">
<CheckBoxes
id="confirm-paste"
state={confirmed}
setState={() => setConfirmed(!confirmed)}
checkBoxText={`I have pasted the redirect URI into ${provider.name}.`}
/>
<div />
<Button
type="submit"
variant="primary"
disabled={!confirmed}
loading={loading}
className="self-center"
>
Confirm Settings
</Button>
</div>
);
}

View File

@@ -1 +0,0 @@
export * from './helpers';

View File

@@ -1,71 +0,0 @@
import type { SelectorOption } from '@/ui/Selector';
import Selector from '@/ui/Selector';
import { Text } from '@/ui/Text';
import { Toggle } from '@/ui/Toggle';
import clsx from 'clsx';
export interface PermissionSettingsProps {
text: string;
desc?: string;
toggle?: boolean;
onChange?: any;
checked?: boolean;
options?: any;
value?: SelectorOption;
} // @TODO: Fix alt attribute on images.
// @FIX: Double border
export function PermissionSetting({
text,
desc,
toggle,
checked = false,
onChange,
options,
value,
}: PermissionSettingsProps) {
return (
<div className="flex flex-row place-content-between py-2">
<div
className={clsx(
'flex flex-col space-y-1 self-center px-0.5',
!desc && 'py-3.5',
desc && 'py-2',
)}
>
<Text
variant="body"
size="normal"
className="font-medium"
color="greyscaleDark"
>
{text}
</Text>
{desc && (
<Text
variant="body"
size="tiny"
className="font-normal"
color="greyscaleDark"
>
{desc}
</Text>
)}
</div>
{toggle ? (
<div className="flex flex-row">
<Toggle checked={checked} onChange={onChange} />
</div>
) : (
<div className="flex flex-row self-center">
<Selector
width="w-28"
options={options}
onChange={onChange}
value={value}
/>
</div>
)}
</div>
);
}

View File

@@ -1,58 +0,0 @@
import type { Provider as ProviderType } from '@/types/providers';
import Status, { StatusEnum } from '@/ui/Status';
import { Text } from '@/ui/Text';
import { ChevronRightIcon } from '@heroicons/react/solid';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
interface ProviderProps {
provider: ProviderType;
enabled: boolean;
}
export function Provider({ provider, enabled }: ProviderProps) {
const { name, logo } = provider;
const {
query: { workspaceSlug, appSlug },
} = useRouter();
const nameLowerCase = name.toLowerCase();
return (
<Link
href={`/${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
passHref
>
<a
href={`${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
className="flex cursor-pointer flex-row place-content-between border-t py-2.5"
>
<div className="grid grid-flow-col items-center gap-2">
<div className="h-6 w-6">
<Image
src={logo}
alt={`Logo of ${name}`}
width={24}
height={24}
layout="responsive"
/>
</div>
<Text className="font-medium" color="greyscaleDark" size="normal">
{name}
</Text>
</div>
<div className="flex flex-row">
{enabled ? (
<Status status={StatusEnum.Live}>Enabled</Status>
) : (
<Status status={StatusEnum.Closed}>Disabled</Status>
)}
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</a>
</Link>
);
}
export default Provider;

View File

@@ -1,183 +0,0 @@
import { CreateUserRoleModal } from '@/components/applications/users/roles/CreateRoleModal';
import { EditUserRoleModal } from '@/components/applications/users/roles/EditUserRoleModal';
import Lock from '@/components/icons/Lock';
import type { GetRolesQuery } from '@/generated/graphql';
import { Modal } from '@/ui';
import { Text } from '@/ui/Text';
import { ChevronRightIcon } from '@heroicons/react/solid';
import clsx from 'clsx';
import type { Dispatch, MouseEvent, MouseEventHandler } from 'react';
import { useReducer } from 'react';
function RolesTableHead() {
return (
<thead>
<tr>
<th className="w-64 py-3 text-left font-medium text-base">
<Text className="text-xs font-bold text-greyscaleDark">Role</Text>
</th>
</tr>
</thead>
);
}
interface UserRoleProps {
role: string;
isSystemRole: boolean;
onClick?: MouseEventHandler<HTMLTableRowElement>;
}
function UserRole({ role, isSystemRole, onClick }: UserRoleProps) {
return (
<tr
className={clsx(isSystemRole ? 'cursor-not-allowed' : 'cursor-pointer')}
onClick={onClick}
>
<td className="py-2">
<Text
size="normal"
className={clsx(
isSystemRole ? 'text-greyscaleGrey' : 'text-greyscaleDark',
'pl-1 font-medium',
)}
>
{role}
</Text>
</td>
<td className="text-right">
{isSystemRole ? (
<div className="inline-flex pr-1">
<Text
size="tiny"
className=" font-mono text-xs font-medium uppercase tracking-wide text-greyscaleGrey"
>
System Role
</Text>
<Lock className="ml-1 h-5 w-5 text-greyscaleGrey" />
</div>
) : (
<div className="inline-flex self-center py-2 pr-1.5">
<ChevronRightIcon className="h-4.5 w-4.5 text-greyscaleDark" />
</div>
)}
</td>
</tr>
);
}
export type UserRoleDetails = {
name: string;
isSystemRole: boolean;
};
export const getUserRoles = (data): UserRoleDetails[] => {
const authUserDefaultAllowedRoles =
data.app.authUserDefaultAllowedRoles.split(',');
return authUserDefaultAllowedRoles.map((role: string) => ({
name: role,
isSystemRole: ['user', 'me'].includes(role),
}));
};
type ModalState = {
visible: boolean;
type: 'create' | 'edit';
payload: UserRoleDetails;
};
type ModalAction = {
type: 'OPEN_CREATE_MODAL' | 'OPEN_EDIT_MODAL' | 'CLOSE_MODAL';
payload?: UserRoleDetails;
};
function modalStateReducer(state: ModalState, action: ModalAction): ModalState {
switch (action.type) {
case 'OPEN_CREATE_MODAL':
return { ...state, visible: true, type: 'create', payload: null };
case 'OPEN_EDIT_MODAL':
return { ...state, visible: true, type: 'edit', payload: action.payload };
case 'CLOSE_MODAL':
return { ...state, visible: false };
default:
throw new Error(`Action type ${action.type} is not supported.`);
}
}
function AddNewUserRole({ dispatch }: { dispatch: Dispatch<ModalAction> }) {
return (
<tr className="cursor-pointer border-y-1 border-solid border-gray-300">
<td className="p-2">
<button
type="button"
onClick={() => dispatch({ type: 'OPEN_CREATE_MODAL' })}
>
<Text className="text-sm+ font-medium text-blue">
Create New Role
</Text>
</button>
</td>
<td />
</tr>
);
}
function RolesTableBody({ data }: { data: GetRolesQuery }) {
const userRoles = getUserRoles(data);
const [
{ visible: modalVisible, type: modalType, payload: modalPayload },
dispatch,
] = useReducer(modalStateReducer, {
visible: false,
type: null,
payload: null,
});
function handleRoleEdit(event: MouseEvent<HTMLTableRowElement>, role: any) {
dispatch({ type: 'OPEN_EDIT_MODAL', payload: role });
}
return (
<>
<Modal
showModal={modalVisible}
close={() => dispatch({ type: 'CLOSE_MODAL' })}
>
{modalType === 'create' ? (
<CreateUserRoleModal
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
/>
) : (
<EditUserRoleModal
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
payload={modalPayload}
/>
)}
</Modal>
<tbody className="divide-y-1 border-t-1 border-b-1 border-solid border-gray-300 ">
{userRoles.map((role) => (
<UserRole
key={role.name}
role={role.name}
isSystemRole={role.isSystemRole}
onClick={
role.isSystemRole
? undefined
: (event) => handleRoleEdit(event, role)
}
/>
))}
<AddNewUserRole dispatch={dispatch} />
</tbody>
</>
);
}
export function RolesTable({ data }: { data: GetRolesQuery }) {
return (
<table className="w-full table-fixed overflow-x-auto">
<RolesTableHead />
<RolesTableBody data={data} />
</table>
);
}

View File

@@ -1,73 +0,0 @@
import type { TextProps } from '@/ui/Text';
import { Text } from '@/ui/Text';
import type { PropsWithChildren, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export interface SettingsSectionProps {
/**
* Title of this section.
*/
title: ReactNode;
/**
* Props to be passed to the title component.
*/
titleProps?: TextProps;
/**
* Props to be passed to the wrapper component.
*/
wrapperProps?: TextProps;
/**
* Description of this section.
*/
desc?: ReactNode;
/**
* Props to be passed to the description component.
*/
descriptionProps?: TextProps;
}
export function SettingsSection({
children,
title,
titleProps,
descriptionProps,
desc,
wrapperProps,
}: PropsWithChildren<SettingsSectionProps>) {
const { className: titleClassName, ...restTitleProps } = titleProps || {};
const { className: wrapperClassName } = wrapperProps || {};
const { className: descriptionClassName, ...restDescriptionProps } =
descriptionProps || {};
return (
<div className={twMerge('mt-10', wrapperClassName)}>
<div className="mx-auto font-display">
<div className="flex flex-col place-content-between">
<div>
<Text
size="large"
variant="heading"
className={twMerge('mb-1.5 font-medium', titleClassName)}
color="greyscaleDark"
{...restTitleProps}
>
{title}
</Text>
{desc && (
<Text
variant="body"
size="normal"
color="greyscaleDark"
className={twMerge('mb-3 font-normal', descriptionClassName)}
{...restDescriptionProps}
>
{desc}
</Text>
)}
</div>
</div>
{children}
</div>
</div>
);
}

View File

@@ -32,25 +32,25 @@ function Users({ users }: any) {
key={user.id}
passHref
>
<tr className="cursor-pointer w-52">
<td className="py-1 pr-6 whitespace-nowrap">
<tr className="w-52 cursor-pointer">
<td className="whitespace-nowrap py-1 pr-6">
<div className="flex items-center">
<IconButton
variant="borderless"
color="secondary"
className="p-1 mr-2"
className="mr-2 p-1"
aria-label="Copy user ID"
onClick={(event) => {
event.stopPropagation();
copy(user.id, `User ID`);
}}
>
<CopyIcon className="w-4 h-4" />
<CopyIcon className="h-4 w-4" />
</IconButton>
<div className="flex-shrink-0 w-8 h-8">
<div className="h-8 w-8 flex-shrink-0">
<Avatar
className="w-8 h-8"
className="h-8 w-8"
avatarUrl={user.avatarUrl}
name={user.displayName}
/>
@@ -63,7 +63,7 @@ function Users({ users }: any) {
<Text
variant="a"
color="greyscaleDark"
className="font-medium cursor-pointer"
className="cursor-pointer font-medium"
size="normal"
>
{user.displayName ||
@@ -78,7 +78,7 @@ function Users({ users }: any) {
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<td className="whitespace-nowrap px-6 py-4">
<Text color="greyscaleDark" className="font-normal" size="normal">
{format(new Date(user.createdAt), 'd MMM yyyy')}
</Text>
@@ -103,13 +103,13 @@ function Users({ users }: any) {
);
})}
</td>
<td className="py-4 pl-6 text-sm font-medium text-right whitespace-nowrap">
<td className="whitespace-nowrap py-4 pl-6 text-right text-sm font-medium">
<Link
href={`/${workspaceSlug}/${appSlug}/users/${user.id}`}
passHref
>
<a href={`${workspaceSlug}/${appSlug}/users/${user.id}`}>
<ChevronRightIcon className="self-center w-4 h-4 ml-2 cursor-pointer" />
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
</a>
</Link>
</td>
@@ -131,7 +131,7 @@ function UserPages({ totalNrOfPages, setCurrentPage }: any) {
<button
type="button"
key={i}
className="px-2 cursor-pointer"
className="cursor-pointer px-2"
onClick={() => {
setCurrentPage(i);
}}
@@ -212,15 +212,15 @@ export function UsersTable({
}
return (
<div className="flex flex-col mt-2 font-display">
<div className="inline-block min-w-full py-2 align-">
<div className="mt-2 flex flex-col font-display">
<div className="align- inline-block min-w-full py-2">
<div className="overflow-hidden border-b">
<table className="min-w-full divide-y divide-gray-200">
<thead className="">
<tr>
<th
scope="col"
className="px-4 py-3 text-xs font-medium tracking-wider text-left text-dark"
className="px-4 py-3 text-left text-xs font-medium tracking-wider text-dark"
>
{data ? (
<TotalUsers
@@ -244,7 +244,7 @@ export function UsersTable({
</th>
<th
scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
>
<Text size="tiny" color="greyscaleDark" className="font-bold">
Signed up at
@@ -253,7 +253,7 @@ export function UsersTable({
<th
scope="col"
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
>
<Text size="tiny" color="greyscaleDark" className="font-bold">
Roles

View File

@@ -1,110 +0,0 @@
import { useWorkspaceContext } from '@/context/workspace-context';
import useCustomClaims from '@/hooks/useCustomClaims';
import type { CustomClaim } from '@/types/application';
import { Alert } from '@/ui/Alert';
import { triggerToast } from '@/utils/toast';
import {
refetchGetAppCustomClaimsQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type {
CreatePermissionVariableBaseFormData,
CreatePermissionVariableModalBaseProps,
} from './CreatePermissionVariableModalBase';
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
export type CreatePermissionVariableFormData =
CreatePermissionVariableBaseFormData;
export type CreatePermissionVariableModalProps = Pick<
CreatePermissionVariableModalBaseProps,
'onClose'
>;
export default function CreatePermissionVariableModal({
onClose,
}: CreatePermissionVariableModalProps) {
const [error, setError] = useState<Error>();
const form = useForm<CreatePermissionVariableFormData>({
reValidateMode: 'onBlur',
});
const {
workspaceContext: { appId },
} = useWorkspaceContext();
const { data: customClaims } = useCustomClaims({ appId });
const [updateApp] = useUpdateAppMutation({
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
});
async function handleSubmit(permissionVariable: CustomClaim) {
setError(undefined);
try {
if (
customClaims.some(
(claim) =>
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
)
) {
throw new Error(
'Permission variable with this field name already exists.',
);
}
await updateApp({
variables: {
id: appId,
app: {
authJwtCustomClaims: [...customClaims, permissionVariable]
.filter((claim) => !claim.system)
.reduce(
(authJwtCustomClaims, claim) => ({
...authJwtCustomClaims,
[claim.key]: claim.value,
}),
{},
),
},
},
});
triggerToast('Permission variable created');
if (!onClose) {
return;
}
onClose();
} catch (updateError) {
if (updateError instanceof Error) {
setError(updateError);
} else {
setError(new Error(updateError));
}
}
}
return (
<FormProvider {...form}>
<CreatePermissionVariableModalBase
title="Create Permission Variable"
type="create"
onSubmit={handleSubmit}
onClose={onClose}
errorComponent={
error && (
<Alert className="mt-4" severity="error">
{error.message}
</Alert>
)
}
/>
</FormProvider>
);
}

View File

@@ -1,187 +0,0 @@
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import type { ChangeEvent, MouseEventHandler, ReactNode } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useFormContext } from 'react-hook-form';
export interface CreatePermissionVariableBaseFormData {
key: string;
value: string;
}
export interface CreateModalBaseProps<T> {
/**
* Title of this modal.
*/
title: string;
/**
* Type of this modal.
*/
type?: 'create' | 'edit';
/**
* Callback to be called when the modal is closed.
*/
onClose?: VoidFunction;
/**
* Callback to be called when remove button is clicked.
*/
onRemove?: MouseEventHandler<HTMLButtonElement>;
/**
* Callback to be called when the form is submitted.
*/
onSubmit: SubmitHandler<T>;
/**
* Error to be displayed.
*/
errorComponent?: ReactNode;
}
export type CreatePermissionVariableModalBaseProps =
CreateModalBaseProps<CreatePermissionVariableBaseFormData>;
export default function CreatePermissionVariableModalBase({
title,
type,
onClose,
onRemove,
onSubmit,
errorComponent,
}: CreatePermissionVariableModalBaseProps) {
const {
handleSubmit,
watch,
register,
formState: { isSubmitting, errors },
} = useFormContext<CreatePermissionVariableBaseFormData>();
const keyHandlers = register('key', {
required: true,
pattern: {
value: /^[a-zA-Z-]+$/i,
message: 'Must contain only letters and hyphens',
},
});
const valueHandlers = register('value', {
required: true,
pattern: {
value: /^[a-zA-Z0-9._[\]]+$/i,
message: 'Must contain only letters, dots, brackets, and underscores',
},
});
const isComplete = !!watch('key') && !!watch('value');
return (
<div className="w-modal p-6 text-left">
<div className="grid w-full grid-flow-col items-center justify-between">
<Text variant="h3" component="h2">
{title}
</Text>
{type === 'edit' && onRemove && (
<Button variant="borderless" color="error" onClick={onRemove}>
Remove
</Button>
)}
</div>
<Text className="mt-2 text-sm+ text-greyscaleDark">
Enter the field name and the path you want to use in this permission
variable.
</Text>
{errorComponent}
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
<div className="my-4 grid grid-flow-row divide-y-1 divide-solid divide-gray-200 border-y border-gray-200">
<Input
{...keyHandlers}
value={watch('key')}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (
event.target.value &&
!/^[a-zA-Z-]+$/gi.test(event.target.value)
) {
// prevent the user from entering invalid characters
return;
}
keyHandlers.onChange(event);
}}
id="key"
variant="inline"
inlineInputProportion="66%"
label="Field name"
fullWidth
startAdornment={
<Text className="min-w-[73px] text-sm+ text-greyscaleGrey">
X-Hasura-
</Text>
}
componentsProps={{
inputWrapper: { className: 'my-1' },
input: {
className: 'border-transparent focus-within:border-solid pl-2',
},
inputRoot: { className: '!pl-[1px]' },
}}
autoFocus
error={!!errors?.key?.message}
helperText={errors?.key?.message}
hideEmptyHelperText
/>
<Input
{...valueHandlers}
value={watch('value')}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (
event.target.value &&
!/^[a-zA-Z-]+$/gi.test(event.target.value)
) {
// prevent the user from entering invalid characters
return;
}
valueHandlers.onChange(event);
}}
id="value"
variant="inline"
inlineInputProportion="66%"
label="Path"
fullWidth
startAdornment={
<Text className="text-sm+ text-greyscaleGrey">user.</Text>
}
componentsProps={{
inputWrapper: { className: 'my-1' },
input: {
className: 'border-transparent focus-within:border-solid pl-2',
},
inputRoot: { className: '!pl-[1px]' },
}}
error={!!errors?.value?.message}
helperText={errors?.value?.message}
hideEmptyHelperText
/>
</div>
<div className="grid gap-2">
<Button
type="submit"
loading={isSubmitting}
disabled={isSubmitting || !isComplete}
>
{type === 'create' ? 'Create Permission Variable' : 'Save Changes'}
</Button>
<Button variant="outlined" color="secondary" onClick={onClose}>
Close
</Button>
</div>
</form>
</div>
);
}

View File

@@ -1,209 +0,0 @@
import { useWorkspaceContext } from '@/context/workspace-context';
import useCustomClaims from '@/hooks/useCustomClaims';
import type { CustomClaim } from '@/types/application';
import { Alert } from '@/ui/Alert';
import { Modal } from '@/ui/Modal';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
import { triggerToast } from '@/utils/toast';
import {
refetchGetAppCustomClaimsQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type {
CreatePermissionVariableBaseFormData,
CreatePermissionVariableModalBaseProps,
} from './CreatePermissionVariableModalBase';
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
export type EditPermissionVariableFormData =
CreatePermissionVariableBaseFormData;
export type EditPermissionVariableModalProps = Pick<
CreatePermissionVariableModalBaseProps,
'onClose'
> & {
/**
* The permission variable to edit.
*/
payload: CustomClaim;
};
export default function EditPermissionVariableModal({
payload: originalCustomClaim,
...props
}: EditPermissionVariableModalProps) {
const [error, setError] = useState<Error>();
const [showRemoveModal, setShowRemoveModal] = useState(false);
const form = useForm<EditPermissionVariableFormData>({
reValidateMode: 'onBlur',
defaultValues: {
key: originalCustomClaim.key || '',
value: originalCustomClaim.value || '',
},
});
const {
workspaceContext: { appId },
} = useWorkspaceContext();
const { data: customClaims } = useCustomClaims({ appId });
const [updateApp] = useUpdateAppMutation({
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
});
async function handleSubmit(permissionVariable: CustomClaim) {
setError(undefined);
try {
if (
originalCustomClaim.key.toLowerCase() !==
permissionVariable.key.toLowerCase() &&
customClaims.some(
(claim) =>
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
)
) {
throw new Error(
'Permission variable with this field name already exists.',
);
}
// we need to preserve the original position of the permission variable
const currentIndex = customClaims.findIndex(
(claim) =>
claim.key.toLowerCase() === originalCustomClaim.key.toLowerCase(),
);
await updateApp({
variables: {
id: appId,
app: {
authJwtCustomClaims: customClaims
.slice(0, currentIndex)
.concat(permissionVariable)
.concat(customClaims.slice(currentIndex + 1))
.filter((claim) => !claim.system)
.reduce(
(authJwtCustomClaims, claim) => ({
...authJwtCustomClaims,
[claim.key]: claim.value,
}),
{},
),
},
},
});
triggerToast(`Permission variable updated`);
if (props.onClose) {
props.onClose();
}
} catch (updateError) {
if (updateError instanceof Error) {
setError(updateError);
} else {
setError(new Error(updateError));
}
}
}
async function handleRemove() {
setError(undefined);
try {
await updateApp({
variables: {
id: appId,
app: {
authJwtCustomClaims: customClaims
.filter(
(claim) =>
claim.key !== originalCustomClaim.key && !claim.system,
)
.reduce(
(authJwtCustomClaims, claim) => ({
...authJwtCustomClaims,
[claim.key]: claim.value,
}),
{},
),
},
},
});
setShowRemoveModal(false);
triggerToast('Permission variable removed');
if (props.onClose) {
props.onClose();
}
} catch (updateError) {
if (updateError instanceof Error) {
setError(updateError);
} else {
setError(new Error(updateError));
}
}
}
return (
<>
<Modal
showModal={showRemoveModal}
close={() => setShowRemoveModal(false)}
>
<div className="grid w-96 grid-flow-row gap-2 p-6 text-left text-greyscaleDark">
<Text variant="h3" component="h2">
Remove {originalCustomClaim.key}?
</Text>
<Text>You will not be able to use it in permissions anymore.</Text>
<Text>
If you have permission checks currently using this property, they
will never resolve to true.
</Text>
<div className="mt-2 grid grid-flow-row gap-2">
<Button color="error" onClick={handleRemove} className="w-full">
Remove Permission Variable
</Button>
<Button
variant="outlined"
color="secondary"
onClick={() => setShowRemoveModal(false)}
className="w-full"
>
Cancel
</Button>
</div>
</div>
</Modal>
<FormProvider {...form}>
<CreatePermissionVariableModalBase
title="Edit Permission Variable"
type="edit"
onSubmit={handleSubmit}
onRemove={() => setShowRemoveModal(true)}
errorComponent={
error && (
<Alert className="mt-4" severity="error">
{error.message}
</Alert>
)
}
{...props}
/>
</FormProvider>
</>
);
}

View File

@@ -1,101 +0,0 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import Loading from '@/ui/Loading';
import {
refetchGetRolesQuery,
useGetRolesQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type {
CreateUserRoleBaseFormData,
CreateUserRoleModalBaseProps,
} from './CreateUserRoleModalBase';
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
export type CreateUserRoleFormData = CreateUserRoleBaseFormData;
export type CreateUserRoleModalProps = Pick<
CreateUserRoleModalBaseProps,
'onClose'
>;
export function CreateUserRoleModal({ onClose }: CreateUserRoleModalProps) {
const [error, setError] = useState<Error>();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const form = useForm<CreateUserRoleBaseFormData>({
reValidateMode: 'onBlur',
});
const [updateApp] = useUpdateAppMutation({
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
});
const {
data: currentRolesData,
loading,
error: getRolesError,
} = useGetRolesQuery({
variables: {
id: currentApplication.id,
},
});
if (loading) {
return <Loading />;
}
if (getRolesError) {
return (
<div className="mx-auto max-w-2.5xl">
<Alert severity="error">{error.message}</Alert>
</div>
);
}
async function handleSubmit(data) {
setError(undefined);
const newAuthUserDefaultAllowedRoles = `${currentRolesData.app.authUserDefaultAllowedRoles},${data.roleName}`;
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
},
},
});
if (!onClose) {
return;
}
onClose();
} catch (updateError) {
setError(updateError);
}
}
return (
<FormProvider {...form}>
<CreateUserRoleModalBase
title="Create New Role"
type="create"
onSubmit={handleSubmit}
onClose={onClose}
errorComponent={
error && (
<Alert className="mt-4" severity="error">
{error.message}
</Alert>
)
}
/>
</FormProvider>
);
}

View File

@@ -1,102 +0,0 @@
import type { CreateModalBaseProps } from '@/components/applications/users/permissions/modal/CreatePermissionVariableModalBase';
import { Input } from '@/ui';
import { Button } from '@/ui/Button';
import { Text } from '@/ui/Text';
import { Controller, useFormContext } from 'react-hook-form';
export interface CreateUserRoleBaseFormData {
roleName: string;
}
export type CreateUserRoleModalBaseProps =
CreateModalBaseProps<CreateUserRoleBaseFormData>;
export type CreateUserRoleModal = Pick<CreateUserRoleModalBaseProps, 'onClose'>;
export function CreateUserRoleModalBase({
title,
type,
onRemove,
onSubmit,
errorComponent,
}: CreateUserRoleModalBaseProps) {
const {
control,
handleSubmit,
formState: { isSubmitting },
} = useFormContext<CreateUserRoleBaseFormData>();
return (
<div className="w-modal- p-6 text-left">
<div className="mx-auto items-center justify-between">
<Text
variant="heading"
className="text-center text-lg font-medium text-greyscaleDark"
>
{title}
</Text>
</div>
{errorComponent}
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
<div className="mt-3 mb-3 divide-y border-t border-b py-1">
<div className="flex flex-row place-content-between py-2">
<div className="flex w-full flex-row">
<Text
color="greyscaleDark"
className="self-center font-medium"
size="normal"
>
New Role Name
</Text>
</div>
<div className="flex w-full">
<Controller
name="roleName"
control={control}
rules={{
required: true,
pattern: {
value: /^[a-zA-Z0-9-_]+$/,
message: 'Must contain only letters, hyphens, and numbers.',
},
}}
render={({ field }) => (
<Input
{...field}
id="roleName"
required
value={field.value || ''}
onChange={(value: string) => {
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
// prevent the user from entering invalid characters
return;
}
field.onChange(value);
}}
/>
)}
/>
</div>
</div>
</div>
<div className="grid gap-2">
<Button
variant="primary"
type="submit"
loading={isSubmitting}
disabled={isSubmitting}
>
{type === 'create' ? 'Create New User Role' : 'Save Changes'}
</Button>
{type === 'edit' && onRemove && (
<Button variant="menu" border onClick={onRemove}>
<Text className="text-sm+ font-medium text-red">Remove Role</Text>
</Button>
)}
</div>
</form>
</div>
);
}

View File

@@ -1,190 +0,0 @@
import type { GetRolesQuery } from '@/generated/graphql';
import {
refetchGetRolesQuery,
useGetRolesQuery,
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import { Button } from '@/ui/Button';
import Loading from '@/ui/Loading';
import { Modal } from '@/ui/Modal';
import { Text } from '@/ui/Text';
import { triggerToast } from '@/utils/toast';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type {
CreateUserRoleBaseFormData,
CreateUserRoleModalBaseProps,
} from './CreateUserRoleModalBase';
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
export type EditUserRoleFormData = CreateUserRoleBaseFormData;
export type EditUserRoleModalProps = Pick<
CreateUserRoleModalBaseProps,
'onClose'
> & {
/**
* The permission variable to edit.
*/
payload: any;
};
export function EditUserRoleModal({
payload: originalRole,
...props
}: EditUserRoleModalProps) {
const [error, setError] = useState<Error>();
const [showRemoveModal, setShowRemoveModal] = useState(false);
const { currentApplication } = useCurrentWorkspaceAndApplication();
const form = useForm<EditUserRoleFormData>({
reValidateMode: 'onBlur',
defaultValues: {
roleName: originalRole.name || '',
},
});
const [updateApp, { loading: loadingUpdateAppMutation }] =
useUpdateAppMutation({
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
});
const {
data: currentRolesData,
loading,
error: getRolesError,
} = useGetRolesQuery({
variables: {
id: currentApplication.id,
},
});
if (loading) {
return <Loading />;
}
if (getRolesError) {
return (
<div className="mx-auto max-w-2.5xl">
<Alert severity="error">{error.message}</Alert>
</div>
);
}
async function handleSubmit(data: EditUserRoleFormData) {
setError(undefined);
const currentUserRoles =
currentRolesData.app.authUserDefaultAllowedRoles.split(',');
const roleBeingEdited = currentUserRoles.find(
(role) => role === originalRole.name,
);
const indexofRoleBeingEdited = currentUserRoles.indexOf(roleBeingEdited);
const newRoleName = data.roleName;
const newAuthUserDefaultAllowedRoles = currentUserRoles.slice();
if (data.roleName !== originalRole.name) {
newAuthUserDefaultAllowedRoles[indexofRoleBeingEdited] = newRoleName;
}
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
authUserDefaultAllowedRoles:
newAuthUserDefaultAllowedRoles.join(','),
},
},
});
triggerToast(`Role "${data.roleName}" updated successfully`);
props.onClose();
} catch (updateError) {
setError(updateError);
}
}
async function handleRemove(data: GetRolesQuery) {
setError(undefined);
// Get the current roles of this application.
const currentUserRoles = data.app.authUserDefaultAllowedRoles.split(',');
// Remove the role from the current roles.
const filteredCurrentUserRoles = currentUserRoles.filter(
(role) => role !== originalRole.name,
);
const newAuthUserDefaultAllowedRoles = filteredCurrentUserRoles.join(',');
try {
await updateApp({
variables: {
id: currentApplication.id,
app: {
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
},
},
});
props.onClose();
triggerToast(`Role "${originalRole.name}" removed successfully`);
} catch (updateError) {
setError(updateError);
}
}
return (
<>
<Modal
showModal={showRemoveModal}
close={() => setShowRemoveModal(false)}
>
<div className="px-6 pt-5 text-center text-greyscaleDark">
<Text variant="heading" className="mb-2 text-lg font-medium">
Remove Role &quot;{originalRole.name}&quot;?
</Text>
<div className="my-4">
<Button
variant="danger"
onClick={() => handleRemove(currentRolesData)}
className="w-full"
loading={loadingUpdateAppMutation}
>
Remove Role
</Button>
<Button
onClick={() => setShowRemoveModal(false)}
className="w-full"
>
Cancel
</Button>
</div>
</div>
</Modal>
<FormProvider {...form}>
<CreateUserRoleModalBase
title="Edit Role"
type="edit"
onSubmit={handleSubmit}
onRemove={() => setShowRemoveModal(true)}
errorComponent={
error && (
<Alert className="mt-4" severity="error">
{error.message}
</Alert>
)
}
{...props}
/>
</FormProvider>
</>
);
}

View File

@@ -1,201 +0,0 @@
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { useState } from 'react';
type EnvModalProps = {
onSubmit: (props: {
name: string;
prodValue: string;
devValue: string;
}) => Promise<void>;
name?: string;
prodValue?: string;
devValue?: string;
close: VoidFunction;
};
interface AddEnvVarModalVariablesError {
hasError: boolean;
message: string;
}
const DISABLED_START_ENV_VARIABLES = [
'NHOST_',
'HASURA_',
'AUTH_',
'STORAGE_',
'POSTGRES_',
];
const DISABLED_ENV_VARIABLES = [
'PATH',
'NODE_PATH',
'PYTHONPATH',
'GEM_PATH',
'HOSTNAME',
'TERM',
'NODE_VERSION',
'YARN_VERSION',
'NODE_ENV',
'HOME',
];
export default function AddEnvVarModal({
name: externalName,
prodValue: externalProdValue,
devValue: externalDevValue,
close,
onSubmit,
}: EnvModalProps) {
const [name, setName] = useState(externalName || '');
const [prodValue, setProdValue] = useState(externalProdValue || '');
const [devValue, setDevValue] = useState(externalDevValue || '');
const [error, setError] = useState<AddEnvVarModalVariablesError>({
hasError: false,
message: '',
});
const noError: AddEnvVarModalVariablesError = {
hasError: false,
message: '',
};
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
setError({ hasError: false, message: '' });
e.preventDefault();
if (
DISABLED_START_ENV_VARIABLES.some((envVar) =>
name.toUpperCase().startsWith(envVar),
)
) {
setError({
hasError: true,
message:
'The environment variable name cannot start with a value that is reserved for an internal environment variable.',
});
return;
}
if (
DISABLED_ENV_VARIABLES.some((envVar) => envVar === name.toUpperCase())
) {
setError({
hasError: true,
message:
'The environment variable name cannot be a value that is reserved for internal use.',
});
return;
}
// only allow alphabet characters and underscores
const onlyLettersWithNumbersStartsWithLetter = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
if (!onlyLettersWithNumbersStartsWithLetter.test(name)) {
setError({
hasError: true,
message:
'The name contains invalid characters. Only letters, digits, and underscores are allowed. Furthermore, the name should start with a letter.',
});
return;
}
if (!name) {
setError({ hasError: true, message: 'Variable name is required.' });
return;
}
if (!prodValue) {
setError({ hasError: true, message: 'Production value is required.' });
return;
}
if (!devValue) {
setError({ hasError: true, message: 'Development value is required.' });
return;
}
await onSubmit({
name,
prodValue,
devValue,
});
close();
};
return (
<form onSubmit={handleSubmit}>
<div className="w-modal px-6 py-6 text-left">
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
{name || 'EXAMPLE_NAME'}
</Text>
<Text variant="subtitle2">
The default value will be available in all environments, unless you
override it. All values are encrypted.
</Text>
<div className="my-2 grid grid-flow-row gap-2">
<Input
id="name"
label="Name"
autoFocus
autoComplete="off"
fullWidth
placeholder="EXAMPLE_NAME"
value={name}
onChange={(event) => {
setError(noError);
setName(event.target.value);
}}
hideEmptyHelperText
/>
<Input
id="prodValue"
label="Production Value"
fullWidth
placeholder="Enter a value"
value={prodValue}
onChange={(event) => {
setError(noError);
setProdValue(event.target.value);
}}
hideEmptyHelperText
/>
<Input
id="devValue"
label="Development Value"
fullWidth
placeholder="Enter a value"
value={devValue}
onChange={(event) => {
setError(noError);
setDevValue(event.target.value);
}}
hideEmptyHelperText
/>
</div>
{error.hasError && (
<Alert severity="warning" className="mb-2">
<Text className="font-medium">Warning</Text>
<Text>{error.message}</Text>
</Alert>
)}
<div className="grid grid-flow-row gap-2">
<Button type="submit">Add</Button>
<Button onClick={close} variant="outlined" color="secondary">
Close
</Button>
</div>
</div>
</div>
</form>
);
}

View File

@@ -146,7 +146,7 @@ function AddPaymentMethodForm({
};
return (
<div className="px-6 pt-6 pb-6 text-left w-modal2">
<div className="w-modal2 px-6 pt-6 pb-6 text-left">
<div className="flex flex-col">
<form onSubmit={handleSubmit}>
<Text
@@ -161,11 +161,11 @@ function AddPaymentMethodForm({
variant="body"
color="greyscaleDark"
size="small"
className="font-normal text-center"
className="text-center font-normal"
>
We&apos;ll store these in your workspace for future use.
</Text>
<div className="w-full px-2 py-2 my-2 mt-6 rounded-lg border-1">
<div className="my-2 mt-6 w-full rounded-lg border-1 px-2 py-2">
<CardElement
onReady={(element) => element.focus()}
options={{

View File

@@ -53,7 +53,7 @@ function ControlledCheckbox(
name={field.name}
ref={mergeRefs([field.ref, ref])}
onChange={(event, checked) => {
setValue(controllerProps?.name || name, checked);
setValue(controllerProps?.name || name, checked, { shouldDirty: true });
if (props.onChange) {
props.onChange(event, checked);

View File

@@ -0,0 +1,56 @@
import type { SwitchProps } from '@/ui/v2/Switch';
import Switch from '@/ui/v2/Switch';
import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
import { useController, useFormContext } from 'react-hook-form';
import type {
ControllerProps,
FieldValues,
UseControllerProps,
} from 'react-hook-form/dist/types';
import mergeRefs from 'react-merge-refs';
export interface ControlledSwitchProps<TFieldValues extends FieldValues = any>
extends SwitchProps {
/**
* Props passed to the react-hook-form controller.
*/
controllerProps?: ControllerProps;
/**
* Name of the field.
*/
name?: string;
/**
* Control for the input field.
*/
control?: UseControllerProps<TFieldValues>['control'];
}
function ControlledSwitch(
{ controllerProps, name, control, ...props }: ControlledSwitchProps,
ref: ForwardedRef<HTMLSpanElement>,
) {
const { setValue } = useFormContext();
const { field } = useController({
...controllerProps,
name: controllerProps?.name || name || '',
control: controllerProps?.control || control,
});
return (
<Switch
{...props}
{...field}
ref={mergeRefs([field.ref, ref])}
onChange={(e) => {
setValue(controllerProps?.name || name, e.target.checked, {
shouldDirty: true,
});
}}
checked={field.value || false}
{...props}
/>
);
}
export default forwardRef(ControlledSwitch);

View File

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

View File

@@ -12,7 +12,13 @@ export type DialogType =
| 'CREATE_TABLE'
| 'EDIT_TABLE'
| 'CREATE_FOREIGN_KEY'
| 'EDIT_FOREIGN_KEY';
| 'EDIT_FOREIGN_KEY'
| 'CREATE_ROLE'
| 'EDIT_ROLE'
| 'CREATE_PERMISSION_VARIABLE'
| 'EDIT_PERMISSION_VARIABLE'
| 'CREATE_ENVIRONMENT_VARIABLE'
| 'EDIT_ENVIRONMENT_VARIABLE';
export interface DialogConfig<TPayload = unknown> {
/**

View File

@@ -1,13 +1,25 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import { CreateForeignKeyForm } from '@/components/data-browser/CreateForeignKeyForm';
import { EditForeignKeyForm } from '@/components/data-browser/EditForeignKeyForm';
import CreateForeignKeyForm from '@/components/data-browser/CreateForeignKeyForm';
import EditForeignKeyForm from '@/components/data-browser/EditForeignKeyForm';
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import AlertDialog from '@/ui/v2/AlertDialog';
import { BaseDialog } from '@/ui/v2/Dialog';
import Drawer from '@/ui/v2/Drawer';
import dynamic from 'next/dynamic';
import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
import type {
BaseSyntheticEvent,
DetailedHTMLProps,
HTMLProps,
PropsWithChildren,
} from 'react';
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
import { twMerge } from 'tailwind-merge';
import type { DialogConfig, DialogType } from './DialogContext';
import DialogContext from './DialogContext';
import {
@@ -16,13 +28,21 @@ import {
drawerReducer,
} from './dialogReducers';
function LoadingComponent() {
function LoadingComponent({
className,
...props
}: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> = {}) {
return (
<div className="grid items-center justify-center px-6 py-4">
<div
{...props}
className={twMerge(
'grid items-center justify-center px-6 py-4',
className,
)}
>
<ActivityIndicator
circularProgressProps={{ className: 'w-5 h-5' }}
label="Loading form..."
delay={500}
/>
</div>
);
@@ -30,27 +50,27 @@ function LoadingComponent() {
const CreateRecordForm = dynamic(
() => import('@/components/data-browser/CreateRecordForm'),
{ ssr: false, loading: LoadingComponent },
{ ssr: false, loading: () => LoadingComponent() },
);
const CreateColumnForm = dynamic(
() => import('@/components/data-browser/CreateColumnForm'),
{ ssr: false, loading: LoadingComponent },
{ ssr: false, loading: () => LoadingComponent() },
);
const EditColumnForm = dynamic(
() => import('@/components/data-browser/EditColumnForm'),
{ ssr: false, loading: LoadingComponent },
{ ssr: false, loading: () => LoadingComponent() },
);
const CreateTableForm = dynamic(
() => import('@/components/data-browser/CreateTableForm'),
{ ssr: false, loading: LoadingComponent },
{ ssr: false, loading: () => LoadingComponent() },
);
const EditTableForm = dynamic(
() => import('@/components/data-browser/EditTableForm'),
{ ssr: false, loading: LoadingComponent },
{ ssr: false, loading: () => LoadingComponent() },
);
function DialogProvider({ children }: PropsWithChildren<unknown>) {
@@ -209,6 +229,25 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
[closeDialog, closeDrawer, onDirtyStateChange, openDialog, openDrawer],
);
const sharedDialogProps = {
...dialogPayload,
onSubmit: async (values: any) => {
await dialogPayload?.onSubmit?.(values);
closeDialog();
},
onCancel: closeDialogWithDirtyGuard,
};
const sharedDrawerProps = {
onSubmit: async () => {
await drawerPayload?.onSubmit();
closeDrawer();
},
onCancel: closeDrawerWithDirtyGuard,
};
return (
<DialogContext.Provider value={contextValue}>
<AlertDialog
@@ -249,33 +288,47 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
open={dialogOpen}
onClose={closeDialogWithDirtyGuard}
TransitionProps={{ onExited: clearDialogContent, unmountOnExit: false }}
PaperProps={{ className: 'max-w-md w-full' }}
PaperProps={{
...dialogProps?.PaperProps,
className: twMerge(
'max-w-md w-full',
dialogProps?.PaperProps?.className,
),
}}
>
<RetryableErrorBoundary
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
>
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
<CreateForeignKeyForm
{...dialogPayload}
onSubmit={async (values) => {
await dialogPayload?.onSubmit(values);
closeDialog();
}}
onCancel={closeDialogWithDirtyGuard}
/>
<CreateForeignKeyForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_FOREIGN_KEY' && (
<EditForeignKeyForm
{...dialogPayload}
onSubmit={async (values) => {
await dialogPayload?.onSubmit(values);
<EditForeignKeyForm {...sharedDialogProps} />
)}
closeDialog();
}}
onCancel={closeDialogWithDirtyGuard}
/>
{activeDialogType === 'CREATE_ROLE' && (
<CreateRoleForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_ROLE' && (
<EditRoleForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
<CreatePermissionVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_PERMISSION_VARIABLE' && (
<EditPermissionVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_ENVIRONMENT_VARIABLE' && (
<CreateEnvironmentVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
<EditEnvironmentVariableForm {...sharedDialogProps} />
)}
</RetryableErrorBoundary>
</BaseDialog>
@@ -292,61 +345,34 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
<RetryableErrorBoundary>
{activeDrawerType === 'CREATE_RECORD' && (
<CreateRecordForm
{...sharedDrawerProps}
columns={drawerPayload?.columns}
onSubmit={async () => {
await drawerPayload?.onSubmit();
closeDrawer();
}}
onCancel={closeDrawerWithDirtyGuard}
/>
)}
{activeDrawerType === 'CREATE_COLUMN' && (
<CreateColumnForm
onSubmit={async () => {
await drawerPayload?.onSubmit();
closeDrawer();
}}
onCancel={closeDrawerWithDirtyGuard}
/>
<CreateColumnForm {...sharedDrawerProps} />
)}
{activeDrawerType === 'EDIT_COLUMN' && (
<EditColumnForm
{...sharedDrawerProps}
column={drawerPayload?.column}
onSubmit={async () => {
await drawerPayload?.onSubmit();
closeDrawer();
}}
onCancel={closeDrawerWithDirtyGuard}
/>
)}
{activeDrawerType === 'CREATE_TABLE' && (
<CreateTableForm
{...sharedDrawerProps}
schema={drawerPayload?.schema}
onSubmit={async () => {
await drawerPayload?.onSubmit();
closeDrawer();
}}
onCancel={closeDrawerWithDirtyGuard}
/>
)}
{activeDrawerType === 'EDIT_TABLE' && (
<EditTableForm
{...sharedDrawerProps}
table={drawerPayload?.table}
schema={drawerPayload?.schema}
onSubmit={async () => {
await drawerPayload?.onSubmit();
closeDrawer();
}}
onCancel={closeDrawerWithDirtyGuard}
/>
)}
</RetryableErrorBoundary>

View File

@@ -109,7 +109,7 @@ function FormFooter({
}, [isDirty, onDirtyStateChange]);
return (
<div className="grid justify-between flex-shrink-0 grid-flow-col gap-3 p-2 border-gray-200 border-t-1">
<div className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 border-gray-200 p-2">
<Button
variant="borderless"
color="secondary"
@@ -139,14 +139,14 @@ export default function BaseTableForm({
return (
<Form
onSubmit={handleExternalSubmit}
className="flex flex-col content-between flex-auto overflow-hidden border-gray-200 border-t-1"
className="flex flex-auto flex-col content-between overflow-hidden border-t-1 border-gray-200"
>
<div className="flex-auto pb-4 overflow-y-auto">
<section className="grid grid-cols-8 px-6 py-3">
<div className="flex-auto overflow-y-auto pb-4">
<section className="grid grid-cols-8 py-3 px-6">
<NameInput />
</section>
<section className="grid grid-cols-8 px-6 py-3 border-gray-200 border-t-1">
<section className="grid grid-cols-8 border-t-1 border-gray-200 py-3 px-6">
<h2 className="col-span-8 mt-3 mb-1.5 text-sm+ font-bold text-greyscaleDark">
Columns
</h2>

View File

@@ -25,7 +25,7 @@ export default function ColumnEditorTable() {
return (
<>
<div role="table" className="col-span-8">
<div className="sticky top-0 z-10 grid w-full grid-cols-12 gap-1 pt-1 pb-2 bg-white">
<div className="sticky top-0 z-10 grid w-full grid-cols-12 gap-1 bg-white pt-1 pb-2">
<div role="columnheader" className="col-span-3">
<InputLabel as="span">
Name
@@ -44,13 +44,13 @@ export default function ColumnEditorTable() {
<InputLabel as="span">Default Value</InputLabel>
</div>
<div role="columnheader" className="col-span-1 text-center truncate">
<div role="columnheader" className="col-span-1 truncate text-center">
<InputLabel as="span" className="truncate">
Nullable
</InputLabel>
</div>
<div role="columnheader" className="col-span-1 text-center truncate">
<div role="columnheader" className="col-span-1 truncate text-center">
<InputLabel as="span" className="truncate">
Unique
</InputLabel>

View File

@@ -58,7 +58,7 @@ export default function CreateColumnForm({
isUnique: false,
isIdentity: false,
},
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseColumnValidationSchema),
});

View File

@@ -24,7 +24,7 @@ export interface CreateForeignKeyFormProps
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
}
export function CreateForeignKeyForm({
export default function CreateForeignKeyForm({
onSubmit,
selectedColumn,
...props
@@ -43,7 +43,7 @@ export function CreateForeignKeyForm({
updateAction: 'RESTRICT',
deleteAction: 'RESTRICT',
},
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseForeignKeyValidationSchema),
});

View File

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

View File

@@ -32,7 +32,7 @@ export default function CreateRecordForm({
return { ...defaultValues, [column.id]: null };
}, {}),
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});

View File

@@ -65,7 +65,7 @@ function DataBrowserSidebarContent({
const [optimisticlyRemovedTable, setOptimisticlyRemovedTable] =
useState<string>();
const [selectedSchema, setSelectedSchema] = useState<string>();
const [selectedSchema, setSelectedSchema] = useState<string>('');
const isSelectedSchemaLocked = isSchemaLocked(selectedSchema);
/**

View File

@@ -121,7 +121,7 @@ export default function DatabaseRecordInputGroup({
const InputLabel = (
<span className="inline-grid grid-flow-col gap-1">
<span className="inline-grid items-center grid-flow-col gap-1">
<span className="inline-grid grid-flow-col items-center gap-1">
{isPrimary && <KeyIcon className="text-base text-inherit" />}
<span>{columnId}</span>

View File

@@ -79,7 +79,7 @@ export default function EditColumnForm({
const form = useForm<BaseColumnFormValues>({
defaultValues: columnValues,
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseColumnValidationSchema),
});

View File

@@ -29,7 +29,7 @@ export interface EditForeignKeyFormProps
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
}
export function EditForeignKeyForm({
export default function EditForeignKeyForm({
foreignKeyRelation,
selectedColumn,
onSubmit,
@@ -49,7 +49,7 @@ export function EditForeignKeyForm({
updateAction: foreignKeyRelation.updateAction,
deleteAction: foreignKeyRelation.deleteAction,
},
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseForeignKeyValidationSchema),
});

View File

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

View File

@@ -92,7 +92,7 @@ export default function EditTableForm({
identityColumnIndex: null,
foreignKeyRelations: [],
},
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseTableValidationSchema),
});

View File

@@ -12,7 +12,7 @@ export function FeedbackReceived({ setFeedbackSent, close }: any) {
}
return (
<div className="grid items-center grid-flow-row gap-4 text-center">
<div className="grid grid-flow-row items-center gap-4 text-center">
<Image
src="/assets/FeedbackReceived.svg"
alt="Light bulb with a checkmark"

View File

@@ -38,13 +38,13 @@ export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
</Text>
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
<div className="grid grid-flow-col gap-2 place-content-between">
<div className="grid grid-flow-col place-content-between gap-2">
<Text className="font-medium">
What do you think we should improve?
</Text>
<Avatar
className="w-6 h-6 rounded-full"
className="h-6 w-6 rounded-full"
name={user?.displayName}
avatarUrl={user?.avatarUrl}
/>

View File

@@ -1,20 +0,0 @@
export default function ExternalLink(
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="2"
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
);
}

View File

@@ -4,19 +4,19 @@ import { twMerge } from 'tailwind-merge';
export interface ContainerProps
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
/**
* Class name passed to the wrapper element.
* Class name passed to the root element.
*/
wrapperClassName?: string;
rootClassName?: string;
}
export default function Container({
children,
className,
wrapperClassName,
rootClassName,
...props
}: ContainerProps) {
return (
<div className={twMerge('mx-auto w-full bg-white', wrapperClassName)}>
<div className={twMerge('mx-auto w-full bg-white', rootClassName)}>
<div
className={twMerge(
'mx-auto max-w-7xl bg-white px-5 pt-6 pb-20',

View File

@@ -55,7 +55,7 @@ function LogsTimePicker({
};
return (
<div className="grid items-center self-center grid-flow-row mx-auto">
<div className="mx-auto grid grid-flow-row items-center self-center">
<div className="border border-[#EAEDF0] px-4 py-2">
<Input
value={format(selectedDate, 'HH:mm:ss')}
@@ -85,7 +85,7 @@ function LogsTimePicker({
}}
/>
</div>
<div className="grid justify-end grid-flow-col px-4 py-2 gap-x-4">
<div className="grid grid-flow-col justify-end gap-x-4 px-4 py-2">
<Button variant="outlined" color="secondary" onClick={handleCancel}>
Cancel
</Button>

View File

@@ -42,7 +42,7 @@ export default function OverviewMigration() {
</Text>
</div>
<div className="flex flex-row mt-6 rounded-lg place-content-between">
<div className="mt-6 flex flex-row place-content-between rounded-lg">
<Button
variant="outlined"
color="secondary"
@@ -64,9 +64,9 @@ export default function OverviewMigration() {
<div className="grid grid-rows-3 gap-4">
{migrationSteps.map((step, index) => (
<div key={step.title} className="col-span-1">
<div className="flex flex-row gap-3 h-11">
<div className="flex h-11 flex-row gap-3">
<div className="flex items-center">
<div className="flex flex-col items-center self-center justify-center w-8 h-8 font-semibold align-middle rounded-md bg-veryLightGray">
<div className="flex h-8 w-8 flex-col items-center justify-center self-center rounded-md bg-veryLightGray align-middle font-semibold">
<span className="text-[15px] font-semibold leading-[22px] text-greyscaleGreyDark">
{index + 1}
</span>

View File

@@ -1,16 +1,19 @@
import { ApplicationMenuItems } from '@/components/applications/ApplicationMenuItems';
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { useDialog } from '@/components/common/DialogProvider';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import CogIcon from '@/ui/v2/icons/CogIcon';
import Text from '@/ui/v2/Text';
import Image from 'next/image';
import Link from 'next/link';
export default function OverviewTopBar() {
const isPlatform = useIsPlatform();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const isPro = !currentApplication?.plan?.isFree;
const { openAlertDialog } = useDialog();
@@ -92,8 +95,17 @@ export default function OverviewTopBar() {
</>
)}
</div>
<ApplicationMenuItems />
<Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/general`}
>
<Button
endIcon={<CogIcon className="h-4 w-4" />}
variant="outlined"
color="secondary"
>
Settings
</Button>
</Link>
</div>
);
}

View File

@@ -0,0 +1,167 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import {
useResetPostgresPasswordMutation,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import Button from '@/ui/v2/Button';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputAdornment from '@/ui/v2/InputAdornment';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword/generateRandomDatabasePassword';
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface ResetDatabasePasswordFormValues {
/**
* The new password to set for the database.
*/
databasePassword: string;
}
export default function ResetDatabasePasswordSettings() {
const [updateApplication] = useUpdateApplicationMutation();
const form = useForm<ResetDatabasePasswordFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
databasePassword: '',
},
mode: 'onSubmit',
criteriaMode: 'all',
shouldFocusError: true,
resolver: yupResolver(resetDatabasePasswordValidationSchema),
});
const {
setValue,
getValues,
register,
formState: { errors },
} = form;
const [resetPostgresPasswordMutation, { loading }] =
useResetPostgresPasswordMutation();
const user = useUserData();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const handleGenerateRandomPassword = () => {
const newRandomDatabasePassword = generateRandomDatabasePassword();
triggerToast('New random database password generated.');
setValue('databasePassword', newRandomDatabasePassword);
};
const handleChangeDatabasePassword = async (
values: ResetDatabasePasswordFormValues,
) => {
try {
await resetPostgresPasswordMutation({
variables: {
appID: currentApplication.id,
newPassword: values.databasePassword,
},
});
await updateApplication({
variables: {
appId: currentApplication.id,
app: {
postgresPassword: values.databasePassword,
},
},
});
form.reset(values);
triggerToast(
`The database password for ${currentApplication.name} has been updated successfully.`,
);
} catch (e) {
triggerToast(
`An error occured while trying to update the database password for ${currentApplication.name}`,
);
await discordAnnounce(
`An error occurred while trying to update the database password: ${currentApplication.name} (${user.email}): ${e.message}`,
);
}
};
return (
<FormProvider {...form}>
<Form onSubmit={handleChangeDatabasePassword}>
<SettingsContainer
title="Reset Password"
description="This password is used for accessing your database."
submitButtonText="Reset"
rootClassName="border-[#F87171]"
primaryActionButtonProps={{
variant: 'contained',
color: 'error',
disabled: Boolean(errors?.databasePassword),
loading,
}}
className="grid grid-flow-row pb-4"
>
<Input
{...register('databasePassword')}
name="databasePassword"
id="databasePassword"
autoComplete="new-password"
type="password"
error={Boolean(errors?.databasePassword)}
fullWidth
hideEmptyHelperText
componentsProps={{
input: { className: 'lg:w-1/2' },
helperText: { component: 'div' },
}}
helperText={
<div className="grid grid-flow-row items-center justify-start gap-1 pt-1">
{errors?.databasePassword?.message}
<div className="grid grid-flow-col items-center justify-start gap-1">
The root Postgres password for your database - it must be
strong and hard to guess.
<Button
onClick={handleGenerateRandomPassword}
className="px-1 py-0.5 text-xs underline underline-offset-2 hover:underline"
variant="borderless"
color="secondary"
>
Generate a password
</Button>
</div>
</div>
}
endAdornment={
<InputAdornment
position="end"
className={twMerge(
'absolute right-2',
Boolean(errors?.databasePassword) && 'invisible',
)}
>
<Button
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
onClick={() => {
copy(getValues('databasePassword'), 'Postgres password');
}}
variant="borderless"
aria-label="Copy password"
>
<CopyIcon className="h-4 w-4" />
</Button>
</InputAdornment>
}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -1,11 +1,13 @@
import ExternalLink from '@/components/icons/ExternalIcon';
import ControlledSwitch from '@/components/common/ControlledSwitch';
import type { ButtonProps } from '@/ui/v2/Button';
import Button from '@/ui/v2/Button';
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
import Link from '@/ui/v2/Link';
import type { SwitchProps } from '@/ui/v2/Switch';
import Switch from '@/ui/v2/Switch';
import Text from '@/ui/v2/Text';
import Image from 'next/image';
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
import { twMerge } from 'tailwind-merge';
export interface SettingsContainerProps
@@ -21,10 +23,14 @@ export interface SettingsContainerProps
* The title for the section.
*/
title: ReactNode | string;
/**
* Custom title for the documentation link.
*/
docsTitle?: ReactNode | string;
/**
* The description for the section.
*/
description: string | ReactNode;
description?: string | ReactNode;
/**
* Link to the documentation.
*
@@ -33,6 +39,8 @@ export interface SettingsContainerProps
docsLink?: string;
/**
* Props for the primary action.
*
* @deprecated Use `slotProps.submitButton` instead.
*/
primaryActionButtonProps?: ButtonProps;
/**
@@ -42,9 +50,52 @@ export interface SettingsContainerProps
*/
submitButtonText?: string;
/**
* Pass a form ID to the submit button.
* If passed, the switch will be rendered as a controlled component.
* The value of the switchId will be the name of the field in the form.
*/
formId?: string;
switchId?: string;
/**
* Function to be called when the switch is toggled.
*/
onEnabledChange?: (enabled: boolean) => void;
/**
* Determines whether or not the the switch is in a toggled state and children are visible.
*/
enabled?: boolean;
/**
* Determines whether or to render the switch.
* @default false
*/
showSwitch?: boolean;
/**
* Custom class names passed to the root element.
*/
rootClassName?: string;
/**
* Custom class names passed to the children wrapper element.
*/
className?: string;
/**
* Props to be passed to different slots inside the component.
*/
slotProps?: {
/**
* Props to be passed to the root element.
*/
root?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
/**
* Props to be passed to the `<Switch />` component.
*/
switch?: SwitchProps;
/**
* Props to be passed to the footer element.
*/
submitButton?: ButtonProps;
/**
* Props to be passed to the footer element.
*/
footer?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
};
}
export default function SettingsContainer({
@@ -54,64 +105,106 @@ export default function SettingsContainer({
description,
icon,
primaryActionButtonProps,
formId,
submitButtonText = 'Save',
className,
onEnabledChange,
enabled,
switchId,
showSwitch = false,
rootClassName,
docsTitle,
slotProps: { root, switch: switchSlot, submitButton, footer } = {},
}: SettingsContainerProps) {
return (
<div
{...root}
className={twMerge(
'grid grid-flow-row gap-4 rounded-lg border-1 border-gray-200 bg-white py-4',
className,
root?.className || rootClassName,
)}
>
<div className="grid grid-flow-col items-center justify-start gap-3 px-4">
{(typeof icon === 'string' && (
<div className="flex items-center self-center justify-self-center align-middle">
<Image src={icon} alt={`icon of ${title}`} width={32} height={32} />
<div className="grid grid-flow-col place-content-between gap-3 px-4">
<div className="grid grid-flow-col gap-4">
{(typeof icon === 'string' && (
<div className="flex items-center self-center justify-self-center align-middle">
<Image
src={icon}
alt={`icon of ${title}`}
width={32}
height={32}
/>
</div>
)) ||
icon}
<div className="grid grid-flow-row gap-1">
<Text className="text-lg font-semibold">{title}</Text>
{description && (
<Text className="text-greyscaleMedium">{description}</Text>
)}
</div>
)) ||
icon}
<div className="grid grid-flow-row gap-1">
<Text className="text-lg font-semibold">{title}</Text>
{description && (
<Text className="text-greyscaleMedium">{description}</Text>
)}
</div>
{!switchId && showSwitch && (
<Switch
checked={enabled}
onChange={(e) => onEnabledChange(e.target.checked)}
className="self-center"
{...switchSlot}
/>
)}
{switchId && showSwitch && (
<ControlledSwitch
className="self-center"
name={switchId}
{...switchSlot}
/>
)}
</div>
{children}
<div className={twMerge('grid grid-flow-row gap-4 px-4', className)}>
{children}
</div>
<div
{...footer}
className={twMerge(
'grid grid-flow-col items-center border-t border-gray-200 px-4 pt-3.5',
'grid grid-flow-col items-center gap-x-2 border-t border-gray-200 px-4 pt-3.5',
docsLink ? 'place-content-between' : 'justify-end',
footer?.className,
)}
>
{docsLink && (
<div className="grid w-full grid-flow-col justify-start gap-x-1 self-center align-middle">
<Text className="text-greyscaleDark">Learn more about</Text>
<Link
href={docsLink || 'https://docs.nhost.io/'}
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="grid grid-flow-col items-center justify-center gap-x-1 font-medium"
>
{title}
<ExternalLink className="h-4 w-4" />
</Link>
<Text>
Learn more about{' '}
<Link
href={docsLink || 'https://docs.nhost.io/'}
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
{docsTitle || title}
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</Text>
</div>
)}
<Button
variant="outlined"
color="secondary"
{...primaryActionButtonProps}
form={formId}
type={formId ? 'submit' : 'button'}
variant={
(submitButton || primaryActionButtonProps)?.disabled
? 'outlined'
: 'contained'
}
color={
(submitButton || primaryActionButtonProps)?.disabled
? 'secondary'
: 'primary'
}
type="submit"
{...(submitButton || primaryActionButtonProps)}
>
{submitButtonText}
</Button>

View File

@@ -1,3 +1,4 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import type { ProjectLayoutProps } from '@/components/layout/ProjectLayout';
import ProjectLayout from '@/components/layout/ProjectLayout';
import type { SettingsSidebarProps } from '@/components/settings/SettingsSidebar';
@@ -33,8 +34,8 @@ export default function SettingsLayout({
{...sidebarProps}
/>
<div className="flex w-full flex-auto flex-col overflow-x-hidden">
{children}
<div className="flex w-full flex-auto flex-col overflow-x-hidden bg-[#fafafa]">
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
</div>
</ProjectLayout>
);

View File

@@ -133,13 +133,6 @@ export default function SettingsSidebar({
>
General
</SettingsNavLink>
<SettingsNavLink
href="/sign-in-methods"
exact={false}
onClick={handleSelect}
>
Sign-In Methods
</SettingsNavLink>
{isK8SPostgresEnabledInCurrentEnvironment && !isProjectUsingRDS && (
<SettingsNavLink
href="/database"
@@ -149,6 +142,21 @@ export default function SettingsSidebar({
Database
</SettingsNavLink>
)}
<SettingsNavLink
href="/authentication"
exact={false}
onClick={handleSelect}
>
Authentication
</SettingsNavLink>
<SettingsNavLink
href="/sign-in-methods"
exact={false}
onClick={handleSelect}
>
Sign-In Methods
</SettingsNavLink>
<SettingsNavLink
href="/roles-and-permissions"
exact={false}
@@ -161,6 +169,10 @@ export default function SettingsSidebar({
SMTP
</SettingsNavLink>
<SettingsNavLink href="/git" exact={false} onClick={handleSelect}>
Git
</SettingsNavLink>
<SettingsNavLink
href="/environment-variables"
exact={false}

View File

@@ -0,0 +1,129 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface AllowedEmailSettingsFormValues {
/**
* Set of email that are allowed to be used for project's users authentication.
*/
authAccessControlAllowedEmails: string;
/**
* Set of email domains that are allowed to be used for project's users authentication.
* @example 'nhost.io'
*/
authAccessControlAllowedEmailDomains: string;
}
export default function AllowedEmailDomainsSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
id: currentApplication?.id,
},
});
const form = useForm<AllowedEmailSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
authAccessControlAllowedEmailDomains:
data?.app?.authAccessControlAllowedEmailDomains,
},
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Allowed Email Settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: AllowedEmailSettingsFormValues,
) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Allowed email settings are being updated...`,
success: `Allowed email settings have been updated successfully.`,
error: `An error occurred while trying to update the project's allowed email settings.`,
},
toastStyleProps,
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleAllowedEmailDomainsChange}>
<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,
}}
docsLink="https://docs.nhost.io/platform/authentication"
enabled={enabled}
onEnabledChange={setEnabled}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
!enabled && 'hidden',
)}
>
<Input
{...register('authAccessControlAllowedEmails')}
name="authAccessControlAllowedEmails"
id="authAccessControlAllowedEmails"
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
className="col-span-2"
label="Allowed Emails (comma separated)"
fullWidth
hideEmptyHelperText
/>
<Input
{...register('authAccessControlAllowedEmailDomains')}
name="authAccessControlAllowedEmailDomains"
id="authAccessControlAllowedEmailDomains"
label="Allowed Email Domains (comma sepated list)"
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
className="col-span-2"
fullWidth
hideEmptyHelperText
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,104 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
export interface AllowedRedirectURLFormValues {
/**
* Set of URLs that are allowed to be redirected to after project's users authentication.
*/
authAccessControlAllowedRedirectUrls: string;
}
export default function AllowedRedirectURLsSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const { data, loading, error } = useGetAppQuery({
variables: {
id: currentApplication?.id,
},
});
const form = useForm<AllowedRedirectURLFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authAccessControlAllowedRedirectUrls:
data?.app?.authAccessControlAllowedRedirectUrls,
},
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading allowed redirect URL settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState } = form;
const handleAllowedRedirectURLsChange = async (
values: AllowedRedirectURLFormValues,
) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Allowed redirect URL settings are being updated...`,
success: `Allowed redirect URL settings have been updated successfully.`,
error: `An error occurred while trying to update the project's allowed redirect URL settings.`,
},
toastStyleProps,
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleAllowedRedirectURLsChange}>
<SettingsContainer
title="Allowed Redirect URLs"
description="Allowed URLs where users can be redirected to after authentication. Separate multiple redirect URLs with comma."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
className="grid grid-flow-row px-4 lg:grid-cols-5"
>
<Input
{...register('authAccessControlAllowedRedirectUrls')}
name="authAccessControlAllowedRedirectUrls"
id="authAccessControlAllowedRedirectUrls"
placeholder="http://localhost:3000, http://localhost:4000"
className="col-span-2"
fullWidth
hideEmptyHelperText
aria-label="Allowed Redirect URLs"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

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