Compare commits
203 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b6df8b9d6 | ||
|
|
a2af5a674d | ||
|
|
c33c1fd6b9 | ||
|
|
041d9b98e3 | ||
|
|
e4b4940397 | ||
|
|
be91f4ed2a | ||
|
|
ec6ba846cf | ||
|
|
d8d8394b3b | ||
|
|
f051a121b2 | ||
|
|
6ed46ce2d4 | ||
|
|
bfb4c1a6cc | ||
|
|
776c8f9237 | ||
|
|
c0773d82e9 | ||
|
|
c46b1383f2 | ||
|
|
beed2eba21 | ||
|
|
70f9610041 | ||
|
|
e91de1088d | ||
|
|
ce1ee40dab | ||
|
|
bd7929f5ed | ||
|
|
2c8559a319 | ||
|
|
bd5ea5ee3a | ||
|
|
3538dbac39 | ||
|
|
03b5cda69a | ||
|
|
4329d04854 | ||
|
|
ca50c5ce0c | ||
|
|
a3271ed014 | ||
|
|
d4fc99a77c | ||
|
|
d90fcf3c24 | ||
|
|
ee70b226fc | ||
|
|
227ef968e6 | ||
|
|
430b37b2e1 | ||
|
|
124620c33e | ||
|
|
ce3ece1ad7 | ||
|
|
c81002622c | ||
|
|
35fa6bb043 | ||
|
|
a4469a5942 | ||
|
|
b8f11a13d7 | ||
|
|
1d1555593f | ||
|
|
001b3dccec | ||
|
|
6755dfb17b | ||
|
|
2ac90dfdec | ||
|
|
093f3906a4 | ||
|
|
6fb81a27ba | ||
|
|
9be41bf594 | ||
|
|
cbb1fc5bc8 | ||
|
|
99fcc36250 | ||
|
|
7e4a756cfe | ||
|
|
5bf61583e0 | ||
|
|
7eac17a1cb | ||
|
|
a41aeeb9ef | ||
|
|
e33df513ff | ||
|
|
323fd5cbe3 | ||
|
|
0ec3abf47c | ||
|
|
ffb3c426d3 | ||
|
|
889ee6589e | ||
|
|
b00d261916 | ||
|
|
6e05ab4628 | ||
|
|
5223ee9353 | ||
|
|
c8c5ace7cc | ||
|
|
c6a4c28579 | ||
|
|
850a049ca2 | ||
|
|
eff3f0aefd | ||
|
|
2b1338f716 | ||
|
|
2b58c60747 | ||
|
|
1b2e3fbd1d | ||
|
|
6f4fdcf73f | ||
|
|
cb529dc60c | ||
|
|
68a449dbfc | ||
|
|
7d0c6d083a | ||
|
|
1353477da1 | ||
|
|
549c7cb7eb | ||
|
|
e131c12d5d | ||
|
|
8bb097c9a7 | ||
|
|
ea31e64a71 | ||
|
|
369b931689 | ||
|
|
e1ec5c1be2 | ||
|
|
9822a160d4 | ||
|
|
7c67a2c437 | ||
|
|
8e8884f4e1 | ||
|
|
9923be41ce | ||
|
|
3141ce5b68 | ||
|
|
9c22a616a7 | ||
|
|
6bc67e95a5 | ||
|
|
0f6074c16f | ||
|
|
c96d7ccdf2 | ||
|
|
fde7ac7c1c | ||
|
|
24ef6071cc | ||
|
|
bb993b6b03 | ||
|
|
89ca34be9a | ||
|
|
b66d095c95 | ||
|
|
0bad9ff4fa | ||
|
|
9a761f4fec | ||
|
|
bd6b55868a | ||
|
|
afb3fe490e | ||
|
|
eaebd2b028 | ||
|
|
f03ecd91a9 | ||
|
|
96f17c39b1 | ||
|
|
cb7c8c6398 | ||
|
|
4bf40995b5 | ||
|
|
ab5f704280 | ||
|
|
f65e4de955 | ||
|
|
7e0e4d05aa | ||
|
|
0954a44f84 | ||
|
|
700cbd9e47 | ||
|
|
3238543b08 | ||
|
|
3f8d68ffab | ||
|
|
f7e706724c | ||
|
|
6b8acd35bd | ||
|
|
2832d7299f | ||
|
|
44c5b386c3 | ||
|
|
44ff6a059f | ||
|
|
5a91c477f0 | ||
|
|
66f73d06a8 | ||
|
|
35d52aab87 | ||
|
|
ddd41aae99 | ||
|
|
832210d8ad | ||
|
|
a09dad060c | ||
|
|
76b63debf0 | ||
|
|
e88684ff2a | ||
|
|
095d6e918c | ||
|
|
6593e8d3eb | ||
|
|
9219838127 | ||
|
|
43b68a79eb | ||
|
|
ac845c6d92 | ||
|
|
892ad66ba1 | ||
|
|
f4af81020b | ||
|
|
6999562b59 | ||
|
|
d167121093 | ||
|
|
822e251b11 | ||
|
|
328c6bb486 | ||
|
|
bef8198cbf | ||
|
|
179313d8a2 | ||
|
|
c3ce004f46 | ||
|
|
7d577a68b7 | ||
|
|
982059e18e | ||
|
|
02c0586467 | ||
|
|
0753e6529c | ||
|
|
e87a14a3fe | ||
|
|
b45aa420d9 | ||
|
|
1d76de3f60 | ||
|
|
9e37ca4cbc | ||
|
|
af57ccce0f | ||
|
|
5f44aefcc6 | ||
|
|
168616df38 | ||
|
|
96f9278c8f | ||
|
|
9fe2ecd317 | ||
|
|
ada5309b49 | ||
|
|
08698f8246 | ||
|
|
0b56e31408 | ||
|
|
d8c45b452d | ||
|
|
c4e3e3f91f | ||
|
|
483fd6c7f4 | ||
|
|
ac37d7bcae | ||
|
|
9adf91ba87 | ||
|
|
d11f6eebb0 | ||
|
|
8a678fbc87 | ||
|
|
6411ec3ec3 | ||
|
|
5187fe76aa | ||
|
|
859f457e4a | ||
|
|
dc2b5b4429 | ||
|
|
b7645e7892 | ||
|
|
b1338246aa | ||
|
|
d04ccd600e | ||
|
|
d483ad5602 | ||
|
|
bcf3e6bc2c | ||
|
|
575ff4e9b5 | ||
|
|
2010638540 | ||
|
|
0346495a79 | ||
|
|
2babb0b6f3 | ||
|
|
1f293d0f0c | ||
|
|
af4c886437 | ||
|
|
c182b3ca4b | ||
|
|
d5344ed31f | ||
|
|
adeb2a6d90 | ||
|
|
921243e4d9 | ||
|
|
1c5178f5fb | ||
|
|
72ad9aa8ee | ||
|
|
1b45db8caf | ||
|
|
9ffb4d0295 | ||
|
|
e56340b792 | ||
|
|
814c6d997a | ||
|
|
7d7a352c33 | ||
|
|
53a704fc7d | ||
|
|
c23eddf33d | ||
|
|
d4147f4713 | ||
|
|
f375eaccf5 | ||
|
|
47f79ba9f3 | ||
|
|
2e010455cf | ||
|
|
7e63c822ec | ||
|
|
276b7d48c3 | ||
|
|
6925b0d510 | ||
|
|
6ff306c4e4 | ||
|
|
aa440fefe6 | ||
|
|
9fbafc6654 | ||
|
|
b086175045 | ||
|
|
36db12297b | ||
|
|
e5885d9bad | ||
|
|
15c13f3bbe | ||
|
|
8d47cafd86 | ||
|
|
408cb6d10c | ||
|
|
4d882703f2 | ||
|
|
437dacaa9e | ||
|
|
088584e79d |
2
.github/workflows/changesets.yaml
vendored
2
.github/workflows/changesets.yaml
vendored
@@ -98,7 +98,7 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Build and push to Docker Hub
|
- name: Build and push to Docker Hub
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v4
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
89
.github/workflows/renovate.yaml
vendored
89
.github/workflows/renovate.yaml
vendored
@@ -1,89 +0,0 @@
|
|||||||
name: Renovate
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
types: [closed]
|
|
||||||
paths-ignore:
|
|
||||||
- 'assets/**'
|
|
||||||
- '**.md'
|
|
||||||
- 'LICENSE'
|
|
||||||
env:
|
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: nhost
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
renovate-changeset:
|
|
||||||
name: Add changeset
|
|
||||||
if: github.event.pull_request.merged == true && startsWith(github.head_ref, 'renovate/')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.GH_PAT }}
|
|
||||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
|
||||||
- name: Install Node and dependencies
|
|
||||||
uses: ./.github/actions/install-dependencies
|
|
||||||
with:
|
|
||||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
|
||||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
|
||||||
BUILD: 'none'
|
|
||||||
- name: Determine bumps
|
|
||||||
id: bumps
|
|
||||||
run: |
|
|
||||||
LAST_NON_PR_SHA=$(git log --no-merges main origin/${{ github.head_ref }} --format=format:%h -- | head -2 | tail -1)
|
|
||||||
echo "result<<EOF" >> $GITHUB_OUTPUT
|
|
||||||
pnpm recursive list --depth -1 --parseable \
|
|
||||||
--filter='!nhost-root' \
|
|
||||||
--filter=[$LAST_NON_PR_SHA] \
|
|
||||||
| xargs -I@ jq ".name" @/package.json \
|
|
||||||
| sort \
|
|
||||||
| uniq -u \
|
|
||||||
| awk '$0=$0": patch"' \
|
|
||||||
>> $GITHUB_OUTPUT
|
|
||||||
echo 'EOF' >> $GITHUB_OUTPUT
|
|
||||||
- name: Install dictionary
|
|
||||||
if: steps.bumps.outputs.result != ''
|
|
||||||
run: sudo apt-get install wbritish
|
|
||||||
- name: Generate changeset file name
|
|
||||||
id: file_name
|
|
||||||
if: steps.bumps.outputs.result != ''
|
|
||||||
run: |
|
|
||||||
FILE_NAME=$(shuf -n 3 /usr/share/dict/words | tr '\n' '-' | sed 's/-$//' | sed 's/'"'"'s//g' | tr '[:upper:]' '[:lower:]')
|
|
||||||
echo "result=./.changeset/${FILE_NAME}.md" >> $GITHUB_OUTPUT
|
|
||||||
- name: Create changeset file
|
|
||||||
if: steps.bumps.outputs.result != ''
|
|
||||||
run: |
|
|
||||||
cat <<EOF > ${{ steps.file_name.outputs.result }}
|
|
||||||
---
|
|
||||||
${{ steps.bumps.outputs.result }}
|
|
||||||
---
|
|
||||||
|
|
||||||
${{ github.event.pull_request.title }}
|
|
||||||
EOF
|
|
||||||
- name: Create Pull Request
|
|
||||||
id: cpr
|
|
||||||
uses: peter-evans/create-pull-request@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PAT }}
|
|
||||||
commit-message: ${{ github.event.pull_request.title }}
|
|
||||||
branch: renovate-changesets
|
|
||||||
delete-branch: true
|
|
||||||
title: 'chore: create changesest from Renovate bumps'
|
|
||||||
labels: |
|
|
||||||
dependencies
|
|
||||||
body: |
|
|
||||||
This PR creates the changesets from the Renovate dependencies that have been merged to main.
|
|
||||||
- name: Enable Pull Request Automerge
|
|
||||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
|
||||||
uses: peter-evans/enable-pull-request-automerge@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_PAT }}
|
|
||||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
|
||||||
- name: Auto approve
|
|
||||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
|
||||||
uses: juliangruber/approve-pull-request-action@v2
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GH_PAT }}
|
|
||||||
number: ${{ steps.cpr.outputs.pull-request-number }}
|
|
||||||
@@ -20,7 +20,9 @@ export default defineConfig({
|
|||||||
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
exclude: ['**/*.spec.ts', '**/*.test.ts', '**/tests/**'],
|
||||||
entryRoot: 'src',
|
entryRoot: 'src',
|
||||||
// Was defaulting to true until version 1.7
|
// Was defaulting to true until version 1.7
|
||||||
skipDiagnostics: true
|
skipDiagnostics: true,
|
||||||
|
// Was defaulting to true until version 2.0
|
||||||
|
copyDtsFiles: true
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
# General Environment Variables
|
||||||
NEXT_PUBLIC_ENV=dev
|
NEXT_PUBLIC_ENV=dev
|
||||||
NEXT_PUBLIC_NHOST_HASURA_URL=http://localhost:9695
|
|
||||||
NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
|
||||||
NEXT_PUBLIC_NHOST_BACKEND_URL=http://localhost:1337
|
|
||||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||||
|
|
||||||
|
# Environment Variables for Self Hosting and Local Development
|
||||||
|
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.nhost.run
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.nhost.run/v1/migrations
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.nhost.run
|
||||||
|
|
||||||
|
# Environment Variables when running the Nhost Dashboard against the Nhost Backend
|
||||||
NEXT_PUBLIC_STRIPE_PK=<nhost_stripe_public_key>
|
NEXT_PUBLIC_STRIPE_PK=<nhost_stripe_public_key>
|
||||||
NEXT_PUBLIC_GITHUB_APP_INSTALL_URL=<github_app_install_url>
|
NEXT_PUBLIC_GITHUB_APP_INSTALL_URL=<github_app_install_url>
|
||||||
NEXT_PUBLIC_ANALYTICS_WRITE_KEY=<analytics_write_key>
|
NEXT_PUBLIC_ANALYTICS_WRITE_KEY=<analytics_write_key>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ module.exports = {
|
|||||||
'error',
|
'error',
|
||||||
{ ignoreTypeReferences: true },
|
{ ignoreTypeReferences: true },
|
||||||
],
|
],
|
||||||
|
'no-console': ['warn', { allow: ['error'] }],
|
||||||
'no-shadow': 'off',
|
'no-shadow': 'off',
|
||||||
'@typescript-eslint/no-shadow': 'error',
|
'@typescript-eslint/no-shadow': 'error',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
|
|||||||
@@ -1,5 +1,89 @@
|
|||||||
# @nhost/dashboard
|
# @nhost/dashboard
|
||||||
|
|
||||||
|
## 0.13.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- bfb4c1a6: chore(dashboard): remove `useAxios` property
|
||||||
|
- d8d8394b: Dashboard: allow to override hasura admin secret in docker
|
||||||
|
- Updated dependencies [ce1ee40d]
|
||||||
|
- @nhost/nextjs@1.13.16
|
||||||
|
- @nhost/react-apollo@5.0.11
|
||||||
|
|
||||||
|
## 0.13.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- beed2eba: Fix docker entrypoint for dashboard
|
||||||
|
- 2c8559a3: fix(dashboard): refresh project list after deleting a project
|
||||||
|
- 4329d048: chore(dashboard): bump `graphiql` dependencies
|
||||||
|
|
||||||
|
## 0.13.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- cbb1fc5b: chore(dashboard): cleanup GraphQL operations
|
||||||
|
|
||||||
|
## 0.13.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 088584e7: feat(dashboard): add support for custom local subdomains
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2ac90dfd: fix(dashboard): improve mobile responsive layout
|
||||||
|
- Updated dependencies [f375eacc]
|
||||||
|
- @nhost/nextjs@1.13.15
|
||||||
|
- @nhost/react-apollo@5.0.10
|
||||||
|
|
||||||
|
## 0.12.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- @nhost/react-apollo@5.0.9
|
||||||
|
- @nhost/nextjs@1.13.14
|
||||||
|
|
||||||
|
## 0.12.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 2b1338f7: chore(dashboard): bump `turbo` to 1.8.3
|
||||||
|
- 5223ee93: fix(dashboard): show correct deployment status on the main page
|
||||||
|
- 850a049c: chore(deps): update docker/build-push-action action to v4
|
||||||
|
- Updated dependencies [850a049c]
|
||||||
|
- @nhost/nextjs@1.13.13
|
||||||
|
- @nhost/react-apollo@5.0.8
|
||||||
|
|
||||||
|
## 0.12.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 4bf40995: chore(deps): bump `typescript` to `4.9.5`
|
||||||
|
- 8bb097c9: chore(deps): bump `vitest`
|
||||||
|
- 35d52aab: chore(deps): replace `cross-fetch` with `isomorphic-unfetch`
|
||||||
|
- Updated dependencies [4bf40995]
|
||||||
|
- Updated dependencies [8bb097c9]
|
||||||
|
- Updated dependencies [35d52aab]
|
||||||
|
- @nhost/react-apollo@5.0.7
|
||||||
|
- @nhost/nextjs@1.13.12
|
||||||
|
|
||||||
|
## 0.12.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- c96d7ccd: fix(dashboard): fix docker builds
|
||||||
|
|
||||||
|
## 0.12.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- d1671210: feat(dashboard): use mimir to manage project configuration
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- f65e4de9: chore(deps): bump @graphql-codegen monorepo to v3
|
||||||
|
|
||||||
## 0.11.20
|
## 0.11.20
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
|
|||||||
RUN apk update
|
RUN apk update
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN yarn global add turbo@1.6.3
|
RUN yarn global add turbo@1.8.3
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ FROM node:16-alpine AS builder
|
|||||||
ARG TURBO_TOKEN
|
ARG TURBO_TOKEN
|
||||||
ARG TURBO_TEAM
|
ARG TURBO_TEAM
|
||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
RUN apk add --no-cache libc6-compat python3 make g++
|
||||||
RUN apk update
|
RUN apk update
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -19,10 +19,15 @@ ENV NEXT_TELEMETRY_DISABLED 1
|
|||||||
ENV NEXT_PUBLIC_ENV dev
|
ENV NEXT_PUBLIC_ENV dev
|
||||||
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
||||||
|
|
||||||
# placeholders for ports, will be replaced on runtime by entrypoint script
|
# placeholders for URLs, will be replaced on runtime by entrypoint script
|
||||||
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_PORT __NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__
|
ENV NEXT_PUBLIC_NHOST_ADMIN_SECRET __NEXT_PUBLIC_NHOST_ADMIN_SECRET__
|
||||||
ENV NEXT_PUBLIC_NHOST_HASURA_PORT __NEXT_PUBLIC_NHOST_HASURA_PORT__
|
ENV NEXT_PUBLIC_NHOST_AUTH_URL __NEXT_PUBLIC_NHOST_AUTH_URL__
|
||||||
ENV NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT __NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__
|
ENV NEXT_PUBLIC_NHOST_FUNCTIONS_URL __NEXT_PUBLIC_NHOST_FUNCTIONS_URL__
|
||||||
|
ENV NEXT_PUBLIC_NHOST_GRAPHQL_URL __NEXT_PUBLIC_NHOST_GRAPHQL_URL__
|
||||||
|
ENV NEXT_PUBLIC_NHOST_STORAGE_URL __NEXT_PUBLIC_NHOST_STORAGE_URL__
|
||||||
|
ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__
|
||||||
|
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
|
||||||
|
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||||
|
|
||||||
RUN yarn global add pnpm@7.17.0
|
RUN yarn global add pnpm@7.17.0
|
||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
|
|||||||
@@ -35,8 +35,17 @@ You can connect the Nhost Dashboard to your locally running backend by setting t
|
|||||||
```bash
|
```bash
|
||||||
NEXT_PUBLIC_ENV=dev
|
NEXT_PUBLIC_ENV=dev
|
||||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||||
|
NEXT_PUBLIC_NHOST_AUTH_URL=https://local.auth.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_FUNCTIONS_URL=https://local.functions.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_GRAPHQL_URL=https://local.graphql.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_STORAGE_URL=https://local.storage.nhost.run/v1
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL=https://local.hasura.nhost.run
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL=https://local.hasura.nhost.run/v1/migrations
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_API_URL=https://local.hasura.nhost.run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This will connect the Nhost Dashboard to your locally running Nhost backend.
|
||||||
|
|
||||||
### Storybook
|
### Storybook
|
||||||
|
|
||||||
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
Components are documented using [Storybook](https://storybook.js.org/). To run Storybook, run the following command:
|
||||||
@@ -45,20 +54,39 @@ Components are documented using [Storybook](https://storybook.js.org/). To run S
|
|||||||
pnpm storybook
|
pnpm storybook
|
||||||
```
|
```
|
||||||
|
|
||||||
### Full list of environment variables
|
### General Environment Variables
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. This should be set to `dev` in most cases. |
|
||||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. Setting this to `true` turns off local development. |
|
| `NEXT_PUBLIC_NHOST_ADMIN_SECRET` | Admin secret for Hasura. Default: `nhost-admin-secret` |
|
||||||
| `NEXT_PUBLIC_NHOST_LOCAL_MIGRATIONS_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `9693` |
|
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running or a self-hosted Nhost backend. Setting this to `true` will connect the Nhost Dashboard to the cloud environment. Default: `false` |
|
||||||
| `NEXT_PUBLIC_NHOST_LOCAL_HASURA_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled and `NEXT_PUBLIC_ENV` is `dev`. Default: `9695` |
|
|
||||||
| `NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `1337` |
|
### Environment Variables for Local Development and Self-Hosting
|
||||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
|
|
||||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
| Name | Description |
|
||||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
| ---- | ----------- |
|
||||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
|
||||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
| `NEXT_PUBLIC_NHOST_AUTH_URL` | The URL of the Auth service. When working locally, point it to the Auth service started by the CLI. When self-hosting, point it to the self-hosted Auth service. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_FUNCTIONS_URL` | The URL of the Functions service. When working locally, point it to the Functions service started by the CLI. When self-hosting, point it to the self-hosted Functions service. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_GRAPHQL_URL` | The URL of the GraphQL service. When working locally, point it to the GraphQL service started by the CLI. When self-hosting, point it to the self-hosted GraphQL service. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_STORAGE_URL` | The URL of the Storage service. When working locally, point it to the Storage service started by the CLI. When self-hosting, point it to the self-hosted Storage service. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL` | The URL of the Hasura Console. When working locally, point it to the Hasura Console started by the CLI. When self-hosting, point it to the self-hosted Hasura Console. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. |
|
||||||
|
|
||||||
|
### Other Environment Variables
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||||
|
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||||
|
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||||
|
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||||
|
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. This is only used if `NEXT_PUBLIC_NHOST_PLATFORM` is `true`. |
|
||||||
|
| `NEXT_PUBLIC_MAINTENANCE_ACTIVE` | Determines whether or not maintenance mode is active. |
|
||||||
|
| `NEXT_PUBLIC_MAINTENANCE_END_DATE` | Date when maintenance mode will end. |
|
||||||
|
| `NEXT_PUBLIC_MAINTENANCE_UNLOCK_SECRET` | Secret that can be used to bypass maintenance mode. |
|
||||||
|
|
||||||
## ESLint Rules
|
## ESLint Rules
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -e
|
set -euo pipefail
|
||||||
|
|
||||||
# read ports from env variables or use defaults
|
# read URLs from env variables (with defaults)
|
||||||
NEXT_PUBLIC_NHOST_MIGRATIONS_PORT="${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT:=9693}"
|
NEXT_PUBLIC_NHOST_ADMIN_SECRET="${NEXT_PUBLIC_NHOST_ADMIN_SECRET:-nhost-admin-secret}"
|
||||||
NEXT_PUBLIC_NHOST_HASURA_PORT="${NEXT_PUBLIC_NHOST_HASURA_PORT:=9695}"
|
NEXT_PUBLIC_NHOST_AUTH_URL="${NEXT_PUBLIC_NHOST_AUTH_URL:-http://localhost:1337/v1/auth}"
|
||||||
NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT="${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT:=1337}"
|
NEXT_PUBLIC_NHOST_FUNCTIONS_URL="${NEXT_PUBLIC_NHOST_FUNCTIONS_URL:-http://localhost:1337/v1/functions}"
|
||||||
|
NEXT_PUBLIC_NHOST_GRAPHQL_URL="${NEXT_PUBLIC_NHOST_GRAPHQL_URL:-http://localhost:1337/v1/graphql}"
|
||||||
|
NEXT_PUBLIC_NHOST_STORAGE_URL="${NEXT_PUBLIC_NHOST_STORAGE_URL:-http://localhost:1337/v1/storage}"
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL="${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL:-http://localhost:9695}"
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL="${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL:-http://localhost:9693}"
|
||||||
|
NEXT_PUBLIC_NHOST_HASURA_API_URL="${NEXT_PUBLIC_NHOST_HASURA_API_URL:-http://localhost:8080}"
|
||||||
|
|
||||||
# replace placeholders
|
# replace placeholders
|
||||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__/${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT}/g" {} +
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_ADMIN_SECRET__~${NEXT_PUBLIC_NHOST_ADMIN_SECRET}~g" {} +
|
||||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_HASURA_PORT__/${NEXT_PUBLIC_NHOST_HASURA_PORT}/g" {} +
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_AUTH_URL__~${NEXT_PUBLIC_NHOST_AUTH_URL}~g" {} +
|
||||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__/${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT}/g" {} +
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_FUNCTIONS_URL__~${NEXT_PUBLIC_NHOST_FUNCTIONS_URL}~g" {} +
|
||||||
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_GRAPHQL_URL__~${NEXT_PUBLIC_NHOST_GRAPHQL_URL}~g" {} +
|
||||||
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_PUBLIC_NHOST_STORAGE_URL}~g" {} +
|
||||||
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
|
||||||
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
|
||||||
|
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/dashboard",
|
"name": "@nhost/dashboard",
|
||||||
"version": "0.11.20",
|
"version": "0.13.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
"@emotion/styled": "^11.10.5",
|
"@emotion/styled": "^11.10.5",
|
||||||
"@fontsource/inter": "^4.5.14",
|
"@fontsource/inter": "^4.5.14",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
"@graphiql/react": "^0.15.0",
|
"@graphiql/react": "^0.17.0",
|
||||||
"@graphiql/toolkit": "^0.8.0",
|
"@graphiql/toolkit": "^0.8.2",
|
||||||
"@headlessui/react": "^1.6.5",
|
"@headlessui/react": "^1.6.5",
|
||||||
"@heroicons/react": "^1.0.6",
|
"@heroicons/react": "^1.0.6",
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"@nhost/nextjs": "workspace:*",
|
"@nhost/nextjs": "workspace:*",
|
||||||
"@nhost/react-apollo": "workspace:*",
|
"@nhost/react-apollo": "workspace:*",
|
||||||
"@segment/snippet": "^4.15.3",
|
"@segment/snippet": "^4.15.3",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^2.0.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tanstack/react-query": "^4.16.1",
|
"@tanstack/react-query": "^4.16.1",
|
||||||
@@ -46,10 +46,9 @@
|
|||||||
"analytics-node": "^6.2.0",
|
"analytics-node": "^6.2.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cross-fetch": "^3.1.5",
|
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"generate-password": "^1.7.0",
|
"generate-password": "^1.7.0",
|
||||||
"graphiql": "^2.2.0",
|
"graphiql": "^2.4.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-request": "^4.3.0",
|
"graphql-request": "^4.3.0",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
@@ -64,7 +63,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-hook-form": "^7.39.5",
|
"react-hook-form": "^7.42.1",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-is": "18.2.0",
|
"react-is": "18.2.0",
|
||||||
"react-loading-skeleton": "^2.2.0",
|
"react-loading-skeleton": "^2.2.0",
|
||||||
@@ -82,10 +81,10 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.2",
|
"@babel/core": "^7.20.2",
|
||||||
"@graphql-codegen/cli": "^2.8.0",
|
"@graphql-codegen/cli": "^3.0.0",
|
||||||
"@graphql-codegen/typescript": "^2.7.1",
|
"@graphql-codegen/typescript": "^3.0.0",
|
||||||
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
"@graphql-codegen/typescript-graphql-request": "^4.5.1",
|
||||||
"@graphql-codegen/typescript-operations": "^2.5.1",
|
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||||
"@next/bundle-analyzer": "^12.3.1",
|
"@next/bundle-analyzer": "^12.3.1",
|
||||||
"@storybook/addon-actions": "^6.5.14",
|
"@storybook/addon-actions": "^6.5.14",
|
||||||
@@ -112,11 +111,12 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"@typescript-eslint/parser": "^5.43.0",
|
"@typescript-eslint/parser": "^5.43.0",
|
||||||
"@vitejs/plugin-react": "^3.0.0",
|
"@vitejs/plugin-react": "^3.0.0",
|
||||||
"@vitest/coverage-c8": "^0.27.0",
|
"@vitest/coverage-c8": "^0.29.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"babel-loader": "^8.3.0",
|
"babel-loader": "^8.3.0",
|
||||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
"csstype": "^3.0.10",
|
"csstype": "^3.0.10",
|
||||||
|
"encoding": "^0.1.13",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-airbnb": "19.0.4",
|
"eslint-config-airbnb": "19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||||
@@ -130,6 +130,7 @@
|
|||||||
"lint-staged": ">=13",
|
"lint-staged": ">=13",
|
||||||
"msw": "^1.0.1",
|
"msw": "^1.0.1",
|
||||||
"msw-storybook-addon": "^1.6.3",
|
"msw-storybook-addon": "^1.6.3",
|
||||||
|
"node-fetch": "^3.3.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-organize-imports": "^3.2.0",
|
"prettier-plugin-organize-imports": "^3.2.0",
|
||||||
@@ -140,10 +141,9 @@
|
|||||||
"tailwindcss": "^3.1.2",
|
"tailwindcss": "^3.1.2",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
"typescript": "^4.8.4",
|
|
||||||
"vite": "^4.0.2",
|
"vite": "^4.0.2",
|
||||||
"vite-tsconfig-paths": "^4.0.3",
|
"vite-tsconfig-paths": "^4.0.3",
|
||||||
"vitest": "^0.27.0",
|
"vitest": "^0.29.0",
|
||||||
"webpack": "^5.75.0"
|
"webpack": "^5.75.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { useDeleteApplicationMutation } from '@/generated/graphql';
|
import {
|
||||||
|
GetOneUserDocument,
|
||||||
|
useDeleteApplicationMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||||
@@ -12,7 +15,9 @@ import { useRouter } from 'next/router';
|
|||||||
|
|
||||||
export default function ApplicationInfo() {
|
export default function ApplicationInfo() {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
const [deleteApplication, { client }] = useDeleteApplicationMutation({
|
||||||
|
refetchQueries: [GetOneUserDocument],
|
||||||
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleClickRemove() {
|
async function handleClickRemove() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import MaintenanceAlert from '@/components/common/MaintenanceAlert';
|
||||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||||
import Container from '@/components/layout/Container';
|
import Container from '@/components/layout/Container';
|
||||||
import { features } from '@/components/overview/features';
|
import { features } from '@/components/overview/features';
|
||||||
@@ -52,6 +53,7 @@ export default function ApplicationLive() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
<MaintenanceAlert />
|
||||||
<OverviewTopBar />
|
<OverviewTopBar />
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ function Plan({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="my-4 grid w-full grid-flow-col items-center justify-between px-1"
|
className="my-4 grid w-full grid-flow-col items-center justify-between gap-2 px-1"
|
||||||
onClick={setPlan}
|
onClick={setPlan}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="grid grid-flow-row gap-y-0.5">
|
<div className="grid grid-flow-row gap-y-0.5">
|
||||||
<div className="flex flex-row items-center">
|
<div className="grid grid-flow-col items-center justify-start gap-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
onChange={setPlan}
|
onChange={setPlan}
|
||||||
checked={selectedPlanId === planId}
|
checked={selectedPlanId === planId}
|
||||||
@@ -47,12 +47,13 @@ function Plan({
|
|||||||
<Text
|
<Text
|
||||||
variant="h3"
|
variant="h3"
|
||||||
component="p"
|
component="p"
|
||||||
className="ml-2 self-center font-medium"
|
className="self-center text-left font-medium"
|
||||||
>
|
>
|
||||||
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Text variant="subtitle2" className="w-64 text-start">
|
|
||||||
|
<Text variant="subtitle2" className="w-full max-w-[256px] text-start">
|
||||||
{planDescriptions[planName]}
|
{planDescriptions[planName]}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,7 +143,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="w-welcome rounded-lg p-6 text-left">
|
<Box className="w-full max-w-xl rounded-lg p-6 text-left">
|
||||||
<Modal
|
<Modal
|
||||||
showModal={paymentModal}
|
showModal={paymentModal}
|
||||||
close={closePaymentModal}
|
close={closePaymentModal}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import generateAppServiceUrl, {
|
|||||||
defaultRemoteBackendSlugs,
|
defaultRemoteBackendSlugs,
|
||||||
} from '@/utils/common/generateAppServiceUrl';
|
} from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
import { getHasuraConsoleServiceUrl } from '@/utils/env';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
interface HasuraDataProps {
|
interface HasuraDataProps {
|
||||||
@@ -22,17 +22,15 @@ interface HasuraDataProps {
|
|||||||
export function HasuraData({ close }: HasuraDataProps) {
|
export function HasuraData({ close }: HasuraDataProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
const projectAdminSecret = currentApplication?.config?.hasura.adminSecret;
|
||||||
|
|
||||||
if (
|
if (!currentApplication?.subdomain || !projectAdminSecret) {
|
||||||
!currentApplication?.subdomain ||
|
|
||||||
!currentApplication?.hasuraGraphqlAdminSecret
|
|
||||||
) {
|
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasuraUrl =
|
const hasuraUrl =
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||||
? `${LOCAL_HASURA_URL}/console`
|
? `${getHasuraConsoleServiceUrl()}`
|
||||||
: generateAppServiceUrl(
|
: generateAppServiceUrl(
|
||||||
currentApplication?.subdomain,
|
currentApplication?.subdomain,
|
||||||
currentApplication?.region.awsName,
|
currentApplication?.region.awsName,
|
||||||
@@ -71,18 +69,11 @@ export function HasuraData({ close }: HasuraDataProps) {
|
|||||||
|
|
||||||
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
||||||
<Text className="font-medium" variant="subtitle2">
|
<Text className="font-medium" variant="subtitle2">
|
||||||
{Array(currentApplication.hasuraGraphqlAdminSecret.length)
|
{Array(projectAdminSecret.length).fill('•').join('')}
|
||||||
.fill('•')
|
|
||||||
.join('')}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() =>
|
onClick={() => copy(projectAdminSecret, 'Hasura admin secret')}
|
||||||
copy(
|
|
||||||
currentApplication.hasuraGraphqlAdminSecret,
|
|
||||||
'Hasura admin secret',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="min-w-0 p-1"
|
className="min-w-0 p-1"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import DeploymentStatusMessage from '@/components/deployments/DeploymentStatusMessage';
|
||||||
import { FindOldApps } from '@/components/home';
|
import { FindOldApps } from '@/components/home';
|
||||||
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
|
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
|
||||||
import type { Application, ApplicationState } from '@/types/application';
|
import type { ApplicationState } from '@/types/application';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import { Avatar } from '@/ui/Avatar';
|
|
||||||
import StateBadge from '@/ui/StateBadge';
|
import StateBadge from '@/ui/StateBadge';
|
||||||
import type { DeploymentStatus } from '@/ui/StatusCircle';
|
import type { DeploymentStatus } from '@/ui/StatusCircle';
|
||||||
import { StatusCircle } from '@/ui/StatusCircle';
|
import { StatusCircle } from '@/ui/StatusCircle';
|
||||||
@@ -10,59 +10,11 @@ import Divider from '@/ui/v2/Divider';
|
|||||||
import Link from '@/ui/v2/Link';
|
import Link from '@/ui/v2/Link';
|
||||||
import List from '@/ui/v2/List';
|
import List from '@/ui/v2/List';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
|
||||||
import { getApplicationStatusString } from '@/utils/helpers';
|
import { getApplicationStatusString } from '@/utils/helpers';
|
||||||
import { formatDistance } from 'date-fns';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import NavLink from 'next/link';
|
import NavLink from 'next/link';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
function ApplicationCreatedAt({ createdAt }: any) {
|
|
||||||
return (
|
|
||||||
<Text component="span" className="text-sm">
|
|
||||||
created{' '}
|
|
||||||
{formatDistance(new Date(createdAt), new Date(), {
|
|
||||||
addSuffix: true,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LastSuccessfulDeployment({ deployment }: any) {
|
|
||||||
return (
|
|
||||||
<span className="flex flex-row">
|
|
||||||
<Avatar
|
|
||||||
component="span"
|
|
||||||
name={deployment.commitUserName}
|
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
|
||||||
className="mr-1 h-4 w-4 self-center"
|
|
||||||
/>
|
|
||||||
<Text component="span" className="self-center text-sm">
|
|
||||||
{deployment.commitUserName} deployed{' '}
|
|
||||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
|
||||||
addSuffix: true,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CurrentDeployment({ deployment }: any) {
|
|
||||||
return (
|
|
||||||
<span className="flex flex-row">
|
|
||||||
<Avatar
|
|
||||||
component="span"
|
|
||||||
name={deployment.commitUserName}
|
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
|
||||||
className="mr-1 h-4 w-4 self-center"
|
|
||||||
/>
|
|
||||||
<Text className="self-center text-sm">
|
|
||||||
{deployment.commitUserName} updated just now
|
|
||||||
</Text>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkStatusOfTheApplication(
|
export function checkStatusOfTheApplication(
|
||||||
stateHistory: ApplicationState[] | [],
|
stateHistory: ApplicationState[] | [],
|
||||||
) {
|
) {
|
||||||
@@ -103,7 +55,7 @@ export function RenderWorkspacesWithApps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const workspaceProjects = workspace.applications
|
const workspaceProjects = workspace.applications
|
||||||
.filter((app: Application) =>
|
.filter((app) =>
|
||||||
app.name.toLowerCase().includes(query.toLowerCase()),
|
app.name.toLowerCase().includes(query.toLowerCase()),
|
||||||
)
|
)
|
||||||
.sort((appA, appB) => {
|
.sort((appA, appB) => {
|
||||||
@@ -141,25 +93,23 @@ export function RenderWorkspacesWithApps({
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
<List className="grid grid-flow-row border-y">
|
<List className="grid grid-flow-row border-y">
|
||||||
{workspaceProjects.map((app, index) => {
|
{workspaceProjects.map((app, index) => {
|
||||||
const isDeployingToProduction = app.deployments[0]
|
const [latestDeployment] = app.deployments;
|
||||||
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
|
||||||
: false;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={app.slug}>
|
<Fragment key={app.slug}>
|
||||||
<ListItem.Root
|
<ListItem.Root
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<div className="grid grid-flow-col gap-px">
|
<div className="grid grid-flow-col gap-px">
|
||||||
{app.deployments[0] && (
|
{latestDeployment && (
|
||||||
<div className="mr-2 flex self-center align-middle">
|
<div className="mr-2 flex self-center align-middle">
|
||||||
<StatusCircle
|
<StatusCircle
|
||||||
status={
|
status={
|
||||||
app.deployments[0]
|
latestDeployment.deploymentStatus as DeploymentStatus
|
||||||
.deploymentStatus as DeploymentStatus
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<StateBadge
|
<StateBadge
|
||||||
status={checkStatusOfTheApplication(
|
status={checkStatusOfTheApplication(
|
||||||
app.appStates,
|
app.appStates,
|
||||||
@@ -190,27 +140,10 @@ export function RenderWorkspacesWithApps({
|
|||||||
<ListItem.Text
|
<ListItem.Text
|
||||||
primary={app.name}
|
primary={app.name}
|
||||||
secondary={
|
secondary={
|
||||||
<>
|
<DeploymentStatusMessage
|
||||||
{isDeployingToProduction && (
|
appCreatedAt={app.createdAt}
|
||||||
<CurrentDeployment
|
deployment={latestDeployment}
|
||||||
deployment={app.deployments[0]}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeployingToProduction &&
|
|
||||||
app.deployments[0] && (
|
|
||||||
<LastSuccessfulDeployment
|
|
||||||
deployment={app.deployments[0]}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeployingToProduction &&
|
|
||||||
!app.deployments[0] && (
|
|
||||||
<ApplicationCreatedAt
|
|
||||||
createdAt={app.createdAt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ListItem.Button>
|
</ListItem.Button>
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ export function UnlockFeatureByUpgrading({
|
|||||||
title: 'Upgrade your plan.',
|
title: 'Upgrade your plan.',
|
||||||
payload: <ChangePlanModal />,
|
payload: <ChangePlanModal />,
|
||||||
props: {
|
props: {
|
||||||
PaperProps: { className: 'p-0' },
|
PaperProps: { className: 'p-0 max-w-xl w-full' },
|
||||||
hidePrimaryAction: true,
|
hidePrimaryAction: true,
|
||||||
hideSecondaryAction: true,
|
hideSecondaryAction: true,
|
||||||
hideTitle: true,
|
hideTitle: true,
|
||||||
maxWidth: 'lg',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
@@ -18,10 +19,12 @@ export interface UserSelectProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const userApplicationClient = useRemoteApplicationGQLClient();
|
const userApplicationClient = useRemoteApplicationGQLClient();
|
||||||
const { data, loading, error } = useRemoteAppGetUsersCustomQuery({
|
const { data, loading, error } = useRemoteAppGetUsersCustomQuery({
|
||||||
client: userApplicationClient,
|
client: userApplicationClient,
|
||||||
variables: { where: {}, limit: 250, offset: 0 },
|
variables: { where: {}, limit: 250, offset: 0 },
|
||||||
|
skip: !currentApplication,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -36,8 +39,6 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { users } = data;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
{...props}
|
{...props}
|
||||||
@@ -57,7 +58,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user: RemoteAppGetUsersCustomQuery['users'][0] = users.find(
|
const user: RemoteAppGetUsersCustomQuery['users'][0] = data?.users.find(
|
||||||
({ id }) => id === userId,
|
({ id }) => id === userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
|
|||||||
>
|
>
|
||||||
<Option value="admin">Admin</Option>
|
<Option value="admin">Admin</Option>
|
||||||
|
|
||||||
{users.map(({ id, displayName, email, phoneNumber }) => (
|
{data?.users.map(({ id, displayName, email, phoneNumber }) => (
|
||||||
<Option key={id} value={id}>
|
<Option key={id} value={id}>
|
||||||
{displayName || email || phoneNumber || id}
|
{displayName || email || phoneNumber || id}
|
||||||
</Option>
|
</Option>
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import {
|
|||||||
import { loadStripe } from '@stripe/stripe-js';
|
import { loadStripe } from '@stripe/stripe-js';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PK!);
|
const stripePromise = process.env.NEXT_PUBLIC_STRIPE_PK
|
||||||
|
? loadStripe(process.env.NEXT_PUBLIC_STRIPE_PK)
|
||||||
|
: null;
|
||||||
|
|
||||||
type AddPaymentMethodFormProps = {
|
type AddPaymentMethodFormProps = {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export default function DataGridPreviewCell<TData extends object>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { presignedUrl } = await appClient.storage
|
const { presignedUrl } = await appClient.storage
|
||||||
.setAdminSecret(currentApplication.hasuraGraphqlAdminSecret)
|
.setAdminSecret(currentApplication.config?.hasura.adminSecret)
|
||||||
.getPresignedUrl({ fileId: id });
|
.getPresignedUrl({ fileId: id });
|
||||||
|
|
||||||
if (!presignedUrl) {
|
if (!presignedUrl) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { BoxProps } from '@/ui/v2/Box';
|
import type { BoxProps } from '@/ui/v2/Box';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
|
import { useRef } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
export interface FormProps extends BoxProps {
|
export interface FormProps extends BoxProps {
|
||||||
@@ -11,6 +12,7 @@ export interface FormProps extends BoxProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||||
|
const formRef = useRef<HTMLDivElement>();
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
@@ -25,6 +27,15 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitButton = Array.from(
|
||||||
|
formRef.current.getElementsByTagName('button'),
|
||||||
|
).find((item) => item.type === 'submit');
|
||||||
|
|
||||||
|
// Disabling submit if the submit button is disabled
|
||||||
|
if (submitButton?.disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
handleSubmit(onSubmit)(event);
|
handleSubmit(onSubmit)(event);
|
||||||
@@ -35,6 +46,7 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
|||||||
// so keyboard events must be handled on the form element itself.
|
// so keyboard events must be handled on the form element itself.
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||||
<Box
|
<Box
|
||||||
|
ref={formRef}
|
||||||
component="form"
|
component="form"
|
||||||
{...props}
|
{...props}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export default function Header({ className, ...props }: HeaderProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
component="header"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'z-40 grid w-full transform-gpu grid-flow-col items-center justify-between gap-2 border-b-1 px-4 py-3',
|
'z-40 grid w-full transform-gpu grid-flow-col items-center justify-between gap-2 border-b-1 px-4 py-3',
|
||||||
className,
|
className,
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ function IconLink(
|
|||||||
: [icon.props?.sx]),
|
: [icon.props?.sx]),
|
||||||
{
|
{
|
||||||
color: (theme) => {
|
color: (theme) => {
|
||||||
|
if (props.disabled) {
|
||||||
|
return theme.palette.mode === 'dark'
|
||||||
|
? 'text.secondary'
|
||||||
|
: 'text.primary';
|
||||||
|
}
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
return 'primary.main';
|
return 'primary.main';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import { Alert } from '@/ui/Alert';
|
||||||
|
|
||||||
|
export default function MaintenanceAlert() {
|
||||||
|
const { maintenanceActive, maintenanceEndDate } = useUI();
|
||||||
|
|
||||||
|
if (!maintenanceActive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateTimeFormat = Intl.DateTimeFormat(undefined, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
timeZoneName: 'short',
|
||||||
|
});
|
||||||
|
|
||||||
|
const parts = dateTimeFormat.formatToParts(maintenanceEndDate);
|
||||||
|
|
||||||
|
const year = parts.find((part) => part.type === 'year')?.value;
|
||||||
|
const month = parts.find((part) => part.type === 'month')?.value;
|
||||||
|
const day = parts.find((part) => part.type === 'day')?.value;
|
||||||
|
const hour = parts.find((part) => part.type === 'hour')?.value;
|
||||||
|
const minute = parts.find((part) => part.type === 'minute')?.value;
|
||||||
|
const timeZone = parts.find((part) => part.type === 'timeZoneName')?.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert severity="warning" className="mt-4">
|
||||||
|
<p>
|
||||||
|
We're currently doing maintenance on our infrastructure. Project
|
||||||
|
creation and project settings are temporarily disabled during the
|
||||||
|
maintenance period.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{maintenanceEndDate && (
|
||||||
|
<p>
|
||||||
|
Maintenance is expected to be completed at {year}-{month}-{day} {hour}
|
||||||
|
:{minute} {timeZone}.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './MaintenanceAlert';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
import permissionVariablesQuery from '@/utils/msw/mocks/graphql/permissionVariablesQuery';
|
||||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||||
import { render, screen } from '@/utils/testUtils';
|
import { render, screen } from '@/utils/testUtils';
|
||||||
@@ -6,7 +6,11 @@ import { setupServer } from 'msw/node';
|
|||||||
import { test, vi } from 'vitest';
|
import { test, vi } from 'vitest';
|
||||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||||
|
|
||||||
const server = setupServer(tableQuery, hasuraMetadataQuery, customClaimsQuery);
|
const server = setupServer(
|
||||||
|
tableQuery,
|
||||||
|
hasuraMetadataQuery,
|
||||||
|
permissionVariablesQuery,
|
||||||
|
);
|
||||||
|
|
||||||
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
|
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
|
||||||
afterEach(() => server.resetHandlers());
|
afterEach(() => server.resetHandlers());
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ export default function DataBrowserSidebar({
|
|||||||
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
|
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isPlatform && !currentApplication?.hasuraGraphqlAdminSecret) {
|
if (isPlatform && !currentApplication?.config?.hasura.adminSecret) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import Button from '@/ui/v2/Button';
|
|||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import convertToHasuraPermissions from '@/utils/dataBrowser/convertToHasuraPermissions';
|
import convertToHasuraPermissions from '@/utils/dataBrowser/convertToHasuraPermissions';
|
||||||
import convertToRuleGroup from '@/utils/dataBrowser/convertToRuleGroup';
|
import convertToRuleGroup from '@/utils/dataBrowser/convertToRuleGroup';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
@@ -258,7 +259,7 @@ export default function RolePermissionEditorForm({
|
|||||||
{
|
{
|
||||||
loading: 'Saving permission...',
|
loading: 'Saving permission...',
|
||||||
success: 'Permission has been saved successfully.',
|
success: 'Permission has been saved successfully.',
|
||||||
error: 'An error occurred while saving the permission.',
|
error: getServerError('An error occurred while saving the permission.'),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -297,7 +298,9 @@ export default function RolePermissionEditorForm({
|
|||||||
{
|
{
|
||||||
loading: 'Deleting permission...',
|
loading: 'Deleting permission...',
|
||||||
success: 'Permission has been deleted successfully.',
|
success: 'Permission has been deleted successfully.',
|
||||||
error: 'An error occurred while deleting the permission.',
|
error: getServerError(
|
||||||
|
'An error occurred while deleting the permission.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import XIcon from '@/ui/v2/icons/XIcon';
|
|||||||
import InputLabel from '@/ui/v2/InputLabel';
|
import InputLabel from '@/ui/v2/InputLabel';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
import { useGetRolesPermissionsQuery } from '@/utils/__generated__/graphql';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||||
@@ -51,8 +51,8 @@ export default function ColumnPresetsSection({
|
|||||||
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
|
||||||
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data: customClaimsData } = useGetAppCustomClaimsQuery({
|
const { data: permissionVariablesData } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
skip: !currentApplication?.id,
|
skip: !currentApplication?.id,
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
@@ -74,8 +74,8 @@ export default function ColumnPresetsSection({
|
|||||||
throw tableError;
|
throw tableError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionVariableOptions = getPermissionVariablesArray(
|
const permissionVariableOptions = getAllPermissionVariables(
|
||||||
customClaimsData?.app?.authJwtCustomClaims,
|
permissionVariablesData?.config?.auth?.session?.accessToken?.customClaims,
|
||||||
).map(({ key }) => ({
|
).map(({ key }) => ({
|
||||||
label: `X-Hasura-${key}`,
|
label: `X-Hasura-${key}`,
|
||||||
value: `X-Hasura-${key}`,
|
value: `X-Hasura-${key}`,
|
||||||
@@ -136,7 +136,7 @@ export default function ColumnPresetsSection({
|
|||||||
disableClearable={false}
|
disableClearable={false}
|
||||||
clearIcon={
|
clearIcon={
|
||||||
<XIcon
|
<XIcon
|
||||||
className="w-4 h-4 mt-px"
|
className="mt-px h-4 w-4"
|
||||||
sx={{ color: theme.palette.text.primary }}
|
sx={{ color: theme.palette.text.primary }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@ export default function ColumnPresetsSection({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="shrink-0 grow-0 flex-[40px]"
|
className="flex-[40px] shrink-0 grow-0"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (fields.length === 1) {
|
if (fields.length === 1) {
|
||||||
remove(index);
|
remove(index);
|
||||||
@@ -199,7 +199,7 @@ export default function ColumnPresetsSection({
|
|||||||
remove(index);
|
remove(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XIcon className="w-4 h-4" />
|
<XIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Form from '@/components/common/Form';
|
|||||||
import type { RuleGroup } from '@/types/dataBrowser';
|
import type { RuleGroup } from '@/types/dataBrowser';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import customClaimsQuery from '@/utils/msw/mocks/graphql/customClaimsQuery';
|
import permissionVariablesQuery from '@/utils/msw/mocks/graphql/permissionVariablesQuery';
|
||||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
@@ -36,7 +36,7 @@ const defaultParameters = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
msw: {
|
msw: {
|
||||||
handlers: [tableQuery, hasuraMetadataQuery, customClaimsQuery],
|
handlers: [tableQuery, hasuraMetadataQuery, permissionVariablesQuery],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
|||||||
import type { InputProps } from '@/ui/v2/Input';
|
import type { InputProps } from '@/ui/v2/Input';
|
||||||
import { inputClasses } from '@/ui/v2/Input';
|
import { inputClasses } from '@/ui/v2/Input';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
|
import { useGetRolesPermissionsQuery } from '@/utils/__generated__/graphql';
|
||||||
import { useController, useFormContext, useWatch } from 'react-hook-form';
|
import { useController, useFormContext, useWatch } from 'react-hook-form';
|
||||||
import useRuleGroupEditor from './useRuleGroupEditor';
|
import useRuleGroupEditor from './useRuleGroupEditor';
|
||||||
|
|
||||||
@@ -117,8 +117,8 @@ export default function RuleValueInput({
|
|||||||
data,
|
data,
|
||||||
loading,
|
loading,
|
||||||
error: customClaimsError,
|
error: customClaimsError,
|
||||||
} = useGetAppCustomClaimsQuery({
|
} = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
skip: !isHasuraInput || !currentApplication?.id,
|
skip: !isHasuraInput || !currentApplication?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -200,8 +200,8 @@ export default function RuleValueInput({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableHasuraPermissionVariables = getPermissionVariablesArray(
|
const availableHasuraPermissionVariables = getAllPermissionVariables(
|
||||||
data?.app?.authJwtCustomClaims,
|
data?.config?.auth?.session?.accessToken?.customClaims,
|
||||||
).map(({ key }) => ({
|
).map(({ key }) => ({
|
||||||
value: `X-Hasura-${key}`,
|
value: `X-Hasura-${key}`,
|
||||||
label: `X-Hasura-${key}`,
|
label: `X-Hasura-${key}`,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
|
|||||||
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Tooltip from '@/ui/v2/Tooltip';
|
import Tooltip from '@/ui/v2/Tooltip';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
||||||
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
|
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
|
||||||
@@ -58,12 +59,12 @@ export default function DeploymentListItem({
|
|||||||
return (
|
return (
|
||||||
<ListItem.Root>
|
<ListItem.Root>
|
||||||
<ListItem.Button
|
<ListItem.Button
|
||||||
className="grid grid-flow-col items-center justify-between gap-2 rounded-none px-2 py-2"
|
className="grid grid-flow-col items-center justify-between gap-2 rounded-none p-2"
|
||||||
component={NavLink}
|
component={NavLink}
|
||||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
|
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
|
||||||
aria-label={commitMessage || 'No commit message'}
|
aria-label={commitMessage || 'No commit message'}
|
||||||
>
|
>
|
||||||
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
|
<div className="grid grid-flow-col items-center justify-center gap-2 self-center">
|
||||||
<ListItem.Avatar>
|
<ListItem.Avatar>
|
||||||
<Avatar
|
<Avatar
|
||||||
name={deployment.commitUserName}
|
name={deployment.commitUserName}
|
||||||
@@ -84,7 +85,7 @@ export default function DeploymentListItem({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-flow-col items-center gap-2">
|
<div className="grid grid-flow-col items-center justify-end gap-2">
|
||||||
{showRedeploy && (
|
{showRedeploy && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
@@ -122,7 +123,9 @@ export default function DeploymentListItem({
|
|||||||
{
|
{
|
||||||
loading: 'Scheduling deployment...',
|
loading: 'Scheduling deployment...',
|
||||||
success: 'Deployment has been scheduled successfully.',
|
success: 'Deployment has been scheduled successfully.',
|
||||||
error: 'An error occurred when scheduling deployment.',
|
error: getServerError(
|
||||||
|
'An error occurred when scheduling deployment.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -139,16 +142,16 @@ export default function DeploymentListItem({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isLive && (
|
{isLive && (
|
||||||
<div className="flex w-12 justify-end">
|
<div className="hidden w-12 justify-end sm:flex">
|
||||||
<Chip size="small" color="success" label="Live" />
|
<Chip size="small" color="success" label="Live" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="w-16 text-right font-mono text-sm- font-medium">
|
<div className="hidden w-16 text-right font-mono text-sm- font-medium sm:block">
|
||||||
{deployment.commitSHA.substring(0, 7)}
|
{deployment.commitSHA.substring(0, 7)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-[80px] text-right font-mono text-sm- font-medium">
|
<div className="text-right font-mono text-sm- font-medium sm:w-20">
|
||||||
<AppDeploymentDuration
|
<AppDeploymentDuration
|
||||||
startedAt={deployment.deploymentStartedAt}
|
startedAt={deployment.deploymentStartedAt}
|
||||||
endedAt={deployment.deploymentEndedAt}
|
endedAt={deployment.deploymentEndedAt}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import type { Deployment } from '@/types/application';
|
||||||
|
import { render, screen } from '@/utils/testUtils';
|
||||||
|
import { test, vi } from 'vitest';
|
||||||
|
import DeploymentStatusMessage from './DeploymentStatusMessage';
|
||||||
|
|
||||||
|
const defaultDeployment: Deployment = {
|
||||||
|
id: 'de305d54-75b4-431b-adb2-eb6b9e546013',
|
||||||
|
commitUserName: 'john.doe',
|
||||||
|
commitUserAvatarUrl: 'https://example.com/avatar.png',
|
||||||
|
deploymentStartedAt: '2023-02-24T12:00:00.000Z',
|
||||||
|
deploymentEndedAt: null,
|
||||||
|
deploymentStatus: 'SCHEDULED',
|
||||||
|
commitSHA: '1234567890',
|
||||||
|
commitMessage: 'Update README.md',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render the avatar of the user who deployed the application', () => {
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={defaultDeployment}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('img', {
|
||||||
|
name: `Avatar of ${defaultDeployment.commitUserName}`,
|
||||||
|
}),
|
||||||
|
).toHaveAttribute(
|
||||||
|
'style',
|
||||||
|
`background-image: url(${defaultDeployment.commitUserAvatarUrl});`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "updated just now" when the deployment is in progress and has not ended', () => {
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={defaultDeployment}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/updated just now/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "updated just now" when the deployment\'s status is DEPLOYED, but it doesn\'t have an end date for some reason', () => {
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={{
|
||||||
|
...defaultDeployment,
|
||||||
|
deploymentStatus: 'DEPLOYED',
|
||||||
|
deploymentEndedAt: null,
|
||||||
|
}}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/updated just now/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "deployed 1 day ago" when the deployment has ended', () => {
|
||||||
|
vi.setSystemTime(new Date('2023-02-25T12:25:00.000Z'));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage
|
||||||
|
deployment={{
|
||||||
|
...defaultDeployment,
|
||||||
|
deploymentStatus: 'DEPLOYED',
|
||||||
|
deploymentEndedAt: '2023-02-24T12:15:00.000Z',
|
||||||
|
}}
|
||||||
|
appCreatedAt="2023-02-24"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/deployed 1 day ago/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render "created 1 day ago" if the application does not have a deployment', () => {
|
||||||
|
vi.setSystemTime(new Date('2023-02-25T12:25:00.000Z'));
|
||||||
|
|
||||||
|
render(
|
||||||
|
<DeploymentStatusMessage deployment={null} appCreatedAt="2023-02-24" />,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/created 1 day ago/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import type { Deployment } from '@/types/application';
|
||||||
|
import { Avatar } from '@/ui/Avatar';
|
||||||
|
import Text from '@/ui/v2/Text';
|
||||||
|
import formatDistance from 'date-fns/formatDistance';
|
||||||
|
|
||||||
|
export interface DeploymentStatusMessageProps {
|
||||||
|
/**
|
||||||
|
* The deployment to render the status message for.
|
||||||
|
*/
|
||||||
|
deployment: Partial<Deployment>;
|
||||||
|
/**
|
||||||
|
* The date the application was created.
|
||||||
|
*/
|
||||||
|
appCreatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeploymentStatusMessage({
|
||||||
|
deployment,
|
||||||
|
appCreatedAt,
|
||||||
|
}: DeploymentStatusMessageProps) {
|
||||||
|
const isDeployingToProduction = [
|
||||||
|
'SCHEDULED',
|
||||||
|
'PENDING',
|
||||||
|
'DEPLOYING',
|
||||||
|
].includes(deployment?.deploymentStatus);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isDeployingToProduction ||
|
||||||
|
(deployment && !deployment.deploymentEndedAt)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<span className="flex flex-row">
|
||||||
|
<Avatar
|
||||||
|
component="span"
|
||||||
|
name={deployment.commitUserName}
|
||||||
|
avatarUrl={deployment.commitUserAvatarUrl}
|
||||||
|
className="mr-1 h-4 w-4 self-center"
|
||||||
|
/>
|
||||||
|
<Text component="span" className="self-center text-sm">
|
||||||
|
{deployment.commitUserName} updated just now
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDeployingToProduction && deployment?.deploymentEndedAt) {
|
||||||
|
return (
|
||||||
|
<span className="grid grid-flow-col">
|
||||||
|
<Avatar
|
||||||
|
component="span"
|
||||||
|
name={deployment.commitUserName}
|
||||||
|
avatarUrl={deployment.commitUserAvatarUrl}
|
||||||
|
className="mr-1 h-4 w-4 self-center"
|
||||||
|
/>
|
||||||
|
<Text component="span" className="self-center truncate text-sm">
|
||||||
|
{deployment.commitUserName} deployed{' '}
|
||||||
|
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text component="span" className="text-sm">
|
||||||
|
created{' '}
|
||||||
|
{formatDistance(new Date(appCreatedAt), new Date(), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './DeploymentStatusMessage';
|
||||||
|
export { default } from './DeploymentStatusMessage';
|
||||||
@@ -12,6 +12,7 @@ import useBuckets from '@/hooks/useBuckets';
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import useFiles from '@/hooks/useFiles';
|
import useFiles from '@/hooks/useFiles';
|
||||||
import useFilesAggregate from '@/hooks/useFilesAggregate';
|
import useFilesAggregate from '@/hooks/useFilesAggregate';
|
||||||
|
import { getHasuraAdminSecret } from '@/utils/env';
|
||||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||||
import type { Files } from '@/utils/__generated__/graphql';
|
import type { Files } from '@/utils/__generated__/graphql';
|
||||||
import { Order_By as OrderBy } from '@/utils/__generated__/graphql';
|
import { Order_By as OrderBy } from '@/utils/__generated__/graphql';
|
||||||
@@ -261,8 +262,8 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
|||||||
const { fileMetadata, error: fileError } = await appClient.storage
|
const { fileMetadata, error: fileError } = await appClient.storage
|
||||||
.setAdminSecret(
|
.setAdminSecret(
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||||
? 'nhost-admin-secret'
|
? getHasuraAdminSecret()
|
||||||
: currentApplication.hasuraGraphqlAdminSecret,
|
: currentApplication.config?.hasura.adminSecret,
|
||||||
)
|
)
|
||||||
.upload({
|
.upload({
|
||||||
file,
|
file,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Button from '@/ui/v2/Button';
|
|||||||
import Chip from '@/ui/v2/Chip';
|
import Chip from '@/ui/v2/Chip';
|
||||||
import type { InputProps } from '@/ui/v2/Input';
|
import type { InputProps } from '@/ui/v2/Input';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { getHasuraAdminSecret } from '@/utils/env';
|
||||||
import { triggerToast } from '@/utils/toast';
|
import { triggerToast } from '@/utils/toast';
|
||||||
import type { Files } from '@/utils/__generated__/graphql';
|
import type { Files } from '@/utils/__generated__/graphql';
|
||||||
import type { PropsWithoutRef } from 'react';
|
import type { PropsWithoutRef } from 'react';
|
||||||
@@ -71,8 +72,8 @@ export default function FilesDataGridControls({
|
|||||||
try {
|
try {
|
||||||
const storageWithAdminSecret = appClient.storage.setAdminSecret(
|
const storageWithAdminSecret = appClient.storage.setAdminSecret(
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||||
? 'nhost-admin-secret'
|
? getHasuraAdminSecret()
|
||||||
: currentApplication.hasuraGraphqlAdminSecret,
|
: currentApplication.config?.hasura.adminSecret,
|
||||||
);
|
);
|
||||||
|
|
||||||
// note: this is not an optimal solution, but we don't have a better way
|
// note: this is not an optimal solution, but we don't have a better way
|
||||||
@@ -120,7 +121,7 @@ export default function FilesDataGridControls({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{numberOfSelectedFiles > 0 ? (
|
{numberOfSelectedFiles > 0 ? (
|
||||||
<div className="mx-auto h-[40px] grid grid-flow-col justify-start items-center gap-2">
|
<div className="mx-auto grid h-[40px] grid-flow-col items-center justify-start gap-2">
|
||||||
<Chip
|
<Chip
|
||||||
color="info"
|
color="info"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { DialogFormProps } from '@/types/common';
|
|||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { slugifyString } from '@/utils/helpers';
|
import { slugifyString } from '@/utils/helpers';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
refetchGetOneUserQuery,
|
refetchGetOneUserQuery,
|
||||||
@@ -141,7 +142,9 @@ export default function EditWorkspaceNameForm({
|
|||||||
{
|
{
|
||||||
loading: 'Updating workspace name...',
|
loading: 'Updating workspace name...',
|
||||||
success: 'Workspace name has been updated successfully.',
|
success: 'Workspace name has been updated successfully.',
|
||||||
error: 'An error occurred while updating the workspace name.',
|
error: getServerError(
|
||||||
|
'An error occurred while updating the workspace name.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -168,7 +171,9 @@ export default function EditWorkspaceNameForm({
|
|||||||
{
|
{
|
||||||
loading: 'Creating new workspace...',
|
loading: 'Creating new workspace...',
|
||||||
success: 'The new workspace has been created successfully.',
|
success: 'The new workspace has been created successfully.',
|
||||||
error: 'An error occurred while creating the new workspace.',
|
error: getServerError(
|
||||||
|
'An error occurred while creating the new workspace.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
|
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
|
||||||
import SearchIcon from '@/ui/v2/icons/SearchIcon';
|
import SearchIcon from '@/ui/v2/icons/SearchIcon';
|
||||||
@@ -11,6 +12,8 @@ interface IndexHeaderAppsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
|
export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto mb-6 grid w-full grid-flow-col place-content-between items-center py-2">
|
<div className="mx-auto mb-6 grid w-full grid-flow-col place-content-between items-center py-2">
|
||||||
<Text variant="h2" component="h1" className="hidden md:block">
|
<Text variant="h2" component="h1" className="hidden md:block">
|
||||||
@@ -36,6 +39,7 @@ export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
startIcon={<PlusCircleIcon />}
|
startIcon={<PlusCircleIcon />}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
New Project
|
New Project
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export function InviteAnnounce() {
|
|||||||
workspaceMemberInviteId: inviteId,
|
workspaceMemberInviteId: inviteId,
|
||||||
isAccepted: false,
|
isAccepted: false,
|
||||||
},
|
},
|
||||||
{ useAxios: false },
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ignoreError) {
|
if (ignoreError) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { UserDataProvider } from '@/context/workspace1-context';
|
import { UserDataProvider } from '@/context/workspace1-context';
|
||||||
import type { Application } from '@/types/application';
|
import type { Project } from '@/types/application';
|
||||||
import { ApplicationStatus } from '@/types/application';
|
import { ApplicationStatus } from '@/types/application';
|
||||||
import type { Workspace } from '@/types/workspace';
|
import type { Workspace } from '@/types/workspace';
|
||||||
import { render, screen, waitForElementToBeRemoved } from '@/utils/testUtils';
|
import { render, screen, waitForElementToBeRemoved } from '@/utils/testUtils';
|
||||||
@@ -36,12 +36,11 @@ vi.mock('next/router', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mockApplication: Application = {
|
const mockApplication: Project = {
|
||||||
id: '1',
|
id: '1',
|
||||||
name: 'Test Application',
|
name: 'Test Application',
|
||||||
slug: 'test-application',
|
slug: 'test-application',
|
||||||
appStates: [],
|
appStates: [],
|
||||||
hasuraGraphqlAdminSecret: 'nhost-admin-secret',
|
|
||||||
subdomain: '',
|
subdomain: '',
|
||||||
isProvisioned: true,
|
isProvisioned: true,
|
||||||
region: {
|
region: {
|
||||||
@@ -56,6 +55,14 @@ const mockApplication: Application = {
|
|||||||
featureFlags: [],
|
featureFlags: [],
|
||||||
providersUpdated: true,
|
providersUpdated: true,
|
||||||
githubRepository: { fullName: 'test/git-project' },
|
githubRepository: { fullName: 'test/git-project' },
|
||||||
|
repositoryProductionBranch: null,
|
||||||
|
nhostBaseFolder: null,
|
||||||
|
plan: null,
|
||||||
|
config: {
|
||||||
|
hasura: {
|
||||||
|
adminSecret: 'nhost-admin-secret',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockWorkspace: Workspace = {
|
const mockWorkspace: Workspace = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import useGitHubModal from '@/components/applications/github/useGitHubModal';
|
import useGitHubModal from '@/components/applications/github/useGitHubModal';
|
||||||
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
|
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
|
||||||
import GithubIcon from '@/components/icons/GithubIcon';
|
import GithubIcon from '@/components/icons/GithubIcon';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
@@ -75,7 +76,7 @@ function OverviewDeploymentList() {
|
|||||||
|
|
||||||
if (!deployments?.length) {
|
if (!deployments?.length) {
|
||||||
return (
|
return (
|
||||||
<Box className="grid grid-flow-row items-center justify-items-center gap-5 overflow-hidden rounded-lg border-1 py-12 px-48 shadow-sm">
|
<Box className="grid grid-flow-row items-center justify-items-center gap-5 overflow-hidden rounded-lg border-1 py-12 px-4 shadow-sm">
|
||||||
<RocketIcon
|
<RocketIcon
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
className="h-10 w-10"
|
className="h-10 w-10"
|
||||||
@@ -85,7 +86,7 @@ function OverviewDeploymentList() {
|
|||||||
<Text className="text-center font-medium" variant="h3">
|
<Text className="text-center font-medium" variant="h3">
|
||||||
No Deployments
|
No Deployments
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="subtitle1" className="text-center">
|
<Text variant="subtitle1" className="max-w-md text-center">
|
||||||
We'll deploy changes automatically when you push to the
|
We'll deploy changes automatically when you push to the
|
||||||
deployment branch in your connected GitHub repository
|
deployment branch in your connected GitHub repository
|
||||||
</Text>
|
</Text>
|
||||||
@@ -146,6 +147,7 @@ function OverviewDeploymentList() {
|
|||||||
export default function OverviewDeployments() {
|
export default function OverviewDeployments() {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { openGitHubModal } = useGitHubModal();
|
const { openGitHubModal } = useGitHubModal();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
const { githubRepository } = currentApplication || {};
|
const { githubRepository } = currentApplication || {};
|
||||||
|
|
||||||
@@ -164,14 +166,14 @@ export default function OverviewDeployments() {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<OverviewDeploymentsTopBar />
|
<OverviewDeploymentsTopBar />
|
||||||
|
|
||||||
<Box className="grid grid-flow-row items-center justify-items-center gap-5 rounded-lg border-1 py-12 px-48 shadow-sm">
|
<Box className="grid grid-flow-row items-center justify-items-center gap-5 rounded-lg border-1 py-12 px-4 shadow-sm">
|
||||||
<RocketIcon strokeWidth={1} className="h-10 w-10" />
|
<RocketIcon strokeWidth={1} className="h-10 w-10" />
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-1">
|
<div className="grid grid-flow-row gap-1">
|
||||||
<Text className="text-center font-medium" variant="h3">
|
<Text className="text-center font-medium" variant="h3">
|
||||||
No Deployments
|
No Deployments
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="subtitle1" className="text-center">
|
<Text variant="subtitle1" className="max-w-sm text-center">
|
||||||
Connect your project with a GitHub repository to create your first
|
Connect your project with a GitHub repository to create your first
|
||||||
deployment
|
deployment
|
||||||
</Text>
|
</Text>
|
||||||
@@ -183,6 +185,7 @@ export default function OverviewDeployments() {
|
|||||||
color="primary"
|
color="primary"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={openGitHubModal}
|
onClick={openGitHubModal}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
<GithubIcon className="mr-1.5 h-4 w-4 self-center" />
|
<GithubIcon className="mr-1.5 h-4 w-4 self-center" />
|
||||||
Connect to GitHub
|
Connect to GitHub
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import GithubIcon from '@/components/icons/GithubIcon';
|
import GithubIcon from '@/components/icons/GithubIcon';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -8,6 +9,7 @@ import NavLink from 'next/link';
|
|||||||
export default function OverviewRepository() {
|
export default function OverviewRepository() {
|
||||||
const { currentWorkspace, currentApplication } =
|
const { currentWorkspace, currentApplication } =
|
||||||
useCurrentWorkspaceAndApplication();
|
useCurrentWorkspaceAndApplication();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -28,6 +30,7 @@ export default function OverviewRepository() {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
className="w-full border-1 hover:border-1"
|
className="w-full border-1 hover:border-1"
|
||||||
startIcon={<GithubIcon />}
|
startIcon={<GithubIcon />}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Connect to GitHub
|
Connect to GitHub
|
||||||
</Button>
|
</Button>
|
||||||
@@ -39,7 +42,7 @@ export default function OverviewRepository() {
|
|||||||
sx={{ backgroundColor: 'grey.200' }}
|
sx={{ backgroundColor: 'grey.200' }}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
className="grid grid-flow-col gap-1.5 ml-2"
|
className="ml-2 grid grid-flow-col gap-1.5"
|
||||||
sx={{ backgroundColor: 'transparent' }}
|
sx={{ backgroundColor: 'transparent' }}
|
||||||
>
|
>
|
||||||
<GithubIcon className="h-4 w-4 self-center" />
|
<GithubIcon className="h-4 w-4 self-center" />
|
||||||
@@ -52,7 +55,11 @@ export default function OverviewRepository() {
|
|||||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
|
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
|
||||||
passHref
|
passHref
|
||||||
>
|
>
|
||||||
<Button variant="borderless" size="small">
|
<Button
|
||||||
|
variant="borderless"
|
||||||
|
size="small"
|
||||||
|
disabled={maintenanceActive}
|
||||||
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
|
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
|
||||||
|
|
||||||
import { useDialog } from '@/components/common/DialogProvider';
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Chip from '@/ui/v2/Chip';
|
import Chip from '@/ui/v2/Chip';
|
||||||
import CogIcon from '@/ui/v2/icons/CogIcon';
|
import CogIcon from '@/ui/v2/icons/CogIcon';
|
||||||
@@ -16,6 +17,7 @@ export default function OverviewTopBar() {
|
|||||||
useCurrentWorkspaceAndApplication();
|
useCurrentWorkspaceAndApplication();
|
||||||
const isPro = !currentApplication?.plan?.isFree;
|
const isPro = !currentApplication?.plan?.isFree;
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
if (!isPlatform) {
|
if (!isPlatform) {
|
||||||
return (
|
return (
|
||||||
@@ -41,9 +43,9 @@ export default function OverviewTopBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row place-content-between items-center py-5">
|
<div className="grid items-center gap-4 pb-5 md:grid-flow-col md:place-content-between md:py-5">
|
||||||
<div className="flex flex-row items-center space-x-2">
|
<div className="grid items-center gap-2 md:grid-flow-col">
|
||||||
<div className="grid grid-flow-col items-center gap-2">
|
<div className="grid grid-flow-col items-center justify-start gap-2">
|
||||||
<div className="h-10 w-10 overflow-hidden rounded-lg">
|
<div className="h-10 w-10 overflow-hidden rounded-lg">
|
||||||
<Image
|
<Image
|
||||||
src="/logos/new.svg"
|
src="/logos/new.svg"
|
||||||
@@ -58,43 +60,44 @@ export default function OverviewTopBar() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPro ? (
|
<Box className="grid grid-flow-col items-center justify-start gap-2">
|
||||||
<Chip
|
{isPro ? (
|
||||||
className="self-center font-medium"
|
|
||||||
size="small"
|
|
||||||
label="Pro Plan"
|
|
||||||
color="primary"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Chip
|
<Chip
|
||||||
className="self-center font-medium"
|
className="self-center font-medium"
|
||||||
size="small"
|
size="small"
|
||||||
label="Free Plan"
|
label="Pro Plan"
|
||||||
color="default"
|
color="primary"
|
||||||
variant="filled"
|
|
||||||
/>
|
/>
|
||||||
<Button
|
) : (
|
||||||
variant="borderless"
|
<>
|
||||||
className="mr-2"
|
<Chip
|
||||||
onClick={() => {
|
className="self-center font-medium"
|
||||||
openAlertDialog({
|
size="small"
|
||||||
title: 'Upgrade your plan.',
|
label="Free Plan"
|
||||||
payload: <ChangePlanModal />,
|
color="default"
|
||||||
props: {
|
variant="filled"
|
||||||
PaperProps: { className: 'p-0' },
|
/>
|
||||||
hidePrimaryAction: true,
|
<Button
|
||||||
hideSecondaryAction: true,
|
variant="borderless"
|
||||||
hideTitle: true,
|
className="mr-2"
|
||||||
maxWidth: 'lg',
|
onClick={() => {
|
||||||
},
|
openAlertDialog({
|
||||||
});
|
title: 'Upgrade your plan.',
|
||||||
}}
|
payload: <ChangePlanModal />,
|
||||||
>
|
props: {
|
||||||
Upgrade
|
PaperProps: { className: 'p-0 max-w-xl w-full' },
|
||||||
</Button>
|
hidePrimaryAction: true,
|
||||||
</>
|
hideSecondaryAction: true,
|
||||||
)}
|
hideTitle: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/general`}
|
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/general`}
|
||||||
@@ -104,6 +107,7 @@ export default function OverviewTopBar() {
|
|||||||
endIcon={<CogIcon className="h-4 w-4" />}
|
endIcon={<CogIcon className="h-4 w-4" />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export function OverviewUsageMetrics() {
|
|||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const remoteAppClient = useRemoteApplicationGQLClient();
|
const remoteAppClient = useRemoteApplicationGQLClient();
|
||||||
|
|
||||||
const [metrics, setMetrics] = useState({
|
const [metrics, setMetrics] = useState({
|
||||||
functions: 0,
|
functions: 0,
|
||||||
storage: 0,
|
storage: 0,
|
||||||
@@ -98,6 +99,7 @@ export function OverviewUsageMetrics() {
|
|||||||
|
|
||||||
const { data: remoteAppMetricsData } = useGetRemoteAppMetricsQuery({
|
const { data: remoteAppMetricsData } = useGetRemoteAppMetricsQuery({
|
||||||
client: remoteAppClient,
|
client: remoteAppClient,
|
||||||
|
skip: !currentApplication,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useResetPostgresPasswordMutation,
|
useResetPostgresPasswordMutation,
|
||||||
useUpdateApplicationMutation,
|
useUpdateApplicationMutation,
|
||||||
@@ -28,6 +29,7 @@ export interface ResetDatabasePasswordFormValues {
|
|||||||
|
|
||||||
export default function ResetDatabasePasswordSettings() {
|
export default function ResetDatabasePasswordSettings() {
|
||||||
const [updateApplication] = useUpdateApplicationMutation();
|
const [updateApplication] = useUpdateApplicationMutation();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
|
|
||||||
const form = useForm<ResetDatabasePasswordFormValues>({
|
const form = useForm<ResetDatabasePasswordFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
@@ -44,11 +46,10 @@ export default function ResetDatabasePasswordSettings() {
|
|||||||
setValue,
|
setValue,
|
||||||
getValues,
|
getValues,
|
||||||
register,
|
register,
|
||||||
formState: { errors },
|
formState: { errors, isDirty, isSubmitting },
|
||||||
} = form;
|
} = form;
|
||||||
|
|
||||||
const [resetPostgresPasswordMutation, { loading }] =
|
const [resetPostgresPasswordMutation] = useResetPostgresPasswordMutation();
|
||||||
useResetPostgresPasswordMutation();
|
|
||||||
const user = useUserData();
|
const user = useUserData();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
@@ -99,12 +100,16 @@ export default function ResetDatabasePasswordSettings() {
|
|||||||
title="Reset Password"
|
title="Reset Password"
|
||||||
description="This password is used for accessing your database."
|
description="This password is used for accessing your database."
|
||||||
submitButtonText="Reset"
|
submitButtonText="Reset"
|
||||||
rootClassName="border-[#F87171]"
|
slotProps={{
|
||||||
primaryActionButtonProps={{
|
root: {
|
||||||
variant: 'contained',
|
sx: { borderColor: (theme) => theme.palette.error.main },
|
||||||
color: 'error',
|
},
|
||||||
disabled: Boolean(errors?.databasePassword),
|
submitButton: {
|
||||||
loading,
|
variant: 'contained',
|
||||||
|
color: 'error',
|
||||||
|
disabled: !isDirty || maintenanceActive,
|
||||||
|
loading: isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
className="grid grid-flow-row pb-4"
|
className="grid grid-flow-row pb-4"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ export default function SettingsSidebar({
|
|||||||
>
|
>
|
||||||
Environment Variables
|
Environment Variables
|
||||||
</SettingsNavLink>
|
</SettingsNavLink>
|
||||||
|
|
||||||
|
<SettingsNavLink
|
||||||
|
href="/secrets"
|
||||||
|
exact={false}
|
||||||
|
onClick={handleSelect}
|
||||||
|
className="hidden"
|
||||||
|
>
|
||||||
|
Secrets
|
||||||
|
</SettingsNavLink>
|
||||||
</List>
|
</List>
|
||||||
</nav>
|
</nav>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -1,68 +1,61 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface AllowedEmailSettingsFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* Determines whether or not the allowed email settings are enabled.
|
allowedEmails: Yup.string().label('Allowed Emails'),
|
||||||
*/
|
allowedEmailDomains: Yup.string().label('Allowed Email Domains'),
|
||||||
enabled: boolean;
|
});
|
||||||
/**
|
|
||||||
* Set of email that are allowed to be used for project's users authentication.
|
export type AllowedEmailSettingsFormValues = Yup.InferType<
|
||||||
*/
|
typeof validationSchema
|
||||||
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() {
|
export default function AllowedEmailDomainsSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { email, emailDomains } = data?.config?.auth?.user || {};
|
||||||
|
|
||||||
const form = useForm<AllowedEmailSettingsFormValues>({
|
const form = useForm<AllowedEmailSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enabled:
|
enabled: email?.allowed?.length > 0 || emailDomains?.allowed?.length > 0,
|
||||||
Boolean(data?.app?.authAccessControlAllowedEmails) ||
|
allowedEmails: email?.allowed?.join(', ') || '',
|
||||||
Boolean(data?.app?.authAccessControlAllowedEmailDomains),
|
allowedEmailDomains: emailDomains?.allowed?.join(', ') || '',
|
||||||
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
|
|
||||||
authAccessControlAllowedEmailDomains:
|
|
||||||
data?.app?.authAccessControlAllowedEmailDomains,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, formState, setValue, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const enabled = watch('enabled');
|
const enabled = watch('enabled');
|
||||||
|
|
||||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!data.app?.authAccessControlAllowedEmails &&
|
|
||||||
!data.app?.authAccessControlAllowedEmailDomains
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue('enabled', true, { shouldDirty: false });
|
|
||||||
}, [data.app, setValue]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -80,29 +73,51 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
const handleAllowedEmailDomainsChange = async (
|
const handleAllowedEmailDomainsChange = async (
|
||||||
values: AllowedEmailSettingsFormValues,
|
values: AllowedEmailSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authAccessControlAllowedEmails: values.enabled
|
auth: {
|
||||||
? values.authAccessControlAllowedEmails
|
user: {
|
||||||
: '',
|
email: {
|
||||||
authAccessControlAllowedEmailDomains: values.enabled
|
blocked: email.blocked,
|
||||||
? values.authAccessControlAllowedEmailDomains
|
allowed:
|
||||||
: '',
|
values.enabled && values.allowedEmails
|
||||||
|
? values.allowedEmails
|
||||||
|
.split(',')
|
||||||
|
.map((allowedEmail) => allowedEmail.trim())
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
emailDomains: {
|
||||||
|
blocked: emailDomains.blocked,
|
||||||
|
allowed:
|
||||||
|
values.enabled && values.allowedEmailDomains
|
||||||
|
? values.allowedEmailDomains
|
||||||
|
.split(',')
|
||||||
|
.map((allowedEmailDomain) => allowedEmailDomain.trim())
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Allowed email settings are being updated...`,
|
{
|
||||||
success: `Allowed email settings have been updated successfully.`,
|
loading: `Allowed email settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's allowed email settings.`,
|
success: `Allowed email settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's allowed email settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error
|
||||||
|
}
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
};
|
};
|
||||||
@@ -115,15 +130,12 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
description="Allow specific email addresses and domains to sign up."
|
description="Allow specific email addresses and domains to sign up."
|
||||||
slotProps={{
|
slotProps={{
|
||||||
submitButton: {
|
submitButton: {
|
||||||
disabled: !formState.isValid || !isDirty,
|
disabled: !isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains"
|
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains"
|
||||||
enabled={enabled}
|
switchId="enabled"
|
||||||
onEnabledChange={(switchEnabled) =>
|
|
||||||
setValue('enabled', switchEnabled, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
||||||
@@ -131,9 +143,9 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlAllowedEmails')}
|
{...register('allowedEmails')}
|
||||||
name="authAccessControlAllowedEmails"
|
name="allowedEmails"
|
||||||
id="authAccessControlAllowedEmails"
|
id="allowedEmails"
|
||||||
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
label="Allowed Emails (comma separated)"
|
label="Allowed Emails (comma separated)"
|
||||||
@@ -141,9 +153,9 @@ export default function AllowedEmailDomainsSettings() {
|
|||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlAllowedEmailDomains')}
|
{...register('allowedEmailDomains')}
|
||||||
name="authAccessControlAllowedEmailDomains"
|
name="allowedEmailDomains"
|
||||||
id="authAccessControlAllowedEmailDomains"
|
id="allowedEmailDomains"
|
||||||
label="Allowed Email Domains (comma sepated list)"
|
label="Allowed Email Domains (comma sepated list)"
|
||||||
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
|
|||||||
@@ -1,36 +1,49 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface AllowedRedirectURLFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
allowedUrls: Yup.string().label('Allowed Redirect URLs'),
|
||||||
* Set of URLs that are allowed to be redirected to after project's users authentication.
|
});
|
||||||
*/
|
|
||||||
authAccessControlAllowedRedirectUrls: string;
|
export type AllowedRedirectURLFormValues = Yup.InferType<
|
||||||
}
|
typeof validationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function AllowedRedirectURLsSettings() {
|
export default function AllowedRedirectURLsSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { allowedUrls } = data?.config?.auth?.redirections || {};
|
||||||
|
|
||||||
const form = useForm<AllowedRedirectURLFormValues>({
|
const form = useForm<AllowedRedirectURLFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authAccessControlAllowedRedirectUrls:
|
allowedUrls: allowedUrls?.join(', ') || '',
|
||||||
data?.app?.authAccessControlAllowedRedirectUrls,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -52,26 +65,38 @@ export default function AllowedRedirectURLsSettings() {
|
|||||||
const handleAllowedRedirectURLsChange = async (
|
const handleAllowedRedirectURLsChange = async (
|
||||||
values: AllowedRedirectURLFormValues,
|
values: AllowedRedirectURLFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
redirections: {
|
||||||
|
allowedUrls: values.allowedUrls
|
||||||
|
? values.allowedUrls.split(',').map((url) => url.trim())
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Allowed redirect URL settings are being updated...`,
|
{
|
||||||
success: `Allowed redirect URL settings have been updated successfully.`,
|
loading: `Allowed redirect URL settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's allowed redirect URL settings.`,
|
success: `Allowed redirect URL settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's allowed redirect URL settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,17 +105,19 @@ export default function AllowedRedirectURLsSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Allowed Redirect URLs"
|
title="Allowed Redirect URLs"
|
||||||
description="Allowed URLs where users can be redirected to after authentication. Separate multiple redirect URLs with comma."
|
description="Allowed URLs where users can be redirected to after authentication. Separate multiple redirect URLs with comma."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls"
|
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls"
|
||||||
className="grid grid-flow-row px-4 lg:grid-cols-5"
|
className="grid grid-flow-row px-4 lg:grid-cols-5"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlAllowedRedirectUrls')}
|
{...register('allowedUrls')}
|
||||||
name="authAccessControlAllowedRedirectUrls"
|
name="allowedUrls"
|
||||||
id="authAccessControlAllowedRedirectUrls"
|
id="allowedUrls"
|
||||||
placeholder="http://localhost:3000, http://localhost:4000"
|
placeholder="http://localhost:3000, http://localhost:4000"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -1,67 +1,58 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BlockedEmailFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* Determines whether or not the blocked email settings are enabled.
|
blockedEmails: Yup.string().label('Blocked Emails'),
|
||||||
*/
|
blockedEmailDomains: Yup.string().label('Blocked Email Domains'),
|
||||||
enabled: boolean;
|
});
|
||||||
/**
|
|
||||||
* Set of emails that are blocked from registering to the user's project.
|
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
*/
|
|
||||||
authAccessControlBlockedEmails: string;
|
|
||||||
/**
|
|
||||||
* Set of email domains that are blocked from registering to the user's project.
|
|
||||||
*/
|
|
||||||
authAccessControlBlockedEmailDomains: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BlockedEmailSettings() {
|
export default function BlockedEmailSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { email, emailDomains } = data?.config?.auth?.user || {};
|
||||||
|
|
||||||
const form = useForm<BlockedEmailFormValues>({
|
const form = useForm<BlockedEmailFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
enabled:
|
enabled: email?.blocked?.length > 0 || emailDomains?.blocked?.length > 0,
|
||||||
Boolean(data?.app?.authAccessControlBlockedEmails) ||
|
blockedEmails: email?.blocked?.join(', ') || '',
|
||||||
Boolean(data?.app?.authAccessControlBlockedEmailDomains),
|
blockedEmailDomains: emailDomains?.blocked?.join(', ') || '',
|
||||||
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
|
|
||||||
authAccessControlBlockedEmailDomains:
|
|
||||||
data?.app?.authAccessControlBlockedEmailDomains,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { register, formState, setValue, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const enabled = watch('enabled');
|
const enabled = watch('enabled');
|
||||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
!data.app?.authAccessControlBlockedEmails &&
|
|
||||||
!data.app?.authAccessControlBlockedEmailDomains
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setValue('enabled', true, { shouldDirty: false });
|
|
||||||
}, [data.app, setValue]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -79,31 +70,63 @@ export default function BlockedEmailSettings() {
|
|||||||
const handleAllowedEmailDomainsChange = async (
|
const handleAllowedEmailDomainsChange = async (
|
||||||
values: BlockedEmailFormValues,
|
values: BlockedEmailFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authAccessControlBlockedEmails: values.enabled
|
auth: {
|
||||||
? values.authAccessControlBlockedEmails
|
user: {
|
||||||
: '',
|
email: {
|
||||||
authAccessControlBlockedEmailDomains: values.enabled
|
allowed: email.allowed,
|
||||||
? values.authAccessControlBlockedEmailDomains
|
blocked:
|
||||||
: '',
|
values.enabled && values.blockedEmails
|
||||||
|
? [
|
||||||
|
...new Set(
|
||||||
|
values.blockedEmails
|
||||||
|
.split(',')
|
||||||
|
.map((blockedEmail) => blockedEmail.trim()),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
emailDomains: {
|
||||||
|
allowed: emailDomains.allowed,
|
||||||
|
blocked:
|
||||||
|
values.enabled && values.blockedEmailDomains
|
||||||
|
? [
|
||||||
|
...new Set(
|
||||||
|
values.blockedEmailDomains
|
||||||
|
.split(',')
|
||||||
|
.map((blockedEmailDomain) =>
|
||||||
|
blockedEmailDomain.trim(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Blocked email and domain settings are being updated...`,
|
{
|
||||||
success: `Blocked email and domain settings have been updated successfully.`,
|
loading: `Blocked email and domain settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's blocked email and domain settings.`,
|
success: `Blocked email and domain settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's blocked email and domain settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,15 +137,12 @@ export default function BlockedEmailSettings() {
|
|||||||
description="Block specific email addresses and domains to sign up."
|
description="Block specific email addresses and domains to sign up."
|
||||||
slotProps={{
|
slotProps={{
|
||||||
submitButton: {
|
submitButton: {
|
||||||
disabled: !formState.isValid || !isDirty,
|
disabled: !isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains"
|
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains"
|
||||||
enabled={enabled}
|
switchId="enabled"
|
||||||
onEnabledChange={(switchEnabled) =>
|
|
||||||
setValue('enabled', switchEnabled, { shouldDirty: true })
|
|
||||||
}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
||||||
@@ -130,9 +150,9 @@ export default function BlockedEmailSettings() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlBlockedEmails')}
|
{...register('blockedEmails')}
|
||||||
name="authAccessControlBlockedEmails"
|
name="blockedEmails"
|
||||||
id="authAccessControlBlockedEmails"
|
id="blockedEmails"
|
||||||
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
label="Blocked Emails (comma separated)"
|
label="Blocked Emails (comma separated)"
|
||||||
@@ -140,9 +160,9 @@ export default function BlockedEmailSettings() {
|
|||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAccessControlBlockedEmailDomains')}
|
{...register('blockedEmailDomains')}
|
||||||
name="authAccessControlBlockedEmailDomains"
|
name="blockedEmailDomains"
|
||||||
id="authAccessControlBlockedEmailDomains"
|
id="blockedEmailDomains"
|
||||||
label="Blocked Email Domains (comma sepated list)"
|
label="Blocked Email Domains (comma sepated list)"
|
||||||
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
|
|||||||
@@ -1,36 +1,47 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
import { useUI } from '@/context/UIContext';
|
||||||
|
import {
|
||||||
|
GetAuthenticationSettingsDocument,
|
||||||
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface ClientURLFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
clientUrl: Yup.string().label('Client URL'),
|
||||||
* The URL of the frontend app of where users are redirected after authenticating.
|
});
|
||||||
*/
|
|
||||||
authClientUrl: string;
|
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export default function ClientURLSettings() {
|
export default function ClientURLSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['GetApp'] });
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAppQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-first',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { clientUrl, allowedUrls } = data?.config?.auth?.redirections || {};
|
||||||
|
|
||||||
const form = useForm<ClientURLFormValues>({
|
const form = useForm<ClientURLFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientUrl: data?.app?.authClientUrl,
|
clientUrl,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -50,26 +61,37 @@ export default function ClientURLSettings() {
|
|||||||
const { register, formState } = form;
|
const { register, formState } = form;
|
||||||
|
|
||||||
const handleClientURLChange = async (values: ClientURLFormValues) => {
|
const handleClientURLChange = async (values: ClientURLFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
redirections: {
|
||||||
|
...values,
|
||||||
|
allowedUrls,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Client URL is being updated...`,
|
{
|
||||||
success: `Client URL has been updated successfully.`,
|
loading: `Client URL is being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Client URL.`,
|
success: `Client URL has been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Client URL.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,22 +100,26 @@ export default function ClientURLSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Client URL"
|
title="Client URL"
|
||||||
description="This should be the URL of your frontend app where users are redirected after authenticating."
|
description="This should be the URL of your frontend app where users are redirected after authenticating."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#client-url"
|
docsLink="https://docs.nhost.io/authentication#client-url"
|
||||||
className="grid grid-flow-row lg:grid-cols-5"
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authClientUrl')}
|
{...register('clientUrl')}
|
||||||
name="authClientUrl"
|
name="clientUrl"
|
||||||
id="authClientUrl"
|
id="clientUrl"
|
||||||
placeholder="http://localhost:3000"
|
placeholder="http://localhost:3000"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
aria-label="Client URL"
|
aria-label="Client URL"
|
||||||
|
error={!!formState.errors?.clientUrl}
|
||||||
|
helperText={formState.errors?.clientUrl?.message}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,46 +1,44 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAuthSettingsQuery,
|
GetAuthenticationSettingsDocument,
|
||||||
useUpdateAppMutation,
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface DisableNewUsersFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
disabled: Yup.boolean(),
|
||||||
* Disable new users from signing up to this project
|
});
|
||||||
*/
|
|
||||||
authDisableNewUsers: boolean;
|
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export default function DisableNewUsersSettings() {
|
export default function DisableNewUsersSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useGetAuthSettingsQuery({
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
fetchPolicy: 'cache-only',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<DisableNewUsersFormValues>({
|
const form = useForm<DisableNewUsersFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authDisableNewUsers: data?.app?.authDisableNewUsers,
|
disabled: !!data?.config?.auth?.signUp?.enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset(() => ({
|
|
||||||
authDisableNewUsers: data?.app?.authDisableNewUsers,
|
|
||||||
}));
|
|
||||||
}, [data?.app?.authDisableNewUsers, form, form.reset]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -55,32 +53,41 @@ export default function DisableNewUsersSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState } = form;
|
||||||
const authDisableNewUsers = watch('authDisableNewUsers');
|
|
||||||
|
|
||||||
const handleDisableNewUsersChange = async (
|
const handleDisableNewUsersChange = async (
|
||||||
values: DisableNewUsersFormValues,
|
values: DisableNewUsersFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
signUp: {
|
||||||
|
enabled: !values.disabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Disabling new user sign ups...`,
|
{
|
||||||
success: `New user sign ups have been disabled successfully.`,
|
loading: `Disabling new user sign ups...`,
|
||||||
error: `An error occurred while trying to disable new user sign ups.`,
|
success: `New user sign ups have been disabled successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to disable new user sign ups.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -88,14 +95,15 @@ export default function DisableNewUsersSettings() {
|
|||||||
<Form onSubmit={handleDisableNewUsersChange}>
|
<Form onSubmit={handleDisableNewUsersChange}>
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Disable New Users"
|
title="Disable New Users"
|
||||||
description="If set, newly registered users are disabled and won’t be able to sign in."
|
description="If set, newly registered users are disabled and won't be able to sign in."
|
||||||
docsLink="https://docs.nhost.io/authentication#disable-new-users"
|
docsLink="https://docs.nhost.io/authentication#disable-new-users"
|
||||||
switchId="authDisableNewUsers"
|
switchId="disabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authDisableNewUsers}
|
slotProps={{
|
||||||
primaryActionButtonProps={{
|
submitButton: {
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,65 +1,63 @@
|
|||||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useGetGravatarSettingsQuery,
|
GetAuthenticationSettingsDocument,
|
||||||
useUpdateAppMutation,
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import {
|
import {
|
||||||
AUTH_GRAVATAR_DEFAULT,
|
AUTH_GRAVATAR_DEFAULT,
|
||||||
AUTH_GRAVATAR_RATING,
|
AUTH_GRAVATAR_RATING,
|
||||||
getToastStyleProps,
|
getToastStyleProps,
|
||||||
} from '@/utils/settings/settingsConstants';
|
} from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface GravatarFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* Gravatar image to use as default.
|
default: Yup.string().label('Default Gravatar'),
|
||||||
*/
|
rating: Yup.string().label('Gravatar Rating'),
|
||||||
authGravatarDefault: string;
|
});
|
||||||
/**
|
|
||||||
* Gravatar image rating.
|
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
*/
|
|
||||||
authGravatarRating: string;
|
|
||||||
/**
|
|
||||||
* Enable Gravatar for this project
|
|
||||||
*/
|
|
||||||
authGravatarEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GravatarSettings() {
|
export default function GravatarSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetGravatarSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
default: defaultGravatar,
|
||||||
|
rating,
|
||||||
|
enabled,
|
||||||
|
} = data?.config?.auth?.user?.gravatar || {};
|
||||||
|
|
||||||
const form = useForm<GravatarFormValues>({
|
const form = useForm<GravatarFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authGravatarDefault: data?.app?.authGravatarDefault || '',
|
default: defaultGravatar || '',
|
||||||
authGravatarRating: data?.app?.authGravatarRating || '',
|
rating: rating || '',
|
||||||
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset(() => ({
|
|
||||||
authGravatarDefault: data?.app?.authGravatarDefault || '',
|
|
||||||
authGravatarRating: data?.app?.authGravatarRating || '',
|
|
||||||
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
|
|
||||||
}));
|
|
||||||
}, [data?.app, form, form.reset]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -75,29 +73,39 @@ export default function GravatarSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authGravatarEnabled = watch('authGravatarEnabled');
|
const gravatarEnabled = watch('enabled') ?? false;
|
||||||
|
|
||||||
const handleGravatarSettingsChange = async (values: GravatarFormValues) => {
|
const handleGravatarSettingsChange = async (values: GravatarFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
user: {
|
||||||
|
gravatar: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Gravatar settings are being updated...`,
|
{
|
||||||
success: `Gravatar settings have been updated successfully.`,
|
loading: `Gravatar settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Gravatar settings.`,
|
success: `Gravatar settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Gravatar settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -106,22 +114,23 @@ export default function GravatarSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Gravatar"
|
title="Gravatar"
|
||||||
description="Use Gravatars for avatar URLs for users."
|
description="Use Gravatars for avatar URLs for users."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#gravatar"
|
docsLink="https://docs.nhost.io/authentication#gravatar"
|
||||||
switchId="authGravatarEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authGravatarEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid grid-flow-col grid-cols-5 grid-rows-2 gap-y-6',
|
'grid grid-flow-col grid-cols-5 grid-rows-2 gap-y-6',
|
||||||
!authGravatarEnabled && 'hidden',
|
!gravatarEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ControlledSelect
|
<ControlledSelect
|
||||||
{...register('authGravatarDefault')}
|
{...register('default')}
|
||||||
id="authGravatarDefault"
|
id="default"
|
||||||
className="col-span-5 lg:col-span-2"
|
className="col-span-5 lg:col-span-2"
|
||||||
placeholder="Default Gravatar"
|
placeholder="Default Gravatar"
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
@@ -135,8 +144,8 @@ export default function GravatarSettings() {
|
|||||||
))}
|
))}
|
||||||
</ControlledSelect>
|
</ControlledSelect>
|
||||||
<ControlledSelect
|
<ControlledSelect
|
||||||
{...register('authGravatarRating')}
|
{...register('rating')}
|
||||||
id="authGravatarRating"
|
id="rating"
|
||||||
className="col-span-5 lg:col-span-2"
|
className="col-span-5 lg:col-span-2"
|
||||||
placeholder="Gravatar Rating"
|
placeholder="Gravatar Rating"
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
|||||||
@@ -1,54 +1,52 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useGetAuthSettingsQuery,
|
GetAuthenticationSettingsDocument,
|
||||||
useUpdateAppMutation,
|
useGetAuthenticationSettingsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useEffect } from 'react';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface MFASettingsFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
* One Time Password issuer
|
issuer: Yup.string().label('OTP Issuer').nullable().required(),
|
||||||
*/
|
});
|
||||||
authMfaTotpIssuer: string;
|
|
||||||
/**
|
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
* Enable Multi Factor Authentication for this project
|
|
||||||
*/
|
|
||||||
authMfaEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MFASettings() {
|
export default function MFASettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||||
const { data, loading, error } = useGetAuthSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||||
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { enabled, issuer } = data?.config?.auth?.totp || {};
|
||||||
|
|
||||||
const form = useForm<MFASettingsFormValues>({
|
const form = useForm<MFASettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
|
issuer,
|
||||||
authMfaEnabled: data?.app?.authMfaEnabled,
|
enabled,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset(() => ({
|
|
||||||
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
|
|
||||||
authMfaEnabled: data?.app?.authMfaEnabled,
|
|
||||||
}));
|
|
||||||
}, [data?.app, form, form.reset]);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
@@ -64,29 +62,37 @@ export default function MFASettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authMfaEnabled = watch('authMfaEnabled');
|
const authMfaEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleMFASettingsChange = async (values: MFASettingsFormValues) => {
|
const handleMFASettingsChange = async (values: MFASettingsFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
totp: values,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Multi-factor authentication settings are being updated...`,
|
{
|
||||||
success: `Multi-factor authentication settings have been updated successfully.`,
|
loading: `Multi-factor authentication settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's multi-factor authentication settings.`,
|
success: `Multi-factor authentication settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's multi-factor authentication settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,13 +101,14 @@ export default function MFASettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Multi-Factor Authentication"
|
title="Multi-Factor Authentication"
|
||||||
description="Enable users to use MFA to sign in"
|
description="Enable users to use MFA to sign in"
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication"
|
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication"
|
||||||
switchId="authMfaEnabled"
|
switchId="enabled"
|
||||||
enabled={authMfaEnabled}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid grid-flow-row lg:grid-cols-5',
|
'grid grid-flow-row lg:grid-cols-5',
|
||||||
@@ -109,14 +116,16 @@ export default function MFASettings() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register('authMfaTotpIssuer')}
|
{...register('issuer')}
|
||||||
name="authMfaTotpIssuer"
|
name="issuer"
|
||||||
id="authMfaTotpIssuer"
|
id="issuer"
|
||||||
label="OTP Issuer"
|
label="OTP Issuer"
|
||||||
placeholder="Name of the One Time Password (OTP) issuer"
|
placeholder="Name of the One Time Password (OTP) issuer"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.issuer}
|
||||||
|
helperText={formState.errors?.issuer?.message}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -8,25 +8,6 @@ import { useEffect } from 'react';
|
|||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BaseEnvironmentVariableFormValues {
|
|
||||||
/**
|
|
||||||
* Identifier of the environment variable.
|
|
||||||
*/
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* The name of the role.
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* Development environment variable value.
|
|
||||||
*/
|
|
||||||
devValue: string;
|
|
||||||
/**
|
|
||||||
* Production environment variable value.
|
|
||||||
*/
|
|
||||||
prodValue: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseEnvironmentVariableFormProps extends DialogFormProps {
|
export interface BaseEnvironmentVariableFormProps extends DialogFormProps {
|
||||||
/**
|
/**
|
||||||
* Determines the mode of the form.
|
* Determines the mode of the form.
|
||||||
@@ -51,8 +32,11 @@ export interface BaseEnvironmentVariableFormProps extends DialogFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
||||||
|
id: Yup.string().label('ID'),
|
||||||
name: Yup.string()
|
name: Yup.string()
|
||||||
.required('This field is required.')
|
.label('Name')
|
||||||
|
.nullable()
|
||||||
|
.required()
|
||||||
.test(
|
.test(
|
||||||
'isEnvVarPermitted',
|
'isEnvVarPermitted',
|
||||||
'This is a reserved name.',
|
'This is a reserved name.',
|
||||||
@@ -78,13 +62,18 @@ export const baseEnvironmentVariableFormValidationSchema = Yup.object({
|
|||||||
(prefix) => !value.startsWith(prefix),
|
(prefix) => !value.startsWith(prefix),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.test('isEnvVarValid', `The name must start with a letter.`, (value) =>
|
.test(
|
||||||
/^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/i.test(value),
|
'isEnvVarValid',
|
||||||
|
'A name must start with a letter and can only contain letters, numbers, and underscores.',
|
||||||
|
(value) => /^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/i.test(value),
|
||||||
),
|
),
|
||||||
devValue: Yup.string().required('This field is required.'),
|
value: Yup.string().label('Value').nullable().required(),
|
||||||
prodValue: Yup.string().required('This field is required.'),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type BaseEnvironmentVariableFormValues = Yup.InferType<
|
||||||
|
typeof baseEnvironmentVariableFormValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export default function BaseEnvironmentVariableForm({
|
export default function BaseEnvironmentVariableForm({
|
||||||
mode = 'edit',
|
mode = 'edit',
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -117,21 +106,7 @@ export default function BaseEnvironmentVariableForm({
|
|||||||
|
|
||||||
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
||||||
<Input
|
<Input
|
||||||
{...register('name', {
|
{...register('name')}
|
||||||
onChange: (event) => {
|
|
||||||
if (
|
|
||||||
event.target.value &&
|
|
||||||
!/^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/g.test(event.target.value)
|
|
||||||
) {
|
|
||||||
// we need to prevent invalid characters from being entered
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
event.target.value = event.target.value.replace(
|
|
||||||
/[^a-zA-Z0-9_]/g,
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
id="name"
|
id="name"
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder="EXAMPLE_NAME"
|
placeholder="EXAMPLE_NAME"
|
||||||
@@ -145,30 +120,18 @@ export default function BaseEnvironmentVariableForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
{...register('prodValue')}
|
{...register('value')}
|
||||||
id="prodValue"
|
id="value"
|
||||||
label="Production Value"
|
label="Value"
|
||||||
placeholder="Enter value"
|
placeholder="Enter value"
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
error={!!errors.prodValue}
|
error={!!errors.value}
|
||||||
helperText={errors?.prodValue?.message}
|
helperText={errors?.value?.message}
|
||||||
fullWidth
|
fullWidth
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoFocus={mode === 'edit'}
|
autoFocus={mode === 'edit'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
|
||||||
{...register('devValue')}
|
|
||||||
id="devValue"
|
|
||||||
label="Development Value"
|
|
||||||
placeholder="Enter value"
|
|
||||||
hideEmptyHelperText
|
|
||||||
error={!!errors.devValue}
|
|
||||||
helperText={errors?.devValue?.message}
|
|
||||||
fullWidth
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
<Button type="submit" loading={isSubmitting}>
|
<Button type="submit" loading={isSubmitting}>
|
||||||
{submitButtonText}
|
{submitButtonText}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import BaseEnvironmentVariableForm, {
|
|||||||
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
|
GetEnvironmentVariablesDocument,
|
||||||
useGetEnvironmentVariablesQuery,
|
useGetEnvironmentVariablesQuery,
|
||||||
useInsertEnvironmentVariablesMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -31,8 +33,7 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
const form = useForm<BaseEnvironmentVariableFormValues>({
|
const form = useForm<BaseEnvironmentVariableFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: '',
|
name: '',
|
||||||
devValue: '',
|
value: '',
|
||||||
prodValue: '',
|
|
||||||
},
|
},
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
||||||
@@ -41,14 +42,14 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [insertEnvironmentVariables] = useInsertEnvironmentVariablesMutation({
|
const availableEnvironmentVariables = data?.config?.global?.environment || [];
|
||||||
refetchQueries: ['getEnvironmentVariables'],
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -68,13 +69,10 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
name,
|
name,
|
||||||
prodValue,
|
value,
|
||||||
devValue,
|
|
||||||
}: BaseEnvironmentVariableFormValues) {
|
}: BaseEnvironmentVariableFormValues) {
|
||||||
if (
|
if (
|
||||||
data?.environmentVariables?.some(
|
availableEnvironmentVariables?.some((variable) => variable.name === name)
|
||||||
(environmentVariable) => environmentVariable.name === name,
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
setError('name', {
|
setError('name', {
|
||||||
message: 'This environment variable already exists.',
|
message: 'This environment variable already exists.',
|
||||||
@@ -83,20 +81,34 @@ export default function CreateEnvironmentVariableForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertEnvironmentVariablePromise = insertEnvironmentVariables({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
environmentVariables: [
|
appId: currentApplication?.id,
|
||||||
{ appId: currentApplication.id, name, prodValue, devValue },
|
config: {
|
||||||
],
|
global: {
|
||||||
|
environment: [
|
||||||
|
...(availableEnvironmentVariables?.map((variable) => ({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value,
|
||||||
|
})) || []),
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
insertEnvironmentVariablePromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Creating environment variable...',
|
loading: 'Creating environment variable...',
|
||||||
success: 'Environment variable has been created successfully.',
|
success: 'Environment variable has been created successfully.',
|
||||||
error: 'An error occurred while creating the environment variable.',
|
error: getServerError(
|
||||||
|
'An error occurred while creating the environment variable.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import BaseEnvironmentVariableForm, {
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { EnvironmentVariable } from '@/types/application';
|
import type { EnvironmentVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
|
GetEnvironmentVariablesDocument,
|
||||||
useGetEnvironmentVariablesQuery,
|
useGetEnvironmentVariablesQuery,
|
||||||
useUpdateEnvironmentVariableMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -38,8 +40,7 @@ export default function EditEnvironmentVariableForm({
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: originalEnvironmentVariable.id || '',
|
id: originalEnvironmentVariable.id || '',
|
||||||
name: originalEnvironmentVariable.name || '',
|
name: originalEnvironmentVariable.name || '',
|
||||||
devValue: originalEnvironmentVariable.devValue || '',
|
value: originalEnvironmentVariable.value || '',
|
||||||
prodValue: originalEnvironmentVariable.prodValue || '',
|
|
||||||
},
|
},
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
resolver: yupResolver(baseEnvironmentVariableFormValidationSchema),
|
||||||
@@ -48,14 +49,14 @@ export default function EditEnvironmentVariableForm({
|
|||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateEnvironmentVariable] = useUpdateEnvironmentVariableMutation({
|
const availableEnvironmentVariables = data?.config?.global?.environment || [];
|
||||||
refetchQueries: ['getEnvironmentVariables'],
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -76,14 +77,13 @@ export default function EditEnvironmentVariableForm({
|
|||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
prodValue,
|
value,
|
||||||
devValue,
|
|
||||||
}: BaseEnvironmentVariableFormValues) {
|
}: BaseEnvironmentVariableFormValues) {
|
||||||
if (
|
if (
|
||||||
data?.environmentVariables?.some(
|
availableEnvironmentVariables.some(
|
||||||
(environmentVariable) =>
|
(variable) =>
|
||||||
environmentVariable.name === name &&
|
variable.name === name &&
|
||||||
environmentVariable.name !== originalEnvironmentVariable.name,
|
variable.name !== originalEnvironmentVariable.name,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setError('name', {
|
setError('name', {
|
||||||
@@ -93,22 +93,36 @@ export default function EditEnvironmentVariableForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateEnvironmentVariablePromise = updateEnvironmentVariable({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id,
|
appId: currentApplication?.id,
|
||||||
environmentVariable: {
|
config: {
|
||||||
prodValue,
|
global: {
|
||||||
devValue,
|
environment: [
|
||||||
|
...availableEnvironmentVariables
|
||||||
|
.filter((variable) => variable.id !== id)
|
||||||
|
.map((variable) => ({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value,
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateEnvironmentVariablePromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Updating environment variable...',
|
loading: 'Updating environment variable...',
|
||||||
success: 'Environment variable has been updated successfully.',
|
success: 'Environment variable has been updated successfully.',
|
||||||
error: 'An error occurred while updating the environment variable.',
|
error: getServerError(
|
||||||
|
'An error occurred while updating the environment variable.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import Button from '@/ui/v2/Button';
|
|||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
refetchGetAppInjectedVariablesQuery,
|
GetEnvironmentVariablesDocument,
|
||||||
useUpdateApplicationMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -65,10 +65,8 @@ export default function EditJwtSecretForm({
|
|||||||
location,
|
location,
|
||||||
}: EditJwtSecretFormProps) {
|
}: EditJwtSecretFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApplication] = useUpdateApplicationMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
refetchGetAppInjectedVariablesQuery({ id: currentApplication?.id }),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { onDirtyStateChange } = useDialog();
|
const { onDirtyStateChange } = useDialog();
|
||||||
@@ -90,26 +88,38 @@ export default function EditJwtSecretForm({
|
|||||||
}, [isDirty, location, onDirtyStateChange]);
|
}, [isDirty, location, onDirtyStateChange]);
|
||||||
|
|
||||||
async function handleSubmit(values: EditJwtSecretFormValues) {
|
async function handleSubmit(values: EditJwtSecretFormValues) {
|
||||||
const updateAppPromise = updateApplication({
|
const parsedJwtSecret = JSON.parse(values.jwtSecret);
|
||||||
|
const isArray = Array.isArray(parsedJwtSecret);
|
||||||
|
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
appId: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
hasuraGraphqlJwtSecret: values.jwtSecret,
|
hasura: {
|
||||||
|
jwtSecrets: isArray ? parsedJwtSecret : [parsedJwtSecret],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppPromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Updating JWT secret...',
|
{
|
||||||
success: 'JWT secret has been updated successfully.',
|
loading: 'Updating JWT secret...',
|
||||||
error: 'An error occurred while updating the JWT secret.',
|
success: 'JWT secret has been updated successfully.',
|
||||||
},
|
error: (arg: Error) =>
|
||||||
getToastStyleProps(),
|
arg?.message
|
||||||
);
|
? `Error: ${arg.message}`
|
||||||
|
: 'An error occurred while updating the JWT secret.',
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
|
} catch {
|
||||||
|
// Note: error is handled above
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { EnvironmentVariable } from '@/types/application';
|
import type { EnvironmentVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -15,12 +16,13 @@ import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
|||||||
import List from '@/ui/v2/List';
|
import List from '@/ui/v2/List';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useDeleteEnvironmentVariableMutation,
|
GetEnvironmentVariablesDocument,
|
||||||
useGetEnvironmentVariablesQuery,
|
useGetEnvironmentVariablesQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
@@ -34,15 +36,29 @@ export interface EnvironmentVariableSettingsFormValues {
|
|||||||
|
|
||||||
export default function EnvironmentVariableSettings() {
|
export default function EnvironmentVariableSettings() {
|
||||||
const { openDialog, openAlertDialog } = useDialog();
|
const { openDialog, openAlertDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
fetchPolicy: 'cache-only',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [deleteEnvironmentVariable] = useDeleteEnvironmentVariableMutation({
|
const availableEnvironmentVariables = [
|
||||||
refetchQueries: ['getEnvironmentVariables'],
|
...(data?.config?.global?.environment || []),
|
||||||
|
].sort((a, b) => {
|
||||||
|
if (a.name < b.name) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a.name > b.name) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -59,21 +75,37 @@ export default function EnvironmentVariableSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteVariable({ id }: EnvironmentVariable) {
|
async function handleDeleteVariable({ id }: EnvironmentVariable) {
|
||||||
const deleteEnvironmentVariablePromise = deleteEnvironmentVariable({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id,
|
appId: currentApplication?.id,
|
||||||
|
config: {
|
||||||
|
global: {
|
||||||
|
environment: availableEnvironmentVariables
|
||||||
|
.filter((variable) => variable.id !== id)
|
||||||
|
.map((variable) => ({
|
||||||
|
name: variable.name,
|
||||||
|
value: variable.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
deleteEnvironmentVariablePromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Deleting environment variable...',
|
{
|
||||||
success: 'Environment variable has been deleted successfully.',
|
loading: 'Deleting environment variable...',
|
||||||
error: 'An error occurred while deleting the environment variable.',
|
success: 'Environment variable has been deleted successfully.',
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
'An error occurred while deleting the environment variable.',
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenCreator() {
|
function handleOpenCreator() {
|
||||||
@@ -120,12 +152,6 @@ export default function EnvironmentVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableEnvironmentVariables =
|
|
||||||
[...data.environmentVariables].sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
|
||||||
) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Project Environment Variables"
|
title="Project Environment Variables"
|
||||||
@@ -141,95 +167,79 @@ export default function EnvironmentVariableSettings() {
|
|||||||
>
|
>
|
||||||
<Box className="grid grid-cols-2 gap-2 border-b-1 px-4 py-3 lg:grid-cols-3">
|
<Box className="grid grid-cols-2 gap-2 border-b-1 px-4 py-3 lg:grid-cols-3">
|
||||||
<Text className="font-medium">Variable Name</Text>
|
<Text className="font-medium">Variable Name</Text>
|
||||||
<Text className="font-medium lg:col-span-2">Updated</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
{availableEnvironmentVariables.length > 0 && (
|
{availableEnvironmentVariables.length > 0 && (
|
||||||
<List>
|
<List>
|
||||||
{availableEnvironmentVariables.map((environmentVariable, index) => {
|
{availableEnvironmentVariables.map((environmentVariable, index) => (
|
||||||
const timestamp = formatDistanceToNowStrict(
|
<Fragment key={environmentVariable.id}>
|
||||||
parseISO(environmentVariable.updatedAt),
|
<ListItem.Root
|
||||||
{ addSuffix: true, roundingMethod: 'floor' },
|
className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3"
|
||||||
);
|
secondaryAction={
|
||||||
|
<Dropdown.Root>
|
||||||
return (
|
<Dropdown.Trigger
|
||||||
<Fragment key={environmentVariable.id}>
|
asChild
|
||||||
<ListItem.Root
|
hideChevron
|
||||||
className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3"
|
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||||
secondaryAction={
|
>
|
||||||
<Dropdown.Root>
|
<IconButton
|
||||||
<Dropdown.Trigger
|
variant="borderless"
|
||||||
asChild
|
color="secondary"
|
||||||
hideChevron
|
disabled={maintenanceActive}
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
|
||||||
>
|
>
|
||||||
<IconButton variant="borderless" color="secondary">
|
<DotsVerticalIcon />
|
||||||
<DotsVerticalIcon />
|
</IconButton>
|
||||||
</IconButton>
|
</Dropdown.Trigger>
|
||||||
</Dropdown.Trigger>
|
|
||||||
|
|
||||||
<Dropdown.Content
|
<Dropdown.Content
|
||||||
menu
|
menu
|
||||||
PaperProps={{ className: 'w-32' }}
|
PaperProps={{ className: 'w-32' }}
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
}}
|
}}
|
||||||
transformOrigin={{
|
transformOrigin={{
|
||||||
vertical: 'top',
|
vertical: 'top',
|
||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<Dropdown.Item
|
||||||
|
onClick={() => handleOpenEditor(environmentVariable)}
|
||||||
>
|
>
|
||||||
<Dropdown.Item
|
<Text className="font-medium">Edit</Text>
|
||||||
onClick={() =>
|
</Dropdown.Item>
|
||||||
handleOpenEditor(environmentVariable)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Text className="font-medium">Edit</Text>
|
|
||||||
</Dropdown.Item>
|
|
||||||
|
|
||||||
<Divider component="li" />
|
<Divider component="li" />
|
||||||
|
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleConfirmDelete(environmentVariable)
|
handleConfirmDelete(environmentVariable)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text className="font-medium" color="error">
|
<Text className="font-medium" color="error">
|
||||||
Delete
|
Delete
|
||||||
</Text>
|
</Text>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Content>
|
</Dropdown.Content>
|
||||||
</Dropdown.Root>
|
</Dropdown.Root>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ListItem.Text className="truncate">
|
<ListItem.Text className="truncate">
|
||||||
{environmentVariable.name}
|
{environmentVariable.name}
|
||||||
</ListItem.Text>
|
</ListItem.Text>
|
||||||
|
</ListItem.Root>
|
||||||
|
|
||||||
<Text
|
<Divider
|
||||||
variant="subtitle1"
|
component="li"
|
||||||
className="truncate lg:col-span-2"
|
className={twMerge(
|
||||||
>
|
index === availableEnvironmentVariables.length - 1
|
||||||
{timestamp === '0 seconds ago' ||
|
? '!mt-4'
|
||||||
timestamp === 'in 0 seconds'
|
: '!my-4',
|
||||||
? 'Now'
|
)}
|
||||||
: timestamp}
|
/>
|
||||||
</Text>
|
</Fragment>
|
||||||
</ListItem.Root>
|
))}
|
||||||
|
|
||||||
<Divider
|
|
||||||
component="li"
|
|
||||||
className={twMerge(
|
|
||||||
index === availableEnvironmentVariables.length - 1
|
|
||||||
? '!mt-4'
|
|
||||||
: '!my-4',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -238,6 +248,7 @@ export default function EnvironmentVariableSettings() {
|
|||||||
variant="borderless"
|
variant="borderless"
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
onClick={handleOpenCreator}
|
onClick={handleOpenCreator}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Create Environment Variable
|
Create Environment Variable
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||||
import { useAppClient } from '@/hooks/useAppClient';
|
import { useAppClient } from '@/hooks/useAppClient';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
@@ -19,9 +20,10 @@ import generateAppServiceUrl, {
|
|||||||
defaultLocalBackendSlugs,
|
defaultLocalBackendSlugs,
|
||||||
defaultRemoteBackendSlugs,
|
defaultRemoteBackendSlugs,
|
||||||
} from '@/utils/common/generateAppServiceUrl';
|
} from '@/utils/common/generateAppServiceUrl';
|
||||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
import { getHasuraConsoleServiceUrl } from '@/utils/env';
|
||||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||||
import { useGetAppInjectedVariablesQuery } from '@/utils/__generated__/graphql';
|
import getJwtSecretsWithoutFalsyValues from '@/utils/settings/getJwtSecretsWithoutFalsyValues';
|
||||||
|
import { useGetEnvironmentVariablesQuery } from '@/utils/__generated__/graphql';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment, useState } from 'react';
|
||||||
|
|
||||||
export default function SystemEnvironmentVariableSettings() {
|
export default function SystemEnvironmentVariableSettings() {
|
||||||
@@ -29,10 +31,22 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
|
||||||
|
|
||||||
const { openDialog } = useDialog();
|
const { openDialog } = useDialog();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetAppInjectedVariablesQuery({
|
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { jwtSecrets, webhookSecret, adminSecret } = data?.config?.hasura || {};
|
||||||
|
const jwtSecretsWithoutFalsyValues = getJwtSecretsWithoutFalsyValues(
|
||||||
|
jwtSecrets || [],
|
||||||
|
);
|
||||||
|
const stringifiedJwtSecrets =
|
||||||
|
jwtSecretsWithoutFalsyValues.length === 1
|
||||||
|
? JSON.stringify(jwtSecretsWithoutFalsyValues[0], null, 2)
|
||||||
|
: JSON.stringify(jwtSecretsWithoutFalsyValues, null, 2);
|
||||||
|
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
|
|
||||||
const appClient = useAppClient();
|
const appClient = useAppClient();
|
||||||
@@ -63,10 +77,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
component: (
|
component: (
|
||||||
<EditJwtSecretForm
|
<EditJwtSecretForm disabled jwtSecret={stringifiedJwtSecrets} />
|
||||||
disabled
|
|
||||||
jwtSecret={data?.app?.hasuraGraphqlJwtSecret}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,9 +94,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
</Text>
|
</Text>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
component: (
|
component: <EditJwtSecretForm jwtSecret={stringifiedJwtSecrets} />,
|
||||||
<EditJwtSecretForm jwtSecret={data?.app?.hasuraGraphqlJwtSecret} />
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +109,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
key: 'NHOST_HASURA_URL',
|
key: 'NHOST_HASURA_URL',
|
||||||
value:
|
value:
|
||||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||||
? `${LOCAL_HASURA_URL}/console`
|
? `${getHasuraConsoleServiceUrl()}/console`
|
||||||
: generateAppServiceUrl(
|
: generateAppServiceUrl(
|
||||||
currentApplication?.subdomain,
|
currentApplication?.subdomain,
|
||||||
currentApplication?.region.awsName,
|
currentApplication?.region.awsName,
|
||||||
@@ -137,7 +146,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
<Text className="truncate" color="secondary">
|
<Text className="truncate" color="secondary">
|
||||||
{showAdminSecret ? (
|
{showAdminSecret ? (
|
||||||
<InlineCode className="!text-sm font-medium">
|
<InlineCode className="!text-sm font-medium">
|
||||||
{currentApplication?.hasuraGraphqlAdminSecret}
|
{adminSecret}
|
||||||
</InlineCode>
|
</InlineCode>
|
||||||
) : (
|
) : (
|
||||||
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
||||||
@@ -170,7 +179,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
<Text className="truncate" color="secondary">
|
<Text className="truncate" color="secondary">
|
||||||
{showWebhookSecret ? (
|
{showWebhookSecret ? (
|
||||||
<InlineCode className="!text-sm font-medium">
|
<InlineCode className="!text-sm font-medium">
|
||||||
{data?.app?.webhookSecret}
|
{webhookSecret}
|
||||||
</InlineCode>
|
</InlineCode>
|
||||||
) : (
|
) : (
|
||||||
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
'●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●'
|
||||||
@@ -234,6 +243,7 @@ export default function SystemEnvironmentVariableSettings() {
|
|||||||
variant="borderless"
|
variant="borderless"
|
||||||
onClick={showEditJwtSecretModal}
|
onClick={showEditJwtSecretModal}
|
||||||
size="small"
|
size="small"
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Edit JWT Secret
|
Edit JWT Secret
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import InlineCode from '@/components/common/InlineCode';
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import { Alert } from '@/ui/Alert';
|
import { Alert } from '@/ui/Alert';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -20,6 +22,7 @@ export interface BaseDirectoryFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BaseDirectorySettings() {
|
export default function BaseDirectorySettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateApp] = useUpdateAppMutation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -54,7 +57,9 @@ export default function BaseDirectorySettings() {
|
|||||||
{
|
{
|
||||||
loading: `The base directory is being updated...`,
|
loading: `The base directory is being updated...`,
|
||||||
success: `The base directory has been updated successfully.`,
|
success: `The base directory has been updated successfully.`,
|
||||||
error: `An error occurred while trying to update the project's base directory.`,
|
error: getServerError(
|
||||||
|
`An error occurred while trying to update the project's base directory.`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -84,9 +89,11 @@ export default function BaseDirectorySettings() {
|
|||||||
<InlineCode className="text-xs">nhost</InlineCode> folder.
|
<InlineCode className="text-xs">nhost</InlineCode> folder.
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/github-integration#base-directory"
|
docsLink="https://docs.nhost.io/platform/github-integration#base-directory"
|
||||||
className="grid grid-flow-row lg:grid-cols-5"
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import { Alert } from '@/ui/Alert';
|
import { Alert } from '@/ui/Alert';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
@@ -19,6 +21,7 @@ export interface DeploymentBranchFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DeploymentBranchSettings() {
|
export default function DeploymentBranchSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateApp] = useUpdateAppMutation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -57,7 +60,9 @@ export default function DeploymentBranchSettings() {
|
|||||||
{
|
{
|
||||||
loading: `The deployment branch is being updated...`,
|
loading: `The deployment branch is being updated...`,
|
||||||
success: `The deployment branch has been updated successfully.`,
|
success: `The deployment branch has been updated successfully.`,
|
||||||
error: `An error occurred while trying to update the project's deployment branch.`,
|
error: getServerError(
|
||||||
|
`An error occurred while trying to update the project's deployment branch.`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -79,9 +84,11 @@ export default function DeploymentBranchSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Deployment Branch"
|
title="Deployment Branch"
|
||||||
description="All commits pushed to this deployment branch will trigger a deployment. You can switch to a different branch here."
|
description="All commits pushed to this deployment branch will trigger a deployment. You can switch to a different branch here."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/github-integration#deployment-branch"
|
docsLink="https://docs.nhost.io/platform/github-integration#deployment-branch"
|
||||||
className="grid grid-flow-row lg:grid-cols-5"
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ export interface BasePermissionVariableFormProps extends DialogFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const basePermissionVariableValidationSchema = Yup.object({
|
export const basePermissionVariableValidationSchema = Yup.object({
|
||||||
key: Yup.string().required('This field is required.'),
|
key: Yup.string().label('Field Name').nullable().required(),
|
||||||
value: Yup.string().required('This field is required.'),
|
value: Yup.string().label('Path').nullable().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BasePermissionVariableFormValues = Yup.InferType<
|
export type BasePermissionVariableFormValues = Yup.InferType<
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import BasePermissionVariableForm, {
|
|||||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import getPermissionVariablesObject from '@/utils/settings/getPermissionVariablesObject';
|
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAppCustomClaimsQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -32,11 +32,14 @@ export default function CreatePermissionVariableForm({
|
|||||||
}: CreatePermissionVariableFormProps) {
|
}: CreatePermissionVariableFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, error, loading } = useGetAppCustomClaimsQuery({
|
const { data, error, loading } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { customClaims: permissionVariables } =
|
||||||
|
data?.config?.auth?.session?.accessToken || {};
|
||||||
|
|
||||||
const form = useForm<BasePermissionVariableFormValues>({
|
const form = useForm<BasePermissionVariableFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
key: '',
|
key: '',
|
||||||
@@ -46,8 +49,8 @@ export default function CreatePermissionVariableForm({
|
|||||||
resolver: yupResolver(basePermissionVariableValidationSchema),
|
resolver: yupResolver(basePermissionVariableValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: ['getAppCustomClaims'],
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -61,9 +64,8 @@ export default function CreatePermissionVariableForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availablePermissionVariables = getPermissionVariablesArray(
|
const availablePermissionVariables =
|
||||||
data?.app?.authJwtCustomClaims,
|
getAllPermissionVariables(permissionVariables);
|
||||||
);
|
|
||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
key,
|
key,
|
||||||
@@ -79,26 +81,29 @@ export default function CreatePermissionVariableForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissionVariablesObject = getPermissionVariablesObject(
|
const existingPermissionVariables =
|
||||||
availablePermissionVariables.filter(
|
permissionVariables?.map((permissionVariable) => ({
|
||||||
(permissionVariable) => !permissionVariable.isSystemClaim,
|
key: permissionVariable.key,
|
||||||
),
|
value: permissionVariable.value,
|
||||||
);
|
})) || [];
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authJwtCustomClaims: {
|
auth: {
|
||||||
...permissionVariablesObject,
|
session: {
|
||||||
[key]: value,
|
accessToken: {
|
||||||
|
customClaims: [...existingPermissionVariables, { key, value }],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Creating permission variable...',
|
loading: 'Creating permission variable...',
|
||||||
success: 'Permission variable has been created successfully.',
|
success: 'Permission variable has been created successfully.',
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import BasePermissionVariableForm, {
|
|||||||
basePermissionVariableValidationSchema,
|
basePermissionVariableValidationSchema,
|
||||||
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
} from '@/components/settings/permissions/BasePermissionVariableForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { CustomClaim } from '@/types/application';
|
import type { PermissionVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import getPermissionVariables from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
import getPermissionVariablesObject from '@/utils/settings/getPermissionVariablesObject';
|
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAppCustomClaimsQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -24,7 +24,7 @@ export interface EditPermissionVariableFormProps
|
|||||||
/**
|
/**
|
||||||
* The permission variable to be edited.
|
* The permission variable to be edited.
|
||||||
*/
|
*/
|
||||||
originalVariable: CustomClaim;
|
originalVariable: PermissionVariable;
|
||||||
/**
|
/**
|
||||||
* Function to be called when the form is submitted.
|
* Function to be called when the form is submitted.
|
||||||
*/
|
*/
|
||||||
@@ -38,11 +38,14 @@ export default function EditPermissionVariableForm({
|
|||||||
}: EditPermissionVariableFormProps) {
|
}: EditPermissionVariableFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const { data, error, loading } = useGetAppCustomClaimsQuery({
|
const { data, error, loading } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { customClaims: permissionVariables } =
|
||||||
|
data?.config?.auth?.session?.accessToken || {};
|
||||||
|
|
||||||
const form = useForm<BasePermissionVariableFormValues>({
|
const form = useForm<BasePermissionVariableFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
key: originalVariable.key || '',
|
key: originalVariable.key || '',
|
||||||
@@ -52,8 +55,8 @@ export default function EditPermissionVariableForm({
|
|||||||
resolver: yupResolver(basePermissionVariableValidationSchema),
|
resolver: yupResolver(basePermissionVariableValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: ['getAppCustomClaims'],
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -67,9 +70,8 @@ export default function EditPermissionVariableForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availablePermissionVariables = getPermissionVariables(
|
const availablePermissionVariables =
|
||||||
data?.app?.authJwtCustomClaims,
|
getAllPermissionVariables(permissionVariables);
|
||||||
);
|
|
||||||
|
|
||||||
async function handleSubmit({
|
async function handleSubmit({
|
||||||
key,
|
key,
|
||||||
@@ -92,36 +94,43 @@ export default function EditPermissionVariableForm({
|
|||||||
(permissionVariable) => permissionVariable.key === originalVariable.key,
|
(permissionVariable) => permissionVariable.key === originalVariable.key,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedPermissionVariables = availablePermissionVariables.map(
|
const updatedPermissionVariables = availablePermissionVariables
|
||||||
(permissionVariable, index) => {
|
.map((permissionVariable, index) => {
|
||||||
if (index === originalPermissionVariableIndex) {
|
if (permissionVariable.isSystemVariable) {
|
||||||
return { key, value };
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return permissionVariable;
|
if (index === originalPermissionVariableIndex) {
|
||||||
},
|
return {
|
||||||
);
|
key,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const permissionVariablesObject = getPermissionVariablesObject(
|
return {
|
||||||
updatedPermissionVariables.filter(
|
key: permissionVariable.key,
|
||||||
(permissionVariable) => !permissionVariable.isSystemClaim,
|
value: permissionVariable.value,
|
||||||
),
|
};
|
||||||
);
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authJwtCustomClaims: {
|
auth: {
|
||||||
...permissionVariablesObject,
|
session: {
|
||||||
[key]: value,
|
accessToken: {
|
||||||
|
customClaims: updatedPermissionVariables,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Updating permission variable...',
|
loading: 'Updating permission variable...',
|
||||||
success: 'Permission variable has been updated successfully.',
|
success: 'Permission variable has been updated successfully.',
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||||
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { CustomClaim } from '@/types/application';
|
import type { PermissionVariable } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Box from '@/ui/v2/Box';
|
import Box from '@/ui/v2/Box';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
@@ -17,35 +18,33 @@ import List from '@/ui/v2/List';
|
|||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import Tooltip from '@/ui/v2/Tooltip';
|
import Tooltip from '@/ui/v2/Tooltip';
|
||||||
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
|
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetAppCustomClaimsQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export interface PermissionVariableSettingsFormValues {
|
|
||||||
/**
|
|
||||||
* Permission variables.
|
|
||||||
*/
|
|
||||||
authJwtCustomClaims: CustomClaim[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PermissionVariableSettings() {
|
export default function PermissionVariableSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { openDialog, openAlertDialog } = useDialog();
|
const { openDialog, openAlertDialog } = useDialog();
|
||||||
|
|
||||||
const { data, loading, error } = useGetAppCustomClaimsQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
fetchPolicy: 'cache-only',
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const { customClaims: permissionVariables } =
|
||||||
refetchQueries: ['getAppCustomClaims'],
|
data?.config?.auth?.session?.accessToken || {};
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -58,32 +57,35 @@ export default function PermissionVariableSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteVariable({ key }: CustomClaim) {
|
async function handleDeleteVariable({ id }: PermissionVariable) {
|
||||||
const filteredCustomClaims = Object.keys(
|
const updateConfigPromise = updateConfig({
|
||||||
data?.app?.authJwtCustomClaims,
|
|
||||||
).filter((customClaimKey) => customClaimKey !== key);
|
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authJwtCustomClaims: filteredCustomClaims.reduce(
|
auth: {
|
||||||
(customClaims, currentKey) => ({
|
session: {
|
||||||
...customClaims,
|
accessToken: {
|
||||||
[currentKey]: data?.app?.authJwtCustomClaims[currentKey],
|
customClaims: permissionVariables
|
||||||
}),
|
?.filter((permissionVariable) => permissionVariable.id !== id)
|
||||||
{},
|
.map((permissionVariable) => ({
|
||||||
),
|
key: permissionVariable.key,
|
||||||
|
value: permissionVariable.value,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Deleting permission variable...',
|
loading: 'Deleting permission variable...',
|
||||||
success: 'Permission variable has been deleted successfully.',
|
success: 'Permission variable has been deleted successfully.',
|
||||||
error: 'An error occurred while trying to delete permission variable.',
|
error: getServerError(
|
||||||
|
'An error occurred while trying to delete permission variable.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -100,7 +102,7 @@ export default function PermissionVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenEditor(originalVariable: CustomClaim) {
|
function handleOpenEditor(originalVariable: PermissionVariable) {
|
||||||
openDialog({
|
openDialog({
|
||||||
title: 'Edit Permission Variable',
|
title: 'Edit Permission Variable',
|
||||||
component: (
|
component: (
|
||||||
@@ -113,7 +115,7 @@ export default function PermissionVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConfirmDelete(originalVariable: CustomClaim) {
|
function handleConfirmDelete(originalVariable: PermissionVariable) {
|
||||||
openAlertDialog({
|
openAlertDialog({
|
||||||
title: 'Delete Permission Variable',
|
title: 'Delete Permission Variable',
|
||||||
payload: (
|
payload: (
|
||||||
@@ -131,9 +133,8 @@ export default function PermissionVariableSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const availablePermissionVariables = getPermissionVariablesArray(
|
const availablePermissionVariables =
|
||||||
data?.app?.authJwtCustomClaims,
|
getAllPermissionVariables(permissionVariables);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
@@ -151,28 +152,33 @@ export default function PermissionVariableSettings() {
|
|||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
<List>
|
<List>
|
||||||
{availablePermissionVariables.map((customClaim, index) => (
|
{availablePermissionVariables.map((permissionVariable, index) => (
|
||||||
<Fragment key={customClaim.key}>
|
<Fragment key={permissionVariable.id}>
|
||||||
<ListItem.Root
|
<ListItem.Root
|
||||||
className="grid grid-cols-2 px-4"
|
className="grid grid-cols-2 px-4"
|
||||||
secondaryAction={
|
secondaryAction={
|
||||||
<Dropdown.Root>
|
<Dropdown.Root>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
customClaim.isSystemClaim
|
permissionVariable.isSystemVariable
|
||||||
? "You can't edit system permission variables"
|
? "You can't edit system permission variables"
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
disableHoverListener={!customClaim.isSystemClaim}
|
disableHoverListener={
|
||||||
hasDisabledChildren={customClaim.isSystemClaim}
|
!permissionVariable.isSystemVariable
|
||||||
|
}
|
||||||
|
hasDisabledChildren={permissionVariable.isSystemVariable}
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<Dropdown.Trigger asChild hideChevron>
|
<Dropdown.Trigger asChild hideChevron>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
disabled={customClaim.isSystemClaim}
|
disabled={
|
||||||
|
permissionVariable.isSystemVariable ||
|
||||||
|
maintenanceActive
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<DotsVerticalIcon />
|
<DotsVerticalIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -192,7 +198,7 @@ export default function PermissionVariableSettings() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => handleOpenEditor(customClaim)}
|
onClick={() => handleOpenEditor(permissionVariable)}
|
||||||
>
|
>
|
||||||
<Text className="font-medium">Edit</Text>
|
<Text className="font-medium">Edit</Text>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
@@ -200,7 +206,7 @@ export default function PermissionVariableSettings() {
|
|||||||
<Divider component="li" />
|
<Divider component="li" />
|
||||||
|
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={() => handleConfirmDelete(customClaim)}
|
onClick={() => handleConfirmDelete(permissionVariable)}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
@@ -218,15 +224,17 @@ export default function PermissionVariableSettings() {
|
|||||||
<ListItem.Text
|
<ListItem.Text
|
||||||
primary={
|
primary={
|
||||||
<>
|
<>
|
||||||
X-Hasura-{customClaim.key}{' '}
|
X-Hasura-{permissionVariable.key}{' '}
|
||||||
{customClaim.isSystemClaim && (
|
{permissionVariable.isSystemVariable && (
|
||||||
<LockIcon className="h-4 w-4" />
|
<LockIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text className="font-medium">user.{customClaim.value}</Text>
|
<Text className="font-medium">
|
||||||
|
user.{permissionVariable.value}
|
||||||
|
</Text>
|
||||||
</ListItem.Root>
|
</ListItem.Root>
|
||||||
|
|
||||||
<Divider
|
<Divider
|
||||||
@@ -246,6 +254,7 @@ export default function PermissionVariableSettings() {
|
|||||||
variant="borderless"
|
variant="borderless"
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
onClick={handleOpenCreator}
|
onClick={handleOpenCreator}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Create Permission Variable
|
Create Permission Variable
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export * from './PermissionVariableSettings';
|
|
||||||
export { default } from './PermissionVariableSettings';
|
export { default } from './PermissionVariableSettings';
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import BaseRoleForm, {
|
|||||||
} from '@/components/settings/roles/BaseRoleForm';
|
} from '@/components/settings/roles/BaseRoleForm';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetRolesQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -30,10 +32,11 @@ export default function CreateRoleForm({
|
|||||||
...props
|
...props
|
||||||
}: CreateRoleFormProps) {
|
}: CreateRoleFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetRolesQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
const { allowed: allowedRoles } = data?.config?.auth?.user?.roles || {};
|
||||||
|
|
||||||
const form = useForm<BaseRoleFormValues>({
|
const form = useForm<BaseRoleFormValues>({
|
||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
@@ -41,7 +44,9 @@ export default function CreateRoleForm({
|
|||||||
resolver: yupResolver(baseRoleFormValidationSchema),
|
resolver: yupResolver(baseRoleFormValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['getRoles'] });
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <ActivityIndicator delay={1000} label="Loading roles..." />;
|
return <ActivityIndicator delay={1000} label="Loading roles..." />;
|
||||||
@@ -52,7 +57,7 @@ export default function CreateRoleForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
const availableRoles = getUserRoles(allowedRoles);
|
||||||
|
|
||||||
async function handleSubmit({ name }: BaseRoleFormValues) {
|
async function handleSubmit({ name }: BaseRoleFormValues) {
|
||||||
if (availableRoles.some((role) => role.name === name)) {
|
if (availableRoles.some((role) => role.name === name)) {
|
||||||
@@ -61,26 +66,40 @@ export default function CreateRoleForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updatedAllowedRoles = allowedRoles ? [...allowedRoles, name] : [name];
|
||||||
|
|
||||||
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authUserDefaultAllowedRoles: `${data?.app?.authUserDefaultAllowedRoles},${name}`,
|
auth: {
|
||||||
|
user: {
|
||||||
|
roles: {
|
||||||
|
allowed: updatedAllowedRoles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppPromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Creating role...',
|
{
|
||||||
success: 'Role has been created successfully.',
|
loading: 'Creating role...',
|
||||||
error: 'An error occurred while trying to create the role.',
|
success: 'Role has been created successfully.',
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
'An error occurred while trying to create the role.',
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
await onSubmit?.();
|
onSubmit?.();
|
||||||
|
} catch (updateConfigError) {
|
||||||
|
console.error(updateConfigError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import BaseRoleForm, {
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { Role } from '@/types/application';
|
import type { Role } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetRolesQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@@ -36,11 +38,14 @@ export default function EditRoleForm({
|
|||||||
...props
|
...props
|
||||||
}: EditRoleFormProps) {
|
}: EditRoleFormProps) {
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { data, loading, error } = useGetRolesQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { allowed: allowedRoles, default: defaultRole } =
|
||||||
|
data?.config?.auth?.user?.roles || {};
|
||||||
|
|
||||||
const form = useForm<BaseRoleFormValues>({
|
const form = useForm<BaseRoleFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: originalRole.name || '',
|
name: originalRole.name || '',
|
||||||
@@ -49,8 +54,8 @@ export default function EditRoleForm({
|
|||||||
resolver: yupResolver(baseRoleFormValidationSchema),
|
resolver: yupResolver(baseRoleFormValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: ['getRoles'],
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -62,7 +67,7 @@ export default function EditRoleForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { setError } = form;
|
const { setError } = form;
|
||||||
const availableRoles = getUserRoles(data?.app?.authUserDefaultAllowedRoles);
|
const availableRoles = getUserRoles(allowedRoles);
|
||||||
|
|
||||||
async function handleSubmit({ name }: BaseRoleFormValues) {
|
async function handleSubmit({ name }: BaseRoleFormValues) {
|
||||||
if (
|
if (
|
||||||
@@ -75,47 +80,55 @@ export default function EditRoleForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAllowedRolesList =
|
const defaultAllowedRolesList = allowedRoles || [];
|
||||||
data?.app?.authUserDefaultAllowedRoles.split(',') || [];
|
|
||||||
|
|
||||||
const originalRoleIndex = defaultAllowedRolesList.findIndex(
|
const originalRoleIndex = defaultAllowedRolesList.findIndex(
|
||||||
(role) => role.trim() === originalRole.name,
|
(role) => role.trim() === originalRole.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
const updatedDefaultAllowedRoles = defaultAllowedRolesList
|
const updatedDefaultAllowedRoles = defaultAllowedRolesList.map(
|
||||||
.map((role, index) => {
|
(role, index) => {
|
||||||
if (index === originalRoleIndex) {
|
if (index === originalRoleIndex) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return role;
|
return role;
|
||||||
})
|
},
|
||||||
.join(',');
|
);
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authUserDefaultRole:
|
auth: {
|
||||||
data?.app?.authUserDefaultRole === originalRole.name
|
user: {
|
||||||
? name
|
roles: {
|
||||||
: data?.app?.authUserDefaultRole,
|
default: defaultRole === originalRole.name ? name : defaultRole,
|
||||||
authUserDefaultAllowedRoles: updatedDefaultAllowedRoles,
|
allowed: updatedDefaultAllowedRoles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppPromise,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: 'Updating role...',
|
{
|
||||||
success: 'Role has been updated successfully.',
|
loading: 'Updating role...',
|
||||||
error: 'An error occurred while trying to update the role.',
|
success: 'Role has been updated successfully.',
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
'An error occurred while trying to update the role.',
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
await onSubmit?.();
|
onSubmit?.();
|
||||||
|
} catch (updateConfigError) {
|
||||||
|
console.error(updateConfigError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
|||||||
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
|
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
|
||||||
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
|
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import type { Role } from '@/types/application';
|
import type { Role } from '@/types/application';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -17,11 +18,13 @@ import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
|||||||
import List from '@/ui/v2/List';
|
import List from '@/ui/v2/List';
|
||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useGetRolesQuery,
|
GetRolesPermissionsDocument,
|
||||||
useUpdateAppMutation,
|
useGetRolesPermissionsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -39,15 +42,20 @@ export interface RoleSettingsFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function RoleSettings() {
|
export default function RoleSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const { openDialog, openAlertDialog } = useDialog();
|
const { openDialog, openAlertDialog } = useDialog();
|
||||||
|
|
||||||
const { data, loading, error } = useGetRolesQuery({
|
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const { allowed: allowedRoles, default: defaultRole } =
|
||||||
refetchQueries: ['getRoles'],
|
data?.config?.auth?.user?.roles || {};
|
||||||
|
|
||||||
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetRolesPermissionsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -59,51 +67,60 @@ export default function RoleSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSetAsDefault({ name }: Role) {
|
async function handleSetAsDefault({ name }: Role) {
|
||||||
const updateAppPromise = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authUserDefaultRole: name,
|
auth: {
|
||||||
|
user: {
|
||||||
|
roles: {
|
||||||
|
allowed: allowedRoles,
|
||||||
|
default: name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Updating default role...',
|
loading: 'Updating default role...',
|
||||||
success: 'Default role has been updated successfully.',
|
success: 'Default role has been updated successfully.',
|
||||||
error: 'An error occurred while trying to update the default role.',
|
error: getServerError(
|
||||||
|
'An error occurred while trying to update the default role.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteRole({ name }: Role) {
|
async function handleDeleteRole({ name }: Role) {
|
||||||
const filteredRoles = data?.app?.authUserDefaultAllowedRoles
|
const updateConfigPromise = updateConfig({
|
||||||
.split(',')
|
|
||||||
.filter((role) => role !== name)
|
|
||||||
.join(',');
|
|
||||||
|
|
||||||
const updateAppPromise = updateApp({
|
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication?.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authUserDefaultAllowedRoles: filteredRoles,
|
auth: {
|
||||||
authUserDefaultRole:
|
user: {
|
||||||
name === data?.app?.authUserDefaultRole
|
roles: {
|
||||||
? 'user'
|
allowed: allowedRoles.filter((role) => role !== name),
|
||||||
: data?.app?.authUserDefaultRole,
|
default: name === defaultRole ? 'user' : defaultRole,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
await toast.promise(
|
||||||
updateAppPromise,
|
updateConfigPromise,
|
||||||
{
|
{
|
||||||
loading: 'Deleting allowed role...',
|
loading: 'Deleting allowed role...',
|
||||||
success: 'Allowed Role has been deleted successfully.',
|
success: 'Allowed Role has been deleted successfully.',
|
||||||
error: 'An error occurred while trying to delete the allowed role.',
|
error: getServerError(
|
||||||
|
'An error occurred while trying to delete the allowed role.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -148,9 +165,7 @@ export default function RoleSettings() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const availableAllowedRoles = getUserRoles(
|
const availableAllowedRoles = getUserRoles(allowedRoles);
|
||||||
data?.app?.authUserDefaultAllowedRoles,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
@@ -158,7 +173,10 @@ export default function RoleSettings() {
|
|||||||
description="Allowed roles are roles users get automatically when they sign up."
|
description="Allowed roles are roles users get automatically when they sign up."
|
||||||
docsLink="https://docs.nhost.io/authentication/users#allowed-roles"
|
docsLink="https://docs.nhost.io/authentication/users#allowed-roles"
|
||||||
rootClassName="gap-0"
|
rootClassName="gap-0"
|
||||||
className="my-2 px-0"
|
className={twMerge(
|
||||||
|
'my-2 px-0',
|
||||||
|
availableAllowedRoles.length === 0 && 'gap-2',
|
||||||
|
)}
|
||||||
slotProps={{ submitButton: { className: 'invisible' } }}
|
slotProps={{ submitButton: { className: 'invisible' } }}
|
||||||
>
|
>
|
||||||
<Box className="border-b-1 px-4 py-3">
|
<Box className="border-b-1 px-4 py-3">
|
||||||
@@ -166,103 +184,110 @@ export default function RoleSettings() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
<List>
|
{availableAllowedRoles.length > 0 && (
|
||||||
{availableAllowedRoles.map((role, index) => (
|
<List>
|
||||||
<Fragment key={role.name}>
|
{availableAllowedRoles.map((role, index) => (
|
||||||
<ListItem.Root
|
<Fragment key={role.name}>
|
||||||
className="px-4"
|
<ListItem.Root
|
||||||
secondaryAction={
|
className="px-4"
|
||||||
<Dropdown.Root>
|
secondaryAction={
|
||||||
<Dropdown.Trigger
|
<Dropdown.Root>
|
||||||
asChild
|
<Dropdown.Trigger
|
||||||
hideChevron
|
asChild
|
||||||
className="absolute right-4 top-1/2 -translate-y-1/2"
|
hideChevron
|
||||||
>
|
className="absolute right-4 top-1/2 -translate-y-1/2"
|
||||||
<IconButton variant="borderless" color="secondary">
|
|
||||||
<DotsVerticalIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Dropdown.Trigger>
|
|
||||||
|
|
||||||
<Dropdown.Content
|
|
||||||
menu
|
|
||||||
PaperProps={{ className: 'w-32' }}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Dropdown.Item onClick={() => handleSetAsDefault(role)}>
|
|
||||||
<Text className="font-medium">Set as Default</Text>
|
|
||||||
</Dropdown.Item>
|
|
||||||
|
|
||||||
<Divider component="li" />
|
|
||||||
|
|
||||||
<Dropdown.Item
|
|
||||||
disabled={role.isSystemRole}
|
|
||||||
onClick={() => handleOpenEditor(role)}
|
|
||||||
>
|
>
|
||||||
<Text className="font-medium">Edit</Text>
|
<IconButton
|
||||||
</Dropdown.Item>
|
variant="borderless"
|
||||||
|
color="secondary"
|
||||||
|
disabled={maintenanceActive}
|
||||||
|
>
|
||||||
|
<DotsVerticalIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Dropdown.Trigger>
|
||||||
|
|
||||||
<Divider component="li" />
|
<Dropdown.Content
|
||||||
|
menu
|
||||||
<Dropdown.Item
|
PaperProps={{ className: 'w-32' }}
|
||||||
disabled={role.isSystemRole}
|
anchorOrigin={{
|
||||||
onClick={() => handleConfirmDelete(role)}
|
vertical: 'bottom',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'right',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text className="font-medium" color="error">
|
<Dropdown.Item onClick={() => handleSetAsDefault(role)}>
|
||||||
Delete
|
<Text className="font-medium">Set as Default</Text>
|
||||||
</Text>
|
</Dropdown.Item>
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Content>
|
|
||||||
</Dropdown.Root>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListItem.Text
|
|
||||||
primaryTypographyProps={{
|
|
||||||
className:
|
|
||||||
'inline-grid grid-flow-col gap-1 items-center h-6 font-medium',
|
|
||||||
}}
|
|
||||||
primary={
|
|
||||||
<>
|
|
||||||
{role.name}
|
|
||||||
|
|
||||||
{role.isSystemRole && <LockIcon className="h-4 w-4" />}
|
<Divider component="li" />
|
||||||
|
|
||||||
{data?.app?.authUserDefaultRole === role.name && (
|
<Dropdown.Item
|
||||||
<Chip
|
disabled={role.isSystemRole}
|
||||||
component="span"
|
onClick={() => handleOpenEditor(role)}
|
||||||
color="info"
|
>
|
||||||
size="small"
|
<Text className="font-medium">Edit</Text>
|
||||||
label="Default"
|
</Dropdown.Item>
|
||||||
/>
|
|
||||||
)}
|
<Divider component="li" />
|
||||||
</>
|
|
||||||
|
<Dropdown.Item
|
||||||
|
disabled={role.isSystemRole}
|
||||||
|
onClick={() => handleConfirmDelete(role)}
|
||||||
|
>
|
||||||
|
<Text className="font-medium" color="error">
|
||||||
|
Delete
|
||||||
|
</Text>
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Content>
|
||||||
|
</Dropdown.Root>
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
</ListItem.Root>
|
<ListItem.Text
|
||||||
|
primaryTypographyProps={{
|
||||||
|
className:
|
||||||
|
'inline-grid grid-flow-col gap-1 items-center h-6 font-medium',
|
||||||
|
}}
|
||||||
|
primary={
|
||||||
|
<>
|
||||||
|
{role.name}
|
||||||
|
|
||||||
<Divider
|
{role.isSystemRole && <LockIcon className="h-4 w-4" />}
|
||||||
component="li"
|
|
||||||
className={twMerge(
|
{defaultRole === role.name && (
|
||||||
index === availableAllowedRoles.length - 1
|
<Chip
|
||||||
? '!mt-4'
|
component="span"
|
||||||
: '!my-4',
|
color="info"
|
||||||
)}
|
size="small"
|
||||||
/>
|
label="Default"
|
||||||
</Fragment>
|
/>
|
||||||
))}
|
)}
|
||||||
</List>
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem.Root>
|
||||||
|
|
||||||
|
<Divider
|
||||||
|
component="li"
|
||||||
|
className={twMerge(
|
||||||
|
index === availableAllowedRoles.length - 1
|
||||||
|
? '!mt-4'
|
||||||
|
: '!my-4',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="mx-4 justify-self-start"
|
className="mx-4 justify-self-start"
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
startIcon={<PlusIcon />}
|
startIcon={<PlusIcon />}
|
||||||
onClick={handleOpenCreator}
|
onClick={handleOpenCreator}
|
||||||
|
disabled={maintenanceActive}
|
||||||
>
|
>
|
||||||
Create Allowed Role
|
Create Allowed Role
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { useDialog } from '@/components/common/DialogProvider';
|
||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import Button from '@/ui/v2/Button';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
export interface BaseSecretFormProps {
|
||||||
|
/**
|
||||||
|
* Determines the mode of the form.
|
||||||
|
*
|
||||||
|
* @default 'edit'
|
||||||
|
*/
|
||||||
|
mode?: 'edit' | 'create';
|
||||||
|
/**
|
||||||
|
* Function to be called when the form is submitted.
|
||||||
|
*/
|
||||||
|
onSubmit: (values: BaseSecretFormValues) => void;
|
||||||
|
/**
|
||||||
|
* Function to be called when the operation is cancelled.
|
||||||
|
*/
|
||||||
|
onCancel?: VoidFunction;
|
||||||
|
/**
|
||||||
|
* Submit button text.
|
||||||
|
*
|
||||||
|
* @default 'Save'
|
||||||
|
*/
|
||||||
|
submitButtonText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const baseSecretFormValidationSchema = Yup.object({
|
||||||
|
name: Yup.string()
|
||||||
|
.required('This field is required.')
|
||||||
|
.test(
|
||||||
|
'isSecretValid',
|
||||||
|
'A name must start with a letter and can only contain letters, numbers, and underscores.',
|
||||||
|
(value) => /^[a-zA-Z]{1,}[a-zA-Z0-9_]*$/i.test(value),
|
||||||
|
),
|
||||||
|
value: Yup.string().required('This field is required.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BaseSecretFormValues = Yup.InferType<
|
||||||
|
typeof baseSecretFormValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default function BaseSecretForm({
|
||||||
|
mode = 'edit',
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
submitButtonText = 'Save',
|
||||||
|
}: BaseSecretFormProps) {
|
||||||
|
const { onDirtyStateChange } = useDialog();
|
||||||
|
const form = useFormContext<BaseSecretFormValues>();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors, dirtyFields, isSubmitting },
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
// react-hook-form's isDirty gets true even if an input field is focused, then
|
||||||
|
// immediately unfocused - we can't rely on that information
|
||||||
|
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirtyStateChange(isDirty, 'dialog');
|
||||||
|
}, [isDirty, onDirtyStateChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
||||||
|
<Form onSubmit={onSubmit} className="grid grid-flow-row gap-4">
|
||||||
|
<Input
|
||||||
|
{...register('name')}
|
||||||
|
id="name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="EXAMPLE_NAME"
|
||||||
|
hideEmptyHelperText
|
||||||
|
error={!!errors.name}
|
||||||
|
helperText={errors?.name?.message}
|
||||||
|
fullWidth
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus={mode === 'create'}
|
||||||
|
disabled={mode === 'edit'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
{...register('value')}
|
||||||
|
id="value"
|
||||||
|
label="Value"
|
||||||
|
placeholder="Enter value"
|
||||||
|
hideEmptyHelperText
|
||||||
|
error={!!errors.value}
|
||||||
|
helperText={errors?.value?.message}
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={5}
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus={mode === 'edit'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="grid grid-flow-row gap-2">
|
||||||
|
<Button type="submit" loading={isSubmitting}>
|
||||||
|
{submitButtonText}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="outlined" color="secondary" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './BaseSecretForm';
|
||||||
|
export { default } from './BaseSecretForm';
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type {
|
||||||
|
BaseSecretFormProps,
|
||||||
|
BaseSecretFormValues,
|
||||||
|
} from '@/components/settings/secrets/BaseSecretForm';
|
||||||
|
import BaseSecretForm, {
|
||||||
|
baseSecretFormValidationSchema,
|
||||||
|
} from '@/components/settings/secrets/BaseSecretForm';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import {
|
||||||
|
GetSecretsDocument,
|
||||||
|
useInsertSecretMutation,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface CreateSecretFormProps
|
||||||
|
extends Pick<BaseSecretFormProps, 'onCancel'> {
|
||||||
|
/**
|
||||||
|
* Function to be called when the form is submitted.
|
||||||
|
*/
|
||||||
|
onSubmit?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CreateSecretForm({
|
||||||
|
onSubmit,
|
||||||
|
...props
|
||||||
|
}: CreateSecretFormProps) {
|
||||||
|
const form = useForm<BaseSecretFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
name: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
resolver: yupResolver(baseSecretFormValidationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [insertSecret] = useInsertSecretMutation({
|
||||||
|
refetchQueries: [GetSecretsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit({ name, value }: BaseSecretFormValues) {
|
||||||
|
const insertSecretPromise = insertSecret({
|
||||||
|
variables: {
|
||||||
|
appId: currentApplication?.id,
|
||||||
|
secret: {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await toast.promise(
|
||||||
|
insertSecretPromise,
|
||||||
|
{
|
||||||
|
loading: 'Creating secret...',
|
||||||
|
success: 'Secret has been created successfully.',
|
||||||
|
error: (arg: Error) =>
|
||||||
|
arg?.message
|
||||||
|
? `Error: ${arg?.message}`
|
||||||
|
: 'An error occurred while creating the secret.',
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
|
onSubmit?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<BaseSecretForm
|
||||||
|
mode="create"
|
||||||
|
submitButtonText="Create"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './CreateSecretForm';
|
||||||
|
export { default } from './CreateSecretForm';
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import type {
|
||||||
|
BaseSecretFormProps,
|
||||||
|
BaseSecretFormValues,
|
||||||
|
} from '@/components/settings/secrets/BaseSecretForm';
|
||||||
|
import BaseSecretForm, {
|
||||||
|
baseSecretFormValidationSchema,
|
||||||
|
} from '@/components/settings/secrets/BaseSecretForm';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import type { Secret } from '@/types/application';
|
||||||
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import {
|
||||||
|
GetSecretsDocument,
|
||||||
|
useUpdateSecretMutation,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface EditSecretFormProps
|
||||||
|
extends Pick<BaseSecretFormProps, 'onCancel'> {
|
||||||
|
/**
|
||||||
|
* The secret to edit.
|
||||||
|
*/
|
||||||
|
originalSecret: Secret;
|
||||||
|
/**
|
||||||
|
* Function to be called when the form is submitted.
|
||||||
|
*/
|
||||||
|
onSubmit?: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EditSecretForm({
|
||||||
|
originalSecret,
|
||||||
|
onSubmit,
|
||||||
|
...props
|
||||||
|
}: EditSecretFormProps) {
|
||||||
|
const form = useForm<BaseSecretFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
name: originalSecret.name,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
resolver: yupResolver(baseSecretFormValidationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateSecret] = useUpdateSecretMutation({
|
||||||
|
refetchQueries: [GetSecretsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit({ name, value }: BaseSecretFormValues) {
|
||||||
|
const updateSecretPromise = updateSecret({
|
||||||
|
variables: {
|
||||||
|
appId: currentApplication?.id,
|
||||||
|
secret: {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await toast.promise(
|
||||||
|
updateSecretPromise,
|
||||||
|
{
|
||||||
|
loading: 'Updating secret...',
|
||||||
|
success: 'Secret has been updated successfully.',
|
||||||
|
error: (arg: Error) =>
|
||||||
|
arg?.message
|
||||||
|
? `Error: ${arg?.message}`
|
||||||
|
: 'An error occurred while updating the secret.',
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
|
onSubmit?.();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<BaseSecretForm
|
||||||
|
mode="edit"
|
||||||
|
submitButtonText="Save"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './EditSecretForm';
|
||||||
|
export { default } from './EditSecretForm';
|
||||||
@@ -1,45 +1,53 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface AnonymousSignInFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean(),
|
||||||
* Enables users to register as an anonymous user.
|
});
|
||||||
*/
|
|
||||||
authAnonymousUsersEnabled: boolean;
|
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export default function AnonymousSignInSettings() {
|
export default function AnonymousSignInSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { enabled } = data?.config?.auth?.method?.anonymous || {};
|
||||||
|
|
||||||
const form = useForm<AnonymousSignInFormValues>({
|
const form = useForm<AnonymousSignInFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authAnonymousUsersEnabled: data.app.authAnonymousUsersEnabled,
|
enabled,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading..."
|
label="Loading anonymous sign-in settings..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -52,26 +60,36 @@ export default function AnonymousSignInSettings() {
|
|||||||
const handlePasswordProtectionSettingsChange = async (
|
const handlePasswordProtectionSettingsChange = async (
|
||||||
values: AnonymousSignInFormValues,
|
values: AnonymousSignInFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
method: {
|
||||||
|
anonymous: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Anonymous sign-in settings are being updated...`,
|
{
|
||||||
success: `Anonymous sign-in settings have been updated successfully.`,
|
loading: `Anonymous sign-in settings are being updated...`,
|
||||||
error: `An error occurred while trying to update Anonymous sign-in settings.`,
|
success: `Anonymous sign-in settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update Anonymous sign-in settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -80,14 +98,13 @@ export default function AnonymousSignInSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Anonymous Users"
|
title="Anonymous Users"
|
||||||
description="Allow users to sign in anonymously."
|
description="Allow users to sign in anonymously."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled:
|
submitButton: {
|
||||||
form.formState.isSubmitting ||
|
disabled: !form.formState.isDirty || maintenanceActive,
|
||||||
!form.formState.isValid ||
|
loading: form.formState.isSubmitting,
|
||||||
!form.formState.isDirty,
|
},
|
||||||
}}
|
}}
|
||||||
enabled={form.getValues('authAnonymousUsersEnabled')}
|
switchId="enabled"
|
||||||
switchId="authAnonymousUsersEnabled"
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -12,60 +14,70 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface AppleProviderFormValues {
|
const validationSchema = Yup.object({
|
||||||
authAppleEnabled: boolean;
|
teamId: Yup.string().label('Team ID').when('enabled', {
|
||||||
authAppleTeamId: string;
|
is: true,
|
||||||
authAppleKeyId: string;
|
then: Yup.string().required(),
|
||||||
authAppleClientId: string;
|
}),
|
||||||
authApplePrivateKey: string;
|
keyId: Yup.string().label('Key ID').when('enabled', {
|
||||||
}
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
clientId: Yup.string().label('Client ID').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
privateKey: Yup.string().label('Private Key').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function AppleProviderSettings() {
|
export default function AppleProviderSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
data: {
|
variables: { appId: currentApplication?.id },
|
||||||
app: {
|
|
||||||
authAppleEnabled,
|
|
||||||
authAppleTeamId,
|
|
||||||
authAppleKeyId,
|
|
||||||
authAppleClientId,
|
|
||||||
authApplePrivateKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
} = useSignInMethodsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, enabled, keyId, privateKey, teamId } =
|
||||||
|
data?.config?.auth?.method?.oauth?.apple || {};
|
||||||
|
|
||||||
const form = useForm<AppleProviderFormValues>({
|
const form = useForm<AppleProviderFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authAppleTeamId,
|
teamId: teamId || '',
|
||||||
authAppleKeyId,
|
keyId: keyId || '',
|
||||||
authAppleClientId,
|
clientId: clientId || '',
|
||||||
authApplePrivateKey,
|
privateKey: privateKey || '',
|
||||||
authAppleEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Apple settings..."
|
label="Loading settings for Apple..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -76,27 +88,44 @@ export default function AppleProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authEnabled = watch('authAppleEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (values: AppleProviderFormValues) => {
|
const handleProviderUpdate = async (values: AppleProviderFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: values,
|
config: {
|
||||||
|
auth: {
|
||||||
|
method: {
|
||||||
|
oauth: {
|
||||||
|
apple: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Apple settings are being updated...`,
|
{
|
||||||
success: `Apple settings have been updated successfully.`,
|
loading: `Apple settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Apple settings.`,
|
success: `Apple settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Apple settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -107,7 +136,7 @@ export default function AppleProviderSettings() {
|
|||||||
description="Allow users to sign in with Apple."
|
description="Allow users to sign in with Apple."
|
||||||
slotProps={{
|
slotProps={{
|
||||||
submitButton: {
|
submitButton: {
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -118,55 +147,62 @@ export default function AppleProviderSettings() {
|
|||||||
? '/assets/brands/light/apple.svg'
|
? '/assets/brands/light/apple.svg'
|
||||||
: '/assets/brands/apple.svg'
|
: '/assets/brands/apple.svg'
|
||||||
}
|
}
|
||||||
switchId="authAppleEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register(`authAppleTeamId`)}
|
{...register('teamId')}
|
||||||
name="authAppleTeamId"
|
name="teamId"
|
||||||
id="authAppleTeamId"
|
id="teamId"
|
||||||
label="Team ID"
|
label="Team ID"
|
||||||
placeholder="Apple Team ID"
|
placeholder="Apple Team ID"
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.teamId}
|
||||||
|
helperText={formState.errors?.teamId?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAppleClientId')}
|
{...register('clientId')}
|
||||||
name="authAppleClientId"
|
name="clientId"
|
||||||
id="authAppleClientId"
|
id="clientId"
|
||||||
label="Service ID"
|
label="Service ID"
|
||||||
placeholder="Apple Service ID"
|
placeholder="Apple Service ID"
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.clientId}
|
||||||
|
helperText={formState.errors?.clientId?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authAppleKeyId')}
|
{...register('keyId')}
|
||||||
name="authAppleKeyId"
|
name="keyId"
|
||||||
id="authAppleKeyId"
|
id="keyId"
|
||||||
label="Key ID"
|
label="Key ID"
|
||||||
placeholder="Apple Key ID"
|
placeholder="Apple Key ID"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.keyId}
|
||||||
|
helperText={formState.errors?.keyId?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authApplePrivateKey')}
|
{...register('privateKey')}
|
||||||
multiline
|
multiline
|
||||||
rows={4}
|
rows={4}
|
||||||
name="authApplePrivateKey"
|
name="privateKey"
|
||||||
id="authApplePrivateKey"
|
id="privateKey"
|
||||||
label="Private Key"
|
label="Private Key"
|
||||||
placeholder="Paste Private Key here"
|
placeholder="Paste Private Key here"
|
||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.privateKey}
|
||||||
|
helperText={formState.errors?.privateKey?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
@@ -199,7 +235,7 @@ export default function AppleProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,59 @@
|
|||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface BaseProviderSettingsFormValues {
|
export const baseProviderValidationSchema = Yup.object({
|
||||||
authEnabled: boolean;
|
clientId: Yup.string().label('Client ID').when('enabled', {
|
||||||
authClientId: string;
|
is: true,
|
||||||
authClientSecret: string;
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
clientSecret: Yup.string().label('Client Secret').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
enabled: Yup.bool(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BaseProviderSettingsFormValues = Yup.InferType<
|
||||||
|
typeof baseProviderValidationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface BaseProviderSettingsProps {
|
||||||
|
/**
|
||||||
|
* The name of the provider. Used to provide unique IDs to the inputs.
|
||||||
|
*/
|
||||||
|
providerName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export default function BaseProviderSettings({
|
||||||
* Third-party auth providers e.g. Google, GitHub.
|
providerName,
|
||||||
*
|
}: BaseProviderSettingsProps) {
|
||||||
* @remarks
|
const { register, formState } =
|
||||||
*
|
useFormContext<BaseProviderSettingsFormValues>();
|
||||||
* 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
{...register(`authClientId`)}
|
{...register('clientId')}
|
||||||
id="authClientId"
|
id={`${providerName}-clientId`}
|
||||||
label="Client ID"
|
label="Client ID"
|
||||||
placeholder="Enter your Client ID"
|
placeholder="Enter your Client ID"
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.clientId}
|
||||||
|
helperText={formState.errors?.clientId?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register(`authClientSecret`)}
|
{...register('clientSecret')}
|
||||||
id="authClientSecret"
|
id={`${providerName}-clientSecret`}
|
||||||
label="Client Secret"
|
label="Client Secret"
|
||||||
placeholder="Enter your Client Secret"
|
placeholder="Enter your Client Secret"
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.clientSecret}
|
||||||
|
helperText={formState.errors?.clientSecret?.message}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,36 +18,43 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function DiscordProviderSettings() {
|
export default function DiscordProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.discord || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authDiscordClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authDiscordClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authDiscordEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Discord settings..."
|
label="Loading settings for Discord..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -54,33 +65,46 @@ export default function DiscordProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication?.id,
|
||||||
app: {
|
config: {
|
||||||
authDiscordClientId: values.authClientId,
|
auth: {
|
||||||
authDiscordClientSecret: values.authClientSecret,
|
method: {
|
||||||
authDiscordEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
discord: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Discord settings are being updated...`,
|
{
|
||||||
success: `Discord settings have been updated successfully.`,
|
loading: `Discord settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Discord settings.`,
|
success: `Discord settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Discrod settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,22 +113,23 @@ export default function DiscordProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Discord"
|
title="Discord"
|
||||||
description="Allow users to sign in with Discord."
|
description="Allow users to sign in with Discord."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-discord"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-discord"
|
||||||
docsTitle="how to sign in users with Discord"
|
docsTitle="how to sign in users with Discord"
|
||||||
icon="/assets/brands/discord.svg"
|
icon="/assets/brands/discord.svg"
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="discord" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -136,7 +161,7 @@ export default function DiscordProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,51 @@
|
|||||||
import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface EmailAndPasswordFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
emailVerificationRequired: Yup.boolean(),
|
||||||
* When enabled, users will need to verify their email by a link sent to their specified email.
|
hibpEnabled: Yup.boolean(),
|
||||||
*/
|
});
|
||||||
authEmailSigninEmailVerifiedRequired: boolean;
|
|
||||||
/**
|
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
* If true, users' passwords will be checked against https://haveibeenpwned.com/Passwords
|
|
||||||
*/
|
|
||||||
authPasswordHibpEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EmailAndPasswordSettings() {
|
export default function EmailAndPasswordSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, error, loading } = useSignInMethodsQuery({
|
const { data, error, loading } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { hibpEnabled, emailVerificationRequired } =
|
||||||
|
data?.config?.auth?.method?.emailPassword || {};
|
||||||
|
|
||||||
const form = useForm<EmailAndPasswordFormValues>({
|
const form = useForm<EmailAndPasswordFormValues>({
|
||||||
reValidateMode: 'onChange',
|
reValidateMode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authPasswordHibpEnabled: data?.app?.authPasswordHibpEnabled || false,
|
hibpEnabled: hibpEnabled || false,
|
||||||
authEmailSigninEmailVerifiedRequired:
|
emailVerificationRequired: emailVerificationRequired || false,
|
||||||
data?.app?.authEmailSigninEmailVerifiedRequired || false,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -62,28 +67,36 @@ export default function EmailAndPasswordSettings() {
|
|||||||
const handleEmailAndPasswordSettingsChange = async (
|
const handleEmailAndPasswordSettingsChange = async (
|
||||||
values: EmailAndPasswordFormValues,
|
values: EmailAndPasswordFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authPasswordHibpEnabled: values.authPasswordHibpEnabled,
|
auth: {
|
||||||
authEmailSigninEmailVerifiedRequired:
|
method: {
|
||||||
values.authEmailSigninEmailVerifiedRequired,
|
emailPassword: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Email and password sign-in settings are being updated...`,
|
{
|
||||||
success: `Email and password sign-in settings have been updated successfully.`,
|
loading: `Email and password sign-in settings are being updated...`,
|
||||||
error: `An error occurred while trying to update email sign-in settings.`,
|
success: `Email and password sign-in settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update email sign-in settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -98,18 +111,16 @@ export default function EmailAndPasswordSettings() {
|
|||||||
showSwitch
|
showSwitch
|
||||||
enabled
|
enabled
|
||||||
slotProps={{
|
slotProps={{
|
||||||
switch: {
|
switch: { disabled: true },
|
||||||
disabled: true,
|
|
||||||
},
|
|
||||||
submitButton: {
|
submitButton: {
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
loading: formState.isSubmitting,
|
loading: formState.isSubmitting,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ControlledCheckbox
|
<ControlledCheckbox
|
||||||
name="authEmailSigninEmailVerifiedRequired"
|
name="emailVerificationRequired"
|
||||||
id="authEmailSigninEmailVerifiedRequired"
|
id="emailVerificationRequired"
|
||||||
label={
|
label={
|
||||||
<span className="inline-grid grid-flow-row gap-y-0.5 text-sm+">
|
<span className="inline-grid grid-flow-row gap-y-0.5 text-sm+">
|
||||||
<Text component="span">Require Verified Emails</Text>
|
<Text component="span">Require Verified Emails</Text>
|
||||||
@@ -121,8 +132,8 @@ export default function EmailAndPasswordSettings() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ControlledCheckbox
|
<ControlledCheckbox
|
||||||
name="authPasswordHibpEnabled"
|
name="hibpEnabled"
|
||||||
id="authPasswordHibpEnabled"
|
id="hibpEnabled"
|
||||||
label={
|
label={
|
||||||
<span className="inline-grid grid-flow-row gap-y-0.5 text-sm+">
|
<span className="inline-grid grid-flow-row gap-y-0.5 text-sm+">
|
||||||
<Text component="span">Password Protection</Text>
|
<Text component="span">Password Protection</Text>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,36 +18,43 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function FacebookProviderSettings() {
|
export default function FacebookProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.facebook || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authFacebookClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authFacebookClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authFacebookEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Facebook settings..."
|
label="Loading settings for Facebook..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -54,33 +65,46 @@ export default function FacebookProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authFacebookClientId: values.authClientId,
|
auth: {
|
||||||
authFacebookClientSecret: values.authClientSecret,
|
method: {
|
||||||
authFacebookEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
facebook: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Facebook settings are being updated...`,
|
{
|
||||||
success: `Facebook settings have been updated successfully.`,
|
loading: `Facebook settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Facebook settings.`,
|
success: `Facebook settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Facebook settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,22 +113,23 @@ export default function FacebookProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Facebook"
|
title="Facebook"
|
||||||
description="Allow users to sign in with Facebook."
|
description="Allow users to sign in with Facebook."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-facebook"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-facebook"
|
||||||
docsTitle="how to sign in users with Facebook"
|
docsTitle="how to sign in users with Facebook"
|
||||||
icon="/assets/brands/facebook.svg"
|
icon="/assets/brands/facebook.svg"
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="facebook" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -136,7 +161,7 @@ export default function FacebookProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,7 +18,9 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
@@ -22,30 +28,35 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
|
|
||||||
export default function GitHubProviderSettings() {
|
export default function GitHubProviderSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.github || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authGithubClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authGithubClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authGithubEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading GitHub settings..."
|
label="Loading settings for GitHub..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -56,33 +67,46 @@ export default function GitHubProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authGithubClientId: values.authClientId,
|
auth: {
|
||||||
authGithubClientSecret: values.authClientSecret,
|
method: {
|
||||||
authGithubEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
github: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `GitHub settings are being updated...`,
|
{
|
||||||
success: `GitHub settings have been updated successfully.`,
|
loading: `GitHub settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's GitHub settings.`,
|
success: `GitHub settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's GitHub settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,9 +115,11 @@ export default function GitHubProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="GitHub"
|
title="GitHub"
|
||||||
description="Allow users to sign in with GitHub."
|
description="Allow users to sign in with GitHub."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-github"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-github"
|
||||||
docsTitle="how to sign in users with GitHub"
|
docsTitle="how to sign in users with GitHub"
|
||||||
@@ -102,15 +128,14 @@ export default function GitHubProviderSettings() {
|
|||||||
? '/assets/brands/light/github.svg'
|
? '/assets/brands/light/github.svg'
|
||||||
: '/assets/brands/github.svg'
|
: '/assets/brands/github.svg'
|
||||||
}
|
}
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="github" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -142,7 +167,7 @@ export default function GitHubProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,36 +18,43 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function GoogleProviderSettings() {
|
export default function GoogleProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.google || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authGoogleClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authGoogleClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authGoogleEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Google settings..."
|
label="Loading settings for Google..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -54,33 +65,46 @@ export default function GoogleProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authGoogleClientId: values.authClientId,
|
auth: {
|
||||||
authGoogleClientSecret: values.authClientSecret,
|
method: {
|
||||||
authGoogleEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
google: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Google settings are being updated...`,
|
{
|
||||||
success: `Google settings have been updated successfully.`,
|
loading: `Google settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Google settings.`,
|
success: `Google settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Google settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,22 +113,23 @@ export default function GoogleProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Google"
|
title="Google"
|
||||||
description="Allow users to sign in with Google."
|
description="Allow users to sign in with Google."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-google"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-google"
|
||||||
docsTitle="how to sign in users with Google"
|
docsTitle="how to sign in users with Google"
|
||||||
icon="/assets/brands/google.svg"
|
icon="/assets/brands/google.svg"
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="google" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -136,7 +161,7 @@ export default function GoogleProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,36 +18,43 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function LinkedInProviderSettings() {
|
export default function LinkedInProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.linkedin || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authLinkedinClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authLinkedinClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authLinkedinEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading..."
|
label="Loading settings for LinkedIn..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -54,33 +65,46 @@ export default function LinkedInProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authLinkedinClientId: values.authClientId,
|
auth: {
|
||||||
authLinkedinClientSecret: values.authClientSecret,
|
method: {
|
||||||
authLinkedinEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
linkedin: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `LinkedIn settings are being updated...`,
|
{
|
||||||
success: `LinkedIn settings have been updated successfully.`,
|
loading: `LinkedIn settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's LinkedIn settings.`,
|
success: `LinkedIn settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's LinkedIn settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,22 +113,23 @@ export default function LinkedInProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="LinkedIn"
|
title="LinkedIn"
|
||||||
description="Allow users to sign in with LinkedIn."
|
description="Allow users to sign in with LinkedIn."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-linkedin"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-linkedin"
|
||||||
docsTitle="how to sign in users with LinkedIn"
|
docsTitle="how to sign in users with LinkedIn"
|
||||||
icon="/assets/brands/linkedin.svg"
|
icon="/assets/brands/linkedin.svg"
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="linkedin" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -136,7 +161,7 @@ export default function LinkedInProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,53 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface MagicLinkFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean(),
|
||||||
* Enables passwordless authentication by email.
|
});
|
||||||
*/
|
|
||||||
authEmailPasswordlessEnabled: boolean;
|
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
}
|
|
||||||
|
|
||||||
export default function MagicLinkSettings() {
|
export default function MagicLinkSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { enabled } = data?.config?.auth?.method?.emailPasswordless || {};
|
||||||
|
|
||||||
const form = useForm<MagicLinkFormValues>({
|
const form = useForm<MagicLinkFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authEmailPasswordlessEnabled: data.app.authEmailPasswordlessEnabled,
|
enabled,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Magic Link settings..."
|
label="Loading settings for Magic Link..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -49,30 +57,39 @@ export default function MagicLinkSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState } = form;
|
||||||
const authEmailPasswordlessEnabled = watch('authEmailPasswordlessEnabled');
|
|
||||||
|
|
||||||
const handleMagicLinkSettingsUpdate = async (values: MagicLinkFormValues) => {
|
const handleMagicLinkSettingsUpdate = async (values: MagicLinkFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
method: {
|
||||||
|
emailPasswordless: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Magic Link settings are being updated...`,
|
{
|
||||||
success: `Magic Link settings have been updated successfully.`,
|
loading: `Magic Link settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Magic Link settings.`,
|
success: `Magic Link settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Magic Link settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,14 +98,15 @@ export default function MagicLinkSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Magic Link"
|
title="Magic Link"
|
||||||
description="Allow users to sign in with a Magic Link."
|
description="Allow users to sign in with a Magic Link."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-magic-link"
|
docsLink="https://docs.nhost.io/authentication/sign-in-with-magic-link"
|
||||||
docsTitle="how to sign in users with Magic Link"
|
docsTitle="how to sign in users with Magic Link"
|
||||||
enabled={authEmailPasswordlessEnabled}
|
switchId="enabled"
|
||||||
switchId="authEmailPasswordlessEnabled"
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Button from '@/ui/v2/Button';
|
|||||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||||
import Link from '@/ui/v2/Link';
|
import Link from '@/ui/v2/Link';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { useConfirmProvidersUpdatedMutation } from '@/utils/__generated__/graphql';
|
import { useConfirmProvidersUpdatedMutation } from '@/utils/__generated__/graphql';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -27,7 +28,9 @@ export default function ProvidersUpdatedAlert() {
|
|||||||
{
|
{
|
||||||
loading: 'Confirming...',
|
loading: 'Confirming...',
|
||||||
success: 'Your settings have been updated successfully.',
|
success: 'Your settings have been updated successfully.',
|
||||||
error: 'An error occurred while trying to confirm the message.',
|
error: getServerError(
|
||||||
|
'An error occurred while trying to confirm the message.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -55,7 +58,7 @@ export default function ProvidersUpdatedAlert() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert className="grid items-center grid-flow-row gap-2 p-4 place-items-center lg:grid-flow-col lg:place-content-between bg-amber-500">
|
<Alert className="grid grid-flow-row place-items-center items-center gap-2 bg-amber-500 p-4 lg:grid-flow-col lg:place-content-between">
|
||||||
<div className="grid grid-flow-row gap-1 text-left">
|
<div className="grid grid-flow-row gap-1 text-left">
|
||||||
<Text className="font-semibold">
|
<Text className="font-semibold">
|
||||||
Please update the Redirect URL for all providers being used
|
Please update the Redirect URL for all providers being used
|
||||||
@@ -74,7 +77,7 @@ export default function ProvidersUpdatedAlert() {
|
|||||||
className="font-medium"
|
className="font-medium"
|
||||||
>
|
>
|
||||||
Read the discussion here.
|
Read the discussion here.
|
||||||
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
|
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
GetSmsSettingsDocument,
|
GetSignInMethodsDocument,
|
||||||
useSignInMethodsQuery,
|
useGetSignInMethodsQuery,
|
||||||
useUpdateAppMutation,
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -11,79 +12,119 @@ import Input from '@/ui/v2/Input';
|
|||||||
import Option from '@/ui/v2/Option';
|
import Option from '@/ui/v2/Option';
|
||||||
import Select from '@/ui/v2/Select';
|
import Select from '@/ui/v2/Select';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface SMSSettingsFormValues {
|
const validationSchema = Yup.object({
|
||||||
authSmsTwilioAccountSid: string;
|
accountSid: Yup.string().label('Account SID').when('enabled', {
|
||||||
authSmsTwilioAuthToken: string;
|
is: true,
|
||||||
authSmsTwilioMessagingServiceId: string;
|
then: Yup.string().required(),
|
||||||
authSmsPasswordlessEnabled: boolean;
|
}),
|
||||||
}
|
authToken: Yup.string().label('Auth Token').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
messagingServiceId: Yup.string()
|
||||||
|
.label('Messaging Service ID')
|
||||||
|
.when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
enabled: Yup.boolean().label('Enabled'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function SMSSettings() {
|
export default function SMSSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
refetchQueries: [GetSmsSettingsDocument],
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, loading } = useSignInMethodsQuery({
|
const { data, error, loading } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
onError: (error) => {
|
|
||||||
throw error;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { accountSid, authToken, messagingServiceId } =
|
||||||
|
data?.config?.provider?.sms || {};
|
||||||
|
const { enabled } = data?.config?.auth?.method?.smsPasswordless || {};
|
||||||
|
|
||||||
const form = useForm<SMSSettingsFormValues>({
|
const form = useForm<SMSSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authSmsTwilioAccountSid: data.app.authSmsTwilioAccountSid,
|
accountSid: accountSid || '',
|
||||||
authSmsTwilioAuthToken: data.app.authSmsTwilioAuthToken,
|
authToken: authToken || '',
|
||||||
authSmsTwilioMessagingServiceId: data.app.authSmsTwilioMessagingServiceId,
|
messagingServiceId: messagingServiceId || '',
|
||||||
authSmsPasswordlessEnabled: data.app.authSmsPasswordlessEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading SMS settings..."
|
label="Loading settings for the SMS provider..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authSmsPasswordlessEnabled = watch('authSmsPasswordlessEnabled');
|
const authSmsPasswordlessEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleSMSSettingsChange = async (values: SMSSettingsFormValues) => {
|
const handleSMSSettingsChange = async (values: SMSSettingsFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
provider: {
|
||||||
|
sms: {
|
||||||
|
accountSid: values.accountSid,
|
||||||
|
authToken: values.authToken,
|
||||||
|
messagingServiceId: values.messagingServiceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
method: {
|
||||||
|
smsPasswordless: {
|
||||||
|
enabled: values.enabled,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `SMS settings are being updated...`,
|
{
|
||||||
success: `SMS settings have been updated successfully.`,
|
loading: `SMS settings are being updated...`,
|
||||||
error: `An error occurred while trying to update SMS settings.`,
|
success: `SMS settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update SMS settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -92,12 +133,13 @@ export default function SMSSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Phone Number (SMS)"
|
title="Phone Number (SMS)"
|
||||||
description="Allow users to sign in with Phone Number (SMS)."
|
description="Allow users to sign in with Phone Number (SMS)."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
switchId="authSmsPasswordlessEnabled"
|
switchId="enabled"
|
||||||
enabled={authSmsPasswordlessEnabled}
|
|
||||||
showSwitch
|
showSwitch
|
||||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-phone-number-sms"
|
docsLink="https://docs.nhost.io/authentication/sign-in-with-phone-number-sms"
|
||||||
docsTitle="how to sign in users with a phone number (SMS)"
|
docsTitle="how to sign in users with a phone number (SMS)"
|
||||||
@@ -136,34 +178,40 @@ export default function SMSSettings() {
|
|||||||
</Option>
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
{...register('authSmsTwilioAccountSid')}
|
{...register('accountSid')}
|
||||||
name="authSmsTwilioAccountSid"
|
name="accountSid"
|
||||||
id="authSmsTwilioAccountSid"
|
id="accountSid"
|
||||||
placeholder="Account SID"
|
placeholder="Account SID"
|
||||||
className="col-span-2 lg:col-span-1"
|
className="col-span-2 lg:col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
label="Account SID"
|
label="Account SID"
|
||||||
|
error={!!formState.errors?.accountSid}
|
||||||
|
helperText={formState.errors?.accountSid?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authSmsTwilioAuthToken')}
|
{...register('authToken')}
|
||||||
name="authSmsTwilioAuthToken"
|
name="authToken"
|
||||||
id="authSmsTwilioAuthToken"
|
id="authToken"
|
||||||
placeholder="Auth Token"
|
placeholder="Auth Token"
|
||||||
className="col-span-2 lg:col-span-1"
|
className="col-span-2 lg:col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
label="Auth Token"
|
label="Auth Token"
|
||||||
|
error={!!formState.errors?.authToken}
|
||||||
|
helperText={formState.errors?.authToken?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authSmsTwilioMessagingServiceId')}
|
{...register('messagingServiceId')}
|
||||||
name="authSmsTwilioMessagingServiceId"
|
name="messagingServiceId"
|
||||||
id="authSmsTwilioMessagingServiceId"
|
id="messagingServiceId"
|
||||||
placeholder="Messaging Service ID"
|
placeholder="Messaging Service ID"
|
||||||
className="col-span-2 lg:col-span-1"
|
className="col-span-2 lg:col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
label="Messaging Service ID"
|
label="Messaging Service ID"
|
||||||
|
error={!!formState.errors?.messagingServiceId}
|
||||||
|
helperText={formState.errors?.messagingServiceId?.message}
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,36 +18,43 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function SpotifyProviderSettings() {
|
export default function SpotifyProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.spotify || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authSpotifyClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authSpotifyClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authSpotifyEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Spotify settings..."
|
label="Loading settings for Spotify..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -54,33 +65,46 @@ export default function SpotifyProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authSpotifyClientId: values.authClientId,
|
auth: {
|
||||||
authSpotifyClientSecret: values.authClientSecret,
|
method: {
|
||||||
authSpotifyEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
spotify: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Spotify settings are being updated...`,
|
{
|
||||||
success: `Spotify settings have been updated successfully.`,
|
loading: `Spotify settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Spotify settings.`,
|
success: `Spotify settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Spotify settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,22 +113,23 @@ export default function SpotifyProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Spotify"
|
title="Spotify"
|
||||||
description="Allow users to sign in with Spotify."
|
description="Allow users to sign in with Spotify."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-spotify"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-spotify"
|
||||||
docsTitle="how to sign in users with Spotify"
|
docsTitle="how to sign in users with Spotify"
|
||||||
icon="/assets/brands/spotify.svg"
|
icon="/assets/brands/spotify.svg"
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="spotify" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -136,7 +161,7 @@ export default function SpotifyProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,7 +18,9 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { useTheme } from '@mui/material';
|
import { useTheme } from '@mui/material';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
@@ -22,30 +28,35 @@ import { twMerge } from 'tailwind-merge';
|
|||||||
|
|
||||||
export default function TwitchProviderSettings() {
|
export default function TwitchProviderSettings() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.twitch || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authTwitchClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authTwitchClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authTwitchEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Twitch Settings..."
|
label="Loading settings for Twitch..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -56,33 +67,46 @@ export default function TwitchProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authTwitchClientId: values.authClientId,
|
auth: {
|
||||||
authTwitchClientSecret: values.authClientSecret,
|
method: {
|
||||||
authTwitchEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
twitch: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Twitch settings are being updated...`,
|
{
|
||||||
success: `Twitch settings have been updated successfully.`,
|
loading: `Twitch settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Twitch settings.`,
|
success: `Twitch settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Twitch settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -91,9 +115,11 @@ export default function TwitchProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Twitch"
|
title="Twitch"
|
||||||
description="Allow users to sign in with Twitch."
|
description="Allow users to sign in with Twitch."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-twitch"
|
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-twitch"
|
||||||
docsTitle="how to sign in users with Twitch"
|
docsTitle="how to sign in users with Twitch"
|
||||||
@@ -102,15 +128,14 @@ export default function TwitchProviderSettings() {
|
|||||||
? '/assets/brands/light/twitch.svg'
|
? '/assets/brands/light/twitch.svg'
|
||||||
: '/assets/brands/twitch.svg'
|
: '/assets/brands/twitch.svg'
|
||||||
}
|
}
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="twitch" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -142,7 +167,7 @@ export default function TwitchProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -12,42 +14,58 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface TwitterProviderFormValues {
|
const validationSchema = Yup.object({
|
||||||
authTwitterConsumerSecret: string;
|
consumerSecret: Yup.string().label('Consumer Secret').when('enabled', {
|
||||||
authTwitterConsumerKey: string;
|
is: true,
|
||||||
authTwitterEnabled: boolean;
|
then: Yup.string().required(),
|
||||||
}
|
}),
|
||||||
|
consumerKey: Yup.string().label('Consumer Key').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function TwitterProviderSettings() {
|
export default function TwitterProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { consumerKey, consumerSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.twitter || {};
|
||||||
|
|
||||||
const form = useForm<TwitterProviderFormValues>({
|
const form = useForm<TwitterProviderFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authTwitterConsumerSecret: data?.app?.authTwitterConsumerSecret,
|
consumerSecret: consumerSecret || '',
|
||||||
authTwitterConsumerKey: data?.app?.authTwitterConsumerKey,
|
consumerKey: consumerKey || '',
|
||||||
authTwitterEnabled: data?.app?.authTwitterEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Twitter settings..."
|
label="Loading settings for Twitter..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -58,29 +76,41 @@ export default function TwitterProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authEnabled = watch('authTwitterEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (values: TwitterProviderFormValues) => {
|
const handleProviderUpdate = async (values: TwitterProviderFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
method: {
|
||||||
|
oauth: {
|
||||||
|
twitter: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Twitter settings are being updated...`,
|
{
|
||||||
success: `Twitter settings have been updated successfully.`,
|
loading: `Twitter settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Twitter settings.`,
|
success: `Twitter settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Twitter settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,39 +119,44 @@ export default function TwitterProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Twitter"
|
title="Twitter"
|
||||||
description="Allow users to sign in with Twitter."
|
description="Allow users to sign in with Twitter."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsTitle="how to sign in users with Twitter"
|
docsTitle="how to sign in users with Twitter"
|
||||||
icon="/assets/brands/twitter.svg"
|
icon="/assets/brands/twitter.svg"
|
||||||
switchId="authTwitterEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
{...register(`authTwitterConsumerKey`)}
|
{...register(`consumerKey`)}
|
||||||
name="authTwitterConsumerKey"
|
name="consumerKey"
|
||||||
id="authTwitterConsumerKey"
|
id="consumerKey"
|
||||||
label="Twitter Consumer Key"
|
label="Twitter Consumer Key"
|
||||||
placeholder="Twitter Consumer Key"
|
placeholder="Twitter Consumer Key"
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.consumerKey}
|
||||||
|
helperText={formState.errors?.consumerKey?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authTwitterConsumerSecret')}
|
{...register('consumerSecret')}
|
||||||
name="authTwitterConsumerSecret"
|
name="consumerSecret"
|
||||||
id="authTwitterConsumerSecret"
|
id="consumerSecret"
|
||||||
label="Twitter Consumer Secret"
|
label="Twitter Consumer Secret"
|
||||||
placeholder="Twitter Consumer Secret"
|
placeholder="Twitter Consumer Secret"
|
||||||
className="col-span-1"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.consumerSecret}
|
||||||
|
helperText={formState.errors?.consumerSecret?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
@@ -154,7 +189,7 @@ export default function TwitterProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface WebAuthnFormValues {
|
const validationSchema = Yup.object({
|
||||||
/**
|
enabled: Yup.boolean(),
|
||||||
* When enabled, passwordless Webauthn authentication can be done
|
});
|
||||||
* via device supported strong authenticators like fingerprint, Face ID, etc.
|
|
||||||
*/
|
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
authWebAuthnEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WebAuthnSettings() {
|
export default function WebAuthnSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { enabled } = data?.config?.auth?.method?.webauthn || {};
|
||||||
|
|
||||||
const form = useForm<WebAuthnFormValues>({
|
const form = useForm<WebAuthnFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authWebAuthnEnabled: data.app.authWebAuthnEnabled,
|
enabled,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -50,30 +57,39 @@ export default function WebAuthnSettings() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState } = form;
|
||||||
const authWebAuthnEnabled = watch('authWebAuthnEnabled');
|
|
||||||
|
|
||||||
const handleWebAuthnSettingsUpdate = async (values: WebAuthnFormValues) => {
|
const handleWebAuthnSettingsUpdate = async (values: WebAuthnFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authWebAuthnEnabled: values.authWebAuthnEnabled,
|
auth: {
|
||||||
|
method: {
|
||||||
|
webauthn: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `WebAuthn settings are being updated...`,
|
{
|
||||||
success: `WebAuthn settings have been updated successfully.`,
|
loading: `WebAuthn settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's WebAuthn settings.`,
|
success: `WebAuthn settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's WebAuthn settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,14 +98,15 @@ export default function WebAuthnSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Security Keys"
|
title="Security Keys"
|
||||||
description="Allow users to sign in with security keys using WebAuthn."
|
description="Allow users to sign in with security keys using WebAuthn."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-security-keys"
|
docsLink="https://docs.nhost.io/authentication/sign-in-with-security-keys"
|
||||||
docsTitle="how to sign in users with security keys"
|
docsTitle="how to sign in users with security keys"
|
||||||
enabled={authWebAuthnEnabled}
|
switchId="enabled"
|
||||||
switchId="authWebAuthnEnabled"
|
|
||||||
showSwitch
|
showSwitch
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
import type { BaseProviderSettingsFormValues } from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
import BaseProviderSettings, {
|
||||||
|
baseProviderValidationSchema,
|
||||||
|
} from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -14,36 +18,43 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export default function WindowsLiveProviderSettings() {
|
export default function WindowsLiveProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.windowslive || {};
|
||||||
|
|
||||||
const form = useForm<BaseProviderSettingsFormValues>({
|
const form = useForm<BaseProviderSettingsFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authClientId: data?.app?.authWindowsLiveClientId,
|
clientId: clientId || '',
|
||||||
authClientSecret: data?.app?.authWindowsLiveClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authEnabled: data?.app?.authWindowsLiveEnabled,
|
enabled: enabled || false,
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(baseProviderValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading Windows Live Settings..."
|
label="Loading settings for Windows Live..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -54,33 +65,46 @@ export default function WindowsLiveProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { formState, watch } = form;
|
const { formState, watch } = form;
|
||||||
const authEnabled = watch('authEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (
|
const handleProviderUpdate = async (
|
||||||
values: BaseProviderSettingsFormValues,
|
values: BaseProviderSettingsFormValues,
|
||||||
) => {
|
) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
authWindowsLiveClientId: values.authClientId,
|
auth: {
|
||||||
authWindowsLiveClientSecret: values.authClientSecret,
|
method: {
|
||||||
authWindowsLiveEnabled: values.authEnabled,
|
oauth: {
|
||||||
|
windowslive: {
|
||||||
|
...values,
|
||||||
|
scope: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `Windows Live settings are being updated...`,
|
{
|
||||||
success: `Windows Live settings have been updated successfully.`,
|
loading: `Windows Live settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's Windows Live settings.`,
|
success: `Windows Live settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's Windows Live settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -89,21 +113,22 @@ export default function WindowsLiveProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="Windows Live"
|
title="Windows Live"
|
||||||
description="Allow users to sign in with Windows Live."
|
description="Allow users to sign in with Windows Live."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsTitle="how to sign in users with Windows Live"
|
docsTitle="how to sign in users with Windows Live"
|
||||||
icon="/assets/brands/windowslive.svg"
|
icon="/assets/brands/windowslive.svg"
|
||||||
switchId="authEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<BaseProviderSettings />
|
<BaseProviderSettings providerName="windowslive" />
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
id="redirectUrl"
|
id="redirectUrl"
|
||||||
@@ -135,7 +160,7 @@ export default function WindowsLiveProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import Form from '@/components/common/Form';
|
import Form from '@/components/common/Form';
|
||||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
|
||||||
|
import { useUI } from '@/context/UIContext';
|
||||||
import {
|
import {
|
||||||
useSignInMethodsQuery,
|
GetSignInMethodsDocument,
|
||||||
useUpdateAppMutation,
|
useGetSignInMethodsQuery,
|
||||||
|
useUpdateConfigMutation,
|
||||||
} from '@/generated/graphql';
|
} from '@/generated/graphql';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
@@ -12,48 +15,68 @@ import Input from '@/ui/v2/Input';
|
|||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
import { copy } from '@/utils/copy';
|
import { copy } from '@/utils/copy';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
export interface WorkOsProviderFormValues {
|
const validationSchema = Yup.object({
|
||||||
authWorkOsEnabled: boolean;
|
clientId: Yup.string().label('Client ID').when('enabled', {
|
||||||
authWorkOsClientId: string;
|
is: true,
|
||||||
authWorkOsClientSecret: string;
|
then: Yup.string().required(),
|
||||||
authWorkOsDefaultDomain: string;
|
}),
|
||||||
authWorkOsDefaultOrganization: string;
|
clientSecret: Yup.string().label('Client Secret').when('enabled', {
|
||||||
authWorkOsDefaultConnection: string;
|
is: true,
|
||||||
}
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
organization: Yup.string().label('Organization').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
connection: Yup.string().label('Connection').when('enabled', {
|
||||||
|
is: true,
|
||||||
|
then: Yup.string().required(),
|
||||||
|
}),
|
||||||
|
enabled: Yup.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||||
|
|
||||||
export default function WorkOsProviderSettings() {
|
export default function WorkOsProviderSettings() {
|
||||||
|
const { maintenanceActive } = useUI();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const [updateApp] = useUpdateAppMutation();
|
const [updateConfig] = useUpdateConfigMutation({
|
||||||
|
refetchQueries: [GetSignInMethodsDocument],
|
||||||
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useSignInMethodsQuery({
|
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||||
variables: {
|
variables: { appId: currentApplication?.id },
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-only',
|
fetchPolicy: 'cache-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { clientId, clientSecret, organization, connection, enabled } =
|
||||||
|
data?.config?.auth?.method?.oauth?.workos || {};
|
||||||
|
|
||||||
const form = useForm<WorkOsProviderFormValues>({
|
const form = useForm<WorkOsProviderFormValues>({
|
||||||
reValidateMode: 'onSubmit',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
authWorkOsClientId: data?.app?.authWorkOsClientId,
|
clientId: clientId || '',
|
||||||
authWorkOsClientSecret: data?.app?.authWorkOsClientSecret,
|
clientSecret: clientSecret || '',
|
||||||
authWorkOsDefaultDomain: data?.app?.authWorkOsDefaultDomain,
|
organization: organization || '',
|
||||||
authWorkOsDefaultOrganization: data?.app?.authWorkOsDefaultOrganization,
|
connection: connection || '',
|
||||||
authWorkOsDefaultConnection: data?.app?.authWorkOsDefaultConnection,
|
enabled: enabled || false,
|
||||||
authWorkOsEnabled: data?.app?.authWorkOsEnabled,
|
|
||||||
},
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<ActivityIndicator
|
<ActivityIndicator
|
||||||
delay={1000}
|
delay={1000}
|
||||||
label="Loading WorkOS settings..."
|
label="Loading settings for WorkOS..."
|
||||||
className="justify-center"
|
className="justify-center"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -64,29 +87,41 @@ export default function WorkOsProviderSettings() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { register, formState, watch } = form;
|
const { register, formState, watch } = form;
|
||||||
const authEnabled = watch('authWorkOsEnabled');
|
const authEnabled = watch('enabled');
|
||||||
|
|
||||||
const handleProviderUpdate = async (values: WorkOsProviderFormValues) => {
|
const handleProviderUpdate = async (values: WorkOsProviderFormValues) => {
|
||||||
const updateAppMutation = updateApp({
|
const updateConfigPromise = updateConfig({
|
||||||
variables: {
|
variables: {
|
||||||
id: currentApplication.id,
|
appId: currentApplication.id,
|
||||||
app: {
|
config: {
|
||||||
...values,
|
auth: {
|
||||||
|
method: {
|
||||||
|
oauth: {
|
||||||
|
workos: values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await toast.promise(
|
try {
|
||||||
updateAppMutation,
|
await toast.promise(
|
||||||
{
|
updateConfigPromise,
|
||||||
loading: `WorkOS settings are being updated...`,
|
{
|
||||||
success: `WorkOS settings have been updated successfully.`,
|
loading: `WorkOS settings are being updated...`,
|
||||||
error: `An error occurred while trying to update the project's WorkOS settings.`,
|
success: `WorkOS settings have been updated successfully.`,
|
||||||
},
|
error: getServerError(
|
||||||
getToastStyleProps(),
|
`An error occurred while trying to update the project's WorkOS settings.`,
|
||||||
);
|
),
|
||||||
|
},
|
||||||
|
getToastStyleProps(),
|
||||||
|
);
|
||||||
|
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
|
} catch {
|
||||||
|
// Note: The toast will handle the error.
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,70 +130,46 @@ export default function WorkOsProviderSettings() {
|
|||||||
<SettingsContainer
|
<SettingsContainer
|
||||||
title="WorkOS"
|
title="WorkOS"
|
||||||
description="Allow users to sign in with WorkOS."
|
description="Allow users to sign in with WorkOS."
|
||||||
primaryActionButtonProps={{
|
slotProps={{
|
||||||
disabled: !formState.isValid || !formState.isDirty,
|
submitButton: {
|
||||||
loading: formState.isSubmitting,
|
disabled: !formState.isDirty || maintenanceActive,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
docsLink="https://docs.nhost.io/authentication/sign-in-with-workos"
|
docsLink="https://docs.nhost.io/authentication/sign-in-with-workos"
|
||||||
docsTitle="how to sign in users with WorkOS"
|
docsTitle="how to sign in users with WorkOS"
|
||||||
icon="/assets/brands/workos.svg"
|
icon="/assets/brands/workos.svg"
|
||||||
switchId="authWorkOsEnabled"
|
switchId="enabled"
|
||||||
showSwitch
|
showSwitch
|
||||||
enabled={authEnabled}
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid-flow-rows grid grid-cols-6 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
'grid grid-flow-row grid-cols-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
!authEnabled && 'hidden',
|
!authEnabled && 'hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<BaseProviderSettings providerName="workos" />
|
||||||
<Input
|
<Input
|
||||||
{...register(`authWorkOsClientId`)}
|
{...register('organization')}
|
||||||
name="authWorkOsClientId"
|
name="organization"
|
||||||
id="authWorkOsClientId"
|
id="organization"
|
||||||
label="Client ID"
|
|
||||||
placeholder="Enter your Client ID"
|
|
||||||
className="col-span-3"
|
|
||||||
fullWidth
|
|
||||||
hideEmptyHelperText
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
{...register('authWorkOsClientSecret')}
|
|
||||||
name="authWorkOsClientSecret"
|
|
||||||
id="authWorkOsClientSecret"
|
|
||||||
label="Client Secret"
|
|
||||||
placeholder="Enter your Client Secret"
|
|
||||||
className="col-span-3"
|
|
||||||
fullWidth
|
|
||||||
hideEmptyHelperText
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
{...register('authWorkOsDefaultOrganization')}
|
|
||||||
name="authWorkOsDefaultOrganization"
|
|
||||||
id="authWorkOsDefaultOrganization"
|
|
||||||
label="Default Organization ID (optional)"
|
label="Default Organization ID (optional)"
|
||||||
placeholder="Default Organization ID"
|
placeholder="Default Organization ID"
|
||||||
className="col-span-2"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.organization}
|
||||||
|
helperText={formState.errors?.organization?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
{...register('authWorkOsDefaultDomain')}
|
{...register('connection')}
|
||||||
name="authWorkOsDefaultDomain"
|
name="connection"
|
||||||
id="authWorkOsDefaultDomain"
|
id="connection"
|
||||||
label="Default Domain (optional)"
|
|
||||||
placeholder="Default Domain"
|
|
||||||
className="col-span-2"
|
|
||||||
fullWidth
|
|
||||||
hideEmptyHelperText
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
{...register('authWorkOsDefaultConnection')}
|
|
||||||
name="authWorkOsDefaultConnection"
|
|
||||||
id="authWorkOsDefaultConnection"
|
|
||||||
label="Default Connection (optional)"
|
label="Default Connection (optional)"
|
||||||
placeholder="Default Connection"
|
placeholder="Default Connection"
|
||||||
className="col-span-2"
|
className="col-span-1"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
|
error={!!formState.errors?.connection}
|
||||||
|
helperText={formState.errors?.connection?.message}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
name="redirectUrl"
|
name="redirectUrl"
|
||||||
@@ -168,7 +179,7 @@ export default function WorkOsProviderSettings() {
|
|||||||
currentApplication.region.awsName,
|
currentApplication.region.awsName,
|
||||||
'auth',
|
'auth',
|
||||||
)}/signin/provider/workos/callback`}
|
)}/signin/provider/workos/callback`}
|
||||||
className="col-span-6"
|
className="col-span-2"
|
||||||
fullWidth
|
fullWidth
|
||||||
hideEmptyHelperText
|
hideEmptyHelperText
|
||||||
label="Redirect URL"
|
label="Redirect URL"
|
||||||
@@ -191,7 +202,7 @@ export default function WorkOsProviderSettings() {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function Alert({
|
|||||||
},
|
},
|
||||||
severity === 'warning' && {
|
severity === 'warning' && {
|
||||||
backgroundColor: 'warning.light',
|
backgroundColor: 'warning.light',
|
||||||
color: 'warning.main',
|
color: 'warning.dark',
|
||||||
},
|
},
|
||||||
severity === 'success' && {
|
severity === 'success' && {
|
||||||
backgroundColor: 'success.light',
|
backgroundColor: 'success.light',
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export function Avatar({
|
|||||||
<Box
|
<Box
|
||||||
style={Object.assign(style, { backgroundImage: `url(${avatarUrl})` })}
|
style={Object.assign(style, { backgroundImage: `url(${avatarUrl})` })}
|
||||||
className={classes}
|
className={classes}
|
||||||
aria-label="Avatar"
|
aria-label={name ? `Avatar of ${name}` : 'Avatar'}
|
||||||
|
role="img"
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,14 +21,17 @@ export default function createTheme(mode: PaletteMode) {
|
|||||||
},
|
},
|
||||||
h2: {
|
h2: {
|
||||||
fontSize: '1.625rem',
|
fontSize: '1.625rem',
|
||||||
|
lineHeight: '2.375rem',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
h3: {
|
h3: {
|
||||||
fontSize: '1.125rem',
|
fontSize: '1.125rem',
|
||||||
|
lineHeight: '1.5rem',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
h4: {
|
h4: {
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
|
lineHeight: '1.375rem',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
subtitle1: {
|
subtitle1: {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Alert } from '@/ui/Alert';
|
|||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import fetch from 'cross-fetch';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
@@ -100,10 +100,9 @@ export default function CreateUserForm({
|
|||||||
{
|
{
|
||||||
loading: 'Creating user...',
|
loading: 'Creating user...',
|
||||||
success: 'User created successfully.',
|
success: 'User created successfully.',
|
||||||
error: (arg) =>
|
error: getServerError(
|
||||||
arg?.message
|
'An error occurred while trying to create the user.',
|
||||||
? `Error: ${arg.message}`
|
),
|
||||||
: 'An error occurred while trying to create the user.',
|
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import getUserRoles from '@/utils/settings/getUserRoles';
|
|||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
RemoteAppGetUsersDocument,
|
RemoteAppGetUsersDocument,
|
||||||
useGetRolesQuery,
|
useGetRolesPermissionsQuery,
|
||||||
useUpdateRemoteAppUserMutation,
|
useUpdateRemoteAppUserMutation,
|
||||||
} from '@/utils/__generated__/graphql';
|
} from '@/utils/__generated__/graphql';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
@@ -137,12 +137,12 @@ export default function EditUserForm({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: dataRoles } = useGetRolesQuery({
|
const { data: dataRoles } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
const allAvailableProjectRoles = getUserRoles(
|
const allAvailableProjectRoles = getUserRoles(
|
||||||
dataRoles?.app?.authUserDefaultAllowedRoles,
|
dataRoles?.config?.auth?.user?.roles?.allowed,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { DialogFormProps } from '@/types/common';
|
|||||||
import { Alert } from '@/ui/Alert';
|
import { Alert } from '@/ui/Alert';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Input from '@/ui/v2/Input';
|
import Input from '@/ui/v2/Input';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||||
import { useUpdateRemoteAppUserMutation } from '@/utils/__generated__/graphql';
|
import { useUpdateRemoteAppUserMutation } from '@/utils/__generated__/graphql';
|
||||||
@@ -78,7 +79,7 @@ export default function EditUserPasswordForm({
|
|||||||
{
|
{
|
||||||
loading: 'Updating user password...',
|
loading: 'Updating user password...',
|
||||||
success: 'User password updated successfully.',
|
success: 'User password updated successfully.',
|
||||||
error: 'Failed to update user password.',
|
error: getServerError('Failed to update user password.'),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ import List from '@/ui/v2/List';
|
|||||||
import { ListItem } from '@/ui/v2/ListItem';
|
import { ListItem } from '@/ui/v2/ListItem';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import getReadableProviderName from '@/utils/common/getReadableProviderName';
|
import getReadableProviderName from '@/utils/common/getReadableProviderName';
|
||||||
|
import getServerError from '@/utils/settings/getServerError';
|
||||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
import {
|
import {
|
||||||
useDeleteRemoteAppUserRolesMutation,
|
useDeleteRemoteAppUserRolesMutation,
|
||||||
useGetRolesQuery,
|
useGetRolesPermissionsQuery,
|
||||||
useInsertRemoteAppUserRolesMutation,
|
useInsertRemoteAppUserRolesMutation,
|
||||||
useRemoteAppDeleteUserMutation,
|
useRemoteAppDeleteUserMutation,
|
||||||
useUpdateRemoteAppUserMutation,
|
useUpdateRemoteAppUserMutation,
|
||||||
@@ -81,13 +82,15 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
|
|||||||
* going to use once the user selects a user of their application; we use it
|
* going to use once the user selects a user of their application; we use it
|
||||||
* in the drawer form.
|
* in the drawer form.
|
||||||
*/
|
*/
|
||||||
const { data: dataRoles } = useGetRolesQuery({
|
const { data: dataRoles } = useGetRolesPermissionsQuery({
|
||||||
variables: { id: currentApplication?.id },
|
variables: { appId: currentApplication?.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { allowed: allowedRoles } = dataRoles?.config?.auth?.user?.roles || {};
|
||||||
|
|
||||||
const allAvailableProjectRoles = useMemo(
|
const allAvailableProjectRoles = useMemo(
|
||||||
() => getUserRoles(dataRoles?.app?.authUserDefaultAllowedRoles),
|
() => getUserRoles(allowedRoles),
|
||||||
[dataRoles],
|
[allowedRoles],
|
||||||
);
|
);
|
||||||
|
|
||||||
async function handleEditUser(
|
async function handleEditUser(
|
||||||
@@ -149,7 +152,9 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
|
|||||||
{
|
{
|
||||||
loading: `Updating user's settings...`,
|
loading: `Updating user's settings...`,
|
||||||
success: 'User settings updated successfully.',
|
success: 'User settings updated successfully.',
|
||||||
error: `An error occurred while trying to update this user's settings.`,
|
error: getServerError(
|
||||||
|
`An error occurred while trying to update this user's settings.`,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
@@ -179,7 +184,9 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
|
|||||||
{
|
{
|
||||||
loading: 'Deleting user...',
|
loading: 'Deleting user...',
|
||||||
success: 'User deleted successfully.',
|
success: 'User deleted successfully.',
|
||||||
error: 'An error occurred while trying to delete this user.',
|
error: getServerError(
|
||||||
|
'An error occurred while trying to delete this user.',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
getToastStyleProps(),
|
getToastStyleProps(),
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user