Compare commits

...

140 Commits

Author SHA1 Message Date
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
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
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
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
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
Chris
412b1fa8c6 add useFileUpload composable for vue 2022-10-19 21:30:29 -04:00
338 changed files with 7983 additions and 5275 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,33 @@ on:
branches: [main]
paths-ignore:
- 'docs/**'
- 'dashboard/**'
- 'examples/**'
- 'assets/**'
- '**.md'
- '!.changeset/**'
- 'LICENSE'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DASHBOARD_PACKAGE: '@nhost/dashboard'
jobs:
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: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
- name: Create PR or Publish release
id: changesets
uses: changesets/action@v1
@@ -36,3 +43,80 @@ 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:
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 }}

View File

@@ -1,19 +1,17 @@
name: 'Dashboard'
on:
push:
branches: [main]
paths:
- 'dashboard/**'
workflow_call:
pull_request:
branches: [main]
types: [opened, synchronize]
paths:
- 'packages/**'
- 'dashboard/**'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build:
name: Build
@@ -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:
@@ -21,7 +21,7 @@ on:
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: nhost
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
build:
name: Build @nhost packages
@@ -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

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,12 @@ 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).
The document generation script that is run in the pre-commit hook requires to be built first. You may need to run the following command before the commit:
```sh
pnpm run build
```
<!-- ## Good practices
- lint
- prettier

View File

@@ -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"/>
@@ -486,8 +508,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"/>
@@ -508,7 +529,8 @@ Here are some ways of contributing to making Nhost better:
<br />
<sub><b>Zach Burnaby</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/komninoschat">
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>

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: {

43
dashboard/CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
# @nhost/dashboard
## 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

50
dashboard/Dockerfile Normal file
View File

@@ -0,0 +1,50 @@
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_NHOST_PLATFORM false
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_URL http://localhost:9693
ENV NEXT_PUBLIC_NHOST_HASURA_URL http://localhost:9695
ENV NEXT_PUBLIC_ENV dev
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 --from=builder /app/dashboard/next.config.js .
COPY --from=builder /app/dashboard/package.json .
COPY --from=builder /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
CMD node dashboard/server.js

View File

@@ -49,8 +49,8 @@ NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
| `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_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. |

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.4.2",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -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.3",
"@nhost/nextjs": "^1.9.0",
"@nhost/nhost-js": "^1.6.1",
"@nhost/react": "^0.15.0",
"@nhost/react-apollo": "^4.9.0",
"@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

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

@@ -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,8 +1,8 @@
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 { generateRemoteAppUrl } from '@/utils/helpers';
@@ -66,7 +66,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

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

@@ -29,7 +29,7 @@ export default function CreatePermissionVariableModal({
const [error, setError] = useState<Error>();
const form = useForm<CreatePermissionVariableFormData>({
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
});
const {

View File

@@ -39,7 +39,7 @@ export default function EditPermissionVariableModal({
const [showRemoveModal, setShowRemoveModal] = useState(false);
const form = useForm<EditPermissionVariableFormData>({
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
defaultValues: {
key: originalCustomClaim.key || '',
value: originalCustomClaim.value || '',

View File

@@ -27,7 +27,7 @@ export function CreateUserRoleModal({ onClose }: CreateUserRoleModalProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const form = useForm<CreateUserRoleBaseFormData>({
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
});
const [updateApp] = useUpdateAppMutation({

View File

@@ -40,7 +40,7 @@ export function EditUserRoleModal({
const [showRemoveModal, setShowRemoveModal] = useState(false);
const { currentApplication } = useCurrentWorkspaceAndApplication();
const form = useForm<EditUserRoleFormData>({
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
defaultValues: {
roleName: originalRole.name || '',
},

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

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

@@ -43,7 +43,7 @@ export function CreateForeignKeyForm({
updateAction: 'RESTRICT',
deleteAction: 'RESTRICT',
},
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseForeignKeyValidationSchema),
});

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

@@ -49,7 +49,7 @@ export function EditForeignKeyForm({
updateAction: foreignKeyRelation.updateAction,
deleteAction: foreignKeyRelation.deleteAction,
},
reValidateMode: 'onBlur',
reValidateMode: 'onSubmit',
resolver: yupResolver(baseForeignKeyValidationSchema),
});

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

@@ -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,7 +1,10 @@
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';
@@ -21,6 +24,10 @@ 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.
*/
@@ -42,9 +49,35 @@ 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 the Switch component.
*/
switchProps?: SwitchProps;
}
export default function SettingsContainer({
@@ -54,64 +87,97 @@ export default function SettingsContainer({
description,
icon,
primaryActionButtonProps,
formId,
submitButtonText = 'Save',
className,
onEnabledChange,
enabled,
switchId,
showSwitch = false,
rootClassName,
switchProps,
docsTitle,
}: SettingsContainerProps) {
return (
<div
className={twMerge(
'grid grid-flow-row gap-4 rounded-lg border-1 border-gray-200 bg-white py-4',
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"
{...switchProps}
/>
)}
{switchId && showSwitch && (
<ControlledSwitch
className="self-center"
name={switchId}
{...switchProps}
/>
)}
</div>
{children}
<div className={twMerge('grid grid-flow-row gap-4 px-4', className)}>
{children}
</div>
<div
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',
)}
>
{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"
variant={
primaryActionButtonProps?.disabled ? 'outlined' : 'contained'
}
color={primaryActionButtonProps?.disabled ? 'secondary' : 'primary'}
type="submit"
{...primaryActionButtonProps}
form={formId}
type={formId ? 'submit' : 'button'}
>
{submitButtonText}
</Button>

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

View File

@@ -0,0 +1,128 @@
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 BlockedEmailFormValues {
/**
* Set of emails that are blocked from registering to the user's project.
*/
authAccessControlBlockedEmails: string;
/**
* Set of email domains that are blocked from registering to the user's project.
*/
authAccessControlBlockedEmailDomains: string;
}
export default function BlockedEmailSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
id: currentApplication?.id,
},
});
const form = useForm<BlockedEmailFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
authAccessControlBlockedEmailDomains:
data?.app?.authAccessControlBlockedEmailDomains,
},
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading blocked emails and domains..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: BlockedEmailFormValues,
) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Blocked email and domain settings are being updated...`,
success: `Blocked email and domain settings have been updated successfully.`,
error: `An error occurred while trying to update the project's blocked email and domain settings.`,
},
toastStyleProps,
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleAllowedEmailDomainsChange}>
<SettingsContainer
title="Blocked Emails and Domains"
description="Block specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
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('authAccessControlBlockedEmails')}
name="authAccessControlBlockedEmails"
id="authAccessControlBlockedEmails"
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
className="col-span-2"
label="Blocked Emails (comma separated)"
fullWidth
hideEmptyHelperText
/>
<Input
{...register('authAccessControlBlockedEmailDomains')}
name="authAccessControlBlockedEmailDomains"
id="authAccessControlBlockedEmailDomains"
label="Blocked 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 './BlockedEmailSettings';
export { default } from './BlockedEmailSettings';

View File

@@ -0,0 +1,102 @@
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 ClientURLFormValues {
/**
* The URL of the frontend app of where users are redirected after authenticating.
*/
authClientUrl: string;
}
export default function ClientURLSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['GetApp'] });
const { data, loading, error } = useGetAppQuery({
variables: {
id: currentApplication?.id,
},
fetchPolicy: 'cache-first',
});
const form = useForm<ClientURLFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authClientUrl: data?.app?.authClientUrl,
},
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading client URL settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState } = form;
const handleClientURLChange = async (values: ClientURLFormValues) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Client URL is being updated...`,
success: `Client URL has been updated successfully.`,
error: `An error occurred while trying to update the project's Client URL.`,
},
{ ...toastStyleProps },
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleClientURLChange}>
<SettingsContainer
title="Client URL"
description="This should be the URL of your frontend app where users are redirected after authenticating."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
className="grid grid-flow-row lg:grid-cols-5"
>
<Input
{...register('authClientUrl')}
name="authClientUrl"
id="authClientUrl"
placeholder="http://localhost:3000"
className="col-span-2"
fullWidth
hideEmptyHelperText
aria-label="Client URL"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,105 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
useGetAuthSettingsQuery,
useUpdateAppMutation,
} from '@/utils/__generated__/graphql';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
export interface DisableNewUsersFormValues {
/**
* Disable new users from signing up to this project
*/
authDisableNewUsers: boolean;
}
export default function DisableNewUsersSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const { data, loading, error } = useGetAuthSettingsQuery({
variables: {
id: currentApplication.id,
},
});
const form = useForm<DisableNewUsersFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authDisableNewUsers: data?.app?.authDisableNewUsers,
},
});
useEffect(() => {
form.reset(() => ({
authDisableNewUsers: data?.app?.authDisableNewUsers,
}));
}, [data?.app?.authDisableNewUsers, form, form.reset]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading disabled sign up settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, watch } = form;
const authDisableNewUsers = watch('authDisableNewUsers');
const handleDisableNewUsersChange = async (
values: DisableNewUsersFormValues,
) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Disabling new user sign ups...`,
success: `New user sign ups have been disabled successfully.`,
error: `An error occurred while trying to disable new user sign ups.`,
},
{ ...toastStyleProps },
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleDisableNewUsersChange}>
<SettingsContainer
title="Disable New Users"
description="If set, newly registered users are disabled and wont be able to sign in."
docsLink="https://docs.nhost.io/platform/authentication"
switchId="authDisableNewUsers"
showSwitch
enabled={authDisableNewUsers}
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,155 @@
import ControlledSelect from '@/components/common/ControlledSelect';
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import {
useGetGravatarSettingsQuery,
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Option from '@/ui/v2/Option';
import {
AUTH_GRAVATAR_DEFAULT,
AUTH_GRAVATAR_RATING,
toastStyleProps,
} from '@/utils/settings/settingsConstants';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface GravatarFormValues {
/**
* Gravatar image to use as default.
*/
authGravatarDefault: string;
/**
* Gravatar image rating.
*/
authGravatarRating: string;
/**
* Enable Gravatar for this project
*/
authGravatarEnabled: boolean;
}
export default function GravatarSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const { data, loading, error } = useGetGravatarSettingsQuery({
variables: {
id: currentApplication?.id,
},
});
const form = useForm<GravatarFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authGravatarDefault: data?.app?.authGravatarDefault || '',
authGravatarRating: data?.app?.authGravatarRating || '',
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
},
});
useEffect(() => {
form.reset(() => ({
authGravatarDefault: data?.app?.authGravatarDefault || '',
authGravatarRating: data?.app?.authGravatarRating || '',
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
}));
}, [data?.app, form, form.reset]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Gravatar settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState, watch } = form;
const authGravatarEnabled = watch('authGravatarEnabled');
const handleGravatarSettingsChange = async (values: GravatarFormValues) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Gravatar settings are being updated...`,
success: `Gravatar settings have been updated successfully.`,
error: `An error occurred while trying to update the project's Gravatar settings.`,
},
{ ...toastStyleProps },
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleGravatarSettingsChange}>
<SettingsContainer
title="Gravatar"
description="Use Gravatars for avatar URLs for users."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
switchId="authGravatarEnabled"
showSwitch
enabled={authGravatarEnabled}
className={twMerge(
'grid grid-flow-col grid-cols-5 grid-rows-2 gap-y-6',
!authGravatarEnabled && 'hidden',
)}
>
<ControlledSelect
{...register('authGravatarDefault')}
id="authGravatarDefault"
className="col-span-5 lg:col-span-2"
placeholder="Default Gravatar"
hideEmptyHelperText
variant="normal"
label="Default"
>
{AUTH_GRAVATAR_DEFAULT.map(({ value, label }) => (
<Option key={value} value={value}>
{label}
</Option>
))}
</ControlledSelect>
<ControlledSelect
{...register('authGravatarRating')}
id="authGravatarRating"
className="col-span-5 lg:col-span-2"
placeholder="Gravatar Rating"
hideEmptyHelperText
label="Rating"
>
{AUTH_GRAVATAR_RATING.map(({ value, label }) => (
<Option key={value} value={value}>
{label}
</Option>
))}
</ControlledSelect>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,125 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import {
useGetAuthSettingsQuery,
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 { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface MFASettingsFormValues {
/**
* One Time Password issuer
*/
authMfaTotpIssuer: string;
/**
* Enable Multi Factor Authentication for this project
*/
authMfaEnabled: boolean;
}
export default function MFASettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const { data, loading, error } = useGetAuthSettingsQuery({
variables: {
id: currentApplication?.id,
},
});
const form = useForm<MFASettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
authMfaEnabled: data?.app?.authMfaEnabled,
},
});
useEffect(() => {
form.reset(() => ({
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
authMfaEnabled: data?.app?.authMfaEnabled,
}));
}, [data?.app, form, form.reset]);
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading multi-factor authentication settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState, watch } = form;
const authMfaEnabled = watch('authMfaEnabled');
const handleMFASettingsChange = async (values: MFASettingsFormValues) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Multi-factor authentication settings are being updated...`,
success: `Multi-factor authentication settings have been updated successfully.`,
error: `An error occurred while trying to update the project's multi-factor authentication settings.`,
},
toastStyleProps,
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleMFASettingsChange}>
<SettingsContainer
title="Multi-Factor Authentication"
description="Enable users to use MFA to sign in"
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/authentication"
switchId="authMfaEnabled"
enabled={authMfaEnabled}
showSwitch
className={twMerge(
'grid grid-flow-row lg:grid-cols-5',
!authMfaEnabled && 'hidden',
)}
>
<Input
{...register('authMfaTotpIssuer')}
name="authMfaTotpIssuer"
id="authMfaTotpIssuer"
label="OTP Issuer"
placeholder="Name of the One Time Password (OTP) issuer"
className="col-span-2"
fullWidth
hideEmptyHelperText
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,125 @@
import Form from '@/components/common/Form';
import InlineCode from '@/components/common/InlineCode';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUpdateAppMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import CheckIcon from '@/ui/v2/icons/CheckIcon';
import Input from '@/ui/v2/Input';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { useApolloClient } from '@apollo/client';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
export interface BaseDirectoryFormValues {
/**
* The relative path where the `nhost` folder is located.
*/
nhostBaseFolder: string;
}
export const toastStyleProps = {
style: {
minWidth: '300px',
backgroundColor: 'rgb(33 50 75)',
color: '#fff',
},
success: {
duration: 5000,
icon: <CheckIcon className="h-4 w-4 bg-transparent" />,
},
};
export default function BaseDirectorySettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const client = useApolloClient();
const form = useForm<BaseDirectoryFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
nhostBaseFolder: currentApplication?.nhostBaseFolder,
},
});
const { register, formState, reset } = form;
useEffect(() => {
reset(() => ({
nhostBaseFolder: currentApplication?.nhostBaseFolder,
}));
}, [currentApplication?.nhostBaseFolder, reset]);
const handleBaseFolderChange = async (values: BaseDirectoryFormValues) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `The base directory is being updated...`,
success: `The base directory has been updated successfully.`,
error: `An error occurred while trying to update the project's base directory.`,
},
{ ...toastStyleProps },
);
form.reset(values);
try {
await client.refetchQueries({ include: ['getOneUser'] });
} catch (error) {
await discordAnnounce(
error.message || 'Error while trying to update application cache',
);
}
};
return (
<FormProvider {...form}>
<Form onSubmit={handleBaseFolderChange}>
<SettingsContainer
title="Base Directory"
description={
<>
The base directory is where the{' '}
<InlineCode className="text-xs">nhost</InlineCode> directory is
located. In other words, the base directory is the parent
directory of the{' '}
<InlineCode className="text-xs">nhost</InlineCode> folder.
</>
}
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/github-integration#base-directory"
className="grid grid-flow-row lg:grid-cols-5"
>
{currentApplication?.githubRepository ? (
<Input
{...register('nhostBaseFolder')}
name="nhostBaseFolder"
id="nhostBaseFolder"
className="col-span-2"
fullWidth
hideEmptyHelperText
/>
) : (
<Alert className="col-span-5 text-left">
To change the Base Folder, you first need to connect your project
to a GitHub repository.
</Alert>
)}
</SettingsContainer>
</Form>{' '}
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,108 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUpdateAppMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import Input from '@/ui/v2/Input';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useApolloClient } from '@apollo/client';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
export interface DeploymentBranchFormValues {
/**
* The git branch to deploy from.
*/
repositoryProductionBranch: string;
}
export default function DeploymentBranchSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const client = useApolloClient();
const form = useForm<DeploymentBranchFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
repositoryProductionBranch:
currentApplication?.repositoryProductionBranch,
},
});
const { register, reset, formState } = form;
useEffect(() => {
reset(() => ({
repositoryProductionBranch:
currentApplication?.repositoryProductionBranch,
}));
}, [currentApplication?.repositoryProductionBranch, reset]);
const handleDeploymentBranchChange = async (
values: DeploymentBranchFormValues,
) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `The deployment branch is being updated...`,
success: `The deployment branch has been updated successfully.`,
error: `An error occurred while trying to update the project's deployment branch.`,
},
{ ...toastStyleProps },
);
form.reset(values);
try {
await client.refetchQueries({ include: ['getOneUser'] });
} catch (error) {
await discordAnnounce(
error.message || 'Error while trying to update application cache',
);
}
};
return (
<FormProvider {...form}>
<Form onSubmit={handleDeploymentBranchChange}>
<SettingsContainer
title="Deployment Branch"
description="All commits pushed to this deployment branch will trigger a deployment. You can switch to a different branch here."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/platform/github-integration#deployment-branch"
className="grid grid-flow-row lg:grid-cols-5"
>
{currentApplication?.githubRepository ? (
<Input
{...register('repositoryProductionBranch')}
name="repositoryProductionBranch"
id="repositoryProductionBranch"
className="col-span-2"
fullWidth
hideEmptyHelperText
/>
) : (
<Alert className="col-span-5 w-full text-left">
To change the Deployment Branch, you first need to connect your
project to a GitHub repository.
</Alert>
)}
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,97 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import {
useSignInMethodsQuery,
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
export interface AnonymousSignInFormValues {
/**
* Enables users to register as an anonymous user.
*/
authAnonymousUsersEnabled: boolean;
}
export default function AnonymousSignInSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const { data, loading, error } = useSignInMethodsQuery({
variables: {
id: currentApplication.id,
},
fetchPolicy: 'cache-only',
});
const form = useForm<AnonymousSignInFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authAnonymousUsersEnabled: data.app.authAnonymousUsersEnabled,
},
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const handlePasswordProtectionSettingsChange = async (
values: AnonymousSignInFormValues,
) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Anonymous sign-in settings are being updated...`,
success: `Anonymous sign-in settings have been updated successfully.`,
error: `An error occurred while trying to update Anonymous sign-in settings.`,
},
toastStyleProps,
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handlePasswordProtectionSettingsChange}>
<SettingsContainer
title="Anonymous Users"
description="Allow users to sign-in anonymously."
primaryActionButtonProps={{
disabled:
form.formState.isSubmitting ||
!form.formState.isValid ||
!form.formState.isDirty,
}}
enabled={form.getValues('authAnonymousUsersEnabled')}
switchId="authAnonymousUsersEnabled"
showSwitch
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,204 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import {
useSignInMethodsQuery,
useUpdateAppMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import IconButton from '@/ui/v2/IconButton';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputAdornment from '@/ui/v2/InputAdornment';
import { copy } from '@/utils/copy';
import { generateRemoteAppUrl } from '@/utils/helpers';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface AppleProviderFormValues {
authAppleEnabled: boolean;
authAppleTeamId: string;
authAppleKeyId: string;
authAppleClientId: string;
authApplePrivateKey: string;
authAppleScope: string;
}
export default function AppleProviderSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const {
data: {
app: {
authAppleEnabled,
authAppleTeamId,
authAppleKeyId,
authAppleClientId,
authApplePrivateKey,
authAppleScope,
},
},
loading,
error,
} = useSignInMethodsQuery({
variables: {
id: currentApplication.id,
},
fetchPolicy: 'cache-only',
});
const form = useForm<AppleProviderFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
authAppleTeamId,
authAppleKeyId,
authAppleClientId,
authApplePrivateKey,
authAppleScope,
authAppleEnabled,
},
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Apple settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState, watch } = form;
const authEnabled = watch('authAppleEnabled');
const handleProviderUpdate = async (values: AppleProviderFormValues) => {
const updateAppMutation = updateApp({
variables: {
id: currentApplication.id,
app: {
...values,
},
},
});
await toast.promise(
updateAppMutation,
{
loading: `Apple settings are being updated...`,
success: `Apple settings have been updated successfully.`,
error: `An error occurred while trying to update the project's Apple settings.`,
},
{ ...toastStyleProps },
);
form.reset(values);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<SettingsContainer
title="Apple"
description="Allows users to sign in with Apple."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
}}
docsLink="https://docs.nhost.io/authentication/sign-in-with-apple"
docsTitle="how to sign in users with Apple"
icon="/logos/Apple.svg"
switchId="authAppleEnabled"
showSwitch
enabled={authEnabled}
className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
!authEnabled && 'hidden',
)}
>
<Input
{...register(`authAppleTeamId`)}
name="authAppleTeamId"
id="authAppleTeamId"
label="Team ID"
placeholder="Apple Team ID"
className="col-span-1"
fullWidth
hideEmptyHelperText
/>
<Input
{...register('authAppleScope')}
name="authAppleScope"
id="authAppleScope"
label="Service ID"
placeholder="Apple Service ID"
className="col-span-1"
fullWidth
hideEmptyHelperText
/>
<Input
{...register('authAppleKeyId')}
name="authAppleKeyId"
id="authAppleKeyId"
label="Key ID"
placeholder="Apple Key ID"
className="col-span-2"
fullWidth
hideEmptyHelperText
/>
<Input
{...register('authApplePrivateKey')}
multiline
rows={4}
name="authApplePrivateKey"
id="authApplePrivateKey"
label="Private Key"
placeholder="Paste Private Key here"
className="col-span-2"
fullWidth
hideEmptyHelperText
/>
<Input
name="redirectUrl"
id="redirectUrl"
defaultValue={`${generateRemoteAppUrl(
currentApplication.subdomain,
)}/v1/auth/signin/provider/apple/callback`}
className="col-span-2"
fullWidth
hideEmptyHelperText
label="Redirect URL"
disabled
endAdornment={
<InputAdornment position="end" className="absolute right-2">
<IconButton
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(
`${generateRemoteAppUrl(
currentApplication.subdomain,
)}/v1/auth/signin/provider/apple/callback`,
'Redirect URL',
);
}}
>
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,61 @@
import Input from '@/ui/v2/Input';
import { useFormContext } from 'react-hook-form';
export interface BaseProviderSettingsFormValues {
authEnabled: boolean;
authClientId: string;
authClientSecret: string;
}
/**
* Third-party auth providers e.g. Google, GitHub.
*
* @remarks
*
* These providers follow the same API structure in our database and in our GraphQL API:
* In the case of adding a new provider to this list it should contain the configuration in the example below.
*
* ```
* auth<Provider>Enabled
* auth<Provider>ClientId
* auth<Provider>ClientSecret
* ```
*
* @example
*
* ```
* authGithubEnabled
* authGithubClientId
* authGithubClientSecret
* ```
*
* @remarks If the provider has a different configuration (more or less fields) it should be added as its own component
* @see {@link 'src\components\settings\sign-in-methods\ProviderTwitterSettings\ProviderTwitterSettings.tsx'}
*
*/
export default function BaseProviderSettings() {
const { register } = useFormContext<BaseProviderSettingsFormValues>();
return (
<>
<Input
{...register(`authClientId`)}
id="authClientId"
label="Client ID"
placeholder="Enter your Client ID"
className="col-span-1"
fullWidth
hideEmptyHelperText
/>
<Input
{...register(`authClientSecret`)}
id="authClientSecret"
label="Client Secret"
placeholder="Enter your Client Secret"
className="col-span-1"
fullWidth
hideEmptyHelperText
/>
</>
);
}

View File

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

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