Compare commits
140 Commits
@nhost/cor
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2907ecb7ff | ||
|
|
05d7f5207f | ||
|
|
07a053ee80 | ||
|
|
61e4414a8f | ||
|
|
4601d84e0e | ||
|
|
4dd2e99159 | ||
|
|
282c6c6d24 | ||
|
|
c78227b085 | ||
|
|
d87e520307 | ||
|
|
bbed04e4da | ||
|
|
273afc9740 | ||
|
|
f4083aa4b3 | ||
|
|
ddd2641726 | ||
|
|
4658aeb31e | ||
|
|
cc8e5fe4a9 | ||
|
|
85c897c717 | ||
|
|
c99e5552e6 | ||
|
|
97a2520ea1 | ||
|
|
964af2912b | ||
|
|
afea682a8c | ||
|
|
fefa2baa2e | ||
|
|
f09b3cfd24 | ||
|
|
dd3b2c41f1 | ||
|
|
aaced20f31 | ||
|
|
3e91c19e13 | ||
|
|
abe0edcacb | ||
|
|
f8dae56bda | ||
|
|
9133726dbe | ||
|
|
7eed617034 | ||
|
|
d4fd4ec3e9 | ||
|
|
19a0288861 | ||
|
|
da975387ac | ||
|
|
e46c77e409 | ||
|
|
6c642d86f3 | ||
|
|
46a77f1ce5 | ||
|
|
6053560b5a | ||
|
|
89bd37bc28 | ||
|
|
0df73a41c9 | ||
|
|
53bdc294e2 | ||
|
|
f6d2042adb | ||
|
|
ba83475ced | ||
|
|
dafc581c08 | ||
|
|
c88b77ef43 | ||
|
|
1470592aac | ||
|
|
4e9a560346 | ||
|
|
766cb61243 | ||
|
|
7a9370abb2 | ||
|
|
73368c87a2 | ||
|
|
ef20f1f504 | ||
|
|
616a71fc89 | ||
|
|
9477e11d4c | ||
|
|
3c0adb4922 | ||
|
|
c1bfc16ec2 | ||
|
|
1fe86f770c | ||
|
|
d5de56256a | ||
|
|
b5e8222b76 | ||
|
|
7f15375a9a | ||
|
|
ffd8660bcc | ||
|
|
9159cf46b1 | ||
|
|
9211743d9c | ||
|
|
cc6aae3fba | ||
|
|
a9fbe8e0fc | ||
|
|
40cbeac221 | ||
|
|
df8e31305d | ||
|
|
90af9f2224 | ||
|
|
037fbdf37a | ||
|
|
843087cb11 | ||
|
|
f2aaff0504 | ||
|
|
ee2f53a052 | ||
|
|
8f5255172e | ||
|
|
c9de90e027 | ||
|
|
3341632f23 | ||
|
|
e005a67ab4 | ||
|
|
1f4bbf75e0 | ||
|
|
e5934d5dfd | ||
|
|
8b368ba2e8 | ||
|
|
16017fb8d2 | ||
|
|
effc0aba52 | ||
|
|
45a81ca823 | ||
|
|
377e8f8c37 | ||
|
|
977d58a938 | ||
|
|
eb7a14cedb | ||
|
|
5a64bdf30a | ||
|
|
7acf96d65f | ||
|
|
57726864fd | ||
|
|
73da6a67f1 | ||
|
|
48964b82e0 | ||
|
|
9d2fdbadc8 | ||
|
|
6bd874b485 | ||
|
|
4b36670897 | ||
|
|
947696efc6 | ||
|
|
a3168a1dae | ||
|
|
29efea2ad8 | ||
|
|
390688feb1 | ||
|
|
20e19ec7db | ||
|
|
b0c58ff351 | ||
|
|
7b7cc74948 | ||
|
|
1f501c829c | ||
|
|
8f9993d8ed | ||
|
|
f53b1f5c13 | ||
|
|
1b8dcf237a | ||
|
|
fc559d9e29 | ||
|
|
fe61dbb6dc | ||
|
|
8f90569230 | ||
|
|
5a11ace8f0 | ||
|
|
c569c5f60c | ||
|
|
fbcef432a3 | ||
|
|
44ae629f86 | ||
|
|
e030856660 | ||
|
|
db118f9769 | ||
|
|
8a48a897a7 | ||
|
|
dce91ec7d8 | ||
|
|
0a3383d6c5 | ||
|
|
97b5310c5d | ||
|
|
5c8c79444a | ||
|
|
eb3041341d | ||
|
|
f57f237e37 | ||
|
|
b1e90e6e2b | ||
|
|
1c947b2995 | ||
|
|
d00f6ed84e | ||
|
|
4c88846d72 | ||
|
|
97dc689d79 | ||
|
|
311417d679 | ||
|
|
ce0e1ee7ae | ||
|
|
1cc6841107 | ||
|
|
4b7fff0440 | ||
|
|
47fc7ffc0e | ||
|
|
7c5d0d0ec6 | ||
|
|
4d9c48f524 | ||
|
|
842e9892c0 | ||
|
|
37fee16552 | ||
|
|
d056fb4dbd | ||
|
|
ed6d9e8a85 | ||
|
|
7840201e91 | ||
|
|
af8891686b | ||
|
|
13efafb000 | ||
|
|
719a3ddcf9 | ||
|
|
d11980f078 | ||
|
|
bfbe8733f6 | ||
|
|
412b1fa8c6 |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/out
|
||||||
|
**/dist
|
||||||
|
**/umd
|
||||||
|
**/.turbo
|
||||||
|
**/.nhost
|
||||||
|
**/coverage
|
||||||
|
**/.next
|
||||||
10
.github/actions/install-dependencies/action.yaml
vendored
10
.github/actions/install-dependencies/action.yaml
vendored
@@ -1,11 +1,16 @@
|
|||||||
name: Install Node and package dependencies
|
name: Install Node and package dependencies
|
||||||
description: 'Install Node dependencies with pnpm'
|
description: 'Install Node dependencies with pnpm'
|
||||||
|
inputs:
|
||||||
|
TURBO_TOKEN:
|
||||||
|
description: 'Turborepo token'
|
||||||
|
TURBO_TEAM:
|
||||||
|
description: 'Turborepo team'
|
||||||
runs:
|
runs:
|
||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- uses: pnpm/action-setup@v2.2.4
|
- uses: pnpm/action-setup@v2.2.4
|
||||||
with:
|
with:
|
||||||
version: 7.9.1
|
version: 7.17.0
|
||||||
run_install: false
|
run_install: false
|
||||||
- name: Get pnpm cache directory
|
- name: Get pnpm cache directory
|
||||||
id: pnpm-cache-dir
|
id: pnpm-cache-dir
|
||||||
@@ -31,3 +36,6 @@ runs:
|
|||||||
- shell: bash
|
- shell: bash
|
||||||
name: Build packages
|
name: Build packages
|
||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||||
|
|||||||
92
.github/workflows/changesets.yaml
vendored
92
.github/workflows/changesets.yaml
vendored
@@ -5,26 +5,33 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'dashboard/**'
|
|
||||||
- 'examples/**'
|
- 'examples/**'
|
||||||
- 'assets/**'
|
- 'assets/**'
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- '!.changeset/**'
|
- '!.changeset/**'
|
||||||
- 'LICENSE'
|
- 'LICENSE'
|
||||||
|
|
||||||
|
env:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
|
DASHBOARD_PACKAGE: '@nhost/dashboard'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version:
|
version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||||
|
dashboardVersion: ${{ steps.dashboard.outputs.dashboardVersion }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# * Install Node and dependencies
|
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
with:
|
||||||
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
- name: Create PR or Publish release
|
- name: Create PR or Publish release
|
||||||
id: changesets
|
id: changesets
|
||||||
uses: changesets/action@v1
|
uses: changesets/action@v1
|
||||||
@@ -36,3 +43,80 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
- name: Check Dashboard tag
|
||||||
|
id: dashboard
|
||||||
|
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||||
|
run: |
|
||||||
|
DASHBOARD_VERSION=$(jq -r .version dashboard/package.json)
|
||||||
|
GIT_TAG="${{ env.DASHBOARD_PACKAGE}}@$DASHBOARD_VERSION"
|
||||||
|
if [ -z "$(git tag -l | grep $GIT_TAG)" ]; then
|
||||||
|
echo "dashboardVersion=$DASHBOARD_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: version
|
||||||
|
name: Dashboard
|
||||||
|
if: needs.version.outputs.dashboardVersion != ''
|
||||||
|
uses: ./.github/workflows/dashboard.yaml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- test
|
||||||
|
- version
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Add git tag
|
||||||
|
run: |
|
||||||
|
git tag "${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}"
|
||||||
|
git push origin --tags
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
nhost/dashboard
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=true
|
||||||
|
type=semver,pattern={{version}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||||
|
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||||
|
type=semver,pattern={{major}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||||
|
type=sha
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Build and push to Docker Hub
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
timeout-minutes: 60
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./dashboard/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
build-args: |
|
||||||
|
TURBO_TOKEN=${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM=${{ env.TURBO_TEAM }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
push: true
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: taiki-e/create-gh-release-action@v1
|
||||||
|
with:
|
||||||
|
changelog: dashboard/CHANGELOG.md
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
prefix: ${{ env.DASHBOARD_PACKAGE }}@
|
||||||
|
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||||
|
- name: Remove tag on failure
|
||||||
|
if: failure()
|
||||||
|
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||||
|
|||||||
29
.github/workflows/dashboard.yaml
vendored
29
.github/workflows/dashboard.yaml
vendored
@@ -1,19 +1,17 @@
|
|||||||
name: 'Dashboard'
|
name: 'Dashboard'
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_call:
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'dashboard/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
paths:
|
paths:
|
||||||
|
- 'packages/**'
|
||||||
- 'dashboard/**'
|
- 'dashboard/**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: nhost
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build
|
name: Build
|
||||||
@@ -26,17 +24,15 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
with:
|
||||||
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
- name: Build the application
|
- name: Build the application
|
||||||
run: pnpm build:dashboard
|
run: pnpm build:dashboard
|
||||||
- uses: actions/cache@v3.0.11
|
|
||||||
with:
|
|
||||||
path: ./*
|
|
||||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_ENV: dev
|
NEXT_PUBLIC_ENV: dev
|
||||||
NEXT_TELEMETRY_DISABLED: 1
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
@@ -45,18 +41,15 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
- uses: actions/cache@v3.0.11
|
|
||||||
id: restore-build
|
|
||||||
with:
|
with:
|
||||||
path: ./*
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pnpm test:dashboard
|
run: pnpm test:dashboard
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_ENV: dev
|
NEXT_PUBLIC_ENV: dev
|
||||||
NEXT_TELEMETRY_DISABLED: 1
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
@@ -65,9 +58,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
- uses: actions/cache@v3.0.11
|
|
||||||
id: restore-build
|
|
||||||
with:
|
with:
|
||||||
path: ./*
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
- run: pnpm lint:dashboard
|
- run: pnpm lint:dashboard
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Tests
|
name: Packages
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -21,7 +21,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||||
TURBO_TEAM: nhost
|
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build @nhost packages
|
name: Build @nhost packages
|
||||||
@@ -33,6 +33,9 @@ jobs:
|
|||||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
with:
|
||||||
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
||||||
# * In a PR, only include packages that have been modified, and their dependencies
|
# * In a PR, only include packages that have been modified, and their dependencies
|
||||||
- name: List examples with an e2e script
|
- name: List examples with an e2e script
|
||||||
@@ -61,6 +64,9 @@ jobs:
|
|||||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
with:
|
||||||
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
||||||
- name: Install Nhost CLI
|
- name: Install Nhost CLI
|
||||||
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
|
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
|
||||||
@@ -92,11 +98,14 @@ jobs:
|
|||||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
with:
|
||||||
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
|
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: pnpm run test
|
run: pnpm run test
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v2
|
uses: codecov/codecov-action@v3
|
||||||
with:
|
with:
|
||||||
files: '**/coverage/coverage-final.json'
|
files: '**/coverage/coverage-final.json'
|
||||||
name: codecov-umbrella
|
name: codecov-umbrella
|
||||||
@@ -113,6 +122,9 @@ jobs:
|
|||||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||||
- name: Install Node and dependencies
|
- name: Install Node and dependencies
|
||||||
uses: ./.github/actions/install-dependencies
|
uses: ./.github/actions/install-dependencies
|
||||||
|
with:
|
||||||
|
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||||
|
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||||
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm run lint
|
run: pnpm run lint
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
- This repository works with **Node 16**
|
||||||
|
|
||||||
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
|
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -97,6 +99,12 @@ You can take a look at the changeset documentation: [How to add a changeset](htt
|
|||||||
|
|
||||||
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
|
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
|
||||||
|
|
||||||
|
The document generation script that is run in the pre-commit hook requires to be built first. You may need to run the following command before the commit:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
<!-- ## Good practices
|
<!-- ## Good practices
|
||||||
- lint
|
- lint
|
||||||
- prettier
|
- prettier
|
||||||
|
|||||||
48
README.md
48
README.md
@@ -344,21 +344,28 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<sub><b>Muttenzer</b></sub>
|
<sub><b>Muttenzer</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/alexander-mart">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Alexander Mart</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/ahmic">
|
<a href="https://github.com/ahmic">
|
||||||
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
|
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>Amir Ahmic</b></sub>
|
<sub><b>Amir Ahmic</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/akd-io">
|
<a href="https://github.com/akd-io">
|
||||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||||
<br />
|
<br />
|
||||||
<sub><b>Anders Kjær Damgaard</b></sub>
|
<sub><b>Anders Kjær Damgaard</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr>
|
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/Sonichigo">
|
<a href="https://github.com/Sonichigo">
|
||||||
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
|
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
|
||||||
@@ -366,6 +373,20 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<sub><b>Animesh Pathak</b></sub>
|
<sub><b>Animesh Pathak</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/chrisli-03">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/11177048?v=4" width="100;" alt="chrisli-03"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Chris</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center">
|
||||||
|
<a href="https://github.com/massless">
|
||||||
|
<img src="https://avatars.githubusercontent.com/u/44389?v=4" width="100;" alt="massless"/>
|
||||||
|
<br />
|
||||||
|
<sub><b>Chris Wetherell</b></sub>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/rustyb">
|
<a href="https://github.com/rustyb">
|
||||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||||
@@ -379,7 +400,8 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Dago</b></sub>
|
<sub><b>Dago</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/dminkovsky">
|
<a href="https://github.com/dminkovsky">
|
||||||
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
||||||
@@ -400,8 +422,7 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Gaurav Agrawal</b></sub>
|
<sub><b>Gaurav Agrawal</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr>
|
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/alveshelio">
|
<a href="https://github.com/alveshelio">
|
||||||
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
||||||
@@ -422,7 +443,8 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Ikko Ashimine</b></sub>
|
<sub><b>Ikko Ashimine</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/jladuval">
|
<a href="https://github.com/jladuval">
|
||||||
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
||||||
@@ -443,8 +465,7 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Lucas Bois</b></sub>
|
<sub><b>Lucas Bois</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr>
|
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/MarcelloTheArcane">
|
<a href="https://github.com/MarcelloTheArcane">
|
||||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||||
@@ -465,7 +486,8 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Nirmalya Ghosh</b></sub>
|
<sub><b>Nirmalya Ghosh</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/quentin-decre">
|
<a href="https://github.com/quentin-decre">
|
||||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||||
@@ -486,8 +508,7 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Tapas Adhikary</b></sub>
|
<sub><b>Tapas Adhikary</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td></tr>
|
</td>
|
||||||
<tr>
|
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/uulwake">
|
<a href="https://github.com/uulwake">
|
||||||
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
|
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
|
||||||
@@ -508,7 +529,8 @@ Here are some ways of contributing to making Nhost better:
|
|||||||
<br />
|
<br />
|
||||||
<sub><b>Zach Burnaby</b></sub>
|
<sub><b>Zach Burnaby</b></sub>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td></tr>
|
||||||
|
<tr>
|
||||||
<td align="center">
|
<td align="center">
|
||||||
<a href="https://github.com/komninoschat">
|
<a href="https://github.com/komninoschat">
|
||||||
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
<img src="https://avatars.githubusercontent.com/u/29049104?v=4" width="100;" alt="komninoschat"/>
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ module.exports = {
|
|||||||
'@storybook/addon-links',
|
'@storybook/addon-links',
|
||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
'@storybook/addon-interactions',
|
'@storybook/addon-interactions',
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Fix Storybook issue with PostCSS@8
|
||||||
|
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||||
|
*/
|
||||||
|
name: '@storybook/addon-postcss',
|
||||||
|
options: {
|
||||||
|
postcssLoaderOptions: {
|
||||||
|
implementation: require('postcss'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
framework: '@storybook/react',
|
framework: '@storybook/react',
|
||||||
core: {
|
core: {
|
||||||
|
|||||||
43
dashboard/CHANGELOG.md
Normal file
43
dashboard/CHANGELOG.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# @nhost/dashboard
|
||||||
|
|
||||||
|
## 0.4.2
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 89bd37bc: fix(dashboard): correct redirect URL input opacity
|
||||||
|
- Updated dependencies [4601d84e]
|
||||||
|
- Updated dependencies [843087cb]
|
||||||
|
- @nhost/react@0.15.0
|
||||||
|
- @nhost/nextjs@1.9.0
|
||||||
|
- @nhost/react-apollo@4.9.0
|
||||||
|
|
||||||
|
## 0.4.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- 766cb612: fix(dashboard): correct redirect URL for oauth providers
|
||||||
|
- Updated dependencies [53bdc294]
|
||||||
|
- Updated dependencies [f2aaff05]
|
||||||
|
- @nhost/nextjs@1.8.3
|
||||||
|
- @nhost/core@0.9.3
|
||||||
|
- @nhost/react@0.14.3
|
||||||
|
- @nhost/nhost-js@1.6.1
|
||||||
|
- @nhost/react-apollo@4.8.3
|
||||||
|
|
||||||
|
## 0.4.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 9211743d: feat(dashboard): migrate Settings page features
|
||||||
|
|
||||||
|
## 0.3.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- 73da6a67: fix(dashboard): avoid using BACKEND_URL locally
|
||||||
|
|
||||||
|
## 0.2.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- db118f97: feat(dashboard): generate Docker image
|
||||||
50
dashboard/Dockerfile
Normal file
50
dashboard/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
FROM node:16-alpine AS pruner
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
RUN apk update
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN yarn global add turbo
|
||||||
|
COPY . .
|
||||||
|
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||||
|
|
||||||
|
FROM node:16-alpine AS builder
|
||||||
|
ARG TURBO_TOKEN
|
||||||
|
ARG TURBO_TEAM
|
||||||
|
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
RUN apk update
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
||||||
|
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_URL http://localhost:9693
|
||||||
|
ENV NEXT_PUBLIC_NHOST_HASURA_URL http://localhost:9695
|
||||||
|
ENV NEXT_PUBLIC_ENV dev
|
||||||
|
|
||||||
|
RUN yarn global add pnpm@7.17.0
|
||||||
|
COPY .gitignore .gitignore
|
||||||
|
COPY --from=pruner /app/out/json/ .
|
||||||
|
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
COPY --from=pruner /app/out/full/ .
|
||||||
|
COPY turbo.json turbo.json
|
||||||
|
COPY config/ config/
|
||||||
|
RUN pnpm build:dashboard
|
||||||
|
|
||||||
|
FROM node:16-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/dashboard/next.config.js .
|
||||||
|
COPY --from=builder /app/dashboard/package.json .
|
||||||
|
COPY --from=builder /app/dashboard/public ./dashboard/public
|
||||||
|
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/standalone/app ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/static ./dashboard/.next/static
|
||||||
|
|
||||||
|
CMD node dashboard/server.js
|
||||||
@@ -49,8 +49,8 @@ NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
|||||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. |
|
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. |
|
||||||
| `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` | URL of Hasura's migrations endpoint. Used only if local development is enabled. |
|
| `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` | URL of Hasura's migrations endpoint. Used only if local development is enabled. |
|
||||||
| `NEXT_PUBLIC_NHOST_HASURA_URL` | URL of the Hasura Console. Used only when `NEXT_PUBLIC_ENV` is `dev`. |
|
| `NEXT_PUBLIC_NHOST_HASURA_URL` | URL of the Hasura Console. Used only when `NEXT_PUBLIC_ENV` is `dev`. |
|
||||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | URL of the local backend. This is `http://localhost:1337` by default. |
|
|
||||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||||
|
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
|
||||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
| `NEXT_PUBLIC_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_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||||
|
|||||||
@@ -13,11 +13,3 @@ generates:
|
|||||||
- 'typescript-react-apollo'
|
- 'typescript-react-apollo'
|
||||||
config:
|
config:
|
||||||
withRefetchFn: true
|
withRefetchFn: true
|
||||||
functions/utils/__generated__/graphql-request.ts:
|
|
||||||
documents:
|
|
||||||
- 'functions/**/*.graphql'
|
|
||||||
- 'functions/**/*.gql'
|
|
||||||
plugins:
|
|
||||||
- 'typescript'
|
|
||||||
- 'typescript-operations'
|
|
||||||
- 'typescript-graphql-request'
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const path = require('path');
|
||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
@@ -5,6 +6,10 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
|||||||
module.exports = withBundleAnalyzer({
|
module.exports = withBundleAnalyzer({
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: false,
|
swcMinify: false,
|
||||||
|
output: 'standalone',
|
||||||
|
experimental: {
|
||||||
|
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||||
|
},
|
||||||
eslint: {
|
eslint: {
|
||||||
dirs: ['src'],
|
dirs: ['src'],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@nhost/dashboard",
|
"name": "@nhost/dashboard",
|
||||||
"version": "0.1.0",
|
"version": "0.4.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
@@ -25,25 +25,25 @@
|
|||||||
"@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.13.2",
|
"@graphiql/react": "^0.14.0",
|
||||||
"@graphiql/toolkit": "^0.8.0",
|
"@graphiql/toolkit": "^0.8.0",
|
||||||
"@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",
|
||||||
"@mui/base": "^5.0.0-alpha.105",
|
"@mui/base": "^5.0.0-alpha.106",
|
||||||
"@mui/material": "^5.10.13",
|
"@mui/material": "^5.10.14",
|
||||||
"@mui/system": "^5.10.13",
|
"@mui/system": "^5.10.14",
|
||||||
"@mui/x-date-pickers": "^5.0.8",
|
"@mui/x-date-pickers": "^5.0.8",
|
||||||
"@nhost/core": "^0.9.1",
|
"@nhost/core": "^0.9.3",
|
||||||
"@nhost/nextjs": "^1.8.1",
|
"@nhost/nextjs": "^1.9.0",
|
||||||
"@nhost/nhost-js": "^1.5.2",
|
"@nhost/nhost-js": "^1.6.1",
|
||||||
"@nhost/react": "^0.14.1",
|
"@nhost/react": "^0.15.0",
|
||||||
"@nhost/react-apollo": "^4.8.1",
|
"@nhost/react-apollo": "^4.9.0",
|
||||||
"@segment/snippet": "^4.15.3",
|
"@segment/snippet": "^4.15.3",
|
||||||
"@tailwindcss/forms": "^0.5.3",
|
"@tailwindcss/forms": "^0.5.3",
|
||||||
"@tanstack/react-query": "^4.16.1",
|
"@tanstack/react-query": "^4.16.1",
|
||||||
"@tanstack/react-table": "^8.5.27",
|
"@tanstack/react-table": "^8.5.30",
|
||||||
"@tanstack/react-virtual": "^3.0.0-beta.22",
|
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||||
"analytics-node": "^6.2.0",
|
"analytics-node": "^6.2.0",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"cross-fetch": "^3.1.5",
|
"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.0.8",
|
"graphiql": "^2.1.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",
|
||||||
@@ -66,13 +66,14 @@
|
|||||||
"randomstring": "^1.2.3",
|
"randomstring": "^1.2.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.39.3",
|
"react-hook-form": "^7.39.5",
|
||||||
"react-hot-toast": "^2.4.0",
|
"react-hot-toast": "^2.4.0",
|
||||||
"react-is": "17.0.2",
|
"react-is": "17.0.2",
|
||||||
"react-loading-skeleton": "^2.2.0",
|
"react-loading-skeleton": "^2.2.0",
|
||||||
"react-merge-refs": "^1.1.0",
|
"react-merge-refs": "^1.1.0",
|
||||||
"react-syntax-highlighter": "^15.4.5",
|
"react-syntax-highlighter": "^15.4.5",
|
||||||
"react-table": "^7.8.0",
|
"react-table": "^7.8.0",
|
||||||
|
"sharp": "^0.31.2",
|
||||||
"slugify": "^1.6.5",
|
"slugify": "^1.6.5",
|
||||||
"smartlook-client": "^6.0.0",
|
"smartlook-client": "^6.0.0",
|
||||||
"stripe": "^10.17.0",
|
"stripe": "^10.17.0",
|
||||||
@@ -95,6 +96,7 @@
|
|||||||
"@storybook/addon-essentials": "^6.5.13",
|
"@storybook/addon-essentials": "^6.5.13",
|
||||||
"@storybook/addon-interactions": "^6.5.13",
|
"@storybook/addon-interactions": "^6.5.13",
|
||||||
"@storybook/addon-links": "^6.5.13",
|
"@storybook/addon-links": "^6.5.13",
|
||||||
|
"@storybook/addon-postcss": "^2.0.0",
|
||||||
"@storybook/builder-webpack5": "^6.5.13",
|
"@storybook/builder-webpack5": "^6.5.13",
|
||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
@@ -114,10 +116,10 @@
|
|||||||
"@types/react-table": "^7.7.12",
|
"@types/react-table": "^7.7.12",
|
||||||
"@types/testing-library__jest-dom": "^5.14.5",
|
"@types/testing-library__jest-dom": "^5.14.5",
|
||||||
"@types/validator": "^13.7.10",
|
"@types/validator": "^13.7.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"@typescript-eslint/parser": "^5.42.0",
|
"@typescript-eslint/parser": "^5.43.0",
|
||||||
"@vitejs/plugin-react": "^2.2.0",
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
"@vitest/coverage-c8": "^0.25.1",
|
"@vitest/coverage-c8": "^0.25.2",
|
||||||
"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",
|
||||||
@@ -125,25 +127,24 @@
|
|||||||
"critters": "^0.0.10",
|
"critters": "^0.0.10",
|
||||||
"csstype": "^3.0.10",
|
"csstype": "^3.0.10",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"eslint": "^8.27.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",
|
||||||
"eslint-config-next": "^13.0.2",
|
"eslint-config-next": "^13.0.2",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.31.11",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"jsdom": "^20.0.2",
|
"jsdom": "^20.0.3",
|
||||||
"lint-staged": ">=13",
|
"lint-staged": ">=13",
|
||||||
"msw": "^0.48.2",
|
"msw": "^0.49.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"postmark": "^2.7.8",
|
"postmark": "^2.7.8",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"prettier-plugin-organize-imports": "^3.2.0",
|
"prettier-plugin-organize-imports": "^3.2.0",
|
||||||
"prettier-plugin-tailwind-css": "^1.5.0",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||||
"react-date-fns-hooks": "^0.9.4",
|
"react-date-fns-hooks": "^0.9.4",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
@@ -151,9 +152,9 @@
|
|||||||
"tailwindcss": "^3.1.2",
|
"tailwindcss": "^3.1.2",
|
||||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||||
"typescript": "^4.8.4",
|
"typescript": "^4.8.4",
|
||||||
"vite": "^3.2.3",
|
"vite": "^3.2.4",
|
||||||
"vite-tsconfig-paths": "^3.5.2",
|
"vite-tsconfig-paths": "^3.6.0",
|
||||||
"vitest": "^0.25.1",
|
"vitest": "^0.25.2",
|
||||||
"webpack": "^5.75.0"
|
"webpack": "^5.75.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
@@ -168,4 +169,4 @@
|
|||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
|
||||||
import features from '@/data/features.json';
|
|
||||||
import {
|
|
||||||
useGetWorkspaceMembersQuery,
|
|
||||||
useUpdateApplicationMutation,
|
|
||||||
} from '@/generated/graphql';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { ApplicationStatus } from '@/types/application';
|
|
||||||
import { Modal } from '@/ui';
|
|
||||||
import Status, { StatusEnum } from '@/ui/Status';
|
|
||||||
import Button from '@/ui/v2/Button';
|
|
||||||
import { Dropdown } from '@/ui/v2/Dropdown';
|
|
||||||
import ChevronDownIcon from '@/ui/v2/icons/ChevronDownIcon';
|
|
||||||
import Tooltip from '@/ui/v2/Tooltip';
|
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
|
||||||
import { getCurrentEnvironment, isDevOrStaging } from '@/utils/helpers';
|
|
||||||
import { triggerToast } from '@/utils/toast';
|
|
||||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
|
||||||
import { useUserData } from '@nhost/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { twMerge } from 'tailwind-merge';
|
|
||||||
import { ChangeApplicationName } from './ChangeApplicationName';
|
|
||||||
import ResetDatabasePasswordForm from './overview/ResetDatabasePasswordForm';
|
|
||||||
import { RemoveApplicationModal } from './RemoveApplicationModal';
|
|
||||||
|
|
||||||
const isK8SPostgresEnabledInCurrentEnvironment = features[
|
|
||||||
'k8s-postgres'
|
|
||||||
].enabled.find((e) => e === getCurrentEnvironment());
|
|
||||||
|
|
||||||
export function ApplicationMenuItems() {
|
|
||||||
const { currentApplication, currentWorkspace } =
|
|
||||||
useCurrentWorkspaceAndApplication();
|
|
||||||
const [updateApplication, { client }] = useUpdateApplicationMutation();
|
|
||||||
const { openAlertDialog } = useDialog();
|
|
||||||
const user = useUserData();
|
|
||||||
const [changeApplicationNameModal, setChangeApplicationNameModal] =
|
|
||||||
useState(false);
|
|
||||||
const [deleteApplicationModal, setDeleteApplicationModal] = useState(false);
|
|
||||||
|
|
||||||
const isProjectUsingRDS = currentApplication?.featureFlags?.find(
|
|
||||||
(feature) => feature.name === 'fleetcontrol_use_rds',
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleTriggerPausing() {
|
|
||||||
try {
|
|
||||||
await updateApplication({
|
|
||||||
variables: {
|
|
||||||
appId: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
desiredState: ApplicationStatus.Paused,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await updateOwnCache(client);
|
|
||||||
discordAnnounce(`${currentApplication.name} set to pause.`);
|
|
||||||
triggerToast(`${currentApplication.name} set to pause.`);
|
|
||||||
} catch (e) {
|
|
||||||
triggerToast(`Error trying to pause ${currentApplication.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: workspaceData, loading } = useGetWorkspaceMembersQuery({
|
|
||||||
variables: { workspaceId: currentWorkspace.id },
|
|
||||||
fetchPolicy: 'cache-first',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOwner = workspaceData.workspace.workspaceMembers.some(
|
|
||||||
(member) => member.user.id === user.id && member.type === 'owner',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
showModal={changeApplicationNameModal}
|
|
||||||
close={() => setChangeApplicationNameModal(!changeApplicationNameModal)}
|
|
||||||
Component={ChangeApplicationName}
|
|
||||||
/>
|
|
||||||
<Modal
|
|
||||||
showModal={deleteApplicationModal}
|
|
||||||
close={() => setDeleteApplicationModal(!deleteApplicationModal)}
|
|
||||||
Component={RemoveApplicationModal}
|
|
||||||
/>
|
|
||||||
<Dropdown.Root>
|
|
||||||
<Dropdown.Trigger asChild hideChevron>
|
|
||||||
<Button
|
|
||||||
endIcon={<ChevronDownIcon className="h-4 w-4" />}
|
|
||||||
variant="outlined"
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
Project Settings
|
|
||||||
</Button>
|
|
||||||
</Dropdown.Trigger>
|
|
||||||
|
|
||||||
<Dropdown.Content
|
|
||||||
menu
|
|
||||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
|
|
||||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
|
||||||
className="mt-1"
|
|
||||||
>
|
|
||||||
<Dropdown.Item
|
|
||||||
className="font-display text-sm font-medium text-dark"
|
|
||||||
onClick={() => setChangeApplicationNameModal(true)}
|
|
||||||
>
|
|
||||||
Change Project Name
|
|
||||||
</Dropdown.Item>
|
|
||||||
{isDevOrStaging() && (
|
|
||||||
<Dropdown.Item
|
|
||||||
className="font-display text-sm font-medium text-dark"
|
|
||||||
onClick={handleTriggerPausing}
|
|
||||||
>
|
|
||||||
<Status status={StatusEnum.Deploying}>Internal</Status>
|
|
||||||
<span className="ml-2 align-middle">Pause App</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
)}
|
|
||||||
{isK8SPostgresEnabledInCurrentEnvironment && !isProjectUsingRDS && (
|
|
||||||
<Dropdown.Item
|
|
||||||
className="font-display text-sm font-medium text-dark"
|
|
||||||
onClick={() => {
|
|
||||||
openAlertDialog({
|
|
||||||
title: 'Reset Database Password',
|
|
||||||
payload: <ResetDatabasePasswordForm />,
|
|
||||||
props: {
|
|
||||||
hidePrimaryAction: true,
|
|
||||||
hideSecondaryAction: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Reset Database Password
|
|
||||||
</Dropdown.Item>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Tooltip
|
|
||||||
title="Only owners of the workspace can delete apps"
|
|
||||||
visible={!isOwner}
|
|
||||||
hasDisabledChildren={!isOwner}
|
|
||||||
>
|
|
||||||
<Dropdown.Item
|
|
||||||
className={twMerge(
|
|
||||||
'font-display text-sm font-medium text-dark',
|
|
||||||
!isOwner
|
|
||||||
? 'cursor-not-allowed text-red text-opacity-70'
|
|
||||||
: 'font-medium text-red',
|
|
||||||
)}
|
|
||||||
onClick={() => setDeleteApplicationModal(true)}
|
|
||||||
disabled={!isOwner}
|
|
||||||
>
|
|
||||||
<span>Delete Project</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Tooltip>
|
|
||||||
</Dropdown.Content>
|
|
||||||
</Dropdown.Root>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ApplicationMenuItems;
|
|
||||||
@@ -56,14 +56,14 @@ export function ChangeApplicationName({ close }: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 py-6 text-left w-modal">
|
<div className="w-modal px-6 py-6 text-left">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Text variant="h3" component="h2">
|
<Text variant="h3" component="h2">
|
||||||
Change Project Name
|
Change Project Name
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="grid grid-flow-row gap-2 mt-4">
|
<div className="mt-4 grid grid-flow-row gap-2">
|
||||||
<Input
|
<Input
|
||||||
label="New Project Name"
|
label="New Project Name"
|
||||||
id="projectName"
|
id="projectName"
|
||||||
@@ -84,7 +84,7 @@ export function ChangeApplicationName({ close }: any) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2 mt-4">
|
<div className="mt-4 grid grid-flow-row gap-2">
|
||||||
<Button type="submit" disabled={applicationError}>
|
<Button type="submit" disabled={applicationError}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function Plan({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="grid items-center justify-between w-full grid-flow-col px-1 my-4"
|
className="my-4 grid w-full grid-flow-col items-center justify-between px-1"
|
||||||
onClick={setPlan}
|
onClick={setPlan}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
@@ -48,7 +48,7 @@ function Plan({
|
|||||||
<Text
|
<Text
|
||||||
variant="h3"
|
variant="h3"
|
||||||
component="p"
|
component="p"
|
||||||
className="self-center ml-2 font-medium"
|
className="ml-2 self-center font-medium"
|
||||||
>
|
>
|
||||||
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -143,7 +143,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 text-left w-welcome">
|
<div className="w-welcome p-6 text-left">
|
||||||
<Modal
|
<Modal
|
||||||
showModal={paymentModal}
|
showModal={paymentModal}
|
||||||
close={closePaymentModal}
|
close={closePaymentModal}
|
||||||
@@ -189,7 +189,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-2 mt-6">
|
<div className="mt-6 grid grid-flow-row gap-2">
|
||||||
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
|
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
|
||||||
{!selectedPlan && 'Change Plan'}
|
{!selectedPlan && 'Change Plan'}
|
||||||
{selectedPlan && isDowngrade && 'Downgrade'}
|
{selectedPlan && isDowngrade && 'Downgrade'}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RetryableErrorBoundary>
|
<RetryableErrorBoundary>
|
||||||
<div className=" h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
|
<div className="h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
|
||||||
{githubRepositoriesToDisplay.map((repo) => (
|
{githubRepositoriesToDisplay.map((repo) => (
|
||||||
<Repo
|
<Repo
|
||||||
key={repo.id}
|
key={repo.id}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export function EnableSMSSignIn({ openSMSSettingsModal }: any) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row self-center mt-3 align-middle">
|
<div className="mt-3 flex flex-row self-center align-middle">
|
||||||
<Text
|
<Text
|
||||||
variant="body"
|
variant="body"
|
||||||
size="normal"
|
size="normal"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
|
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
|
||||||
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
|
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||||
import Link from '@/ui/v2/Link';
|
import Link from '@/ui/v2/Link';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||||
@@ -66,7 +66,7 @@ export function HasuraData({ close }: HasuraDataProps) {
|
|||||||
underline="none"
|
underline="none"
|
||||||
>
|
>
|
||||||
Open Hasura
|
Open Hasura
|
||||||
<ExternalLink className="ml-0.5 h-4 w-4" />
|
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{close && (
|
{close && (
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function EditJWTSecretModal({ close }: any) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="px-6 py-4 w-modal"
|
className="w-modal px-6 py-4"
|
||||||
onSubmit={handleSubmit(handleEditJWTSecret)}
|
onSubmit={handleSubmit(handleEditJWTSecret)}
|
||||||
>
|
>
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
@@ -154,7 +154,7 @@ export function EditJWTSecretModal({ close }: any) {
|
|||||||
|
|
||||||
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
|
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
|
||||||
return (
|
return (
|
||||||
<div className="px-6 py-4 w-modal">
|
<div className="w-modal px-6 py-4">
|
||||||
<div className="grid grid-flow-row gap-2">
|
<div className="grid grid-flow-row gap-2">
|
||||||
<div className="grid grid-flow-row text-left">
|
<div className="grid grid-flow-row text-left">
|
||||||
<Text variant="h3" component="h2">
|
<Text variant="h3" component="h2">
|
||||||
@@ -179,7 +179,7 @@ export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-sm mx-auto text-center">
|
<div className="mx-auto max-w-sm text-center">
|
||||||
<Text variant="subtitle2">
|
<Text variant="subtitle2">
|
||||||
Already using a third party auth service? <br />
|
Already using a third party auth service? <br />
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { triggerToast } from '@/utils/toast';
|
|||||||
import { useDeleteApplicationMutation } from '@/utils/__generated__/graphql';
|
import { useDeleteApplicationMutation } from '@/utils/__generated__/graphql';
|
||||||
import router from 'next/router';
|
import router from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export interface RemoveApplicationModalProps {
|
export interface RemoveApplicationModalProps {
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +27,10 @@ export interface RemoveApplicationModalProps {
|
|||||||
* Description of the modal
|
* Description of the modal
|
||||||
*/
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Class name to be applied to the modal.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RemoveApplicationModal({
|
export function RemoveApplicationModal({
|
||||||
@@ -33,6 +38,7 @@ export function RemoveApplicationModal({
|
|||||||
handler,
|
handler,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
className,
|
||||||
}: RemoveApplicationModalProps) {
|
}: RemoveApplicationModalProps) {
|
||||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
||||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||||
@@ -72,18 +78,14 @@ export function RemoveApplicationModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-modal text-left">
|
<div className={twMerge('w-full max-w-sm p-6 text-left', className)}>
|
||||||
<div className="grid grid-flow-row gap-1">
|
<div className="grid grid-flow-row gap-1">
|
||||||
<Text variant="h3" component="h2">
|
<Text variant="h3" component="h2">
|
||||||
{title || 'Delete Project'}
|
{title || 'Delete Project'}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text variant="subtitle2">
|
<Text variant="subtitle2">
|
||||||
{description ? (
|
{description || 'Are you sure you want to delete this app?'}
|
||||||
<div>{description}</div>
|
|
||||||
) : (
|
|
||||||
<div>Are you sure you want to delete this app?</div>
|
|
||||||
)}
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text variant="subtitle2" className="font-bold !text-rose-600">
|
<Text variant="subtitle2" className="font-bold !text-rose-600">
|
||||||
@@ -98,33 +100,15 @@ export function RemoveApplicationModal({
|
|||||||
checked={remove}
|
checked={remove}
|
||||||
onChange={(_event, checked) => setRemove(checked)}
|
onChange={(_event, checked) => setRemove(checked)}
|
||||||
aria-label="Confirm Delete Project #1"
|
aria-label="Confirm Delete Project #1"
|
||||||
componentsProps={{
|
|
||||||
formControlLabel: {
|
|
||||||
componentsProps: {
|
|
||||||
typography: {
|
|
||||||
className: '!text-sm+',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="accept-2"
|
id="accept-2"
|
||||||
label="I understand this action can not be undone"
|
label="I understand this action cannot be undone"
|
||||||
className="py-2"
|
className="py-2"
|
||||||
checked={remove2}
|
checked={remove2}
|
||||||
onChange={(_event, checked) => setRemove2(checked)}
|
onChange={(_event, checked) => setRemove2(checked)}
|
||||||
aria-label="Confirm Delete Project #2"
|
aria-label="Confirm Delete Project #2"
|
||||||
componentsProps={{
|
|
||||||
formControlLabel: {
|
|
||||||
componentsProps: {
|
|
||||||
typography: {
|
|
||||||
className: '!text-sm+',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { ContainerAllWorkspacesApplications } from './ContainerAllWorkspacesAppl
|
|||||||
|
|
||||||
function ApplicationCreatedAt({ createdAt }: any) {
|
function ApplicationCreatedAt({ createdAt }: any) {
|
||||||
return (
|
return (
|
||||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||||
created{' '}
|
created{' '}
|
||||||
{formatDistance(new Date(createdAt), new Date(), {
|
{formatDistance(new Date(createdAt), new Date(), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
@@ -30,9 +30,9 @@ function LastSuccesfulDeployment({ deployment }: any) {
|
|||||||
<Avatar
|
<Avatar
|
||||||
name={deployment.commitUserName}
|
name={deployment.commitUserName}
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
avatarUrl={deployment.commitUserAvatarUrl}
|
||||||
className="self-center w-4 h-4 mr-1"
|
className="mr-1 h-4 w-4 self-center"
|
||||||
/>
|
/>
|
||||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||||
{deployment.commitUserName} deployed{' '}
|
{deployment.commitUserName} deployed{' '}
|
||||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
@@ -48,9 +48,9 @@ function CurrentDeployment({ deployment }: any) {
|
|||||||
<Avatar
|
<Avatar
|
||||||
name={deployment.commitUserName}
|
name={deployment.commitUserName}
|
||||||
avatarUrl={deployment.commitUserAvatarUrl}
|
avatarUrl={deployment.commitUserAvatarUrl}
|
||||||
className="self-center w-4 h-4 mr-1"
|
className="mr-1 h-4 w-4 self-center"
|
||||||
/>
|
/>
|
||||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||||
{deployment.commitUserName} updated just now
|
{deployment.commitUserName} updated just now
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +103,7 @@ export function RenderWorkspacesWithApps({
|
|||||||
variant="a"
|
variant="a"
|
||||||
color="greyscaleGrey"
|
color="greyscaleGrey"
|
||||||
size="normal"
|
size="normal"
|
||||||
className="mb-3 font-medium cursor-pointer"
|
className="mb-3 cursor-pointer font-medium"
|
||||||
>
|
>
|
||||||
{workspace.name}
|
{workspace.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -138,16 +138,16 @@ export function RenderWorkspacesWithApps({
|
|||||||
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
||||||
: false;
|
: false;
|
||||||
return (
|
return (
|
||||||
<div key={app.slug} className="py-4 cursor-pointer">
|
<div key={app.slug} className="cursor-pointer py-4">
|
||||||
<Link href={`${workspace?.slug}/${app.slug}`} passHref>
|
<Link href={`${workspace?.slug}/${app.slug}`} passHref>
|
||||||
<a
|
<a
|
||||||
href={`${workspace?.slug}/${app.slug}`}
|
href={`${workspace?.slug}/${app.slug}`}
|
||||||
className="flex px-2 bg-white rounded-sm place-content-between border-divide"
|
className="flex place-content-between rounded-sm border-divide bg-white px-2"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col self-center w-full">
|
<div className="flex w-full flex-col self-center">
|
||||||
<div className="flex flex-row w-full place-content-between">
|
<div className="flex w-full flex-row place-content-between">
|
||||||
<div className="flex flex-row items-center self-center">
|
<div className="flex flex-row items-center self-center">
|
||||||
<div className="w-10 h-10 overflow-hidden rounded-lg">
|
<div className="h-10 w-10 overflow-hidden rounded-lg">
|
||||||
<Image
|
<Image
|
||||||
src="/logos/new.svg"
|
src="/logos/new.svg"
|
||||||
alt="Nhost Logo"
|
alt="Nhost Logo"
|
||||||
@@ -155,12 +155,12 @@ export function RenderWorkspacesWithApps({
|
|||||||
height={40}
|
height={40}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col ml-2 text-left">
|
<div className="ml-2 flex flex-col text-left">
|
||||||
<div>
|
<div>
|
||||||
<Text
|
<Text
|
||||||
color="dark"
|
color="dark"
|
||||||
size="normal"
|
size="normal"
|
||||||
className="self-center font-medium text-left capitalize cursor-pointer"
|
className="cursor-pointer self-center text-left font-medium capitalize"
|
||||||
>
|
>
|
||||||
{app.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -192,7 +192,7 @@ export function RenderWorkspacesWithApps({
|
|||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="flex self-center align-middle">
|
<div className="flex self-center align-middle">
|
||||||
{app.deployments[0] && (
|
{app.deployments[0] && (
|
||||||
<div className="flex self-center mr-2 align-middle">
|
<div className="mr-2 flex self-center align-middle">
|
||||||
<StatusCircle
|
<StatusCircle
|
||||||
status={
|
status={
|
||||||
app.deployments[0]
|
app.deployments[0]
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
|||||||
normalizedFunctionData.logs.length === 0
|
normalizedFunctionData.logs.length === 0
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-white rounded-lg">
|
<div className="w-full rounded-lg text-white">
|
||||||
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
|
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||||
<div className="font-mono text-xs text-grey">
|
<div className="font-mono text-xs text-grey">
|
||||||
There are no stored logs yet. Try calling your function for logs to
|
There are no stored logs yet. Try calling your function for logs to
|
||||||
appear.
|
appear.
|
||||||
@@ -50,12 +50,12 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-white rounded-lg">
|
<div className="w-full rounded-lg text-white">
|
||||||
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
|
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||||
{normalizedFunctionData.logs.map((log) => (
|
{normalizedFunctionData.logs.map((log) => (
|
||||||
<div
|
<div
|
||||||
key={`${log.date}-${log.message.slice(66)}`}
|
key={`${log.date}-${log.message.slice(66)}`}
|
||||||
className="flex text-sm "
|
className=" flex text-sm"
|
||||||
>
|
>
|
||||||
<div id={`#-${log.date}`}>
|
<div id={`#-${log.date}`}>
|
||||||
<pre className="inline">
|
<pre className="inline">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export function EditRepositorySettings({
|
|||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const form = useForm<EditRepositorySettingsFormData>({
|
const form = useForm<EditRepositorySettingsFormData>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
productionBranch: currentApplication.repositoryProductionBranch || 'main',
|
productionBranch: currentApplication.repositoryProductionBranch || 'main',
|
||||||
repoBaseFolder: currentApplication.nhostBaseFolder,
|
repoBaseFolder: currentApplication.nhostBaseFolder,
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ export function EditRepositorySettingsModal({
|
|||||||
return (
|
return (
|
||||||
<div className="px-1">
|
<div className="px-1">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="w-8 h-8 mx-auto">
|
<div className="mx-auto h-8 w-8">
|
||||||
<GithubIcon className="w-8 h-8 text-greyscaleDark" />
|
<GithubIcon className="h-8 w-8 text-greyscaleDark" />
|
||||||
</div>
|
</div>
|
||||||
<Text
|
<Text
|
||||||
variant="subHeading"
|
variant="subHeading"
|
||||||
@@ -95,7 +95,7 @@ export function EditRepositorySettingsModal({
|
|||||||
variant="body"
|
variant="body"
|
||||||
color="greyscaleDark"
|
color="greyscaleDark"
|
||||||
size="small"
|
size="small"
|
||||||
className="font-normal text-center"
|
className="text-center font-normal"
|
||||||
>
|
>
|
||||||
{selectedRepoId
|
{selectedRepoId
|
||||||
? `We'll deploy changes automatically when you push to the deployment branch. `
|
? `We'll deploy changes automatically when you push to the deployment branch. `
|
||||||
@@ -110,7 +110,7 @@ export function EditRepositorySettingsModal({
|
|||||||
<div className="">
|
<div className="">
|
||||||
<RepoAndBranch />
|
<RepoAndBranch />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col mt-2">
|
<div className="mt-2 flex flex-col">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -123,7 +123,7 @@ export function EditRepositorySettingsModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div className="flex flex-col mt-2">
|
<div className="mt-2 flex flex-col">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
|
||||||
import GithubIcon from '@/components/icons/GithubIcon';
|
import GithubIcon from '@/components/icons/GithubIcon';
|
||||||
|
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';
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export default function GitHubInstallNhostApplication() {
|
|||||||
underline="none"
|
underline="none"
|
||||||
>
|
>
|
||||||
Configure the Nhost application on GitHub{' '}
|
Configure the Nhost application on GitHub{' '}
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,19 +20,19 @@ export function GitHubNoRepositoriesAdded({
|
|||||||
variant="body"
|
variant="body"
|
||||||
color="greyscaleDark"
|
color="greyscaleDark"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
className="font-normal text-center"
|
className="text-center font-normal"
|
||||||
>
|
>
|
||||||
Check the Nhost app's settings on your GitHub account, or install
|
Check the Nhost app's settings on your GitHub account, or install
|
||||||
the app on a new account.
|
the app on a new account.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div className="py-3 my-2 border-t border-b">
|
<div className="my-2 border-t border-b py-3">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
{filteredGitHubAppInstallations.map((githubApp) => (
|
{filteredGitHubAppInstallations.map((githubApp) => (
|
||||||
<div key={githubApp.id} className="flex items-center mr-4">
|
<div key={githubApp.id} className="mr-4 flex items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
avatarUrl={githubApp.accountAvatarUrl as string}
|
avatarUrl={githubApp.accountAvatarUrl as string}
|
||||||
className="w-5 h-5 mr-1"
|
className="mr-1 h-5 w-5"
|
||||||
/>
|
/>
|
||||||
{githubApp.accountLogin}
|
{githubApp.accountLogin}
|
||||||
</div>
|
</div>
|
||||||
@@ -45,9 +45,9 @@ export function GitHubNoRepositoriesAdded({
|
|||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
transparent
|
transparent
|
||||||
type={null}
|
type={null}
|
||||||
className="text-xs font-medium cursor-pointer text-blue"
|
className="cursor-pointer text-xs font-medium text-blue"
|
||||||
>
|
>
|
||||||
<PlusSmIcon className="w-4 h-4 mr-1 border rounded-full border-btn" />
|
<PlusSmIcon className="mr-1 h-4 w-4 rounded-full border border-btn" />
|
||||||
Configure the Nhost application on GitHub.
|
Configure the Nhost application on GitHub.
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
import { useDialog } from '@/components/common/DialogProvider';
|
|
||||||
import Form from '@/components/common/Form';
|
|
||||||
import {
|
|
||||||
useResetPostgresPasswordMutation,
|
|
||||||
useUpdateApplicationMutation,
|
|
||||||
} from '@/generated/graphql';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { Text } from '@/ui';
|
|
||||||
import Button from '@/ui/v2/Button';
|
|
||||||
import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
|
||||||
import Input from '@/ui/v2/Input';
|
|
||||||
import InputAdornment from '@/ui/v2/InputAdornment';
|
|
||||||
import { copy } from '@/utils/copy';
|
|
||||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
|
||||||
import { generateRandomPassword, schema } from '@/utils/generateRandomPassword';
|
|
||||||
import { triggerToast } from '@/utils/toast';
|
|
||||||
import { useUserData } from '@nhost/react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
export interface ResetDatabasePasswordFormProps {
|
|
||||||
/**
|
|
||||||
* The new password to set for the database.
|
|
||||||
*/
|
|
||||||
newDatabasePassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ResetDatabasePasswordForm() {
|
|
||||||
const [passwordError, setPasswordError] = useState('');
|
|
||||||
const [updateApplication] = useUpdateApplicationMutation();
|
|
||||||
|
|
||||||
const form = useForm<ResetDatabasePasswordFormProps>({
|
|
||||||
reValidateMode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
newDatabasePassword: generateRandomPassword(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { setValue, getValues, register } = form;
|
|
||||||
|
|
||||||
const { closeAlertDialog } = useDialog();
|
|
||||||
const [resetPostgresPasswordMutation, { loading }] =
|
|
||||||
useResetPostgresPasswordMutation();
|
|
||||||
const user = useUserData();
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
|
|
||||||
const handleGenerateRandomPassword = () => {
|
|
||||||
const newRandomDatabasePassword = generateRandomPassword();
|
|
||||||
setPasswordError('');
|
|
||||||
triggerToast('New random database password generated.');
|
|
||||||
setValue('newDatabasePassword', newRandomDatabasePassword);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeDatabasePassword = async (
|
|
||||||
data: ResetDatabasePasswordFormProps,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
await resetPostgresPasswordMutation({
|
|
||||||
variables: {
|
|
||||||
appID: currentApplication.id,
|
|
||||||
newPassword: data.newDatabasePassword,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await updateApplication({
|
|
||||||
variables: {
|
|
||||||
appId: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
postgresPassword: data.newDatabasePassword,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
closeAlertDialog();
|
|
||||||
triggerToast(`${currentApplication.name} Database Password changed.`);
|
|
||||||
} catch (e) {
|
|
||||||
triggerToast(
|
|
||||||
`Error trying to change database password for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
await discordAnnounce(
|
|
||||||
`Error trying to change database password: ${currentApplication.name} (${user.email}): ${e.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<Form className="mx-0.5" onSubmit={handleChangeDatabasePassword}>
|
|
||||||
<Input
|
|
||||||
{...register('newDatabasePassword')}
|
|
||||||
name="newDatabasePassword"
|
|
||||||
id="newDatabasePassword"
|
|
||||||
autoComplete="new-password"
|
|
||||||
type="password"
|
|
||||||
error={Boolean(passwordError)}
|
|
||||||
helperText={
|
|
||||||
<>
|
|
||||||
{passwordError && <div className="pb-2">{passwordError}</div>}
|
|
||||||
<Text className="font-normal" size="tiny" color="greyscaleDark">
|
|
||||||
The root postgres password for your database - it must be strong
|
|
||||||
and hard to guess.{' '}
|
|
||||||
<Button
|
|
||||||
onClick={handleGenerateRandomPassword}
|
|
||||||
className="contents text-xs "
|
|
||||||
>
|
|
||||||
<span className="ml-1 font-medium text-greyscaleDark underline underline-offset-2">
|
|
||||||
Generate a password
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
endAdornment={
|
|
||||||
<InputAdornment position="end" className="absolute right-2">
|
|
||||||
<Button
|
|
||||||
sx={{ minWidth: 0, padding: 0 }}
|
|
||||||
color="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
copy(getValues('newDatabasePassword'), 'Postgres password');
|
|
||||||
}}
|
|
||||||
variant="borderless"
|
|
||||||
aria-label="Copy your newly randomly generated password to the clipboard."
|
|
||||||
>
|
|
||||||
<CopyIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</InputAdornment>
|
|
||||||
}
|
|
||||||
onChange={async (e) => {
|
|
||||||
if (e.target.value.length === 0) {
|
|
||||||
setValue('newDatabasePassword', e.target.value);
|
|
||||||
|
|
||||||
setPasswordError('Please enter a password');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setValue('newDatabasePassword', e.target.value);
|
|
||||||
setPasswordError('');
|
|
||||||
try {
|
|
||||||
await schema.validate({
|
|
||||||
'Database Password': e.target.value,
|
|
||||||
});
|
|
||||||
setPasswordError('');
|
|
||||||
} catch (validationError) {
|
|
||||||
setPasswordError(validationError.message);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-6 grid grid-flow-col place-content-between py-2">
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
variant="borderless"
|
|
||||||
onClick={closeAlertDialog}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={Boolean(passwordError)}
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
Reset Database Password
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './ResetDatabasePasswordForm';
|
|
||||||
export { default } from './ResetDatabasePasswordForm';
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
|
|
||||||
|
|
||||||
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
|
|
||||||
// refactor
|
|
||||||
export interface AppleProviderSettingsFormProps {
|
|
||||||
authProviderClientId: string;
|
|
||||||
authProviderTeamId: string;
|
|
||||||
authProviderKeyId: string;
|
|
||||||
authProviderClientSecret: string;
|
|
||||||
handleClientIdChange: (value: string) => void;
|
|
||||||
handleTeamIdChange: (value: string) => void;
|
|
||||||
handleKeyIdChange: (value: string) => void;
|
|
||||||
handleClientSecretChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppleProviderSettingsForm({
|
|
||||||
authProviderClientId,
|
|
||||||
authProviderTeamId,
|
|
||||||
authProviderKeyId,
|
|
||||||
authProviderClientSecret,
|
|
||||||
handleClientIdChange,
|
|
||||||
handleTeamIdChange,
|
|
||||||
handleKeyIdChange,
|
|
||||||
handleClientSecretChange,
|
|
||||||
}: AppleProviderSettingsFormProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3 divide-y-1 divide-divide">
|
|
||||||
<ProviderSetting
|
|
||||||
title="Team ID"
|
|
||||||
desc="Copy from Apple and enter here"
|
|
||||||
inputPlaceholder="Paste Team ID here"
|
|
||||||
input
|
|
||||||
inputValue={authProviderTeamId}
|
|
||||||
inputOnChange={handleTeamIdChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title="Service ID"
|
|
||||||
desc="Copy from Apple and enter here"
|
|
||||||
inputPlaceholder="Paste Service ID here"
|
|
||||||
input
|
|
||||||
inputValue={authProviderClientId}
|
|
||||||
inputOnChange={handleClientIdChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title="Key ID"
|
|
||||||
desc="Copy from Apple and enter here"
|
|
||||||
inputPlaceholder="Paste Key ID here"
|
|
||||||
input
|
|
||||||
inputValue={authProviderKeyId}
|
|
||||||
inputOnChange={handleKeyIdChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title="Private Key"
|
|
||||||
desc="Copy from Apple and enter here"
|
|
||||||
inputPlaceholder="Paste Private Key here"
|
|
||||||
input
|
|
||||||
inputValue={authProviderClientSecret.replace(/\\n/gi, '\n')}
|
|
||||||
inputOnChange={handleClientSecretChange}
|
|
||||||
inputType="text"
|
|
||||||
multiline
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './AppleProviderSettingsForm';
|
|
||||||
export { default } from './AppleProviderSettingsForm';
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
|
|
||||||
import type { Provider } from '@/types/providers';
|
|
||||||
|
|
||||||
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
|
|
||||||
// refactor
|
|
||||||
export interface GeneralProviderSettingsFormProps {
|
|
||||||
provider: Provider;
|
|
||||||
authProviderClientId: string;
|
|
||||||
authProviderClientSecret: string;
|
|
||||||
handleClientIdChange: (value: string) => void;
|
|
||||||
handleClientSecretChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function GeneralProviderSettingsForm({
|
|
||||||
provider,
|
|
||||||
authProviderClientId,
|
|
||||||
authProviderClientSecret,
|
|
||||||
handleClientIdChange,
|
|
||||||
handleClientSecretChange,
|
|
||||||
}: GeneralProviderSettingsFormProps) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3 divide-y-1 divide-divide">
|
|
||||||
<ProviderSetting
|
|
||||||
title={`${provider.name} Client ID`}
|
|
||||||
desc={`Copy from ${provider.name} and enter here`}
|
|
||||||
inputPlaceholder="Paste Client ID here"
|
|
||||||
input
|
|
||||||
inputValue={authProviderClientId}
|
|
||||||
inputOnChange={handleClientIdChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title={`${provider.name} Client Secret`}
|
|
||||||
desc={`Copy from ${provider.name} and enter here`}
|
|
||||||
inputPlaceholder="Paste secret here"
|
|
||||||
input
|
|
||||||
inputValue={authProviderClientSecret}
|
|
||||||
inputOnChange={handleClientSecretChange}
|
|
||||||
inputType="password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './GeneralProviderSettingsForm';
|
|
||||||
export { default } from './GeneralProviderSettingsForm';
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { capitalize } from '@/utils/helpers';
|
|
||||||
import Image from 'next/image';
|
|
||||||
|
|
||||||
export interface PreviewProps {
|
|
||||||
provider: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Preview({ provider }: PreviewProps) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center p-10">
|
|
||||||
<Image
|
|
||||||
src={`/assets/social-providers/${provider.toLowerCase()}-preview.svg`}
|
|
||||||
alt={`${capitalize(provider)} sign in preview`}
|
|
||||||
className="mx-auto w-full max-w-md"
|
|
||||||
width={480}
|
|
||||||
height={267}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import Help from '@/components/icons/Help';
|
|
||||||
import type { Provider } from '@/types/providers';
|
|
||||||
import { Text } from '@/ui/Text';
|
|
||||||
import { resolveProvider } from '@/utils/resolveProvider';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
type ProviderHeaderProps = {
|
|
||||||
provider: Provider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProviderHeader({ provider }: ProviderHeaderProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
const providerId = router.query.providerId as string;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-row items-center space-x-2">
|
|
||||||
<div className="w-14">
|
|
||||||
<Image
|
|
||||||
src={`/assets/${resolveProvider(providerId)}.svg`}
|
|
||||||
alt={`Logo of ${provider.name}`}
|
|
||||||
width={56}
|
|
||||||
height={56}
|
|
||||||
layout="responsive"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row place-content-between">
|
|
||||||
<Text color="dark" className="font-medium capitalize" size="big">
|
|
||||||
{provider.name}
|
|
||||||
</Text>
|
|
||||||
{provider.docsLink && (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<a href={provider.docsLink} target="_blank" rel="noreferrer">
|
|
||||||
<Help className="h-10 w-10" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProviderHeader;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { useGetAppLoginDataQuery } from '@/generated/graphql';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
|
||||||
import { ProviderPage } from './ProviderPage';
|
|
||||||
|
|
||||||
export function ProviderPagePreload() {
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
|
|
||||||
const { data, loading, error } = useGetAppLoginDataQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication?.id,
|
|
||||||
},
|
|
||||||
skip: !currentApplication?.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <ActivityIndicator delay={500} label="Loading providers..." />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ProviderPage app={data?.app} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProviderPagePreload;
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Preview } from '@/components/applications/providers/Preview';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { useFormSaver } from '@/hooks/useFormSaver';
|
|
||||||
import type { Provider } from '@/types/providers';
|
|
||||||
import { FormSaver } from '@/ui/FormSaver';
|
|
||||||
import { Text } from '@/ui/Text';
|
|
||||||
import { Toggle } from '@/ui/Toggle';
|
|
||||||
import { getDynamicVariables } from '@/utils/getDynamicVariables';
|
|
||||||
import { triggerToast } from '@/utils/toast';
|
|
||||||
import { useUpdateAppMutation } from '@/utils/__generated__/graphql';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
type ProviderInfoProps = {
|
|
||||||
provider: Provider;
|
|
||||||
authProviderEnabled: boolean;
|
|
||||||
setAuthProviderEnabled: (enabled: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProviderInfo({
|
|
||||||
provider,
|
|
||||||
authProviderEnabled,
|
|
||||||
setAuthProviderEnabled,
|
|
||||||
}: ProviderInfoProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
const providerId = router.query.providerId as string;
|
|
||||||
const { showFormSaver, setShowFormSaver, submitState } = useFormSaver();
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
|
|
||||||
const [updateApp, { client }] = useUpdateAppMutation();
|
|
||||||
|
|
||||||
const { authEnabled } = getDynamicVariables(providerId, {}, true);
|
|
||||||
|
|
||||||
const handleFormSubmit = async () => {
|
|
||||||
try {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
[authEnabled as string]: authProviderEnabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await client.refetchQueries({
|
|
||||||
include: ['getAppLoginData'],
|
|
||||||
});
|
|
||||||
|
|
||||||
setShowFormSaver(false);
|
|
||||||
triggerToast('Settings saved');
|
|
||||||
} catch (error) {
|
|
||||||
// TODO: Display error to user and use a logging solution
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{showFormSaver && (
|
|
||||||
<FormSaver
|
|
||||||
show={showFormSaver}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowFormSaver(false);
|
|
||||||
setAuthProviderEnabled(false);
|
|
||||||
}}
|
|
||||||
onSave={() => {
|
|
||||||
handleFormSubmit();
|
|
||||||
}}
|
|
||||||
loading={submitState.loading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="mt-8 flex flex-row place-content-between">
|
|
||||||
<div className=" space-y-3">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Text
|
|
||||||
variant="body"
|
|
||||||
color="greyscaleDark"
|
|
||||||
className=" font-bold"
|
|
||||||
size="normal"
|
|
||||||
>
|
|
||||||
Let users sign in with
|
|
||||||
<span className="ml-1 capitalize">{provider.name}</span>
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="self-center">
|
|
||||||
<Toggle
|
|
||||||
checked={authProviderEnabled}
|
|
||||||
onChange={() => {
|
|
||||||
if (authProviderEnabled) {
|
|
||||||
setShowFormSaver(true);
|
|
||||||
}
|
|
||||||
setAuthProviderEnabled(!authProviderEnabled);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!authProviderEnabled && <Preview provider={providerId} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProviderInfo;
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import providers from '@/data/providers.json';
|
|
||||||
import { resolveProvider } from '@/utils/resolveProvider';
|
|
||||||
import type { GetAppFragment } from '@/utils/__generated__/graphql';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { ProviderHeader } from './ProviderHeader';
|
|
||||||
import { ProviderInfo } from './ProviderInfo';
|
|
||||||
import { ProviderSettings } from './ProviderSettings';
|
|
||||||
|
|
||||||
type ProviderPageProps = {
|
|
||||||
app: GetAppFragment;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProviderPage({ app }: ProviderPageProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const providerId = router.query.providerId as string;
|
|
||||||
|
|
||||||
const providerEnabled = app[`auth${resolveProvider(providerId)}Enabled`];
|
|
||||||
|
|
||||||
const [authProviderEnabled, setAuthProviderEnabled] =
|
|
||||||
useState(providerEnabled);
|
|
||||||
|
|
||||||
const provider = providers.find(
|
|
||||||
(availableProvider) => providerId === availableProvider.name.toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ProviderHeader provider={provider} />
|
|
||||||
<ProviderInfo
|
|
||||||
provider={provider}
|
|
||||||
authProviderEnabled={authProviderEnabled}
|
|
||||||
setAuthProviderEnabled={setAuthProviderEnabled}
|
|
||||||
/>
|
|
||||||
<ProviderSettings
|
|
||||||
provider={provider}
|
|
||||||
app={app}
|
|
||||||
authProviderEnabled={authProviderEnabled}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProviderPage;
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
import {
|
|
||||||
ProviderSetting,
|
|
||||||
ProviderSettingsSave,
|
|
||||||
} from '@/components/applications/settings/providers';
|
|
||||||
import type { GetAppFragment } from '@/generated/graphql';
|
|
||||||
import { useUpdateAppMutation } from '@/generated/graphql';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { useFormSaver } from '@/hooks/useFormSaver';
|
|
||||||
import type { Provider } from '@/types/providers';
|
|
||||||
import { Alert } from '@/ui/Alert';
|
|
||||||
import { FormSaver } from '@/ui/FormSaver';
|
|
||||||
import Button from '@/ui/v2/Button';
|
|
||||||
import ChevronDownIcon from '@/ui/v2/icons/ChevronDownIcon';
|
|
||||||
import ChevronUpIcon from '@/ui/v2/icons/ChevronUpIcon';
|
|
||||||
import { capitalize, generateRemoteAppUrl } from '@/utils/helpers';
|
|
||||||
import { resolveProvider } from '@/utils/resolveProvider';
|
|
||||||
import { triggerToast } from '@/utils/toast';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import AppleProviderSettingsForm from './AppleProviderSettingsForm';
|
|
||||||
import GeneralProviderSettingsForm from './GeneralProviderSettingsForm';
|
|
||||||
import WorkOsProviderSettingsForm from './WorkOsProviderSettingsForm';
|
|
||||||
|
|
||||||
export interface ProviderSettingsProps {
|
|
||||||
provider: Provider;
|
|
||||||
app: GetAppFragment;
|
|
||||||
authProviderEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO 1: Simplify this component, improve the reusability by redesigning the
|
|
||||||
// way the component renders the content, because it's hard to create a provider
|
|
||||||
// specific layout with the current implementation.
|
|
||||||
|
|
||||||
// TODO 2: Change the form to use react-hook-form, so that we can avoid passing
|
|
||||||
// too much props around these components (e.g: passing xy and handleXyChange to
|
|
||||||
// children would not be necessary at all).
|
|
||||||
|
|
||||||
// TODO 3: This is an accessibility improvement, but labels should be connected
|
|
||||||
// to the inputs.
|
|
||||||
export function ProviderSettings({
|
|
||||||
provider,
|
|
||||||
app,
|
|
||||||
authProviderEnabled,
|
|
||||||
}: ProviderSettingsProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
const providerId = router.query.providerId as string;
|
|
||||||
const [hideSettings, setHideSettings] = useState(false);
|
|
||||||
const [hasSettings, setHasSettings] = useState(false);
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
|
|
||||||
const {
|
|
||||||
authClientId,
|
|
||||||
authClientSecret,
|
|
||||||
authTeamId,
|
|
||||||
authKeyId,
|
|
||||||
authDefaultDomain,
|
|
||||||
authDefaultOrganization,
|
|
||||||
authDefaultConnection,
|
|
||||||
// TODO: This function should be extracted from this component and also it
|
|
||||||
// should be checked why values are used from it's return value **inside**
|
|
||||||
// the function body.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
||||||
} = getProviderSpecificVariables(providerId);
|
|
||||||
|
|
||||||
const [authProviderClientSecret, setAuthProviderClientSecret] = useState(
|
|
||||||
app[authClientSecret] || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [authProviderClientId, setAuthProviderClientId] = useState(
|
|
||||||
app[authClientId] || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [authProviderTeamId, setAuthProviderTeamId] = useState(
|
|
||||||
app[authTeamId] || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [authProviderKeyId, setAuthProviderKeyId] = useState(
|
|
||||||
app[authKeyId] || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
const [authProviderDefaultDomain, setAuthProviderDefaultDomain] = useState(
|
|
||||||
app[authDefaultDomain] || '',
|
|
||||||
);
|
|
||||||
const [authProviderDefaultOrganization, setAuthProviderDefaultOrganization] =
|
|
||||||
useState(app[authDefaultOrganization] || '');
|
|
||||||
const [authProviderDefaultConnection, setAuthProviderDefaultConnection] =
|
|
||||||
useState(app[authDefaultConnection] || '');
|
|
||||||
|
|
||||||
const [callError, setCallError] = useState({ error: false, message: '' });
|
|
||||||
|
|
||||||
const [updateApp, { client, loading }] = useUpdateAppMutation();
|
|
||||||
const { showFormSaver, setShowFormSaver, submitState } = useFormSaver();
|
|
||||||
|
|
||||||
function getProviderSpecificVariables(
|
|
||||||
targetProvider: string,
|
|
||||||
{ prefill = true } = {},
|
|
||||||
) {
|
|
||||||
if (targetProvider === 'twitter') {
|
|
||||||
if (!prefill) {
|
|
||||||
return {
|
|
||||||
authTwitterEnabled: authProviderEnabled,
|
|
||||||
authTwitterConsumerKey: authProviderClientId,
|
|
||||||
authTwitterConsumerSecret: authProviderClientSecret,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authEnabled: 'authTwitterEnabled',
|
|
||||||
authClientId: 'authTwitterConsumerKey',
|
|
||||||
authClientSecret: 'authTwitterConsumerSecret',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetProvider === 'apple') {
|
|
||||||
if (!prefill) {
|
|
||||||
return {
|
|
||||||
authAppleEnabled: authProviderEnabled,
|
|
||||||
authAppleClientId: authProviderClientId,
|
|
||||||
authAppleKeyId: authProviderKeyId,
|
|
||||||
authAppleTeamId: authProviderTeamId,
|
|
||||||
authApplePrivateKey: authProviderClientSecret.replace(/\n/gi, '\\n'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authEnabled: 'authAppleEnabled',
|
|
||||||
authClientId: 'authAppleClientId',
|
|
||||||
authClientSecret: 'authApplePrivateKey',
|
|
||||||
authTeamId: 'authAppleTeamId',
|
|
||||||
authKeyId: 'authAppleKeyId',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetProvider === 'workos') {
|
|
||||||
if (!prefill) {
|
|
||||||
return {
|
|
||||||
authWorkOsEnabled: authProviderEnabled,
|
|
||||||
authWorkOsClientId: authProviderClientId,
|
|
||||||
authWorkOsClientSecret: authProviderClientSecret,
|
|
||||||
authWorkOsDefaultDomain: authProviderDefaultDomain,
|
|
||||||
authWorkOsDefaultOrganization: authProviderDefaultOrganization,
|
|
||||||
authWorkOsDefaultConnection: authProviderDefaultConnection,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authEnabled: 'authWorkOsEnabled',
|
|
||||||
authClientId: 'authWorkOsClientId',
|
|
||||||
authClientSecret: 'authWorkOsClientSecret',
|
|
||||||
authDefaultDomain: 'authWorkOsDefaultDomain',
|
|
||||||
authDefaultOrganization: 'authWorkOsDefaultOrganization',
|
|
||||||
authDefaultConnection: 'authWorkOsDefaultConnection',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const authEnabled = `auth${resolveProvider(providerId)}Enabled`;
|
|
||||||
const clientId = `auth${resolveProvider(providerId)}ClientId`;
|
|
||||||
const clientSecret = `auth${resolveProvider(providerId)}ClientSecret`;
|
|
||||||
|
|
||||||
if (!prefill) {
|
|
||||||
return {
|
|
||||||
[authEnabled]: authProviderEnabled,
|
|
||||||
[clientId]: authProviderClientId,
|
|
||||||
[clientSecret]: authProviderClientSecret,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authEnabled,
|
|
||||||
authClientId: clientId,
|
|
||||||
authClientSecret: clientSecret,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Gets the particular providerId GQL field.
|
|
||||||
const { authEnabled } = getProviderSpecificVariables(providerId);
|
|
||||||
// Checks if the providerId field is enabled on the app that we get from origin.
|
|
||||||
if (app[authEnabled]) {
|
|
||||||
setHasSettings(true);
|
|
||||||
setHideSettings(true);
|
|
||||||
}
|
|
||||||
}, [hasSettings, setHasSettings, app]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const {
|
|
||||||
authClientId: clientId,
|
|
||||||
authTeamId: teamId,
|
|
||||||
authKeyId: keyId,
|
|
||||||
authClientSecret: clientSecret,
|
|
||||||
} = getProviderSpecificVariables(providerId);
|
|
||||||
|
|
||||||
// This side effect checks if the clientId or secret doesn't equal the app's clientId or secret and shows the form saver, which can be used to save the new changes.
|
|
||||||
if (
|
|
||||||
hasSettings &&
|
|
||||||
(app[clientSecret] !== authProviderClientSecret ||
|
|
||||||
app[clientId] !== authProviderClientId ||
|
|
||||||
app[teamId] !== authProviderTeamId ||
|
|
||||||
app[keyId] !== authProviderKeyId)
|
|
||||||
) {
|
|
||||||
setShowFormSaver(true);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
hasSettings,
|
|
||||||
authProviderClientSecret,
|
|
||||||
authProviderClientId,
|
|
||||||
authProviderTeamId,
|
|
||||||
authProviderKeyId,
|
|
||||||
authClientSecret,
|
|
||||||
authClientId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleSettingsToggle = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setHideSettings(!hideSettings);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e?: React.SyntheticEvent<HTMLFormElement>) => {
|
|
||||||
if (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
...getProviderSpecificVariables(providerId, { prefill: false }),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
setCallError({ error: true, message: error.message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await client.refetchQueries({
|
|
||||||
include: ['getAppLoginData'],
|
|
||||||
});
|
|
||||||
setShowFormSaver(false);
|
|
||||||
triggerToast('Settings saved');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClientIdChange = (value: string) => {
|
|
||||||
setCallError({ error: false, message: '' });
|
|
||||||
setAuthProviderClientId(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTeamIdChange = (value: string) => {
|
|
||||||
setCallError({ error: false, message: '' });
|
|
||||||
setAuthProviderTeamId(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyIdChange = (value: string) => {
|
|
||||||
setCallError({ error: false, message: '' });
|
|
||||||
setAuthProviderKeyId(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClientSecretChange = (value: string) => {
|
|
||||||
setCallError({ error: false, message: '' });
|
|
||||||
setAuthProviderClientSecret(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{showFormSaver && (
|
|
||||||
<FormSaver
|
|
||||||
show={showFormSaver}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowFormSaver(false);
|
|
||||||
}}
|
|
||||||
onSave={() => {
|
|
||||||
handleSubmit();
|
|
||||||
}}
|
|
||||||
loading={submitState.loading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
{authProviderEnabled && (
|
|
||||||
<div className="mt-8 space-y-3 divide-y-1 divide-divide border-t border-b pb-2">
|
|
||||||
{!hideSettings && (
|
|
||||||
<>
|
|
||||||
{providerId === 'apple' && (
|
|
||||||
<AppleProviderSettingsForm
|
|
||||||
authProviderClientId={authProviderClientId}
|
|
||||||
authProviderTeamId={authProviderTeamId}
|
|
||||||
authProviderKeyId={authProviderKeyId}
|
|
||||||
authProviderClientSecret={authProviderClientSecret}
|
|
||||||
handleClientIdChange={handleClientIdChange}
|
|
||||||
handleTeamIdChange={handleTeamIdChange}
|
|
||||||
handleKeyIdChange={handleKeyIdChange}
|
|
||||||
handleClientSecretChange={handleClientSecretChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{providerId !== 'apple' && (
|
|
||||||
<GeneralProviderSettingsForm
|
|
||||||
provider={provider}
|
|
||||||
authProviderClientId={authProviderClientId}
|
|
||||||
authProviderClientSecret={authProviderClientSecret}
|
|
||||||
handleClientIdChange={handleClientIdChange}
|
|
||||||
handleClientSecretChange={handleClientSecretChange}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{providerId === 'workos' && (
|
|
||||||
<WorkOsProviderSettingsForm
|
|
||||||
defaultDomain={authProviderDefaultDomain}
|
|
||||||
defaultOrganization={authProviderDefaultOrganization}
|
|
||||||
defaultConnection={authProviderDefaultConnection}
|
|
||||||
handleDefaultDomainChange={setAuthProviderDefaultDomain}
|
|
||||||
handleDefaultOrganizationChange={
|
|
||||||
setAuthProviderDefaultOrganization
|
|
||||||
}
|
|
||||||
handleDefaultConnectionChange={
|
|
||||||
setAuthProviderDefaultConnection
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title={hideSettings ? 'Login button URL' : 'OAuth Callback URL'}
|
|
||||||
desc={
|
|
||||||
hideSettings
|
|
||||||
? `Use this in your frontend`
|
|
||||||
: `Paste into ${capitalize(providerId)}`
|
|
||||||
}
|
|
||||||
inputPlaceholder=""
|
|
||||||
input={false}
|
|
||||||
showCopy
|
|
||||||
link={
|
|
||||||
hideSettings
|
|
||||||
? `${generateRemoteAppUrl(
|
|
||||||
app.subdomain,
|
|
||||||
)}/v1/auth/signin/provider/${providerId.toLowerCase()}`
|
|
||||||
: `${generateRemoteAppUrl(
|
|
||||||
app.subdomain,
|
|
||||||
)}/v1/auth/signin/provider/${providerId.toLowerCase()}/callback`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{callError.error && (
|
|
||||||
<Alert severity="error">
|
|
||||||
{callError.message ||
|
|
||||||
'Error trying to update login provider settings.'}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{authProviderEnabled && hasSettings && (
|
|
||||||
<div className="mt-4 px-2">
|
|
||||||
<Button
|
|
||||||
variant="borderless"
|
|
||||||
onClick={handleSettingsToggle}
|
|
||||||
className="grid grid-flow-col gap-1.5 text-xs"
|
|
||||||
>
|
|
||||||
{hideSettings ? (
|
|
||||||
<>
|
|
||||||
View Settings
|
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Hide Settings
|
|
||||||
<ChevronUpIcon className="h-4 w-4" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{authProviderEnabled && !hasSettings && (
|
|
||||||
<ProviderSettingsSave provider={provider} loading={loading} />
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { ProviderSetting } from '@/components/applications/settings/providers/helpers';
|
|
||||||
|
|
||||||
// TODO: See TODO comment in ProviderSettings.tsx about the react-hook-form
|
|
||||||
// refactor
|
|
||||||
export interface WorkOsProviderSettingsFormProps {
|
|
||||||
defaultDomain: string;
|
|
||||||
defaultOrganization: string;
|
|
||||||
defaultConnection: string;
|
|
||||||
handleDefaultDomainChange: (value: string) => void;
|
|
||||||
handleDefaultOrganizationChange: (value: string) => void;
|
|
||||||
handleDefaultConnectionChange: (value: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function WorkOsProviderSettingsForm({
|
|
||||||
defaultDomain,
|
|
||||||
defaultOrganization,
|
|
||||||
defaultConnection,
|
|
||||||
handleDefaultDomainChange,
|
|
||||||
handleDefaultOrganizationChange,
|
|
||||||
handleDefaultConnectionChange,
|
|
||||||
}: WorkOsProviderSettingsFormProps) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-flow-row gap-3 divide-y-1">
|
|
||||||
<ProviderSetting
|
|
||||||
title="Default Domain"
|
|
||||||
desc=""
|
|
||||||
inputPlaceholder=""
|
|
||||||
input
|
|
||||||
inputValue={defaultDomain}
|
|
||||||
inputOnChange={handleDefaultDomainChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title="Default Organization"
|
|
||||||
desc=""
|
|
||||||
inputPlaceholder=""
|
|
||||||
input
|
|
||||||
inputValue={defaultOrganization}
|
|
||||||
inputOnChange={handleDefaultOrganizationChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSetting
|
|
||||||
title="Default Connection"
|
|
||||||
desc=""
|
|
||||||
inputPlaceholder=""
|
|
||||||
input
|
|
||||||
inputValue={defaultConnection}
|
|
||||||
inputOnChange={handleDefaultConnectionChange}
|
|
||||||
inputType="text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './WorkOsProviderSettingsForm';
|
|
||||||
export { default } from './WorkOsProviderSettingsForm';
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { PermissionSetting } from '@/components/applications/users/PermissionSetting';
|
|
||||||
import { SettingsSection } from '@/components/applications/users/SettingsSection';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
|
||||||
import { Alert } from '@/ui/Alert';
|
|
||||||
import DelayedLoading from '@/ui/DelayedLoading';
|
|
||||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
|
||||||
import {
|
|
||||||
useGetAuthSettingsQuery,
|
|
||||||
useUpdateAppMutation,
|
|
||||||
} from '@/utils/__generated__/graphql';
|
|
||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
|
|
||||||
export function GeneralPermissions() {
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
const [updateApp] = useUpdateAppMutation();
|
|
||||||
const client = useApolloClient();
|
|
||||||
const { submitState, setSubmitState } = useSubmitState();
|
|
||||||
let toastId: string;
|
|
||||||
|
|
||||||
const { loading, data, error } = useGetAuthSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <DelayedLoading delay={500} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full bg-white ">
|
|
||||||
<SettingsSection
|
|
||||||
title="General Permissions"
|
|
||||||
desc="These settings affect all users in your project."
|
|
||||||
>
|
|
||||||
{submitState.error && (
|
|
||||||
<Alert severity="error">{submitState.error.message}</Alert>
|
|
||||||
)}
|
|
||||||
<div className="divide-y-1 border-t border-b">
|
|
||||||
<PermissionSetting
|
|
||||||
text="Disable New Users"
|
|
||||||
desc="If set, newly registered users are disabled and won't be able to sign in."
|
|
||||||
toggle
|
|
||||||
checked={data.app.authDisableNewUsers}
|
|
||||||
onChange={async () => {
|
|
||||||
try {
|
|
||||||
toastId = showLoadingToast('Saving changes...');
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authDisableNewUsers: !data.app.authDisableNewUsers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
|
||||||
toast.remove(toastId);
|
|
||||||
triggerToast(
|
|
||||||
`Disable new users ${
|
|
||||||
data.app.authDisableNewUsers ? `Disabled` : `Enabled`
|
|
||||||
} for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
} catch (updateError) {
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toastId) {
|
|
||||||
toast.remove(toastId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['authDisableNewUsers'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PermissionSetting
|
|
||||||
text="Allow Anonymous Users"
|
|
||||||
desc="Enables users to register as an anonymous user."
|
|
||||||
toggle
|
|
||||||
checked={data.app.authAnonymousUsersEnabled}
|
|
||||||
onChange={async () => {
|
|
||||||
setSubmitState({
|
|
||||||
loading: true,
|
|
||||||
error: null,
|
|
||||||
fieldsWithError: [],
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
toastId = showLoadingToast('Saving changes...');
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authAnonymousUsersEnabled:
|
|
||||||
!data.app.authAnonymousUsersEnabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
|
||||||
toast.remove(toastId);
|
|
||||||
triggerToast(
|
|
||||||
`Anonymous users registration ${
|
|
||||||
data.app.authAnonymousUsersEnabled ? `disabled` : `enabled`
|
|
||||||
} for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
} catch (updateError) {
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toastId) {
|
|
||||||
toast.remove(toastId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['authAnonymousUsersEnabled'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingsSection>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GeneralPermissions;
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import { PermissionSetting } from '@/components/applications/users/PermissionSetting';
|
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
|
||||||
import { Toggle } from '@/ui';
|
|
||||||
import { Alert } from '@/ui/Alert';
|
|
||||||
import DelayedLoading from '@/ui/DelayedLoading';
|
|
||||||
import { Text } from '@/ui/Text';
|
|
||||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
|
||||||
import {
|
|
||||||
useGetGravatarSettingsQuery,
|
|
||||||
useUpdateAppMutation,
|
|
||||||
} from '@/utils/__generated__/graphql';
|
|
||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
|
|
||||||
export function GravatarSettings() {
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
const [updateApp] = useUpdateAppMutation();
|
|
||||||
const client = useApolloClient();
|
|
||||||
const [currentDefaultGravatar, setCurrentDefaultGravatar] = useState({
|
|
||||||
id: 'blank',
|
|
||||||
name: 'blank',
|
|
||||||
disabled: false,
|
|
||||||
slug: 'blank',
|
|
||||||
});
|
|
||||||
const [currentGravatarRating, setCurrentGravatarRating] = useState({
|
|
||||||
id: 'g',
|
|
||||||
name: 'g',
|
|
||||||
disabled: false,
|
|
||||||
slug: 'g',
|
|
||||||
});
|
|
||||||
|
|
||||||
const { submitState, setSubmitState } = useSubmitState();
|
|
||||||
|
|
||||||
const { loading, data, error } = useGetGravatarSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let toastId: string;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentDefaultGravatar((previousDefaultGravatar) => ({
|
|
||||||
...previousDefaultGravatar,
|
|
||||||
name: data.app.authGravatarDefault,
|
|
||||||
id: data.app.authGravatarDefault,
|
|
||||||
slug: data.app.authGravatarDefault,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setCurrentGravatarRating((previousGravatarRating) => ({
|
|
||||||
...previousGravatarRating,
|
|
||||||
name: data.app.authGravatarRating,
|
|
||||||
id: data.app.authGravatarRating,
|
|
||||||
slug: data.app.authGravatarRating,
|
|
||||||
}));
|
|
||||||
}, [data, setCurrentDefaultGravatar, setCurrentGravatarRating]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <DelayedLoading delay={500} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto w-full font-display">
|
|
||||||
<div className="flex flex-row place-content-between">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Text
|
|
||||||
variant="body"
|
|
||||||
size="large"
|
|
||||||
className="font-medium"
|
|
||||||
color="greyscaleDark"
|
|
||||||
>
|
|
||||||
Gravatar Settings
|
|
||||||
</Text>
|
|
||||||
<div>
|
|
||||||
<Text
|
|
||||||
variant="body"
|
|
||||||
size="normal"
|
|
||||||
color="greyscaleDark"
|
|
||||||
className="mt-1"
|
|
||||||
>
|
|
||||||
Enable Gravatars as avatar URL for users.
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mr-2 flex flex-row">
|
|
||||||
<Toggle
|
|
||||||
checked={data.app.authGravatarEnabled}
|
|
||||||
onChange={async () => {
|
|
||||||
try {
|
|
||||||
toastId = showLoadingToast('Saving changes...');
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authGravatarEnabled: !data.app.authGravatarEnabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await client.refetchQueries({
|
|
||||||
include: ['getGravatarSettings'],
|
|
||||||
});
|
|
||||||
toast.remove(toastId);
|
|
||||||
triggerToast(
|
|
||||||
`Gravatars ${
|
|
||||||
data.app.authGravatarEnabled ? `Disabled` : `Enabled`
|
|
||||||
} for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
} catch (updateError) {
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toastId) {
|
|
||||||
toast.remove(toastId);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['authGravatarEnabled'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{submitState.error && (
|
|
||||||
<Alert severity="error" className="mt-4">
|
|
||||||
{submitState.error.message}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.app.authGravatarEnabled && (
|
|
||||||
<div className="mt-6 mb-12 flex flex-col divide-y-1 divide-divide border-t border-b">
|
|
||||||
<PermissionSetting
|
|
||||||
text="AUTH_GRAVATAR_DEFAULT"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
id: '404',
|
|
||||||
name: '404',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mp',
|
|
||||||
name: 'mp',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'identicon',
|
|
||||||
name: 'identicon',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'monsterid',
|
|
||||||
name: 'monsterid',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'waatar',
|
|
||||||
name: 'waatar',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'retro',
|
|
||||||
name: 'retro',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'robohash',
|
|
||||||
name: 'robohash',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'blank',
|
|
||||||
name: 'blank',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={currentDefaultGravatar}
|
|
||||||
onChange={async (v: { id: string }) => {
|
|
||||||
try {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authGravatarDefault: v.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
client.refetchQueries({ include: ['getGravatarSettings'] });
|
|
||||||
triggerToast(
|
|
||||||
`Changed default gravatar for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
} catch (updateError) {
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['authGravatarDefault'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PermissionSetting
|
|
||||||
text="AUTH_GRAVATAR_RATING"
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
id: 'g',
|
|
||||||
name: 'g',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'pg',
|
|
||||||
name: 'pg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'r',
|
|
||||||
name: 'r',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'x',
|
|
||||||
name: 'x',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={currentGravatarRating}
|
|
||||||
onChange={async (v: { id: string }) => {
|
|
||||||
try {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authGravatarRating: v.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
client.refetchQueries({ include: ['getGravatarSettings'] });
|
|
||||||
triggerToast(
|
|
||||||
`Changed Gravatar rating for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
} catch (updateError) {
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['authGravatarRating'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default GravatarSettings;
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
|
||||||
import { useFormSaver } from '@/hooks/useFormSaver';
|
|
||||||
import { FormSaver, Toggle } from '@/ui';
|
|
||||||
import { Alert } from '@/ui/Alert';
|
|
||||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
|
||||||
import Input from '@/ui/v2/Input';
|
|
||||||
import Text from '@/ui/v2/Text';
|
|
||||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
|
||||||
import {
|
|
||||||
useGetAuthSettingsQuery,
|
|
||||||
useUpdateAppMutation,
|
|
||||||
} from '@/utils/__generated__/graphql';
|
|
||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
|
||||||
import toast from 'react-hot-toast';
|
|
||||||
|
|
||||||
export function MultiFactorAuthentication() {
|
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
|
||||||
const [updateApp] = useUpdateAppMutation();
|
|
||||||
const [OTPIssuer, setOTPIssuer] = useState('');
|
|
||||||
const client = useApolloClient();
|
|
||||||
const { showFormSaver, setShowFormSaver, submitState, setSubmitState } =
|
|
||||||
useFormSaver();
|
|
||||||
|
|
||||||
const toastId = useRef<string>();
|
|
||||||
|
|
||||||
const { loading, data, error } = useGetAuthSettingsQuery({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.app.authMfaTotpIssuer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setOTPIssuer(data.app.authMfaTotpIssuer);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<ActivityIndicator
|
|
||||||
delay={500}
|
|
||||||
label="Loading settings..."
|
|
||||||
className="mx-auto"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSaveForm() {
|
|
||||||
setSubmitState({
|
|
||||||
loading: true,
|
|
||||||
error: null,
|
|
||||||
fieldsWithError: [],
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authMfaTotpIssuer: OTPIssuer,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: null,
|
|
||||||
fieldsWithError: [],
|
|
||||||
});
|
|
||||||
setShowFormSaver(false);
|
|
||||||
triggerToast('All changes saved');
|
|
||||||
} catch (updateError) {
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['OTPIssuer'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToggleMFA() {
|
|
||||||
try {
|
|
||||||
toastId.current = showLoadingToast('Saving changes...');
|
|
||||||
await updateApp({
|
|
||||||
variables: {
|
|
||||||
id: currentApplication.id,
|
|
||||||
app: {
|
|
||||||
authMfaEnabled: !data.app.authMfaEnabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await client.refetchQueries({ include: ['getAuthSettings'] });
|
|
||||||
|
|
||||||
if (toastId?.current) {
|
|
||||||
toast.remove(toastId.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerToast(
|
|
||||||
`Multi-Factor Authentication ${
|
|
||||||
data.app.authMfaEnabled ? `Disabled` : `Enabled`
|
|
||||||
} for ${currentApplication.name}`,
|
|
||||||
);
|
|
||||||
} catch (updateError) {
|
|
||||||
if (toastId?.current) {
|
|
||||||
toast.remove(toastId.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateError instanceof Error) {
|
|
||||||
triggerToast(updateError.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitState({
|
|
||||||
loading: false,
|
|
||||||
error: updateError,
|
|
||||||
fieldsWithError: ['authMfaEnabled'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid w-full grid-flow-row gap-4">
|
|
||||||
{showFormSaver && (
|
|
||||||
<FormSaver
|
|
||||||
show={showFormSaver}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowFormSaver(false);
|
|
||||||
}}
|
|
||||||
onSave={handleSaveForm}
|
|
||||||
loading={submitState.loading}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-row place-content-between">
|
|
||||||
<div className="grid grid-flow-row gap-1.5">
|
|
||||||
<Text variant="h3" component="h2">
|
|
||||||
Multi-Factor Authentication
|
|
||||||
</Text>
|
|
||||||
<Text>Enable users to use multi-factor authentication (MFA).</Text>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mr-2 flex flex-row">
|
|
||||||
<Toggle
|
|
||||||
checked={data.app.authMfaEnabled}
|
|
||||||
onChange={handleToggleMFA}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{submitState.error && (
|
|
||||||
<Alert severity="error">{submitState.error.message}</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{data.app.authMfaEnabled && (
|
|
||||||
<div className="border-t border-b border-gray-200 py-4">
|
|
||||||
<Input
|
|
||||||
id="otpIssuer"
|
|
||||||
label="Name of the One Time Password (OTP) issuer"
|
|
||||||
onChange={(e) => {
|
|
||||||
setShowFormSaver(true);
|
|
||||||
setOTPIssuer(e.target.value);
|
|
||||||
}}
|
|
||||||
variant="inline"
|
|
||||||
value={OTPIssuer}
|
|
||||||
error={submitState.fieldsWithError?.includes('OTPIssuer')}
|
|
||||||
placeholder={currentApplication.name}
|
|
||||||
fullWidth
|
|
||||||
hideEmptyHelperText
|
|
||||||
inlineInputProportion="50%"
|
|
||||||
componentsProps={{
|
|
||||||
label: { className: 'text-sm+' },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MultiFactorAuthentication;
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import Copy from '@/components/icons/Copy';
|
|
||||||
import type { Provider } from '@/types/providers';
|
|
||||||
import { Button } from '@/ui/Button';
|
|
||||||
import CheckBoxes from '@/ui/Checkboxes';
|
|
||||||
import { Input } from '@/ui/Input';
|
|
||||||
import { Text } from '@/ui/Text';
|
|
||||||
import { copy } from '@/utils/copy';
|
|
||||||
import clsx from 'clsx';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
// TODO: Instead of a `helpers.tsx`, we should have designated files for these
|
|
||||||
// components
|
|
||||||
type ProviderSettingsProps = {
|
|
||||||
title: string;
|
|
||||||
desc: string;
|
|
||||||
inputPlaceholder?: string;
|
|
||||||
input: boolean;
|
|
||||||
inputValue?: string;
|
|
||||||
inputOnChange?: (v: string) => void;
|
|
||||||
inputType?: 'text' | 'password';
|
|
||||||
multiline?: boolean;
|
|
||||||
link?: string;
|
|
||||||
showCopy?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProviderSetting({
|
|
||||||
title,
|
|
||||||
desc,
|
|
||||||
inputPlaceholder,
|
|
||||||
input,
|
|
||||||
inputValue,
|
|
||||||
inputOnChange,
|
|
||||||
inputType,
|
|
||||||
link,
|
|
||||||
showCopy = false,
|
|
||||||
multiline,
|
|
||||||
}: ProviderSettingsProps) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'flex w-full flex-row items-center justify-between px-2 pt-3 pb-1',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex w-80 flex-col">
|
|
||||||
<Text
|
|
||||||
variant="body"
|
|
||||||
color="greyscaleDark"
|
|
||||||
size="normal"
|
|
||||||
className="font-medium capitalize"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
<Text color="greyscaleDark" size="tiny" className="font-normal">
|
|
||||||
{desc}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full flex-row place-content-between self-center">
|
|
||||||
{input ? (
|
|
||||||
<Input
|
|
||||||
placeholder={inputPlaceholder || ''}
|
|
||||||
className="h-full w-full"
|
|
||||||
type={inputType}
|
|
||||||
value={inputValue}
|
|
||||||
onChange={inputOnChange}
|
|
||||||
multiline={multiline}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-row self-center align-middle">
|
|
||||||
<Text
|
|
||||||
color="greyscaleDark"
|
|
||||||
size="tiny"
|
|
||||||
className="self-center font-normal"
|
|
||||||
>
|
|
||||||
{link}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{showCopy && (
|
|
||||||
<Copy
|
|
||||||
className="ml-1 mr-4 h-4 w-4 cursor-pointer self-center text-greyscaleDark"
|
|
||||||
onClick={() => {
|
|
||||||
copy(link as string, title);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProviderSettingsSaveProps = {
|
|
||||||
provider: Provider;
|
|
||||||
loading: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function ProviderSettingsSave({
|
|
||||||
provider,
|
|
||||||
loading,
|
|
||||||
}: ProviderSettingsSaveProps) {
|
|
||||||
const [confirmed, setConfirmed] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-4 flex w-full flex-row place-content-between px-2">
|
|
||||||
<CheckBoxes
|
|
||||||
id="confirm-paste"
|
|
||||||
state={confirmed}
|
|
||||||
setState={() => setConfirmed(!confirmed)}
|
|
||||||
checkBoxText={`I have pasted the redirect URI into ${provider.name}.`}
|
|
||||||
/>
|
|
||||||
<div />
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="primary"
|
|
||||||
disabled={!confirmed}
|
|
||||||
loading={loading}
|
|
||||||
className="self-center"
|
|
||||||
>
|
|
||||||
Confirm Settings
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './helpers';
|
|
||||||
@@ -32,25 +32,25 @@ function Users({ users }: any) {
|
|||||||
key={user.id}
|
key={user.id}
|
||||||
passHref
|
passHref
|
||||||
>
|
>
|
||||||
<tr className="cursor-pointer w-52">
|
<tr className="w-52 cursor-pointer">
|
||||||
<td className="py-1 pr-6 whitespace-nowrap">
|
<td className="whitespace-nowrap py-1 pr-6">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="p-1 mr-2"
|
className="mr-2 p-1"
|
||||||
aria-label="Copy user ID"
|
aria-label="Copy user ID"
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
copy(user.id, `User ID`);
|
copy(user.id, `User ID`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CopyIcon className="w-4 h-4" />
|
<CopyIcon className="h-4 w-4" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<div className="flex-shrink-0 w-8 h-8">
|
<div className="h-8 w-8 flex-shrink-0">
|
||||||
<Avatar
|
<Avatar
|
||||||
className="w-8 h-8"
|
className="h-8 w-8"
|
||||||
avatarUrl={user.avatarUrl}
|
avatarUrl={user.avatarUrl}
|
||||||
name={user.displayName}
|
name={user.displayName}
|
||||||
/>
|
/>
|
||||||
@@ -63,7 +63,7 @@ function Users({ users }: any) {
|
|||||||
<Text
|
<Text
|
||||||
variant="a"
|
variant="a"
|
||||||
color="greyscaleDark"
|
color="greyscaleDark"
|
||||||
className="font-medium cursor-pointer"
|
className="cursor-pointer font-medium"
|
||||||
size="normal"
|
size="normal"
|
||||||
>
|
>
|
||||||
{user.displayName ||
|
{user.displayName ||
|
||||||
@@ -78,7 +78,7 @@ function Users({ users }: any) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="whitespace-nowrap px-6 py-4">
|
||||||
<Text color="greyscaleDark" className="font-normal" size="normal">
|
<Text color="greyscaleDark" className="font-normal" size="normal">
|
||||||
{format(new Date(user.createdAt), 'd MMM yyyy')}
|
{format(new Date(user.createdAt), 'd MMM yyyy')}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -103,13 +103,13 @@ function Users({ users }: any) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-4 pl-6 text-sm font-medium text-right whitespace-nowrap">
|
<td className="whitespace-nowrap py-4 pl-6 text-right text-sm font-medium">
|
||||||
<Link
|
<Link
|
||||||
href={`/${workspaceSlug}/${appSlug}/users/${user.id}`}
|
href={`/${workspaceSlug}/${appSlug}/users/${user.id}`}
|
||||||
passHref
|
passHref
|
||||||
>
|
>
|
||||||
<a href={`${workspaceSlug}/${appSlug}/users/${user.id}`}>
|
<a href={`${workspaceSlug}/${appSlug}/users/${user.id}`}>
|
||||||
<ChevronRightIcon className="self-center w-4 h-4 ml-2 cursor-pointer" />
|
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
@@ -131,7 +131,7 @@ function UserPages({ totalNrOfPages, setCurrentPage }: any) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={i}
|
key={i}
|
||||||
className="px-2 cursor-pointer"
|
className="cursor-pointer px-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCurrentPage(i);
|
setCurrentPage(i);
|
||||||
}}
|
}}
|
||||||
@@ -212,15 +212,15 @@ export function UsersTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col mt-2 font-display">
|
<div className="mt-2 flex flex-col font-display">
|
||||||
<div className="inline-block min-w-full py-2 align-">
|
<div className="align- inline-block min-w-full py-2">
|
||||||
<div className="overflow-hidden border-b">
|
<div className="overflow-hidden border-b">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="">
|
<thead className="">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-4 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
className="px-4 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||||
>
|
>
|
||||||
{data ? (
|
{data ? (
|
||||||
<TotalUsers
|
<TotalUsers
|
||||||
@@ -244,7 +244,7 @@ export function UsersTable({
|
|||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||||
>
|
>
|
||||||
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
||||||
Signed up at
|
Signed up at
|
||||||
@@ -253,7 +253,7 @@ export function UsersTable({
|
|||||||
|
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||||
>
|
>
|
||||||
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
||||||
Roles
|
Roles
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function CreatePermissionVariableModal({
|
|||||||
const [error, setError] = useState<Error>();
|
const [error, setError] = useState<Error>();
|
||||||
|
|
||||||
const form = useForm<CreatePermissionVariableFormData>({
|
const form = useForm<CreatePermissionVariableFormData>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export default function EditPermissionVariableModal({
|
|||||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||||
|
|
||||||
const form = useForm<EditPermissionVariableFormData>({
|
const form = useForm<EditPermissionVariableFormData>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
key: originalCustomClaim.key || '',
|
key: originalCustomClaim.key || '',
|
||||||
value: originalCustomClaim.value || '',
|
value: originalCustomClaim.value || '',
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function CreateUserRoleModal({ onClose }: CreateUserRoleModalProps) {
|
|||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
const form = useForm<CreateUserRoleBaseFormData>({
|
const form = useForm<CreateUserRoleBaseFormData>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
});
|
});
|
||||||
|
|
||||||
const [updateApp] = useUpdateAppMutation({
|
const [updateApp] = useUpdateAppMutation({
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export function EditUserRoleModal({
|
|||||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
const form = useForm<EditUserRoleFormData>({
|
const form = useForm<EditUserRoleFormData>({
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
roleName: originalRole.name || '',
|
roleName: originalRole.name || '',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function AddPaymentMethodForm({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 pt-6 pb-6 text-left w-modal2">
|
<div className="w-modal2 px-6 pt-6 pb-6 text-left">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<Text
|
<Text
|
||||||
@@ -161,11 +161,11 @@ function AddPaymentMethodForm({
|
|||||||
variant="body"
|
variant="body"
|
||||||
color="greyscaleDark"
|
color="greyscaleDark"
|
||||||
size="small"
|
size="small"
|
||||||
className="font-normal text-center"
|
className="text-center font-normal"
|
||||||
>
|
>
|
||||||
We'll store these in your workspace for future use.
|
We'll store these in your workspace for future use.
|
||||||
</Text>
|
</Text>
|
||||||
<div className="w-full px-2 py-2 my-2 mt-6 rounded-lg border-1">
|
<div className="my-2 mt-6 w-full rounded-lg border-1 px-2 py-2">
|
||||||
<CardElement
|
<CardElement
|
||||||
onReady={(element) => element.focus()}
|
onReady={(element) => element.focus()}
|
||||||
options={{
|
options={{
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function ControlledCheckbox(
|
|||||||
name={field.name}
|
name={field.name}
|
||||||
ref={mergeRefs([field.ref, ref])}
|
ref={mergeRefs([field.ref, ref])}
|
||||||
onChange={(event, checked) => {
|
onChange={(event, checked) => {
|
||||||
setValue(controllerProps?.name || name, checked);
|
setValue(controllerProps?.name || name, checked, { shouldDirty: true });
|
||||||
|
|
||||||
if (props.onChange) {
|
if (props.onChange) {
|
||||||
props.onChange(event, checked);
|
props.onChange(event, checked);
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { SwitchProps } from '@/ui/v2/Switch';
|
||||||
|
import Switch from '@/ui/v2/Switch';
|
||||||
|
import type { ForwardedRef } from 'react';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { useController, useFormContext } from 'react-hook-form';
|
||||||
|
import type {
|
||||||
|
ControllerProps,
|
||||||
|
FieldValues,
|
||||||
|
UseControllerProps,
|
||||||
|
} from 'react-hook-form/dist/types';
|
||||||
|
import mergeRefs from 'react-merge-refs';
|
||||||
|
|
||||||
|
export interface ControlledSwitchProps<TFieldValues extends FieldValues = any>
|
||||||
|
extends SwitchProps {
|
||||||
|
/**
|
||||||
|
* Props passed to the react-hook-form controller.
|
||||||
|
*/
|
||||||
|
controllerProps?: ControllerProps;
|
||||||
|
/**
|
||||||
|
* Name of the field.
|
||||||
|
*/
|
||||||
|
name?: string;
|
||||||
|
/**
|
||||||
|
* Control for the input field.
|
||||||
|
*/
|
||||||
|
control?: UseControllerProps<TFieldValues>['control'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ControlledSwitch(
|
||||||
|
{ controllerProps, name, control, ...props }: ControlledSwitchProps,
|
||||||
|
ref: ForwardedRef<HTMLSpanElement>,
|
||||||
|
) {
|
||||||
|
const { setValue } = useFormContext();
|
||||||
|
const { field } = useController({
|
||||||
|
...controllerProps,
|
||||||
|
name: controllerProps?.name || name || '',
|
||||||
|
control: controllerProps?.control || control,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
{...props}
|
||||||
|
{...field}
|
||||||
|
ref={mergeRefs([field.ref, ref])}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(controllerProps?.name || name, e.target.checked, {
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
checked={field.value || false}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forwardRef(ControlledSwitch);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ControlledSwitch';
|
||||||
|
export { default } from './ControlledSwitch';
|
||||||
@@ -109,7 +109,7 @@ function FormFooter({
|
|||||||
}, [isDirty, onDirtyStateChange]);
|
}, [isDirty, onDirtyStateChange]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid justify-between flex-shrink-0 grid-flow-col gap-3 p-2 border-gray-200 border-t-1">
|
<div className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 border-gray-200 p-2">
|
||||||
<Button
|
<Button
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -139,14 +139,14 @@ export default function BaseTableForm({
|
|||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
onSubmit={handleExternalSubmit}
|
onSubmit={handleExternalSubmit}
|
||||||
className="flex flex-col content-between flex-auto overflow-hidden border-gray-200 border-t-1"
|
className="flex flex-auto flex-col content-between overflow-hidden border-t-1 border-gray-200"
|
||||||
>
|
>
|
||||||
<div className="flex-auto pb-4 overflow-y-auto">
|
<div className="flex-auto overflow-y-auto pb-4">
|
||||||
<section className="grid grid-cols-8 px-6 py-3">
|
<section className="grid grid-cols-8 py-3 px-6">
|
||||||
<NameInput />
|
<NameInput />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid grid-cols-8 px-6 py-3 border-gray-200 border-t-1">
|
<section className="grid grid-cols-8 border-t-1 border-gray-200 py-3 px-6">
|
||||||
<h2 className="col-span-8 mt-3 mb-1.5 text-sm+ font-bold text-greyscaleDark">
|
<h2 className="col-span-8 mt-3 mb-1.5 text-sm+ font-bold text-greyscaleDark">
|
||||||
Columns
|
Columns
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default function ColumnEditorTable() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div role="table" className="col-span-8">
|
<div role="table" className="col-span-8">
|
||||||
<div className="sticky top-0 z-10 grid w-full grid-cols-12 gap-1 pt-1 pb-2 bg-white">
|
<div className="sticky top-0 z-10 grid w-full grid-cols-12 gap-1 bg-white pt-1 pb-2">
|
||||||
<div role="columnheader" className="col-span-3">
|
<div role="columnheader" className="col-span-3">
|
||||||
<InputLabel as="span">
|
<InputLabel as="span">
|
||||||
Name
|
Name
|
||||||
@@ -44,13 +44,13 @@ export default function ColumnEditorTable() {
|
|||||||
<InputLabel as="span">Default Value</InputLabel>
|
<InputLabel as="span">Default Value</InputLabel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="columnheader" className="col-span-1 text-center truncate">
|
<div role="columnheader" className="col-span-1 truncate text-center">
|
||||||
<InputLabel as="span" className="truncate">
|
<InputLabel as="span" className="truncate">
|
||||||
Nullable
|
Nullable
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div role="columnheader" className="col-span-1 text-center truncate">
|
<div role="columnheader" className="col-span-1 truncate text-center">
|
||||||
<InputLabel as="span" className="truncate">
|
<InputLabel as="span" className="truncate">
|
||||||
Unique
|
Unique
|
||||||
</InputLabel>
|
</InputLabel>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export default function CreateColumnForm({
|
|||||||
isUnique: false,
|
isUnique: false,
|
||||||
isIdentity: false,
|
isIdentity: false,
|
||||||
},
|
},
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseColumnValidationSchema),
|
resolver: yupResolver(baseColumnValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function CreateForeignKeyForm({
|
|||||||
updateAction: 'RESTRICT',
|
updateAction: 'RESTRICT',
|
||||||
deleteAction: 'RESTRICT',
|
deleteAction: 'RESTRICT',
|
||||||
},
|
},
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseForeignKeyValidationSchema),
|
resolver: yupResolver(baseForeignKeyValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function CreateRecordForm({
|
|||||||
|
|
||||||
return { ...defaultValues, [column.id]: null };
|
return { ...defaultValues, [column.id]: null };
|
||||||
}, {}),
|
}, {}),
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(validationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ function DataBrowserSidebarContent({
|
|||||||
const [optimisticlyRemovedTable, setOptimisticlyRemovedTable] =
|
const [optimisticlyRemovedTable, setOptimisticlyRemovedTable] =
|
||||||
useState<string>();
|
useState<string>();
|
||||||
|
|
||||||
const [selectedSchema, setSelectedSchema] = useState<string>();
|
const [selectedSchema, setSelectedSchema] = useState<string>('');
|
||||||
const isSelectedSchemaLocked = isSchemaLocked(selectedSchema);
|
const isSelectedSchemaLocked = isSchemaLocked(selectedSchema);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default function DatabaseRecordInputGroup({
|
|||||||
|
|
||||||
const InputLabel = (
|
const InputLabel = (
|
||||||
<span className="inline-grid grid-flow-col gap-1">
|
<span className="inline-grid grid-flow-col gap-1">
|
||||||
<span className="inline-grid items-center grid-flow-col gap-1">
|
<span className="inline-grid grid-flow-col items-center gap-1">
|
||||||
{isPrimary && <KeyIcon className="text-base text-inherit" />}
|
{isPrimary && <KeyIcon className="text-base text-inherit" />}
|
||||||
|
|
||||||
<span>{columnId}</span>
|
<span>{columnId}</span>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default function EditColumnForm({
|
|||||||
|
|
||||||
const form = useForm<BaseColumnFormValues>({
|
const form = useForm<BaseColumnFormValues>({
|
||||||
defaultValues: columnValues,
|
defaultValues: columnValues,
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseColumnValidationSchema),
|
resolver: yupResolver(baseColumnValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function EditForeignKeyForm({
|
|||||||
updateAction: foreignKeyRelation.updateAction,
|
updateAction: foreignKeyRelation.updateAction,
|
||||||
deleteAction: foreignKeyRelation.deleteAction,
|
deleteAction: foreignKeyRelation.deleteAction,
|
||||||
},
|
},
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseForeignKeyValidationSchema),
|
resolver: yupResolver(baseForeignKeyValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default function EditTableForm({
|
|||||||
identityColumnIndex: null,
|
identityColumnIndex: null,
|
||||||
foreignKeyRelations: [],
|
foreignKeyRelations: [],
|
||||||
},
|
},
|
||||||
reValidateMode: 'onBlur',
|
reValidateMode: 'onSubmit',
|
||||||
resolver: yupResolver(baseTableValidationSchema),
|
resolver: yupResolver(baseTableValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export function FeedbackReceived({ setFeedbackSent, close }: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid items-center grid-flow-row gap-4 text-center">
|
<div className="grid grid-flow-row items-center gap-4 text-center">
|
||||||
<Image
|
<Image
|
||||||
src="/assets/FeedbackReceived.svg"
|
src="/assets/FeedbackReceived.svg"
|
||||||
alt="Light bulb with a checkmark"
|
alt="Light bulb with a checkmark"
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||||
<div className="grid grid-flow-col gap-2 place-content-between">
|
<div className="grid grid-flow-col place-content-between gap-2">
|
||||||
<Text className="font-medium">
|
<Text className="font-medium">
|
||||||
What do you think we should improve?
|
What do you think we should improve?
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Avatar
|
<Avatar
|
||||||
className="w-6 h-6 rounded-full"
|
className="h-6 w-6 rounded-full"
|
||||||
name={user?.displayName}
|
name={user?.displayName}
|
||||||
avatarUrl={user?.avatarUrl}
|
avatarUrl={user?.avatarUrl}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
export default function ExternalLink(
|
|
||||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -55,7 +55,7 @@ function LogsTimePicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid items-center self-center grid-flow-row mx-auto">
|
<div className="mx-auto grid grid-flow-row items-center self-center">
|
||||||
<div className="border border-[#EAEDF0] px-4 py-2">
|
<div className="border border-[#EAEDF0] px-4 py-2">
|
||||||
<Input
|
<Input
|
||||||
value={format(selectedDate, 'HH:mm:ss')}
|
value={format(selectedDate, 'HH:mm:ss')}
|
||||||
@@ -85,7 +85,7 @@ function LogsTimePicker({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid justify-end grid-flow-col px-4 py-2 gap-x-4">
|
<div className="grid grid-flow-col justify-end gap-x-4 px-4 py-2">
|
||||||
<Button variant="outlined" color="secondary" onClick={handleCancel}>
|
<Button variant="outlined" color="secondary" onClick={handleCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function OverviewMigration() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row mt-6 rounded-lg place-content-between">
|
<div className="mt-6 flex flex-row place-content-between rounded-lg">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -64,9 +64,9 @@ export default function OverviewMigration() {
|
|||||||
<div className="grid grid-rows-3 gap-4">
|
<div className="grid grid-rows-3 gap-4">
|
||||||
{migrationSteps.map((step, index) => (
|
{migrationSteps.map((step, index) => (
|
||||||
<div key={step.title} className="col-span-1">
|
<div key={step.title} className="col-span-1">
|
||||||
<div className="flex flex-row gap-3 h-11">
|
<div className="flex h-11 flex-row gap-3">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className="flex flex-col items-center self-center justify-center w-8 h-8 font-semibold align-middle rounded-md bg-veryLightGray">
|
<div className="flex h-8 w-8 flex-col items-center justify-center self-center rounded-md bg-veryLightGray align-middle font-semibold">
|
||||||
<span className="text-[15px] font-semibold leading-[22px] text-greyscaleGreyDark">
|
<span className="text-[15px] font-semibold leading-[22px] text-greyscaleGreyDark">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { ApplicationMenuItems } from '@/components/applications/ApplicationMenuItems';
|
|
||||||
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 useIsPlatform from '@/hooks/common/useIsPlatform';
|
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
import Chip from '@/ui/v2/Chip';
|
import Chip from '@/ui/v2/Chip';
|
||||||
|
import CogIcon from '@/ui/v2/icons/CogIcon';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function OverviewTopBar() {
|
export default function OverviewTopBar() {
|
||||||
const isPlatform = useIsPlatform();
|
const isPlatform = useIsPlatform();
|
||||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
const { currentWorkspace, currentApplication } =
|
||||||
|
useCurrentWorkspaceAndApplication();
|
||||||
const isPro = !currentApplication?.plan?.isFree;
|
const isPro = !currentApplication?.plan?.isFree;
|
||||||
const { openAlertDialog } = useDialog();
|
const { openAlertDialog } = useDialog();
|
||||||
|
|
||||||
@@ -92,8 +95,17 @@ export default function OverviewTopBar() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Link
|
||||||
<ApplicationMenuItems />
|
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/general`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
endIcon={<CogIcon className="h-4 w-4" />}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import {
|
||||||
|
useResetPostgresPasswordMutation,
|
||||||
|
useUpdateApplicationMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import Button from '@/ui/v2/Button';
|
||||||
|
import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
|
import { copy } from '@/utils/copy';
|
||||||
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword/generateRandomDatabasePassword';
|
||||||
|
import { resetDatabasePasswordValidationSchema } from '@/utils/settings/resetDatabasePasswordValidationSchema';
|
||||||
|
import { triggerToast } from '@/utils/toast';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import { useUserData } from '@nhost/react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface ResetDatabasePasswordFormValues {
|
||||||
|
/**
|
||||||
|
* The new password to set for the database.
|
||||||
|
*/
|
||||||
|
databasePassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResetDatabasePasswordSettings() {
|
||||||
|
const [updateApplication] = useUpdateApplicationMutation();
|
||||||
|
|
||||||
|
const form = useForm<ResetDatabasePasswordFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
databasePassword: '',
|
||||||
|
},
|
||||||
|
mode: 'onSubmit',
|
||||||
|
criteriaMode: 'all',
|
||||||
|
shouldFocusError: true,
|
||||||
|
resolver: yupResolver(resetDatabasePasswordValidationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setValue,
|
||||||
|
getValues,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
const [resetPostgresPasswordMutation, { loading }] =
|
||||||
|
useResetPostgresPasswordMutation();
|
||||||
|
const user = useUserData();
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
|
||||||
|
const handleGenerateRandomPassword = () => {
|
||||||
|
const newRandomDatabasePassword = generateRandomDatabasePassword();
|
||||||
|
triggerToast('New random database password generated.');
|
||||||
|
setValue('databasePassword', newRandomDatabasePassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeDatabasePassword = async (
|
||||||
|
values: ResetDatabasePasswordFormValues,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await resetPostgresPasswordMutation({
|
||||||
|
variables: {
|
||||||
|
appID: currentApplication.id,
|
||||||
|
newPassword: values.databasePassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await updateApplication({
|
||||||
|
variables: {
|
||||||
|
appId: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
postgresPassword: values.databasePassword,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
|
||||||
|
triggerToast(
|
||||||
|
`The database password for ${currentApplication.name} has been updated successfully.`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
triggerToast(
|
||||||
|
`An error occured while trying to update the database password for ${currentApplication.name}`,
|
||||||
|
);
|
||||||
|
await discordAnnounce(
|
||||||
|
`An error occurred while trying to update the database password: ${currentApplication.name} (${user.email}): ${e.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleChangeDatabasePassword}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Reset Password"
|
||||||
|
description="This password is used for accessing your database."
|
||||||
|
submitButtonText="Reset"
|
||||||
|
rootClassName="border-[#F87171]"
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
variant: 'contained',
|
||||||
|
color: 'error',
|
||||||
|
disabled: Boolean(errors?.databasePassword),
|
||||||
|
loading,
|
||||||
|
}}
|
||||||
|
className="grid grid-flow-row pb-4"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('databasePassword')}
|
||||||
|
name="databasePassword"
|
||||||
|
id="databasePassword"
|
||||||
|
autoComplete="new-password"
|
||||||
|
type="password"
|
||||||
|
error={Boolean(errors?.databasePassword)}
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
componentsProps={{
|
||||||
|
input: { className: 'lg:w-1/2' },
|
||||||
|
helperText: { component: 'div' },
|
||||||
|
}}
|
||||||
|
helperText={
|
||||||
|
<div className="grid grid-flow-row items-center justify-start gap-1 pt-1">
|
||||||
|
{errors?.databasePassword?.message}
|
||||||
|
<div className="grid grid-flow-col items-center justify-start gap-1">
|
||||||
|
The root Postgres password for your database - it must be
|
||||||
|
strong and hard to guess.
|
||||||
|
<Button
|
||||||
|
onClick={handleGenerateRandomPassword}
|
||||||
|
className="px-1 py-0.5 text-xs underline underline-offset-2 hover:underline"
|
||||||
|
variant="borderless"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
Generate a password
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment
|
||||||
|
position="end"
|
||||||
|
className={twMerge(
|
||||||
|
'absolute right-2',
|
||||||
|
Boolean(errors?.databasePassword) && 'invisible',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
sx={{ minWidth: 0, padding: 0 }}
|
||||||
|
color="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
copy(getValues('databasePassword'), 'Postgres password');
|
||||||
|
}}
|
||||||
|
variant="borderless"
|
||||||
|
aria-label="Copy password"
|
||||||
|
>
|
||||||
|
<CopyIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ResetDatabasePasswordSettings';
|
||||||
|
export { default } from './ResetDatabasePasswordSettings';
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
import ControlledSwitch from '@/components/common/ControlledSwitch';
|
||||||
import type { ButtonProps } from '@/ui/v2/Button';
|
import type { ButtonProps } from '@/ui/v2/Button';
|
||||||
import Button from '@/ui/v2/Button';
|
import Button from '@/ui/v2/Button';
|
||||||
|
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||||
import Link from '@/ui/v2/Link';
|
import Link from '@/ui/v2/Link';
|
||||||
|
import type { SwitchProps } from '@/ui/v2/Switch';
|
||||||
|
import Switch from '@/ui/v2/Switch';
|
||||||
import Text from '@/ui/v2/Text';
|
import Text from '@/ui/v2/Text';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
|
import type { DetailedHTMLProps, HTMLProps, ReactNode } from 'react';
|
||||||
@@ -21,6 +24,10 @@ export interface SettingsContainerProps
|
|||||||
* The title for the section.
|
* The title for the section.
|
||||||
*/
|
*/
|
||||||
title: ReactNode | string;
|
title: ReactNode | string;
|
||||||
|
/**
|
||||||
|
* Custom title for the documentation link.
|
||||||
|
*/
|
||||||
|
docsTitle?: ReactNode | string;
|
||||||
/**
|
/**
|
||||||
* The description for the section.
|
* The description for the section.
|
||||||
*/
|
*/
|
||||||
@@ -42,9 +49,35 @@ export interface SettingsContainerProps
|
|||||||
*/
|
*/
|
||||||
submitButtonText?: string;
|
submitButtonText?: string;
|
||||||
/**
|
/**
|
||||||
* Pass a form ID to the submit button.
|
* If passed, the switch will be rendered as a controlled component.
|
||||||
|
* The value of the switchId will be the name of the field in the form.
|
||||||
*/
|
*/
|
||||||
formId?: string;
|
switchId?: string;
|
||||||
|
/**
|
||||||
|
* Function to be called when the switch is toggled.
|
||||||
|
*/
|
||||||
|
onEnabledChange?: (enabled: boolean) => void;
|
||||||
|
/**
|
||||||
|
* Determines whether or not the the switch is in a toggled state and children are visible.
|
||||||
|
*/
|
||||||
|
enabled?: boolean;
|
||||||
|
/**
|
||||||
|
* Determines whether or to render the switch.
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
showSwitch?: boolean;
|
||||||
|
/**
|
||||||
|
* Custom class names passed to the root element.
|
||||||
|
*/
|
||||||
|
rootClassName?: string;
|
||||||
|
/**
|
||||||
|
* Custom class names passed to the children wrapper element.
|
||||||
|
*/
|
||||||
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Props to be passed to the Switch component.
|
||||||
|
*/
|
||||||
|
switchProps?: SwitchProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsContainer({
|
export default function SettingsContainer({
|
||||||
@@ -54,64 +87,97 @@ export default function SettingsContainer({
|
|||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
primaryActionButtonProps,
|
primaryActionButtonProps,
|
||||||
formId,
|
|
||||||
submitButtonText = 'Save',
|
submitButtonText = 'Save',
|
||||||
className,
|
className,
|
||||||
|
onEnabledChange,
|
||||||
|
enabled,
|
||||||
|
switchId,
|
||||||
|
showSwitch = false,
|
||||||
|
rootClassName,
|
||||||
|
switchProps,
|
||||||
|
docsTitle,
|
||||||
}: SettingsContainerProps) {
|
}: SettingsContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid grid-flow-row gap-4 rounded-lg border-1 border-gray-200 bg-white py-4',
|
'grid grid-flow-row gap-4 rounded-lg border-1 border-gray-200 bg-white py-4',
|
||||||
className,
|
rootClassName,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="grid grid-flow-col items-center justify-start gap-3 px-4">
|
<div className="grid grid-flow-col place-content-between gap-3 px-4">
|
||||||
{(typeof icon === 'string' && (
|
<div className="grid grid-flow-col gap-4">
|
||||||
<div className="flex items-center self-center justify-self-center align-middle">
|
{(typeof icon === 'string' && (
|
||||||
<Image src={icon} alt={`icon of ${title}`} width={32} height={32} />
|
<div className="flex items-center self-center justify-self-center align-middle">
|
||||||
|
<Image
|
||||||
|
src={icon}
|
||||||
|
alt={`icon of ${title}`}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)) ||
|
||||||
|
icon}
|
||||||
|
|
||||||
|
<div className="grid grid-flow-row gap-1">
|
||||||
|
<Text className="text-lg font-semibold">{title}</Text>
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<Text className="text-greyscaleMedium">{description}</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)) ||
|
|
||||||
icon}
|
|
||||||
|
|
||||||
<div className="grid grid-flow-row gap-1">
|
|
||||||
<Text className="text-lg font-semibold">{title}</Text>
|
|
||||||
|
|
||||||
{description && (
|
|
||||||
<Text className="text-greyscaleMedium">{description}</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{!switchId && showSwitch && (
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => onEnabledChange(e.target.checked)}
|
||||||
|
className="self-center"
|
||||||
|
{...switchProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{switchId && showSwitch && (
|
||||||
|
<ControlledSwitch
|
||||||
|
className="self-center"
|
||||||
|
name={switchId}
|
||||||
|
{...switchProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{children}
|
<div className={twMerge('grid grid-flow-row gap-4 px-4', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'grid grid-flow-col items-center border-t border-gray-200 px-4 pt-3.5',
|
'grid grid-flow-col items-center gap-x-2 border-t border-gray-200 px-4 pt-3.5',
|
||||||
docsLink ? 'place-content-between' : 'justify-end',
|
docsLink ? 'place-content-between' : 'justify-end',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{docsLink && (
|
{docsLink && (
|
||||||
<div className="grid w-full grid-flow-col justify-start gap-x-1 self-center align-middle">
|
<div className="grid w-full grid-flow-col justify-start gap-x-1 self-center align-middle">
|
||||||
<Text className="text-greyscaleDark">Learn more about</Text>
|
<Text>
|
||||||
<Link
|
Learn more about{' '}
|
||||||
href={docsLink || 'https://docs.nhost.io/'}
|
<Link
|
||||||
target="_blank"
|
href={docsLink || 'https://docs.nhost.io/'}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
underline="hover"
|
rel="noopener noreferrer"
|
||||||
className="grid grid-flow-col items-center justify-center gap-x-1 font-medium"
|
underline="hover"
|
||||||
>
|
className="font-medium"
|
||||||
{title}
|
>
|
||||||
<ExternalLink className="h-4 w-4" />
|
{docsTitle || title}
|
||||||
</Link>
|
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant={
|
||||||
color="secondary"
|
primaryActionButtonProps?.disabled ? 'outlined' : 'contained'
|
||||||
|
}
|
||||||
|
color={primaryActionButtonProps?.disabled ? 'secondary' : 'primary'}
|
||||||
|
type="submit"
|
||||||
{...primaryActionButtonProps}
|
{...primaryActionButtonProps}
|
||||||
form={formId}
|
|
||||||
type={formId ? 'submit' : 'button'}
|
|
||||||
>
|
>
|
||||||
{submitButtonText}
|
{submitButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -133,13 +133,6 @@ export default function SettingsSidebar({
|
|||||||
>
|
>
|
||||||
General
|
General
|
||||||
</SettingsNavLink>
|
</SettingsNavLink>
|
||||||
<SettingsNavLink
|
|
||||||
href="/sign-in-methods"
|
|
||||||
exact={false}
|
|
||||||
onClick={handleSelect}
|
|
||||||
>
|
|
||||||
Sign-In Methods
|
|
||||||
</SettingsNavLink>
|
|
||||||
{isK8SPostgresEnabledInCurrentEnvironment && !isProjectUsingRDS && (
|
{isK8SPostgresEnabledInCurrentEnvironment && !isProjectUsingRDS && (
|
||||||
<SettingsNavLink
|
<SettingsNavLink
|
||||||
href="/database"
|
href="/database"
|
||||||
@@ -149,6 +142,21 @@ export default function SettingsSidebar({
|
|||||||
Database
|
Database
|
||||||
</SettingsNavLink>
|
</SettingsNavLink>
|
||||||
)}
|
)}
|
||||||
|
<SettingsNavLink
|
||||||
|
href="/authentication"
|
||||||
|
exact={false}
|
||||||
|
onClick={handleSelect}
|
||||||
|
>
|
||||||
|
Authentication
|
||||||
|
</SettingsNavLink>
|
||||||
|
<SettingsNavLink
|
||||||
|
href="/sign-in-methods"
|
||||||
|
exact={false}
|
||||||
|
onClick={handleSelect}
|
||||||
|
>
|
||||||
|
Sign-In Methods
|
||||||
|
</SettingsNavLink>
|
||||||
|
|
||||||
<SettingsNavLink
|
<SettingsNavLink
|
||||||
href="/roles-and-permissions"
|
href="/roles-and-permissions"
|
||||||
exact={false}
|
exact={false}
|
||||||
@@ -161,6 +169,10 @@ export default function SettingsSidebar({
|
|||||||
SMTP
|
SMTP
|
||||||
</SettingsNavLink>
|
</SettingsNavLink>
|
||||||
|
|
||||||
|
<SettingsNavLink href="/git" exact={false} onClick={handleSelect}>
|
||||||
|
Git
|
||||||
|
</SettingsNavLink>
|
||||||
|
|
||||||
<SettingsNavLink
|
<SettingsNavLink
|
||||||
href="/environment-variables"
|
href="/environment-variables"
|
||||||
exact={false}
|
exact={false}
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface AllowedEmailSettingsFormValues {
|
||||||
|
/**
|
||||||
|
* Set of email that are allowed to be used for project's users authentication.
|
||||||
|
*/
|
||||||
|
authAccessControlAllowedEmails: string;
|
||||||
|
/**
|
||||||
|
* Set of email domains that are allowed to be used for project's users authentication.
|
||||||
|
* @example 'nhost.io'
|
||||||
|
*/
|
||||||
|
authAccessControlAllowedEmailDomains: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AllowedEmailDomainsSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAppQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<AllowedEmailSettingsFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
|
||||||
|
authAccessControlAllowedEmailDomains:
|
||||||
|
data?.app?.authAccessControlAllowedEmailDomains,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading Allowed Email Settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState } = form;
|
||||||
|
|
||||||
|
const handleAllowedEmailDomainsChange = async (
|
||||||
|
values: AllowedEmailSettingsFormValues,
|
||||||
|
) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Allowed email settings are being updated...`,
|
||||||
|
success: `Allowed email settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's allowed email settings.`,
|
||||||
|
},
|
||||||
|
toastStyleProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleAllowedEmailDomainsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Allowed Emails and Domains"
|
||||||
|
description="Allow specific email addresses and domains to sign up."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
enabled={enabled}
|
||||||
|
onEnabledChange={setEnabled}
|
||||||
|
showSwitch
|
||||||
|
className={twMerge(
|
||||||
|
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
||||||
|
!enabled && 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('authAccessControlAllowedEmails')}
|
||||||
|
name="authAccessControlAllowedEmails"
|
||||||
|
id="authAccessControlAllowedEmails"
|
||||||
|
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
||||||
|
className="col-span-2"
|
||||||
|
label="Allowed Emails (comma separated)"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('authAccessControlAllowedEmailDomains')}
|
||||||
|
name="authAccessControlAllowedEmailDomains"
|
||||||
|
id="authAccessControlAllowedEmailDomains"
|
||||||
|
label="Allowed Email Domains (comma sepated list)"
|
||||||
|
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './AllowedEmailSettings';
|
||||||
|
export { default } from './AllowedEmailSettings';
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface AllowedRedirectURLFormValues {
|
||||||
|
/**
|
||||||
|
* Set of URLs that are allowed to be redirected to after project's users authentication.
|
||||||
|
*/
|
||||||
|
authAccessControlAllowedRedirectUrls: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AllowedRedirectURLsSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAppQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<AllowedRedirectURLFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authAccessControlAllowedRedirectUrls:
|
||||||
|
data?.app?.authAccessControlAllowedRedirectUrls,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading allowed redirect URL settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState } = form;
|
||||||
|
|
||||||
|
const handleAllowedRedirectURLsChange = async (
|
||||||
|
values: AllowedRedirectURLFormValues,
|
||||||
|
) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Allowed redirect URL settings are being updated...`,
|
||||||
|
success: `Allowed redirect URL settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's allowed redirect URL settings.`,
|
||||||
|
},
|
||||||
|
toastStyleProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleAllowedRedirectURLsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Allowed Redirect URLs"
|
||||||
|
description="Allowed URLs where users can be redirected to after authentication. Separate multiple redirect URLs with comma."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
className="grid grid-flow-row px-4 lg:grid-cols-5"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('authAccessControlAllowedRedirectUrls')}
|
||||||
|
name="authAccessControlAllowedRedirectUrls"
|
||||||
|
id="authAccessControlAllowedRedirectUrls"
|
||||||
|
placeholder="http://localhost:3000, http://localhost:4000"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
aria-label="Allowed Redirect URLs"
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './AllowedRedirectURLsSettings';
|
||||||
|
export { default } from './AllowedRedirectURLsSettings';
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface BlockedEmailFormValues {
|
||||||
|
/**
|
||||||
|
* Set of emails that are blocked from registering to the user's project.
|
||||||
|
*/
|
||||||
|
authAccessControlBlockedEmails: string;
|
||||||
|
/**
|
||||||
|
* Set of email domains that are blocked from registering to the user's project.
|
||||||
|
*/
|
||||||
|
authAccessControlBlockedEmailDomains: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlockedEmailSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
const [enabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAppQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<BlockedEmailFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
|
||||||
|
authAccessControlBlockedEmailDomains:
|
||||||
|
data?.app?.authAccessControlBlockedEmailDomains,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading blocked emails and domains..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState } = form;
|
||||||
|
|
||||||
|
const handleAllowedEmailDomainsChange = async (
|
||||||
|
values: BlockedEmailFormValues,
|
||||||
|
) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Blocked email and domain settings are being updated...`,
|
||||||
|
success: `Blocked email and domain settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's blocked email and domain settings.`,
|
||||||
|
},
|
||||||
|
toastStyleProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleAllowedEmailDomainsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Blocked Emails and Domains"
|
||||||
|
description="Block specific email addresses and domains to sign up."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
enabled={enabled}
|
||||||
|
onEnabledChange={setEnabled}
|
||||||
|
showSwitch
|
||||||
|
className={twMerge(
|
||||||
|
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',
|
||||||
|
!enabled && 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('authAccessControlBlockedEmails')}
|
||||||
|
name="authAccessControlBlockedEmails"
|
||||||
|
id="authAccessControlBlockedEmails"
|
||||||
|
placeholder="These emails (separated by comma, e.g, david@ikea.com, lisa@mycompany.com)"
|
||||||
|
className="col-span-2"
|
||||||
|
label="Blocked Emails (comma separated)"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('authAccessControlBlockedEmailDomains')}
|
||||||
|
name="authAccessControlBlockedEmailDomains"
|
||||||
|
id="authAccessControlBlockedEmailDomains"
|
||||||
|
label="Blocked Email Domains (comma sepated list)"
|
||||||
|
placeholder="These email domains (separated by comma, e.g, ikea.com, mycompany.com)"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './BlockedEmailSettings';
|
||||||
|
export { default } from './BlockedEmailSettings';
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useGetAppQuery, useUpdateAppMutation } from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface ClientURLFormValues {
|
||||||
|
/**
|
||||||
|
* The URL of the frontend app of where users are redirected after authenticating.
|
||||||
|
*/
|
||||||
|
authClientUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClientURLSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation({ refetchQueries: ['GetApp'] });
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAppQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication?.id,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<ClientURLFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authClientUrl: data?.app?.authClientUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading client URL settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState } = form;
|
||||||
|
|
||||||
|
const handleClientURLChange = async (values: ClientURLFormValues) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Client URL is being updated...`,
|
||||||
|
success: `Client URL has been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's Client URL.`,
|
||||||
|
},
|
||||||
|
{ ...toastStyleProps },
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleClientURLChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Client URL"
|
||||||
|
description="This should be the URL of your frontend app where users are redirected after authenticating."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('authClientUrl')}
|
||||||
|
name="authClientUrl"
|
||||||
|
id="authClientUrl"
|
||||||
|
placeholder="http://localhost:3000"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
aria-label="Client URL"
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './ClientURLSettings';
|
||||||
|
export { default } from './ClientURLSettings';
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import {
|
||||||
|
useGetAuthSettingsQuery,
|
||||||
|
useUpdateAppMutation,
|
||||||
|
} from '@/utils/__generated__/graphql';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface DisableNewUsersFormValues {
|
||||||
|
/**
|
||||||
|
* Disable new users from signing up to this project
|
||||||
|
*/
|
||||||
|
authDisableNewUsers: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DisableNewUsersSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthSettingsQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<DisableNewUsersFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authDisableNewUsers: data?.app?.authDisableNewUsers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset(() => ({
|
||||||
|
authDisableNewUsers: data?.app?.authDisableNewUsers,
|
||||||
|
}));
|
||||||
|
}, [data?.app?.authDisableNewUsers, form, form.reset]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading disabled sign up settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { formState, watch } = form;
|
||||||
|
const authDisableNewUsers = watch('authDisableNewUsers');
|
||||||
|
|
||||||
|
const handleDisableNewUsersChange = async (
|
||||||
|
values: DisableNewUsersFormValues,
|
||||||
|
) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Disabling new user sign ups...`,
|
||||||
|
success: `New user sign ups have been disabled successfully.`,
|
||||||
|
error: `An error occurred while trying to disable new user sign ups.`,
|
||||||
|
},
|
||||||
|
{ ...toastStyleProps },
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleDisableNewUsersChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Disable New Users"
|
||||||
|
description="If set, newly registered users are disabled and won’t be able to sign in."
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
switchId="authDisableNewUsers"
|
||||||
|
showSwitch
|
||||||
|
enabled={authDisableNewUsers}
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './DisableNewUsersSettings';
|
||||||
|
export { default } from './DisableNewUsersSettings';
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import {
|
||||||
|
useGetGravatarSettingsQuery,
|
||||||
|
useUpdateAppMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import Option from '@/ui/v2/Option';
|
||||||
|
import {
|
||||||
|
AUTH_GRAVATAR_DEFAULT,
|
||||||
|
AUTH_GRAVATAR_RATING,
|
||||||
|
toastStyleProps,
|
||||||
|
} from '@/utils/settings/settingsConstants';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface GravatarFormValues {
|
||||||
|
/**
|
||||||
|
* Gravatar image to use as default.
|
||||||
|
*/
|
||||||
|
authGravatarDefault: string;
|
||||||
|
/**
|
||||||
|
* Gravatar image rating.
|
||||||
|
*/
|
||||||
|
authGravatarRating: string;
|
||||||
|
/**
|
||||||
|
* Enable Gravatar for this project
|
||||||
|
*/
|
||||||
|
authGravatarEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GravatarSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetGravatarSettingsQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<GravatarFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authGravatarDefault: data?.app?.authGravatarDefault || '',
|
||||||
|
authGravatarRating: data?.app?.authGravatarRating || '',
|
||||||
|
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset(() => ({
|
||||||
|
authGravatarDefault: data?.app?.authGravatarDefault || '',
|
||||||
|
authGravatarRating: data?.app?.authGravatarRating || '',
|
||||||
|
authGravatarEnabled: data?.app?.authGravatarEnabled || false,
|
||||||
|
}));
|
||||||
|
}, [data?.app, form, form.reset]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading Gravatar settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState, watch } = form;
|
||||||
|
const authGravatarEnabled = watch('authGravatarEnabled');
|
||||||
|
|
||||||
|
const handleGravatarSettingsChange = async (values: GravatarFormValues) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Gravatar settings are being updated...`,
|
||||||
|
success: `Gravatar settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's Gravatar settings.`,
|
||||||
|
},
|
||||||
|
{ ...toastStyleProps },
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleGravatarSettingsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Gravatar"
|
||||||
|
description="Use Gravatars for avatar URLs for users."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
switchId="authGravatarEnabled"
|
||||||
|
showSwitch
|
||||||
|
enabled={authGravatarEnabled}
|
||||||
|
className={twMerge(
|
||||||
|
'grid grid-flow-col grid-cols-5 grid-rows-2 gap-y-6',
|
||||||
|
!authGravatarEnabled && 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ControlledSelect
|
||||||
|
{...register('authGravatarDefault')}
|
||||||
|
id="authGravatarDefault"
|
||||||
|
className="col-span-5 lg:col-span-2"
|
||||||
|
placeholder="Default Gravatar"
|
||||||
|
hideEmptyHelperText
|
||||||
|
variant="normal"
|
||||||
|
label="Default"
|
||||||
|
>
|
||||||
|
{AUTH_GRAVATAR_DEFAULT.map(({ value, label }) => (
|
||||||
|
<Option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</ControlledSelect>
|
||||||
|
<ControlledSelect
|
||||||
|
{...register('authGravatarRating')}
|
||||||
|
id="authGravatarRating"
|
||||||
|
className="col-span-5 lg:col-span-2"
|
||||||
|
placeholder="Gravatar Rating"
|
||||||
|
hideEmptyHelperText
|
||||||
|
label="Rating"
|
||||||
|
>
|
||||||
|
{AUTH_GRAVATAR_RATING.map(({ value, label }) => (
|
||||||
|
<Option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</ControlledSelect>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './GravatarSettings';
|
||||||
|
export { default } from './GravatarSettings';
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import {
|
||||||
|
useGetAuthSettingsQuery,
|
||||||
|
useUpdateAppMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface MFASettingsFormValues {
|
||||||
|
/**
|
||||||
|
* One Time Password issuer
|
||||||
|
*/
|
||||||
|
authMfaTotpIssuer: string;
|
||||||
|
/**
|
||||||
|
* Enable Multi Factor Authentication for this project
|
||||||
|
*/
|
||||||
|
authMfaEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MFASettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
|
||||||
|
const { data, loading, error } = useGetAuthSettingsQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<MFASettingsFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
|
||||||
|
authMfaEnabled: data?.app?.authMfaEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset(() => ({
|
||||||
|
authMfaTotpIssuer: data?.app?.authMfaTotpIssuer,
|
||||||
|
authMfaEnabled: data?.app?.authMfaEnabled,
|
||||||
|
}));
|
||||||
|
}, [data?.app, form, form.reset]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading multi-factor authentication settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState, watch } = form;
|
||||||
|
const authMfaEnabled = watch('authMfaEnabled');
|
||||||
|
|
||||||
|
const handleMFASettingsChange = async (values: MFASettingsFormValues) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Multi-factor authentication settings are being updated...`,
|
||||||
|
success: `Multi-factor authentication settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's multi-factor authentication settings.`,
|
||||||
|
},
|
||||||
|
toastStyleProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleMFASettingsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Multi-Factor Authentication"
|
||||||
|
description="Enable users to use MFA to sign in"
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/authentication"
|
||||||
|
switchId="authMfaEnabled"
|
||||||
|
enabled={authMfaEnabled}
|
||||||
|
showSwitch
|
||||||
|
className={twMerge(
|
||||||
|
'grid grid-flow-row lg:grid-cols-5',
|
||||||
|
!authMfaEnabled && 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register('authMfaTotpIssuer')}
|
||||||
|
name="authMfaTotpIssuer"
|
||||||
|
id="authMfaTotpIssuer"
|
||||||
|
label="OTP Issuer"
|
||||||
|
placeholder="Name of the One Time Password (OTP) issuer"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './MFASettings';
|
||||||
|
export { default } from './MFASettings';
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import InlineCode from '@/components/common/InlineCode';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import { Alert } from '@/ui/Alert';
|
||||||
|
import CheckIcon from '@/ui/v2/icons/CheckIcon';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface BaseDirectoryFormValues {
|
||||||
|
/**
|
||||||
|
* The relative path where the `nhost` folder is located.
|
||||||
|
*/
|
||||||
|
nhostBaseFolder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toastStyleProps = {
|
||||||
|
style: {
|
||||||
|
minWidth: '300px',
|
||||||
|
backgroundColor: 'rgb(33 50 75)',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
duration: 5000,
|
||||||
|
icon: <CheckIcon className="h-4 w-4 bg-transparent" />,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BaseDirectorySettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
const client = useApolloClient();
|
||||||
|
|
||||||
|
const form = useForm<BaseDirectoryFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
nhostBaseFolder: currentApplication?.nhostBaseFolder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, formState, reset } = form;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(() => ({
|
||||||
|
nhostBaseFolder: currentApplication?.nhostBaseFolder,
|
||||||
|
}));
|
||||||
|
}, [currentApplication?.nhostBaseFolder, reset]);
|
||||||
|
|
||||||
|
const handleBaseFolderChange = async (values: BaseDirectoryFormValues) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `The base directory is being updated...`,
|
||||||
|
success: `The base directory has been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's base directory.`,
|
||||||
|
},
|
||||||
|
{ ...toastStyleProps },
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.refetchQueries({ include: ['getOneUser'] });
|
||||||
|
} catch (error) {
|
||||||
|
await discordAnnounce(
|
||||||
|
error.message || 'Error while trying to update application cache',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleBaseFolderChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Base Directory"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
The base directory is where the{' '}
|
||||||
|
<InlineCode className="text-xs">nhost</InlineCode> directory is
|
||||||
|
located. In other words, the base directory is the parent
|
||||||
|
directory of the{' '}
|
||||||
|
<InlineCode className="text-xs">nhost</InlineCode> folder.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/github-integration#base-directory"
|
||||||
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
>
|
||||||
|
{currentApplication?.githubRepository ? (
|
||||||
|
<Input
|
||||||
|
{...register('nhostBaseFolder')}
|
||||||
|
name="nhostBaseFolder"
|
||||||
|
id="nhostBaseFolder"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Alert className="col-span-5 text-left">
|
||||||
|
To change the Base Folder, you first need to connect your project
|
||||||
|
to a GitHub repository.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>{' '}
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './BaseDirectorySettings';
|
||||||
|
export { default } from './BaseDirectorySettings';
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import { useUpdateAppMutation } from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import { Alert } from '@/ui/Alert';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface DeploymentBranchFormValues {
|
||||||
|
/**
|
||||||
|
* The git branch to deploy from.
|
||||||
|
*/
|
||||||
|
repositoryProductionBranch: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DeploymentBranchSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
const client = useApolloClient();
|
||||||
|
|
||||||
|
const form = useForm<DeploymentBranchFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
repositoryProductionBranch:
|
||||||
|
currentApplication?.repositoryProductionBranch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, reset, formState } = form;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reset(() => ({
|
||||||
|
repositoryProductionBranch:
|
||||||
|
currentApplication?.repositoryProductionBranch,
|
||||||
|
}));
|
||||||
|
}, [currentApplication?.repositoryProductionBranch, reset]);
|
||||||
|
|
||||||
|
const handleDeploymentBranchChange = async (
|
||||||
|
values: DeploymentBranchFormValues,
|
||||||
|
) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `The deployment branch is being updated...`,
|
||||||
|
success: `The deployment branch has been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's deployment branch.`,
|
||||||
|
},
|
||||||
|
{ ...toastStyleProps },
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.refetchQueries({ include: ['getOneUser'] });
|
||||||
|
} catch (error) {
|
||||||
|
await discordAnnounce(
|
||||||
|
error.message || 'Error while trying to update application cache',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleDeploymentBranchChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Deployment Branch"
|
||||||
|
description="All commits pushed to this deployment branch will trigger a deployment. You can switch to a different branch here."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/platform/github-integration#deployment-branch"
|
||||||
|
className="grid grid-flow-row lg:grid-cols-5"
|
||||||
|
>
|
||||||
|
{currentApplication?.githubRepository ? (
|
||||||
|
<Input
|
||||||
|
{...register('repositoryProductionBranch')}
|
||||||
|
name="repositoryProductionBranch"
|
||||||
|
id="repositoryProductionBranch"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Alert className="col-span-5 w-full text-left">
|
||||||
|
To change the Deployment Branch, you first need to connect your
|
||||||
|
project to a GitHub repository.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './DeploymentBranchSettings';
|
||||||
|
export { default } from './DeploymentBranchSettings';
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import {
|
||||||
|
useSignInMethodsQuery,
|
||||||
|
useUpdateAppMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
|
||||||
|
export interface AnonymousSignInFormValues {
|
||||||
|
/**
|
||||||
|
* Enables users to register as an anonymous user.
|
||||||
|
*/
|
||||||
|
authAnonymousUsersEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnonymousSignInSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
|
||||||
|
const { data, loading, error } = useSignInMethodsQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<AnonymousSignInFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authAnonymousUsersEnabled: data.app.authAnonymousUsersEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordProtectionSettingsChange = async (
|
||||||
|
values: AnonymousSignInFormValues,
|
||||||
|
) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Anonymous sign-in settings are being updated...`,
|
||||||
|
success: `Anonymous sign-in settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update Anonymous sign-in settings.`,
|
||||||
|
},
|
||||||
|
toastStyleProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handlePasswordProtectionSettingsChange}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Anonymous Users"
|
||||||
|
description="Allow users to sign-in anonymously."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled:
|
||||||
|
form.formState.isSubmitting ||
|
||||||
|
!form.formState.isValid ||
|
||||||
|
!form.formState.isDirty,
|
||||||
|
}}
|
||||||
|
enabled={form.getValues('authAnonymousUsersEnabled')}
|
||||||
|
switchId="authAnonymousUsersEnabled"
|
||||||
|
showSwitch
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './AnonymousSignInSettings';
|
||||||
|
export { default } from './AnonymousSignInSettings';
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
import Form from '@/components/common/Form';
|
||||||
|
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||||
|
import {
|
||||||
|
useSignInMethodsQuery,
|
||||||
|
useUpdateAppMutation,
|
||||||
|
} from '@/generated/graphql';
|
||||||
|
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||||
|
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||||
|
import IconButton from '@/ui/v2/IconButton';
|
||||||
|
import CopyIcon from '@/ui/v2/icons/CopyIcon';
|
||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import InputAdornment from '@/ui/v2/InputAdornment';
|
||||||
|
import { copy } from '@/utils/copy';
|
||||||
|
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||||
|
import { toastStyleProps } from '@/utils/settings/settingsConstants';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { toast } from 'react-hot-toast';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export interface AppleProviderFormValues {
|
||||||
|
authAppleEnabled: boolean;
|
||||||
|
authAppleTeamId: string;
|
||||||
|
authAppleKeyId: string;
|
||||||
|
authAppleClientId: string;
|
||||||
|
authApplePrivateKey: string;
|
||||||
|
authAppleScope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AppleProviderSettings() {
|
||||||
|
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||||
|
const [updateApp] = useUpdateAppMutation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
app: {
|
||||||
|
authAppleEnabled,
|
||||||
|
authAppleTeamId,
|
||||||
|
authAppleKeyId,
|
||||||
|
authAppleClientId,
|
||||||
|
authApplePrivateKey,
|
||||||
|
authAppleScope,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
} = useSignInMethodsQuery({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<AppleProviderFormValues>({
|
||||||
|
reValidateMode: 'onSubmit',
|
||||||
|
defaultValues: {
|
||||||
|
authAppleTeamId,
|
||||||
|
authAppleKeyId,
|
||||||
|
authAppleClientId,
|
||||||
|
authApplePrivateKey,
|
||||||
|
authAppleScope,
|
||||||
|
authAppleEnabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<ActivityIndicator
|
||||||
|
delay={1000}
|
||||||
|
label="Loading Apple settings..."
|
||||||
|
className="justify-center"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { register, formState, watch } = form;
|
||||||
|
const authEnabled = watch('authAppleEnabled');
|
||||||
|
|
||||||
|
const handleProviderUpdate = async (values: AppleProviderFormValues) => {
|
||||||
|
const updateAppMutation = updateApp({
|
||||||
|
variables: {
|
||||||
|
id: currentApplication.id,
|
||||||
|
app: {
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.promise(
|
||||||
|
updateAppMutation,
|
||||||
|
{
|
||||||
|
loading: `Apple settings are being updated...`,
|
||||||
|
success: `Apple settings have been updated successfully.`,
|
||||||
|
error: `An error occurred while trying to update the project's Apple settings.`,
|
||||||
|
},
|
||||||
|
{ ...toastStyleProps },
|
||||||
|
);
|
||||||
|
|
||||||
|
form.reset(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<Form onSubmit={handleProviderUpdate}>
|
||||||
|
<SettingsContainer
|
||||||
|
title="Apple"
|
||||||
|
description="Allows users to sign in with Apple."
|
||||||
|
primaryActionButtonProps={{
|
||||||
|
disabled: !formState.isValid || !formState.isDirty,
|
||||||
|
loading: formState.isSubmitting,
|
||||||
|
}}
|
||||||
|
docsLink="https://docs.nhost.io/authentication/sign-in-with-apple"
|
||||||
|
docsTitle="how to sign in users with Apple"
|
||||||
|
icon="/logos/Apple.svg"
|
||||||
|
switchId="authAppleEnabled"
|
||||||
|
showSwitch
|
||||||
|
enabled={authEnabled}
|
||||||
|
className={twMerge(
|
||||||
|
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||||
|
!authEnabled && 'hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...register(`authAppleTeamId`)}
|
||||||
|
name="authAppleTeamId"
|
||||||
|
id="authAppleTeamId"
|
||||||
|
label="Team ID"
|
||||||
|
placeholder="Apple Team ID"
|
||||||
|
className="col-span-1"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('authAppleScope')}
|
||||||
|
name="authAppleScope"
|
||||||
|
id="authAppleScope"
|
||||||
|
label="Service ID"
|
||||||
|
placeholder="Apple Service ID"
|
||||||
|
className="col-span-1"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('authAppleKeyId')}
|
||||||
|
name="authAppleKeyId"
|
||||||
|
id="authAppleKeyId"
|
||||||
|
label="Key ID"
|
||||||
|
placeholder="Apple Key ID"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register('authApplePrivateKey')}
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
name="authApplePrivateKey"
|
||||||
|
id="authApplePrivateKey"
|
||||||
|
label="Private Key"
|
||||||
|
placeholder="Paste Private Key here"
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="redirectUrl"
|
||||||
|
id="redirectUrl"
|
||||||
|
defaultValue={`${generateRemoteAppUrl(
|
||||||
|
currentApplication.subdomain,
|
||||||
|
)}/v1/auth/signin/provider/apple/callback`}
|
||||||
|
className="col-span-2"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
label="Redirect URL"
|
||||||
|
disabled
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end" className="absolute right-2">
|
||||||
|
<IconButton
|
||||||
|
sx={{ minWidth: 0, padding: 0 }}
|
||||||
|
color="secondary"
|
||||||
|
variant="borderless"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
copy(
|
||||||
|
`${generateRemoteAppUrl(
|
||||||
|
currentApplication.subdomain,
|
||||||
|
)}/v1/auth/signin/provider/apple/callback`,
|
||||||
|
'Redirect URL',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="w-4 h-4" />
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SettingsContainer>
|
||||||
|
</Form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './AppleProviderSettings';
|
||||||
|
export { default } from './AppleProviderSettings';
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import Input from '@/ui/v2/Input';
|
||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
export interface BaseProviderSettingsFormValues {
|
||||||
|
authEnabled: boolean;
|
||||||
|
authClientId: string;
|
||||||
|
authClientSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Third-party auth providers e.g. Google, GitHub.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
*
|
||||||
|
* These providers follow the same API structure in our database and in our GraphQL API:
|
||||||
|
* In the case of adding a new provider to this list it should contain the configuration in the example below.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* auth<Provider>Enabled
|
||||||
|
* auth<Provider>ClientId
|
||||||
|
* auth<Provider>ClientSecret
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* authGithubEnabled
|
||||||
|
* authGithubClientId
|
||||||
|
* authGithubClientSecret
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @remarks If the provider has a different configuration (more or less fields) it should be added as its own component
|
||||||
|
* @see {@link 'src\components\settings\sign-in-methods\ProviderTwitterSettings\ProviderTwitterSettings.tsx'}
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export default function BaseProviderSettings() {
|
||||||
|
const { register } = useFormContext<BaseProviderSettingsFormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
{...register(`authClientId`)}
|
||||||
|
id="authClientId"
|
||||||
|
label="Client ID"
|
||||||
|
placeholder="Enter your Client ID"
|
||||||
|
className="col-span-1"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
{...register(`authClientSecret`)}
|
||||||
|
id="authClientSecret"
|
||||||
|
label="Client Secret"
|
||||||
|
placeholder="Enter your Client Secret"
|
||||||
|
className="col-span-1"
|
||||||
|
fullWidth
|
||||||
|
hideEmptyHelperText
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './BaseProviderSettings';
|
||||||
|
export { default } from './BaseProviderSettings';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user