Compare commits
307 Commits
@nhost/rea
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e113bfb1f | ||
|
|
f1d358d77c | ||
|
|
d558ef9ecf | ||
|
|
75c7ba7f12 | ||
|
|
d62dfe19a2 | ||
|
|
0dbef188b1 | ||
|
|
8857314e22 | ||
|
|
85f1c4a98e | ||
|
|
efa6b5755d | ||
|
|
44f13f6240 | ||
|
|
2b19416787 | ||
|
|
e01cb2ed49 | ||
|
|
388eef041f | ||
|
|
4e5d43f300 | ||
|
|
db342f453e | ||
|
|
54386a3b56 | ||
|
|
ff40b99f84 | ||
|
|
33f8f1d78a | ||
|
|
c50fe47ab4 | ||
|
|
0580f832c8 | ||
|
|
7d1eb099c0 | ||
|
|
e15322296b | ||
|
|
91a2bf905b | ||
|
|
0f9393fe27 | ||
|
|
aebb822549 | ||
|
|
1e2be6fadf | ||
|
|
aafbf5173d | ||
|
|
01e13e2f8c | ||
|
|
4364647501 | ||
|
|
ef117c284e | ||
|
|
3f919c0a80 | ||
|
|
49e447e7b7 | ||
|
|
66b4f3d0be | ||
|
|
aa7fdafe8b | ||
|
|
7d6de3b289 | ||
|
|
57e41f77a9 | ||
|
|
f5c2a0ef4f | ||
|
|
d52bc8cca5 | ||
|
|
04a3e4c965 | ||
|
|
853c0c5775 | ||
|
|
2e6923dc73 | ||
|
|
7d6d70d0c7 | ||
|
|
7a2100cc17 | ||
|
|
5d55f3fa60 | ||
|
|
8b0c44a93c | ||
|
|
e0cc7cce0a | ||
|
|
6e7d5e0dd4 | ||
|
|
54c143ebf6 | ||
|
|
8b9fa0b150 | ||
|
|
c3bb79e1dd | ||
|
|
128d21e4ec | ||
|
|
40e503c356 | ||
|
|
d007e0ade8 | ||
|
|
fa32513ba7 | ||
|
|
8893d9e010 | ||
|
|
81d2fd865c | ||
|
|
fe3c462099 | ||
|
|
f8b082cb02 | ||
|
|
0c748e6ee6 | ||
|
|
e2c4ca85b3 | ||
|
|
0165b998c2 | ||
|
|
5d970cc229 | ||
|
|
7167170663 | ||
|
|
0f77de2dd0 | ||
|
|
6ae91e48d1 | ||
|
|
69db1594cc | ||
|
|
158cf0da49 | ||
|
|
7992fc3baa | ||
|
|
85d9596956 | ||
|
|
16d383516e | ||
|
|
2ca193ccf3 | ||
|
|
ab8e12003d | ||
|
|
29cdf6b125 | ||
|
|
41cc3dc5d0 | ||
|
|
6b67c9996a | ||
|
|
23274dee41 | ||
|
|
a5b55c2667 | ||
|
|
1263676eb3 | ||
|
|
b1b647ad96 | ||
|
|
21bbaf5e95 | ||
|
|
eef9c91403 | ||
|
|
1742cb444d | ||
|
|
c4f374d7f3 | ||
|
|
369ec13070 | ||
|
|
101129eef2 | ||
|
|
228fda0364 | ||
|
|
74085c67a2 | ||
|
|
a273725419 | ||
|
|
c5240f8d74 | ||
|
|
4490068257 | ||
|
|
3601de3f85 | ||
|
|
ac9404610b | ||
|
|
63570db57c | ||
|
|
538ed78f5a | ||
|
|
b1a31ecb00 | ||
|
|
3d151c448c | ||
|
|
bac8ace434 | ||
|
|
fdd417ed25 | ||
|
|
a402fc17de | ||
|
|
4416ceb9cf | ||
|
|
4762ebf61e | ||
|
|
73e28b5831 | ||
|
|
2a7dc5060f | ||
|
|
9b8ede40a9 | ||
|
|
f005c20d99 | ||
|
|
4adfd613b6 | ||
|
|
b6da82c8e3 | ||
|
|
816456edc4 | ||
|
|
deaf0e86d4 | ||
|
|
23f8206f18 | ||
|
|
9dde4d7988 | ||
|
|
26385b9cf9 | ||
|
|
6d318206ef | ||
|
|
4d727b78a1 | ||
|
|
de0a125e98 | ||
|
|
ea1ad29031 | ||
|
|
3da40e5712 | ||
|
|
b9087a4add | ||
|
|
1b7a6d0252 | ||
|
|
1417d3e794 | ||
|
|
e187923858 | ||
|
|
8a60ed4074 | ||
|
|
d7d11a44a7 | ||
|
|
062e4691cd | ||
|
|
a95d49fa2c | ||
|
|
d14fc96899 | ||
|
|
93db718254 | ||
|
|
c367bd58b9 | ||
|
|
0bfed4d9e1 | ||
|
|
1f3aecd379 | ||
|
|
42306ea3bb | ||
|
|
1b12a175f6 | ||
|
|
32060aaea0 | ||
|
|
f94cace5f2 | ||
|
|
5de965d9a5 | ||
|
|
e10b3adc11 | ||
|
|
457db76b06 | ||
|
|
1e952a026e | ||
|
|
2f4c040789 | ||
|
|
74648752b4 | ||
|
|
09d218a3fe | ||
|
|
2e8938dbb0 | ||
|
|
ec60d03536 | ||
|
|
2f3767552f | ||
|
|
bc401c0dd2 | ||
|
|
2907ecb7ff | ||
|
|
05d7f5207f | ||
|
|
07a053ee80 | ||
|
|
2145243b19 | ||
|
|
61e4414a8f | ||
|
|
4601d84e0e | ||
|
|
ca012d790c | ||
|
|
aeda14ef53 | ||
|
|
3fa5e2005a | ||
|
|
4dd2e99159 | ||
|
|
282c6c6d24 | ||
|
|
beadd84adb | ||
|
|
f8f55d2b99 | ||
|
|
03a98d4f3a | ||
|
|
8ed8e04ab6 | ||
|
|
587efd4551 | ||
|
|
c78227b085 | ||
|
|
d87e520307 | ||
|
|
bbed04e4da | ||
|
|
273afc9740 | ||
|
|
f4083aa4b3 | ||
|
|
ddd2641726 | ||
|
|
4658aeb31e | ||
|
|
cc8e5fe4a9 | ||
|
|
85c897c717 | ||
|
|
c99e5552e6 | ||
|
|
97a2520ea1 | ||
|
|
964af2912b | ||
|
|
a48dd5bf74 | ||
|
|
ef53df5cb3 | ||
|
|
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 | ||
|
|
7055ffc37a | ||
|
|
c68ce6d480 | ||
|
|
412b1fa8c6 | ||
|
|
98a149c8bf | ||
|
|
ceb558975e | ||
|
|
7a87321a7e | ||
|
|
9349766c0a | ||
|
|
31655191a3 | ||
|
|
e3b91efa84 | ||
|
|
cfe736776a | ||
|
|
481bf237cc | ||
|
|
33ce9bf1b9 |
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
|
||||
description: 'Install Node dependencies with pnpm'
|
||||
inputs:
|
||||
TURBO_TOKEN:
|
||||
description: 'Turborepo token'
|
||||
TURBO_TEAM:
|
||||
description: 'Turborepo team'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 7.9.1
|
||||
version: 7.17.0
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
@@ -31,3 +36,6 @@ runs:
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
run: pnpm build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
|
||||
120
.github/workflows/changesets.yaml
vendored
120
.github/workflows/changesets.yaml
vendored
@@ -5,26 +5,34 @@ on:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'dashboard/**'
|
||||
- 'examples/**'
|
||||
- 'assets/**'
|
||||
- '**.md'
|
||||
- '!.changeset/**'
|
||||
- 'LICENSE'
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: nhost
|
||||
DASHBOARD_PACKAGE: '@nhost/dashboard'
|
||||
|
||||
jobs:
|
||||
version:
|
||||
name: Version
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
outputs:
|
||||
hasChangesets: ${{ steps.changesets.outputs.hasChangesets }}
|
||||
dashboardVersion: ${{ steps.dashboard.outputs.dashboardVersion }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# * Install Node and dependencies
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Create PR or Publish release
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
@@ -36,3 +44,107 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Check Dashboard tag
|
||||
id: dashboard
|
||||
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||
run: |
|
||||
DASHBOARD_VERSION=$(jq -r .version dashboard/package.json)
|
||||
GIT_TAG="${{ env.DASHBOARD_PACKAGE}}@$DASHBOARD_VERSION"
|
||||
if [ -z "$(git tag -l | grep $GIT_TAG)" ]; then
|
||||
echo "dashboardVersion=$DASHBOARD_VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
test:
|
||||
needs: version
|
||||
name: Dashboard
|
||||
if: needs.version.outputs.dashboardVersion != ''
|
||||
uses: ./.github/workflows/dashboard.yaml
|
||||
secrets: inherit
|
||||
|
||||
publish-docker:
|
||||
name: Publish to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test
|
||||
- version
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Add git tag
|
||||
run: |
|
||||
git tag "${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}"
|
||||
git push origin --tags
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
nhost/dashboard
|
||||
tags: |
|
||||
type=raw,value=latest,enable=true
|
||||
type=semver,pattern={{version}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=semver,pattern={{major}},value=v${{ needs.version.outputs.dashboardVersion }}
|
||||
type=sha
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v3
|
||||
timeout-minutes: 60
|
||||
with:
|
||||
context: .
|
||||
file: ./dashboard/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
TURBO_TOKEN=${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM=${{ env.TURBO_TEAM }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
- name: Create GitHub Release
|
||||
uses: taiki-e/create-gh-release-action@v1
|
||||
with:
|
||||
changelog: dashboard/CHANGELOG.md
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: ${{ env.DASHBOARD_PACKAGE }}@
|
||||
ref: refs/tags/${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
- name: Remove tag on failure
|
||||
if: failure()
|
||||
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
|
||||
|
||||
publish-vercel:
|
||||
name: Publish to Vercel
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- publish-docker
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Setup Vercel CLI
|
||||
run: pnpm add -g vercel
|
||||
- name: Trigger a Vercel deployment
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
|
||||
run: |
|
||||
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
|
||||
|
||||
27
.github/workflows/dashboard.yaml
vendored
27
.github/workflows/dashboard.yaml
vendored
@@ -1,14 +1,12 @@
|
||||
name: 'Dashboard'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'dashboard/**'
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- 'dashboard/**'
|
||||
|
||||
env:
|
||||
@@ -26,17 +24,15 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Build the application
|
||||
run: pnpm build:dashboard
|
||||
- uses: actions/cache@v3.0.11
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
|
||||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
@@ -45,18 +41,15 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
- uses: actions/cache@v3.0.11
|
||||
id: restore-build
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- name: Run tests
|
||||
run: pnpm test:dashboard
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
NEXT_PUBLIC_ENV: dev
|
||||
NEXT_TELEMETRY_DISABLED: 1
|
||||
@@ -65,9 +58,7 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
- uses: actions/cache@v3.0.11
|
||||
id: restore-build
|
||||
with:
|
||||
path: ./*
|
||||
key: ${{ github.sha }}-${{ github.run_number }}-${{ github.run_attempt }}
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
- run: pnpm lint:dashboard
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Tests
|
||||
name: Packages
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -33,6 +33,9 @@ jobs:
|
||||
# * Install Node and dependencies. Package downloads will be cached for the next jobs.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * List packagesthat has an `e2e` script, except the root, and return an array of their name and path
|
||||
# * In a PR, only include packages that have been modified, and their dependencies
|
||||
- name: List examples with an e2e script
|
||||
@@ -61,6 +64,9 @@ jobs:
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Install Nhost CLI if a `nhost/config.yaml` file is found
|
||||
- name: Install Nhost CLI
|
||||
if: hashFiles(format('{0}/nhost/config.yaml', matrix.package.path)) != ''
|
||||
@@ -92,11 +98,14 @@ jobs:
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `test` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Run unit tests
|
||||
run: pnpm run test
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: '**/coverage/coverage-final.json'
|
||||
name: codecov-umbrella
|
||||
@@ -113,6 +122,9 @@ jobs:
|
||||
# * Install Node and dependencies. Package dependencies won't be downloaded again as they have been cached by the `build` job.
|
||||
- name: Install Node and dependencies
|
||||
uses: ./.github/actions/install-dependencies
|
||||
with:
|
||||
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ env.TURBO_TEAM }}
|
||||
# * Run every `lint` script in the workspace . Dependencies build is cached by Turborepo
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,6 +48,10 @@ todo.md
|
||||
.netlify
|
||||
.monorepo-example
|
||||
|
||||
# Local Vercel folder
|
||||
.vercel
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# TypeDoc output
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint.workingDirectories": ["./dashboard"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## Requirements
|
||||
|
||||
- This repository works with **Node 16**
|
||||
|
||||
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
|
||||
|
||||
```sh
|
||||
@@ -97,6 +99,7 @@ You can take a look at the changeset documentation: [How to add a changeset](htt
|
||||
|
||||
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
|
||||
|
||||
|
||||
<!-- ## Good practices
|
||||
- lint
|
||||
- prettier
|
||||
|
||||
79
README.md
79
README.md
@@ -179,14 +179,21 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Grégory D'Angelo</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/guicurcio">
|
||||
<img src="https://avatars.githubusercontent.com/u/20285232?v=4" width="100;" alt="guicurcio"/>
|
||||
<br />
|
||||
<sub><b>Guido Curcio</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/subatuba21">
|
||||
<img src="https://avatars.githubusercontent.com/u/34824571?v=4" width="100;" alt="subatuba21"/>
|
||||
@@ -221,15 +228,15 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Christopher Möller</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/GavanWilhite">
|
||||
<img src="https://avatars.githubusercontent.com/u/2085119?v=4" width="100;" alt="GavanWilhite"/>
|
||||
<br />
|
||||
<sub><b>Gavan Wilhite</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/FuzzyReason">
|
||||
<img src="https://avatars.githubusercontent.com/u/62517920?v=4" width="100;" alt="FuzzyReason"/>
|
||||
@@ -237,13 +244,6 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Vadim Smirnov</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ejkkan">
|
||||
<img src="https://avatars.githubusercontent.com/u/32518962?v=4" width="100;" alt="ejkkan"/>
|
||||
<br />
|
||||
<sub><b>Erik Magnusson</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/macmac49">
|
||||
<img src="https://avatars.githubusercontent.com/u/831190?v=4" width="100;" alt="macmac49"/>
|
||||
@@ -344,21 +344,28 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Muttenzer</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alexander-mart">
|
||||
<img src="https://avatars.githubusercontent.com/u/14993551?v=4" width="100;" alt="alexander-mart"/>
|
||||
<br />
|
||||
<sub><b>Alexander Mart</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/ahmic">
|
||||
<img src="https://avatars.githubusercontent.com/u/13452362?v=4" width="100;" alt="ahmic"/>
|
||||
<br />
|
||||
<sub><b>Amir Ahmic</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/akd-io">
|
||||
<img src="https://avatars.githubusercontent.com/u/30059155?v=4" width="100;" alt="akd-io"/>
|
||||
<br />
|
||||
<sub><b>Anders Kjær Damgaard</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/Sonichigo">
|
||||
<img src="https://avatars.githubusercontent.com/u/53110238?v=4" width="100;" alt="Sonichigo"/>
|
||||
@@ -366,6 +373,20 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Animesh Pathak</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/chrisli-03">
|
||||
<img src="https://avatars.githubusercontent.com/u/11177048?v=4" width="100;" alt="chrisli-03"/>
|
||||
<br />
|
||||
<sub><b>Chris</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/massless">
|
||||
<img src="https://avatars.githubusercontent.com/u/44389?v=4" width="100;" alt="massless"/>
|
||||
<br />
|
||||
<sub><b>Chris Wetherell</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/rustyb">
|
||||
<img src="https://avatars.githubusercontent.com/u/53086?v=4" width="100;" alt="rustyb"/>
|
||||
@@ -379,7 +400,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Dago</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/dminkovsky">
|
||||
<img src="https://avatars.githubusercontent.com/u/218725?v=4" width="100;" alt="dminkovsky"/>
|
||||
@@ -400,8 +422,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Gaurav Agrawal</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/alveshelio">
|
||||
<img src="https://avatars.githubusercontent.com/u/8176422?v=4" width="100;" alt="alveshelio"/>
|
||||
@@ -422,7 +443,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Ikko Ashimine</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/jladuval">
|
||||
<img src="https://avatars.githubusercontent.com/u/1935359?v=4" width="100;" alt="jladuval"/>
|
||||
@@ -443,8 +465,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Lucas Bois</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/MarcelloTheArcane">
|
||||
<img src="https://avatars.githubusercontent.com/u/21159570?v=4" width="100;" alt="MarcelloTheArcane"/>
|
||||
@@ -465,7 +486,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Nirmalya Ghosh</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/quentin-decre">
|
||||
<img src="https://avatars.githubusercontent.com/u/1137511?v=4" width="100;" alt="quentin-decre"/>
|
||||
@@ -473,6 +495,13 @@ Here are some ways of contributing to making Nhost better:
|
||||
<sub><b>Quentin Decré</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/elephant3">
|
||||
<img src="https://avatars.githubusercontent.com/u/48279149?v=4" width="100;" alt="elephant3"/>
|
||||
<br />
|
||||
<sub><b>Siarhei Lipchyk</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/altschuler">
|
||||
<img src="https://avatars.githubusercontent.com/u/956928?v=4" width="100;" alt="altschuler"/>
|
||||
@@ -486,8 +515,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Tapas Adhikary</b></sub>
|
||||
</a>
|
||||
</td></tr>
|
||||
<tr>
|
||||
</td>
|
||||
<td align="center">
|
||||
<a href="https://github.com/uulwake">
|
||||
<img src="https://avatars.githubusercontent.com/u/22399181?v=4" width="100;" alt="uulwake"/>
|
||||
@@ -501,7 +529,8 @@ Here are some ways of contributing to making Nhost better:
|
||||
<br />
|
||||
<sub><b>Vadim</b></sub>
|
||||
</a>
|
||||
</td>
|
||||
</td></tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<a href="https://github.com/TheRedLancer">
|
||||
<img src="https://avatars.githubusercontent.com/u/58493767?v=4" width="100;" alt="TheRedLancer"/>
|
||||
|
||||
@@ -21,7 +21,7 @@ module.exports = {
|
||||
'tests/**/*.ts',
|
||||
'tests/**/*.d.ts'
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'simple-import-sort', 'cypress'],
|
||||
plugins: ['@typescript-eslint', 'cypress'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
@@ -30,31 +30,6 @@ module.exports = {
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
'no-use-before-define': 'off',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'simple-import-sort/imports': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
// Node.js builtins. You could also generate this regex if you use a `.js` config.
|
||||
// For example: `^(${require("module").builtinModules.join("|")})(/|$)`
|
||||
[
|
||||
'^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)'
|
||||
],
|
||||
// Packages
|
||||
['^\\w'],
|
||||
// Internal packages.
|
||||
['^(@|config/)(/*|$)'],
|
||||
// Side effect imports.
|
||||
['^\\u0000'],
|
||||
// Parent imports. Put `..` last.
|
||||
['^\\.\\.(?!/?$)', '^\\.\\./?$'],
|
||||
// Other relative imports. Put same-folder imports and `.` last.
|
||||
['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
|
||||
// Style imports.
|
||||
['^.+\\.s?css$']
|
||||
]
|
||||
}
|
||||
],
|
||||
'import/no-anonymous-default-export': [
|
||||
'error',
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['next', 'airbnb', 'airbnb-typescript', 'airbnb/hooks', 'prettier'],
|
||||
|
||||
@@ -6,6 +6,18 @@ module.exports = {
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
{
|
||||
/**
|
||||
* Fix Storybook issue with PostCSS@8
|
||||
* @see https://github.com/storybookjs/storybook/issues/12668#issuecomment-773958085
|
||||
*/
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
|
||||
94
dashboard/CHANGELOG.md
Normal file
94
dashboard/CHANGELOG.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 44f13f62: chore(dashboard): cleanup unused files
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- e01cb2ed: chore(dashboard): change settings sidebar menu item density
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- db342f45: chore(dashboard): refactor Roles and Permissions settings sections
|
||||
- 8b9fa0b1: feat(dashboard): add Environment Variables page
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [66b4f3d0]
|
||||
- Updated dependencies [2e6923dc]
|
||||
- Updated dependencies [ef117c28]
|
||||
- Updated dependencies [aebb8225]
|
||||
- @nhost/core@0.9.4
|
||||
- @nhost/nhost-js@1.6.2
|
||||
- @nhost/nextjs@1.9.1
|
||||
- @nhost/react@0.15.1
|
||||
- @nhost/react-apollo@4.9.1
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- eef9c914: feat(dashboard): add Roles and Permissions page
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a48dd5bf: feat(dashboard): make backend port configurable
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5de965d9: fix(dashboard): alphabetic ordering of providers
|
||||
- b9087a4a: fix(dashboard): console -> dashboard terminology
|
||||
- ca012d79: docs(workos): WorkOS Docs
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 89bd37bc: fix(dashboard): correct redirect URL input opacity
|
||||
- Updated dependencies [4601d84e]
|
||||
- Updated dependencies [843087cb]
|
||||
- @nhost/react@0.15.0
|
||||
- @nhost/nextjs@1.9.0
|
||||
- @nhost/react-apollo@4.9.0
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 766cb612: fix(dashboard): correct redirect URL for oauth providers
|
||||
- Updated dependencies [53bdc294]
|
||||
- Updated dependencies [f2aaff05]
|
||||
- @nhost/nextjs@1.8.3
|
||||
- @nhost/core@0.9.3
|
||||
- @nhost/react@0.14.3
|
||||
- @nhost/nhost-js@1.6.1
|
||||
- @nhost/react-apollo@4.8.3
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 9211743d: feat(dashboard): migrate Settings page features
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 73da6a67: fix(dashboard): avoid using BACKEND_URL locally
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- db118f97: feat(dashboard): generate Docker image
|
||||
55
dashboard/Dockerfile
Normal file
55
dashboard/Dockerfile
Normal file
@@ -0,0 +1,55 @@
|
||||
FROM node:16-alpine AS pruner
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
RUN turbo prune --scope="@nhost/dashboard" --docker
|
||||
|
||||
FROM node:16-alpine AS builder
|
||||
ARG TURBO_TOKEN
|
||||
ARG TURBO_TEAM
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk update
|
||||
WORKDIR /app
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NEXT_PUBLIC_ENV dev
|
||||
ENV NEXT_PUBLIC_NHOST_PLATFORM false
|
||||
|
||||
# placeholders for ports, will be replaced on runtime by entrypoint script
|
||||
ENV NEXT_PUBLIC_NHOST_MIGRATIONS_PORT __NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_PORT __NEXT_PUBLIC_NHOST_HASURA_PORT__
|
||||
ENV NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT __NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__
|
||||
|
||||
RUN yarn global add pnpm@7.17.0
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY --from=pruner /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY config/ config/
|
||||
RUN pnpm build:dashboard
|
||||
|
||||
FROM node:16-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
COPY --chown=nextjs:nodejs dashboard/docker-entrypoint.sh .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/next.config.js .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/package.json .
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/public ./dashboard/public
|
||||
|
||||
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/standalone/app ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/dashboard/.next/static ./dashboard/.next/static
|
||||
|
||||
ENTRYPOINT ["./docker-entrypoint.sh"]
|
||||
CMD ["node", "dashboard/server.js"]
|
||||
@@ -30,31 +30,27 @@ First, you need to run the following command to start your backend locally:
|
||||
cd <your_nhost_project> && nhost dev
|
||||
```
|
||||
|
||||
Two environment variables are required to connect the Nhost Dashboard to your local backend:
|
||||
You can connect the Nhost Dashboard to your locally running backend by setting the following environment variables in `.env.development.local`:
|
||||
|
||||
- `NEXT_PUBLIC_NHOST_PLATFORM` should be set to `false`, because otherwise the Nhost Dashboard will try to connect to the Nhost platform.
|
||||
- `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` should be set to `http://localhost:9693` unless Hasura is configured to run on a different port. This is the URL of Hasura's migrations endpoint.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
```bash
|
||||
NEXT_PUBLIC_ENV=dev
|
||||
NEXT_PUBLIC_NHOST_PLATFORM=false
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
```
|
||||
|
||||
### Full list of environment variables
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------------ |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. |
|
||||
| `NEXT_PUBLIC_NHOST_MIGRATIONS_URL` | URL of Hasura's migrations endpoint. Used only if local development is enabled. |
|
||||
| `NEXT_PUBLIC_NHOST_HASURA_URL` | URL of the Hasura Console. Used only when `NEXT_PUBLIC_ENV` is `dev`. |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | URL of the local backend. This is `http://localhost:1337` by default. |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||
| Name | Description |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_ENV` | `dev`, `staging` or `prod`. Should be set to `dev` in most cases. |
|
||||
| `NEXT_PUBLIC_NHOST_PLATFORM` | This should be set to `false` to connect the Nhost Dashboard to a locally running Nhost backend. Setting this to `true` turns off local development. |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_MIGRATIONS_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `9693` |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_HASURA_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled and `NEXT_PUBLIC_ENV` is `dev`. Default: `9695` |
|
||||
| `NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT` | Custom port that was passed to the CLI. Used only if local development is enabled. Default: `1337` |
|
||||
| `NEXT_PUBLIC_NHOST_BACKEND_URL` | Backend URL. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_STRIPE_PK` | Stripe public key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_GITHUB_APP_INSTALL_URL` | URL of the GitHub application. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_ANALYTICS_WRITE_KEY` | Analytics key. Not necessary for local development. |
|
||||
| `NEXT_PUBLIC_NHOST_BRAGI_WEBSOCKET` | URL of the Bragi websocket. Not necessary for local development. |
|
||||
|
||||
## ESLint Rules
|
||||
|
||||
@@ -67,6 +63,7 @@ NEXT_PUBLIC_NHOST_MIGRATIONS_URL=http://localhost:9693
|
||||
| `import/extensions` | JS / TS files should be imported without file extensions. |
|
||||
| `react/jsx-filename-extension` | JSX should only appear in `.jsx` and `.tsx` files. |
|
||||
| `react/jsx-no-bind` | Further investigation must be made on the performance impact of functions directly passed as props to components. |
|
||||
| `import/order` | Until we have a better auto-formatter, we disable this rule. |
|
||||
| `import/no-extraneous-dependencies` | `devDependencies` should be excluded from the list of disallowed imports. |
|
||||
| `curly` | By default it only enforces curly braces for multi-line blocks, but it should be enforced for single-line blocks as well. |
|
||||
| `no-restricted-exports` | `export { default } from './module'` is used heavily in `@/ui/v2` which is a restricted export by default. |
|
||||
|
||||
15
dashboard/docker-entrypoint.sh
Executable file
15
dashboard/docker-entrypoint.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# read ports from env variables or use defaults
|
||||
NEXT_PUBLIC_NHOST_MIGRATIONS_PORT="${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT:=9693}"
|
||||
NEXT_PUBLIC_NHOST_HASURA_PORT="${NEXT_PUBLIC_NHOST_HASURA_PORT:=9695}"
|
||||
NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT="${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT:=1337}"
|
||||
|
||||
# replace placeholders
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_MIGRATIONS_PORT__/${NEXT_PUBLIC_NHOST_MIGRATIONS_PORT}/g" {} +
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_HASURA_PORT__/${NEXT_PUBLIC_NHOST_HASURA_PORT}/g" {} +
|
||||
find dashboard -type f -exec sed -i "s/__NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT__/${NEXT_PUBLIC_NHOST_LOCAL_BACKEND_PORT}/g" {} +
|
||||
|
||||
exec "$@"
|
||||
@@ -13,11 +13,3 @@ generates:
|
||||
- 'typescript-react-apollo'
|
||||
config:
|
||||
withRefetchFn: true
|
||||
functions/utils/__generated__/graphql-request.ts:
|
||||
documents:
|
||||
- 'functions/**/*.graphql'
|
||||
- 'functions/**/*.gql'
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
- 'typescript-graphql-request'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const path = require('path');
|
||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
@@ -5,6 +6,10 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
module.exports = withBundleAnalyzer({
|
||||
reactStrictMode: true,
|
||||
swcMinify: false,
|
||||
output: 'standalone',
|
||||
experimental: {
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
},
|
||||
eslint: {
|
||||
dirs: ['src'],
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.1.0",
|
||||
"version": "0.7.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -8,7 +8,7 @@
|
||||
"build": "next build --no-lint",
|
||||
"analyze": "ANALYZE=true pnpm build --no-lint",
|
||||
"start": "next start",
|
||||
"lint": "next lint --max-warnings 6",
|
||||
"lint": "next lint --max-warnings 3",
|
||||
"test": "vitest",
|
||||
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
|
||||
"nhost:dev": "nhost dev -d",
|
||||
@@ -25,25 +25,25 @@
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fontsource/inter": "^4.5.14",
|
||||
"@fontsource/roboto-mono": "^4.5.8",
|
||||
"@graphiql/react": "^0.13.2",
|
||||
"@graphiql/react": "^0.14.0",
|
||||
"@graphiql/toolkit": "^0.8.0",
|
||||
"@headlessui/react": "^1.6.5",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
"@hookform/resolvers": "^2.9.10",
|
||||
"@mui/base": "^5.0.0-alpha.105",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/system": "^5.10.13",
|
||||
"@mui/base": "^5.0.0-alpha.106",
|
||||
"@mui/material": "^5.10.14",
|
||||
"@mui/system": "^5.10.14",
|
||||
"@mui/x-date-pickers": "^5.0.8",
|
||||
"@nhost/core": "^0.9.1",
|
||||
"@nhost/nextjs": "^1.8.1",
|
||||
"@nhost/nhost-js": "^1.5.2",
|
||||
"@nhost/react": "^0.14.1",
|
||||
"@nhost/react-apollo": "^4.8.1",
|
||||
"@nhost/core": "^0.9.4",
|
||||
"@nhost/nextjs": "^1.9.1",
|
||||
"@nhost/nhost-js": "^1.6.2",
|
||||
"@nhost/react": "^0.15.1",
|
||||
"@nhost/react-apollo": "^4.9.1",
|
||||
"@segment/snippet": "^4.15.3",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tanstack/react-query": "^4.16.1",
|
||||
"@tanstack/react-table": "^8.5.27",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.22",
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||
"analytics-node": "^6.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -51,7 +51,7 @@
|
||||
"cross-fetch": "^3.1.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.0.8",
|
||||
"graphiql": "^2.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^4.3.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
@@ -66,13 +66,14 @@
|
||||
"randomstring": "^1.2.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.39.3",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-hot-toast": "^2.4.0",
|
||||
"react-is": "17.0.2",
|
||||
"react-loading-skeleton": "^2.2.0",
|
||||
"react-merge-refs": "^1.1.0",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-table": "^7.8.0",
|
||||
"sharp": "^0.31.2",
|
||||
"slugify": "^1.6.5",
|
||||
"smartlook-client": "^6.0.0",
|
||||
"stripe": "^10.17.0",
|
||||
@@ -95,6 +96,7 @@
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/addon-postcss": "^2.0.0",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
@@ -114,10 +116,10 @@
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/validator": "^13.7.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.1",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"@vitest/coverage-c8": "^0.25.1",
|
||||
"@vitest/coverage-c8": "^0.25.2",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"babel-loader": "^8.3.0",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
@@ -125,25 +127,24 @@
|
||||
"critters": "^0.0.10",
|
||||
"csstype": "^3.0.10",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^8.27.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-airbnb": "19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-next": "^13.0.2",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
||||
"eslint-plugin-react": "^7.31.10",
|
||||
"eslint-plugin-react": "^7.31.11",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"express": "^4.18.2",
|
||||
"express-validator": "^6.14.2",
|
||||
"jsdom": "^20.0.2",
|
||||
"jsdom": "^20.0.3",
|
||||
"lint-staged": ">=13",
|
||||
"msw": "^0.48.2",
|
||||
"msw": "^0.49.0",
|
||||
"postcss": "^8.4.19",
|
||||
"postmark": "^2.7.8",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwind-css": "^1.5.0",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
@@ -151,9 +152,9 @@
|
||||
"tailwindcss": "^3.1.2",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.2.3",
|
||||
"vite-tsconfig-paths": "^3.5.2",
|
||||
"vitest": "^0.25.1",
|
||||
"vite": "^3.2.4",
|
||||
"vite-tsconfig-paths": "^3.6.0",
|
||||
"vitest": "^0.25.2",
|
||||
"webpack": "^5.75.0"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -168,4 +169,4 @@
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1
dashboard/public/assets/twilio.svg
Normal file
1
dashboard/public/assets/twilio.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg width="21" height="21" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10.08 0c5.578 0 10.08 4.507 10.08 10.09 0 5.584-4.502 10.09-10.08 10.09A10.072 10.072 0 0 1 0 10.09C0 4.507 4.503 0 10.08 0Zm0 2.69a7.375 7.375 0 0 0-7.392 7.4c0 4.104 3.293 7.4 7.392 7.4 4.1 0 7.392-3.296 7.392-7.4 0-4.103-3.293-7.4-7.392-7.4Zm-2.486 7.804c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.083-2.086c0-1.143.94-2.085 2.083-2.085Zm4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086a2.095 2.095 0 0 1-2.084-2.086c0-1.143.941-2.085 2.084-2.085Zm0-4.978c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 10.483 7.6c0-1.143.941-2.085 2.084-2.085Zm-4.973 0c1.142 0 2.083.942 2.083 2.085 0 1.144-.94 2.086-2.083 2.086A2.095 2.095 0 0 1 5.51 7.6c0-1.143.94-2.085 2.083-2.085Z" fill="#F22F46"/></svg>
|
||||
|
After Width: | Height: | Size: 869 B |
@@ -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;
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { inputErrorMessages } from '@/utils/getErrorMessage';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useUpdateApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export function ChangeApplicationName({ close }: any) {
|
||||
const [updateAppName, { client }] = useUpdateApplicationMutation({});
|
||||
const { workspaceContext } = useWorkspaceContext();
|
||||
const [name, setName] = useState(workspaceContext.appName);
|
||||
const [applicationError, setApplicationError] = useState<any>('');
|
||||
const { currentWorkspace, currentApplication } =
|
||||
useCurrentWorkspaceAndApplication();
|
||||
const router = useRouter();
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
const slug = slugifyString(name);
|
||||
if (slug.length < 4 || slug.length > 32) {
|
||||
setApplicationError('Slug should be within 4 and 32 characters.');
|
||||
return;
|
||||
}
|
||||
if (!inputErrorMessages(name, setName, setApplicationError, 'project')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAppName({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
name,
|
||||
slug,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast('Project name changed');
|
||||
} catch (error) {
|
||||
await discordAnnounce(
|
||||
`Error trying to delete project: ${currentApplication.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
await updateOwnCache(client);
|
||||
await router.push(`/${currentWorkspace.slug}/${slug}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6 py-6 text-left w-modal">
|
||||
<div className="flex flex-col">
|
||||
<Text variant="h3" component="h2">
|
||||
Change Project Name
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-flow-row gap-2 mt-4">
|
||||
<Input
|
||||
label="New Project Name"
|
||||
id="projectName"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value);
|
||||
setApplicationError('');
|
||||
}}
|
||||
fullWidth
|
||||
autoFocus
|
||||
helperText={`https://app.nhost.io/${
|
||||
currentWorkspace.slug
|
||||
}/${slugifyString(name)}`}
|
||||
/>
|
||||
|
||||
{applicationError && (
|
||||
<Alert severity="error">{applicationError}</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2 mt-4">
|
||||
<Button type="submit" disabled={applicationError}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={close}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangeApplicationName;
|
||||
@@ -30,7 +30,7 @@ function Plan({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="grid items-center justify-between w-full grid-flow-col px-1 my-4"
|
||||
className="my-4 grid w-full grid-flow-col items-center justify-between px-1"
|
||||
onClick={setPlan}
|
||||
tabIndex={-1}
|
||||
>
|
||||
@@ -48,7 +48,7 @@ function Plan({
|
||||
<Text
|
||||
variant="h3"
|
||||
component="p"
|
||||
className="self-center ml-2 font-medium"
|
||||
className="ml-2 self-center font-medium"
|
||||
>
|
||||
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
|
||||
</Text>
|
||||
@@ -143,7 +143,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 text-left w-welcome">
|
||||
<div className="w-welcome p-6 text-left">
|
||||
<Modal
|
||||
showModal={paymentModal}
|
||||
close={closePaymentModal}
|
||||
@@ -189,7 +189,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2 mt-6">
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
|
||||
{!selectedPlan && 'Change Plan'}
|
||||
{selectedPlan && isDowngrade && 'Downgrade'}
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function ConnectGithubModal({ close }: ConnectGithubModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
<RetryableErrorBoundary>
|
||||
<div className=" h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
|
||||
<div className="h-import divide-y-1 divide-divide overflow-y-auto border-t-1 border-b-1">
|
||||
{githubRepositoriesToDisplay.map((repo) => (
|
||||
<Repo
|
||||
key={repo.id}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import type { EnvironmentVariableFragment } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
refetchGetEnvironmentVariablesWhereQuery,
|
||||
useDeleteEnvironmentVariableMutation,
|
||||
useUpdateEnvironmentVariableMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
type EnvModalProps = {
|
||||
show: boolean;
|
||||
close: VoidFunction;
|
||||
envVar: EnvironmentVariableFragment;
|
||||
};
|
||||
|
||||
export default function EditEnvVarModal({
|
||||
show,
|
||||
close,
|
||||
envVar,
|
||||
}: EnvModalProps) {
|
||||
const { workspaceContext } = useWorkspaceContext();
|
||||
const { appId } = workspaceContext;
|
||||
|
||||
const [updateEnvVar, { loading: updateLoading }] =
|
||||
useUpdateEnvironmentVariableMutation({
|
||||
refetchQueries: [
|
||||
refetchGetEnvironmentVariablesWhereQuery({
|
||||
where: {
|
||||
appId: {
|
||||
_eq: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [deleteEnvVar, { loading: deleteLoading }] =
|
||||
useDeleteEnvironmentVariableMutation({
|
||||
refetchQueries: [
|
||||
refetchGetEnvironmentVariablesWhereQuery({
|
||||
where: {
|
||||
appId: {
|
||||
_eq: appId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const [prodValue, setProdValue] = useState(envVar.prodValue || '');
|
||||
const [devValue, setDevValue] = useState(envVar.devValue || '');
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await updateEnvVar({
|
||||
variables: {
|
||||
id: envVar.id,
|
||||
environmentVariable: {
|
||||
prodValue,
|
||||
devValue,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
close();
|
||||
triggerToast('Error updating environment variable');
|
||||
return;
|
||||
}
|
||||
triggerToast(`Environment variable ${envVar.name} updated successfully`);
|
||||
close();
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteEnvVar({
|
||||
variables: {
|
||||
id: envVar.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
close();
|
||||
triggerToast('Error deleting environment variable');
|
||||
return;
|
||||
}
|
||||
triggerToast(`Environment variable ${envVar.name} removed successfully`);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal showModal={show} close={close}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{envVar.name}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
The default value will be available in all environments, unless
|
||||
you override it. All values are encrypted.
|
||||
</Text>
|
||||
|
||||
<div className="my-2 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
autoFocus
|
||||
disabled
|
||||
autoComplete="off"
|
||||
defaultValue={envVar.name}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={prodValue}
|
||||
onChange={(event) => setProdValue(event.target.value)}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={devValue}
|
||||
onChange={(event) => setDevValue(event.target.value)}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={updateLoading}>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
loading={deleteLoading}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button onClick={close} variant="outlined" color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,327 +0,0 @@
|
||||
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
|
||||
import TwilioIcon from '@/components/icons/TwilioIcon';
|
||||
import {
|
||||
useGetSmsSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Button, Input } from '@/ui';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormContext,
|
||||
} from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function EditSMSSettingsForm({
|
||||
close,
|
||||
isAlreadyEnabled,
|
||||
}: {
|
||||
close: () => void;
|
||||
isAlreadyEnabled: boolean;
|
||||
}) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useFormContext<EditSMSSettingsFormData>();
|
||||
const { control } = useFormContext<EditSMSSettingsFormData>();
|
||||
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
let toastId: string;
|
||||
|
||||
const client = useApolloClient();
|
||||
const isNotCompleted =
|
||||
!watch('accountSID') ||
|
||||
!watch('authToken') ||
|
||||
!watch('messagingServiceSID');
|
||||
|
||||
const handleEditSMSSettings = async (data: EditSMSSettingsFormData) => {
|
||||
try {
|
||||
toastId = showLoadingToast('Updating SMS settings...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authSmsTwilioAccountSid: data.accountSID,
|
||||
authSmsTwilioAuthToken: data.authToken,
|
||||
authSmsTwilioMessagingServiceId: data.messagingServiceSID,
|
||||
authSmsPasswordlessEnabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
await client.refetchQueries({ include: ['getSMSSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast('SMS settings updated successfully.');
|
||||
close();
|
||||
} catch (error) {
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(handleEditSMSSettings)}
|
||||
className="flex w-full flex-col pb-1"
|
||||
autoComplete="off"
|
||||
>
|
||||
{errors &&
|
||||
Object.entries(errors).map(([type, error]) => (
|
||||
<Alert key={type} className="mb-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
<div>
|
||||
<div className="flex flex-row place-content-between border-t border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Account SID
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="accountSID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message:
|
||||
'The Account SID must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="accountSID"
|
||||
placeholder="Account SID"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Auth Token
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="authToken"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message:
|
||||
'The Auth Token must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="authToken"
|
||||
placeholder="Auth Token"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_/.]+$/gi.test(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row place-content-between border-b px-2 py-2.5">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
Messaging Service SID
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="messagingServiceSID"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[+a-zA-Z0-9-_/.]+$/,
|
||||
message:
|
||||
'The Messaging Service SID must either be a valid phone number or contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="messagingServiceSID"
|
||||
required
|
||||
placeholder="Messaging Service SID"
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[+a-zA-Z0-9-_/.]+$/gi.test(value)) {
|
||||
return;
|
||||
}
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="text-grayscaleDark mt-2 border text-sm+ font-normal"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || isNotCompleted}
|
||||
>
|
||||
{isAlreadyEnabled ? 'Update SMS Settings' : 'Enable SMS'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function EditSMSSettingsModal({
|
||||
close,
|
||||
isAlreadyEnabled,
|
||||
}: {
|
||||
close: () => void;
|
||||
isAlreadyEnabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="w-modal px-6 py-4 text-left">
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto mt-2.5">
|
||||
<TwilioIcon className=" text-greyscaleDark" />
|
||||
</div>
|
||||
<Text
|
||||
variant="subHeading"
|
||||
color="greyscaleDark"
|
||||
size="large"
|
||||
className="mt-3 text-center"
|
||||
>
|
||||
Set up Twilio SMS Service
|
||||
</Text>
|
||||
<Text
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="mt-0.5 mb-6 text-center font-normal"
|
||||
>
|
||||
SMS messages are sent through Twilio. Create an account and a
|
||||
messaging service at https://console.twilio.com.
|
||||
</Text>
|
||||
<div>
|
||||
<ErrorBoundary fallbackRender={ErrorBoundaryFallback}>
|
||||
<EditSMSSettingsForm
|
||||
close={close}
|
||||
isAlreadyEnabled={isAlreadyEnabled}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface EditSMSSettingsProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export interface EditSMSSettingsFormData {
|
||||
accountSID: string;
|
||||
authToken: string;
|
||||
messagingServiceSID: string;
|
||||
}
|
||||
|
||||
export function EditSMSSettings({ close }: EditSMSSettingsProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<EditSMSSettingsFormData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
accountSID: '',
|
||||
authToken: '',
|
||||
messagingServiceSID: '',
|
||||
},
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSmsSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setValue('accountSID', data.app.authSmsTwilioAccountSid);
|
||||
form.setValue('authToken', data.app.authSmsTwilioAuthToken);
|
||||
form.setValue(
|
||||
'messagingServiceSID',
|
||||
data.app.authSmsTwilioMessagingServiceId,
|
||||
);
|
||||
}, [data, form]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<EditSMSSettingsModal
|
||||
close={close}
|
||||
isAlreadyEnabled={data.app.authSmsPasswordlessEnabled}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
import {
|
||||
useGetSmsSettingsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Text, Toggle } from '@/ui';
|
||||
import DelayedLoading from '@/ui/DelayedLoading';
|
||||
import { showLoadingToast, triggerToast } from '@/utils/toast';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import clsx from 'clsx';
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export function EnableSMSSignIn({ openSMSSettingsModal }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApp] = useUpdateAppMutation();
|
||||
|
||||
const { data, loading, error } = useGetSmsSettingsQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
const [enableSMSLoginMethod, setEnableSMSLoginMethod] = useState(false);
|
||||
const client = useApolloClient();
|
||||
let toastId: string;
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEnableSMSLoginMethod(data.app.authSmsPasswordlessEnabled);
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return <DelayedLoading delay={500} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const handleDisable = async () => {
|
||||
try {
|
||||
toastId = showLoadingToast('Disabling SMS login...');
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authSmsPasswordlessEnabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
setEnableSMSLoginMethod(false);
|
||||
await client.refetchQueries({ include: ['getSMSSettings'] });
|
||||
toast.remove(toastId);
|
||||
triggerToast('Passwordless SMS disabled.');
|
||||
} catch (updateError) {
|
||||
if (toastId) {
|
||||
toast.remove(toastId);
|
||||
}
|
||||
|
||||
throw updateError;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-20">
|
||||
<div className="mx-auto font-display">
|
||||
<div className="flex flex-col place-content-between">
|
||||
<div className="">
|
||||
<div className="flex flex-row place-content-between">
|
||||
<div className="relative flex flex-row">
|
||||
<Image
|
||||
src="/assets/SMS.svg"
|
||||
width={24}
|
||||
height={24}
|
||||
alt="Phone Number (SMS)"
|
||||
/>
|
||||
<Text
|
||||
variant="body"
|
||||
size="large"
|
||||
className="ml-2 font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
Phone Number (SMS)
|
||||
</Text>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(
|
||||
'ml-2 align-bottom text-sm- font-medium text-blue transition-opacity duration-300',
|
||||
!enableSMSLoginMethod && 'invisible opacity-0',
|
||||
enableSMSLoginMethod && 'opacity-100',
|
||||
)}
|
||||
onClick={() => openSMSSettingsModal()}
|
||||
>
|
||||
Edit SMS settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<Toggle
|
||||
checked={enableSMSLoginMethod}
|
||||
onChange={async () => {
|
||||
if (enableSMSLoginMethod) {
|
||||
await handleDisable();
|
||||
} else {
|
||||
openSMSSettingsModal();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row self-center mt-3 align-middle">
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className="self-center"
|
||||
>
|
||||
Sign in users with Phone Number (SMS).
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EnableSMSSignIn;
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ConnectionDetail } from '@/components/applications/ConnectionDetail';
|
||||
import { LoadingScreen } from '@/components/common/LoadingScreen';
|
||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { LOCAL_HASURA_URL } from '@/utils/env';
|
||||
import { generateRemoteAppUrl } from '@/utils/helpers';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -24,7 +25,7 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? process.env.NEXT_PUBLIC_NHOST_HASURA_URL || 'http://localhost:9695'
|
||||
? LOCAL_HASURA_URL
|
||||
: generateRemoteAppUrl(currentApplication.subdomain);
|
||||
|
||||
return (
|
||||
@@ -66,7 +67,7 @@ export function HasuraData({ close }: HasuraDataProps) {
|
||||
underline="none"
|
||||
>
|
||||
Open Hasura
|
||||
<ExternalLink className="ml-0.5 h-4 w-4" />
|
||||
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
|
||||
{close && (
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useSubmitState } from '@/hooks/useSubmitState';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppInjectedVariablesQuery,
|
||||
useUpdateApplicationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
|
||||
export interface EditCustomUserJWTTokenData {
|
||||
customUserJWTToken: string;
|
||||
}
|
||||
|
||||
export type JWTSecretModalState = 'SHOW' | 'EDIT';
|
||||
|
||||
export interface JWTSecretModalProps {
|
||||
close: () => void;
|
||||
data?: any;
|
||||
jwtSecret: string;
|
||||
initialModalState?: JWTSecretModalState;
|
||||
}
|
||||
|
||||
export function EditJWTSecretModal({ close }: any) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const { submitState, setSubmitState } = useSubmitState();
|
||||
|
||||
const [updateApplication] = useUpdateApplicationMutation({
|
||||
refetchQueries: [
|
||||
refetchGetAppInjectedVariablesQuery({ id: currentApplication.id }),
|
||||
],
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
} = useForm<EditCustomUserJWTTokenData>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
customUserJWTToken: '',
|
||||
},
|
||||
});
|
||||
|
||||
const handleEditJWTSecret = async (data: EditCustomUserJWTTokenData) => {
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
try {
|
||||
JSON.parse(data.customUserJWTToken);
|
||||
} catch (error) {
|
||||
setSubmitState({
|
||||
error: new Error('The custom JWT token should be valid json.'),
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApplication({
|
||||
variables: {
|
||||
appId: currentApplication.id,
|
||||
app: {
|
||||
hasuraGraphqlJwtSecret: data.customUserJWTToken,
|
||||
},
|
||||
},
|
||||
});
|
||||
triggerToast(
|
||||
`Successfully added custom JWT token to ${currentApplication.name}.`,
|
||||
);
|
||||
close();
|
||||
} catch (error) {
|
||||
triggerToast(
|
||||
`Error adding custom JWT token to ${currentApplication.name}`,
|
||||
);
|
||||
setSubmitState({ error, loading: false, fieldsWithError: [] });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
className="px-6 py-4 w-modal"
|
||||
onSubmit={handleSubmit(handleEditJWTSecret)}
|
||||
>
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row text-left">
|
||||
<Text variant="h3" component="h2">
|
||||
Add Custom JWT Secret
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
You can add your custom JWT key here. Hasura will use this key to
|
||||
validate the identity of your users.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{submitState.error && (
|
||||
<Alert severity="error">{submitState.error.message}</Alert>
|
||||
)}
|
||||
|
||||
<Controller
|
||||
name="customUserJWTToken"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Paste your custom JWT token here..."
|
||||
componentsProps={{
|
||||
inputRoot: {
|
||||
className: 'font-mono bg-header',
|
||||
},
|
||||
}}
|
||||
aria-label="Custom JWT token"
|
||||
type="text"
|
||||
value={field.value}
|
||||
onBlur={() =>
|
||||
setSubmitState({
|
||||
error: null,
|
||||
loading: false,
|
||||
fieldsWithError: [],
|
||||
})
|
||||
}
|
||||
multiline
|
||||
rows={6}
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit" loading={isSubmitting}>
|
||||
Save Changes
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function ShowJWTTokenModal({ JWTKey, editJWTSecret }: any) {
|
||||
return (
|
||||
<div className="px-6 py-4 w-modal">
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-row text-left">
|
||||
<Text variant="h3" component="h2">
|
||||
Auth JWT Secret
|
||||
</Text>
|
||||
<Text variant="subtitle2">
|
||||
This is the key used for generating JWTs. It's HMAC-SHA-based
|
||||
and the same as configured in Hasura.
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
defaultValue={JWTKey}
|
||||
multiline
|
||||
disabled
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
rows={6}
|
||||
componentsProps={{
|
||||
inputRoot: { className: 'font-mono' },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-w-sm mx-auto text-center">
|
||||
<Text variant="subtitle2">
|
||||
Already using a third party auth service? <br />
|
||||
<button
|
||||
type="button"
|
||||
className="mt-0.5 ml-0.5 text-xs font-medium text-blue"
|
||||
onClick={() => {
|
||||
editJWTSecret();
|
||||
}}
|
||||
>
|
||||
Add your custom JWT token
|
||||
</button>
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function JWTSecretModal({
|
||||
close,
|
||||
data,
|
||||
jwtSecret,
|
||||
initialModalState,
|
||||
}: any) {
|
||||
const [jwtSecretModalState, setJwtSecretModalState] =
|
||||
useState<JWTSecretModalState>(initialModalState || 'SHOW');
|
||||
|
||||
const editJWTSecret = () => {
|
||||
setJwtSecretModalState('EDIT');
|
||||
};
|
||||
|
||||
if (jwtSecretModalState === 'EDIT') {
|
||||
return <EditJWTSecretModal close={close} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowJWTTokenModal
|
||||
JWTKey={jwtSecret || data}
|
||||
editJWTSecret={editJWTSecret}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { triggerToast } from '@/utils/toast';
|
||||
import { useDeleteApplicationMutation } from '@/utils/__generated__/graphql';
|
||||
import router from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface RemoveApplicationModalProps {
|
||||
/**
|
||||
@@ -26,6 +27,10 @@ export interface RemoveApplicationModalProps {
|
||||
* Description of the modal
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Class name to be applied to the modal.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function RemoveApplicationModal({
|
||||
@@ -33,6 +38,7 @@ export function RemoveApplicationModal({
|
||||
handler,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
}: RemoveApplicationModalProps) {
|
||||
const [deleteApplication, { client }] = useDeleteApplicationMutation();
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
@@ -72,18 +78,14 @@ export function RemoveApplicationModal({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-modal text-left">
|
||||
<div className={twMerge('w-full max-w-sm p-6 text-left', className)}>
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{title || 'Delete Project'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
{description ? (
|
||||
<div>{description}</div>
|
||||
) : (
|
||||
<div>Are you sure you want to delete this app?</div>
|
||||
)}
|
||||
{description || 'Are you sure you want to delete this app?'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2" className="font-bold !text-rose-600">
|
||||
@@ -98,33 +100,15 @@ export function RemoveApplicationModal({
|
||||
checked={remove}
|
||||
onChange={(_event, checked) => setRemove(checked)}
|
||||
aria-label="Confirm Delete Project #1"
|
||||
componentsProps={{
|
||||
formControlLabel: {
|
||||
componentsProps: {
|
||||
typography: {
|
||||
className: '!text-sm+',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
id="accept-2"
|
||||
label="I understand this action can not be undone"
|
||||
label="I understand this action cannot be undone"
|
||||
className="py-2"
|
||||
checked={remove2}
|
||||
onChange={(_event, checked) => setRemove2(checked)}
|
||||
aria-label="Confirm Delete Project #2"
|
||||
componentsProps={{
|
||||
formControlLabel: {
|
||||
componentsProps: {
|
||||
typography: {
|
||||
className: '!text-sm+',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ContainerAllWorkspacesApplications } from './ContainerAllWorkspacesAppl
|
||||
|
||||
function ApplicationCreatedAt({ createdAt }: any) {
|
||||
return (
|
||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
||||
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||
created{' '}
|
||||
{formatDistance(new Date(createdAt), new Date(), {
|
||||
addSuffix: true,
|
||||
@@ -30,9 +30,9 @@ function LastSuccesfulDeployment({ deployment }: any) {
|
||||
<Avatar
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="self-center w-4 h-4 mr-1"
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
||||
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||
{deployment.commitUserName} deployed{' '}
|
||||
{formatDistance(new Date(deployment.deploymentEndedAt), new Date(), {
|
||||
addSuffix: true,
|
||||
@@ -48,9 +48,9 @@ function CurrentDeployment({ deployment }: any) {
|
||||
<Avatar
|
||||
name={deployment.commitUserName}
|
||||
avatarUrl={deployment.commitUserAvatarUrl}
|
||||
className="self-center w-4 h-4 mr-1"
|
||||
className="mr-1 h-4 w-4 self-center"
|
||||
/>
|
||||
<Text color="dark" className="self-center text-sm cursor-pointer">
|
||||
<Text color="dark" className="cursor-pointer self-center text-sm">
|
||||
{deployment.commitUserName} updated just now
|
||||
</Text>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@ export function RenderWorkspacesWithApps({
|
||||
variant="a"
|
||||
color="greyscaleGrey"
|
||||
size="normal"
|
||||
className="mb-3 font-medium cursor-pointer"
|
||||
className="mb-3 cursor-pointer font-medium"
|
||||
>
|
||||
{workspace.name}
|
||||
</Text>
|
||||
@@ -138,16 +138,16 @@ export function RenderWorkspacesWithApps({
|
||||
? app.deployments[0].deploymentStatus === 'DEPLOYING'
|
||||
: false;
|
||||
return (
|
||||
<div key={app.slug} className="py-4 cursor-pointer">
|
||||
<div key={app.slug} className="cursor-pointer py-4">
|
||||
<Link href={`${workspace?.slug}/${app.slug}`} passHref>
|
||||
<a
|
||||
href={`${workspace?.slug}/${app.slug}`}
|
||||
className="flex px-2 bg-white rounded-sm place-content-between border-divide"
|
||||
className="flex place-content-between rounded-sm border-divide bg-white px-2"
|
||||
>
|
||||
<div className="flex flex-col self-center w-full">
|
||||
<div className="flex flex-row w-full place-content-between">
|
||||
<div className="flex w-full flex-col self-center">
|
||||
<div className="flex w-full flex-row place-content-between">
|
||||
<div className="flex flex-row items-center self-center">
|
||||
<div className="w-10 h-10 overflow-hidden rounded-lg">
|
||||
<div className="h-10 w-10 overflow-hidden rounded-lg">
|
||||
<Image
|
||||
src="/logos/new.svg"
|
||||
alt="Nhost Logo"
|
||||
@@ -155,12 +155,12 @@ export function RenderWorkspacesWithApps({
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col ml-2 text-left">
|
||||
<div className="ml-2 flex flex-col text-left">
|
||||
<div>
|
||||
<Text
|
||||
color="dark"
|
||||
size="normal"
|
||||
className="self-center font-medium text-left capitalize cursor-pointer"
|
||||
className="cursor-pointer self-center text-left font-medium capitalize"
|
||||
>
|
||||
{app.name}
|
||||
</Text>
|
||||
@@ -192,7 +192,7 @@ export function RenderWorkspacesWithApps({
|
||||
<div className="flex flex-row">
|
||||
<div className="flex self-center align-middle">
|
||||
{app.deployments[0] && (
|
||||
<div className="flex self-center mr-2 align-middle">
|
||||
<div className="mr-2 flex self-center align-middle">
|
||||
<StatusCircle
|
||||
status={
|
||||
app.deployments[0]
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Avatar } from '@/ui/Avatar';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import Image from 'next/image';
|
||||
|
||||
export function SelectedWorkspaceOnNewApp({ current }: any) {
|
||||
const user = nhost.auth.getUser();
|
||||
|
||||
return (
|
||||
<div className="flex flex-row space-x-2 self-center">
|
||||
{current.name === 'Default Workspace' ? (
|
||||
<Avatar
|
||||
className="h-5 w-5 self-center rounded-full"
|
||||
name={user?.displayName}
|
||||
avatarUrl={user?.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-5 w-5 overflow-hidden rounded-md">
|
||||
<Image src="/logos/new.svg" alt="Nhost Logo" width={20} height={20} />
|
||||
</div>
|
||||
)}
|
||||
<Text size="small" color="greyscaleDark" className="font-normal">
|
||||
{current.name}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default SelectedWorkspaceOnNewApp;
|
||||
@@ -38,8 +38,8 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
normalizedFunctionData.logs.length === 0
|
||||
) {
|
||||
return (
|
||||
<div className="w-full text-white rounded-lg">
|
||||
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
|
||||
<div className="w-full rounded-lg text-white">
|
||||
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||
<div className="font-mono text-xs text-grey">
|
||||
There are no stored logs yet. Try calling your function for logs to
|
||||
appear.
|
||||
@@ -50,12 +50,12 @@ export function FunctionsLogsTerminalPage({ functionName }: any) {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="w-full text-white rounded-lg">
|
||||
<div className="px-4 py-4 overflow-auto font-mono rounded-lg shadow-sm h-terminal bg-log">
|
||||
<div className="w-full rounded-lg text-white">
|
||||
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
|
||||
{normalizedFunctionData.logs.map((log) => (
|
||||
<div
|
||||
key={`${log.date}-${log.message.slice(66)}`}
|
||||
className="flex text-sm "
|
||||
className=" flex text-sm"
|
||||
>
|
||||
<div id={`#-${log.date}`}>
|
||||
<pre className="inline">
|
||||
|
||||
@@ -24,7 +24,7 @@ export function EditRepositorySettings({
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<EditRepositorySettingsFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
productionBranch: currentApplication.repositoryProductionBranch || 'main',
|
||||
repoBaseFolder: currentApplication.nhostBaseFolder,
|
||||
|
||||
@@ -78,8 +78,8 @@ export function EditRepositorySettingsModal({
|
||||
return (
|
||||
<div className="px-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="w-8 h-8 mx-auto">
|
||||
<GithubIcon className="w-8 h-8 text-greyscaleDark" />
|
||||
<div className="mx-auto h-8 w-8">
|
||||
<GithubIcon className="h-8 w-8 text-greyscaleDark" />
|
||||
</div>
|
||||
<Text
|
||||
variant="subHeading"
|
||||
@@ -95,7 +95,7 @@ export function EditRepositorySettingsModal({
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="font-normal text-center"
|
||||
className="text-center font-normal"
|
||||
>
|
||||
{selectedRepoId
|
||||
? `We'll deploy changes automatically when you push to the deployment branch. `
|
||||
@@ -110,7 +110,7 @@ export function EditRepositorySettingsModal({
|
||||
<div className="">
|
||||
<RepoAndBranch />
|
||||
</div>
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="mt-2 flex flex-col">
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
@@ -123,7 +123,7 @@ export function EditRepositorySettingsModal({
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-col mt-2">
|
||||
<div className="mt-2 flex flex-col">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ExternalLink from '@/components/icons/ExternalIcon';
|
||||
import GithubIcon from '@/components/icons/GithubIcon';
|
||||
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import Link from '@/ui/v2/Link';
|
||||
import Text from '@/ui/v2/Text';
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function GitHubInstallNhostApplication() {
|
||||
underline="none"
|
||||
>
|
||||
Configure the Nhost application on GitHub{' '}
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<ArrowSquareOutIcon className="h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,19 +20,19 @@ export function GitHubNoRepositoriesAdded({
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="tiny"
|
||||
className="font-normal text-center"
|
||||
className="text-center font-normal"
|
||||
>
|
||||
Check the Nhost app's settings on your GitHub account, or install
|
||||
the app on a new account.
|
||||
</Text>
|
||||
|
||||
<div className="py-3 my-2 border-t border-b">
|
||||
<div className="my-2 border-t border-b py-3">
|
||||
<div className="flex">
|
||||
{filteredGitHubAppInstallations.map((githubApp) => (
|
||||
<div key={githubApp.id} className="flex items-center mr-4">
|
||||
<div key={githubApp.id} className="mr-4 flex items-center">
|
||||
<Avatar
|
||||
avatarUrl={githubApp.accountAvatarUrl as string}
|
||||
className="w-5 h-5 mr-1"
|
||||
className="mr-1 h-5 w-5"
|
||||
/>
|
||||
{githubApp.accountLogin}
|
||||
</div>
|
||||
@@ -45,9 +45,9 @@ export function GitHubNoRepositoriesAdded({
|
||||
rel="noreferrer noopener"
|
||||
transparent
|
||||
type={null}
|
||||
className="text-xs font-medium cursor-pointer text-blue"
|
||||
className="cursor-pointer text-xs font-medium text-blue"
|
||||
>
|
||||
<PlusSmIcon className="w-4 h-4 mr-1 border rounded-full border-btn" />
|
||||
<PlusSmIcon className="mr-1 h-4 w-4 rounded-full border border-btn" />
|
||||
Configure the Nhost application on GitHub.
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './AppDeployments';
|
||||
@@ -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';
|
||||
@@ -1,71 +0,0 @@
|
||||
import type { SelectorOption } from '@/ui/Selector';
|
||||
import Selector from '@/ui/Selector';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Toggle } from '@/ui/Toggle';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface PermissionSettingsProps {
|
||||
text: string;
|
||||
desc?: string;
|
||||
toggle?: boolean;
|
||||
onChange?: any;
|
||||
checked?: boolean;
|
||||
options?: any;
|
||||
value?: SelectorOption;
|
||||
} // @TODO: Fix alt attribute on images.
|
||||
// @FIX: Double border
|
||||
|
||||
export function PermissionSetting({
|
||||
text,
|
||||
desc,
|
||||
toggle,
|
||||
checked = false,
|
||||
onChange,
|
||||
options,
|
||||
value,
|
||||
}: PermissionSettingsProps) {
|
||||
return (
|
||||
<div className="flex flex-row place-content-between py-2">
|
||||
<div
|
||||
className={clsx(
|
||||
'flex flex-col space-y-1 self-center px-0.5',
|
||||
!desc && 'py-3.5',
|
||||
desc && 'py-2',
|
||||
)}
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
className="font-medium"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text
|
||||
variant="body"
|
||||
size="tiny"
|
||||
className="font-normal"
|
||||
color="greyscaleDark"
|
||||
>
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{toggle ? (
|
||||
<div className="flex flex-row">
|
||||
<Toggle checked={checked} onChange={onChange} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row self-center">
|
||||
<Selector
|
||||
width="w-28"
|
||||
options={options}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { Provider as ProviderType } from '@/types/providers';
|
||||
import Status, { StatusEnum } from '@/ui/Status';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
interface ProviderProps {
|
||||
provider: ProviderType;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export function Provider({ provider, enabled }: ProviderProps) {
|
||||
const { name, logo } = provider;
|
||||
|
||||
const {
|
||||
query: { workspaceSlug, appSlug },
|
||||
} = useRouter();
|
||||
|
||||
const nameLowerCase = name.toLowerCase();
|
||||
return (
|
||||
<Link
|
||||
href={`/${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
|
||||
passHref
|
||||
>
|
||||
<a
|
||||
href={`${workspaceSlug}/${appSlug}/settings/sign-in-methods/${nameLowerCase}`}
|
||||
className="flex cursor-pointer flex-row place-content-between border-t py-2.5"
|
||||
>
|
||||
<div className="grid grid-flow-col items-center gap-2">
|
||||
<div className="h-6 w-6">
|
||||
<Image
|
||||
src={logo}
|
||||
alt={`Logo of ${name}`}
|
||||
width={24}
|
||||
height={24}
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
<Text className="font-medium" color="greyscaleDark" size="normal">
|
||||
{name}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
{enabled ? (
|
||||
<Status status={StatusEnum.Live}>Enabled</Status>
|
||||
) : (
|
||||
<Status status={StatusEnum.Closed}>Disabled</Status>
|
||||
)}
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default Provider;
|
||||
@@ -1,183 +0,0 @@
|
||||
import { CreateUserRoleModal } from '@/components/applications/users/roles/CreateRoleModal';
|
||||
import { EditUserRoleModal } from '@/components/applications/users/roles/EditUserRoleModal';
|
||||
import Lock from '@/components/icons/Lock';
|
||||
import type { GetRolesQuery } from '@/generated/graphql';
|
||||
import { Modal } from '@/ui';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ChevronRightIcon } from '@heroicons/react/solid';
|
||||
import clsx from 'clsx';
|
||||
import type { Dispatch, MouseEvent, MouseEventHandler } from 'react';
|
||||
import { useReducer } from 'react';
|
||||
|
||||
function RolesTableHead() {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-64 py-3 text-left font-medium text-base">
|
||||
<Text className="text-xs font-bold text-greyscaleDark">Role</Text>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
interface UserRoleProps {
|
||||
role: string;
|
||||
isSystemRole: boolean;
|
||||
onClick?: MouseEventHandler<HTMLTableRowElement>;
|
||||
}
|
||||
|
||||
function UserRole({ role, isSystemRole, onClick }: UserRoleProps) {
|
||||
return (
|
||||
<tr
|
||||
className={clsx(isSystemRole ? 'cursor-not-allowed' : 'cursor-pointer')}
|
||||
onClick={onClick}
|
||||
>
|
||||
<td className="py-2">
|
||||
<Text
|
||||
size="normal"
|
||||
className={clsx(
|
||||
isSystemRole ? 'text-greyscaleGrey' : 'text-greyscaleDark',
|
||||
'pl-1 font-medium',
|
||||
)}
|
||||
>
|
||||
{role}
|
||||
</Text>
|
||||
</td>
|
||||
<td className="text-right">
|
||||
{isSystemRole ? (
|
||||
<div className="inline-flex pr-1">
|
||||
<Text
|
||||
size="tiny"
|
||||
className=" font-mono text-xs font-medium uppercase tracking-wide text-greyscaleGrey"
|
||||
>
|
||||
System Role
|
||||
</Text>
|
||||
<Lock className="ml-1 h-5 w-5 text-greyscaleGrey" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex self-center py-2 pr-1.5">
|
||||
<ChevronRightIcon className="h-4.5 w-4.5 text-greyscaleDark" />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export type UserRoleDetails = {
|
||||
name: string;
|
||||
isSystemRole: boolean;
|
||||
};
|
||||
|
||||
export const getUserRoles = (data): UserRoleDetails[] => {
|
||||
const authUserDefaultAllowedRoles =
|
||||
data.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
return authUserDefaultAllowedRoles.map((role: string) => ({
|
||||
name: role,
|
||||
isSystemRole: ['user', 'me'].includes(role),
|
||||
}));
|
||||
};
|
||||
|
||||
type ModalState = {
|
||||
visible: boolean;
|
||||
type: 'create' | 'edit';
|
||||
payload: UserRoleDetails;
|
||||
};
|
||||
|
||||
type ModalAction = {
|
||||
type: 'OPEN_CREATE_MODAL' | 'OPEN_EDIT_MODAL' | 'CLOSE_MODAL';
|
||||
payload?: UserRoleDetails;
|
||||
};
|
||||
|
||||
function modalStateReducer(state: ModalState, action: ModalAction): ModalState {
|
||||
switch (action.type) {
|
||||
case 'OPEN_CREATE_MODAL':
|
||||
return { ...state, visible: true, type: 'create', payload: null };
|
||||
case 'OPEN_EDIT_MODAL':
|
||||
return { ...state, visible: true, type: 'edit', payload: action.payload };
|
||||
case 'CLOSE_MODAL':
|
||||
return { ...state, visible: false };
|
||||
default:
|
||||
throw new Error(`Action type ${action.type} is not supported.`);
|
||||
}
|
||||
}
|
||||
|
||||
function AddNewUserRole({ dispatch }: { dispatch: Dispatch<ModalAction> }) {
|
||||
return (
|
||||
<tr className="cursor-pointer border-y-1 border-solid border-gray-300">
|
||||
<td className="p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'OPEN_CREATE_MODAL' })}
|
||||
>
|
||||
<Text className="text-sm+ font-medium text-blue">
|
||||
Create New Role
|
||||
</Text>
|
||||
</button>
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function RolesTableBody({ data }: { data: GetRolesQuery }) {
|
||||
const userRoles = getUserRoles(data);
|
||||
const [
|
||||
{ visible: modalVisible, type: modalType, payload: modalPayload },
|
||||
dispatch,
|
||||
] = useReducer(modalStateReducer, {
|
||||
visible: false,
|
||||
type: null,
|
||||
payload: null,
|
||||
});
|
||||
|
||||
function handleRoleEdit(event: MouseEvent<HTMLTableRowElement>, role: any) {
|
||||
dispatch({ type: 'OPEN_EDIT_MODAL', payload: role });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={modalVisible}
|
||||
close={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
>
|
||||
{modalType === 'create' ? (
|
||||
<CreateUserRoleModal
|
||||
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
/>
|
||||
) : (
|
||||
<EditUserRoleModal
|
||||
onClose={() => dispatch({ type: 'CLOSE_MODAL' })}
|
||||
payload={modalPayload}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<tbody className="divide-y-1 border-t-1 border-b-1 border-solid border-gray-300 ">
|
||||
{userRoles.map((role) => (
|
||||
<UserRole
|
||||
key={role.name}
|
||||
role={role.name}
|
||||
isSystemRole={role.isSystemRole}
|
||||
onClick={
|
||||
role.isSystemRole
|
||||
? undefined
|
||||
: (event) => handleRoleEdit(event, role)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<AddNewUserRole dispatch={dispatch} />
|
||||
</tbody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RolesTable({ data }: { data: GetRolesQuery }) {
|
||||
return (
|
||||
<table className="w-full table-fixed overflow-x-auto">
|
||||
<RolesTableHead />
|
||||
<RolesTableBody data={data} />
|
||||
</table>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import type { TextProps } from '@/ui/Text';
|
||||
import { Text } from '@/ui/Text';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface SettingsSectionProps {
|
||||
/**
|
||||
* Title of this section.
|
||||
*/
|
||||
title: ReactNode;
|
||||
/**
|
||||
* Props to be passed to the title component.
|
||||
*/
|
||||
titleProps?: TextProps;
|
||||
/**
|
||||
* Props to be passed to the wrapper component.
|
||||
*/
|
||||
wrapperProps?: TextProps;
|
||||
/**
|
||||
* Description of this section.
|
||||
*/
|
||||
desc?: ReactNode;
|
||||
/**
|
||||
* Props to be passed to the description component.
|
||||
*/
|
||||
descriptionProps?: TextProps;
|
||||
}
|
||||
|
||||
export function SettingsSection({
|
||||
children,
|
||||
title,
|
||||
titleProps,
|
||||
descriptionProps,
|
||||
desc,
|
||||
wrapperProps,
|
||||
}: PropsWithChildren<SettingsSectionProps>) {
|
||||
const { className: titleClassName, ...restTitleProps } = titleProps || {};
|
||||
const { className: wrapperClassName } = wrapperProps || {};
|
||||
const { className: descriptionClassName, ...restDescriptionProps } =
|
||||
descriptionProps || {};
|
||||
|
||||
return (
|
||||
<div className={twMerge('mt-10', wrapperClassName)}>
|
||||
<div className="mx-auto font-display">
|
||||
<div className="flex flex-col place-content-between">
|
||||
<div>
|
||||
<Text
|
||||
size="large"
|
||||
variant="heading"
|
||||
className={twMerge('mb-1.5 font-medium', titleClassName)}
|
||||
color="greyscaleDark"
|
||||
{...restTitleProps}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{desc && (
|
||||
<Text
|
||||
variant="body"
|
||||
size="normal"
|
||||
color="greyscaleDark"
|
||||
className={twMerge('mb-3 font-normal', descriptionClassName)}
|
||||
{...restDescriptionProps}
|
||||
>
|
||||
{desc}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -32,25 +32,25 @@ function Users({ users }: any) {
|
||||
key={user.id}
|
||||
passHref
|
||||
>
|
||||
<tr className="cursor-pointer w-52">
|
||||
<td className="py-1 pr-6 whitespace-nowrap">
|
||||
<tr className="w-52 cursor-pointer">
|
||||
<td className="whitespace-nowrap py-1 pr-6">
|
||||
<div className="flex items-center">
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="p-1 mr-2"
|
||||
className="mr-2 p-1"
|
||||
aria-label="Copy user ID"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
copy(user.id, `User ID`);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
|
||||
<div className="flex-shrink-0 w-8 h-8">
|
||||
<div className="h-8 w-8 flex-shrink-0">
|
||||
<Avatar
|
||||
className="w-8 h-8"
|
||||
className="h-8 w-8"
|
||||
avatarUrl={user.avatarUrl}
|
||||
name={user.displayName}
|
||||
/>
|
||||
@@ -63,7 +63,7 @@ function Users({ users }: any) {
|
||||
<Text
|
||||
variant="a"
|
||||
color="greyscaleDark"
|
||||
className="font-medium cursor-pointer"
|
||||
className="cursor-pointer font-medium"
|
||||
size="normal"
|
||||
>
|
||||
{user.displayName ||
|
||||
@@ -78,7 +78,7 @@ function Users({ users }: any) {
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<td className="whitespace-nowrap px-6 py-4">
|
||||
<Text color="greyscaleDark" className="font-normal" size="normal">
|
||||
{format(new Date(user.createdAt), 'd MMM yyyy')}
|
||||
</Text>
|
||||
@@ -103,13 +103,13 @@ function Users({ users }: any) {
|
||||
);
|
||||
})}
|
||||
</td>
|
||||
<td className="py-4 pl-6 text-sm font-medium text-right whitespace-nowrap">
|
||||
<td className="whitespace-nowrap py-4 pl-6 text-right text-sm font-medium">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/${appSlug}/users/${user.id}`}
|
||||
passHref
|
||||
>
|
||||
<a href={`${workspaceSlug}/${appSlug}/users/${user.id}`}>
|
||||
<ChevronRightIcon className="self-center w-4 h-4 ml-2 cursor-pointer" />
|
||||
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center" />
|
||||
</a>
|
||||
</Link>
|
||||
</td>
|
||||
@@ -131,7 +131,7 @@ function UserPages({ totalNrOfPages, setCurrentPage }: any) {
|
||||
<button
|
||||
type="button"
|
||||
key={i}
|
||||
className="px-2 cursor-pointer"
|
||||
className="cursor-pointer px-2"
|
||||
onClick={() => {
|
||||
setCurrentPage(i);
|
||||
}}
|
||||
@@ -212,15 +212,15 @@ export function UsersTable({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mt-2 font-display">
|
||||
<div className="inline-block min-w-full py-2 align-">
|
||||
<div className="mt-2 flex flex-col font-display">
|
||||
<div className="align- inline-block min-w-full py-2">
|
||||
<div className="overflow-hidden border-b">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-4 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
||||
className="px-4 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||
>
|
||||
{data ? (
|
||||
<TotalUsers
|
||||
@@ -244,7 +244,7 @@ export function UsersTable({
|
||||
</th>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
||||
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||
>
|
||||
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
||||
Signed up at
|
||||
@@ -253,7 +253,7 @@ export function UsersTable({
|
||||
|
||||
<th
|
||||
scope="col"
|
||||
className="px-6 py-3 text-xs font-medium tracking-wider text-left text-dark"
|
||||
className="px-6 py-3 text-left text-xs font-medium tracking-wider text-dark"
|
||||
>
|
||||
<Text size="tiny" color="greyscaleDark" className="font-bold">
|
||||
Roles
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import useCustomClaims from '@/hooks/useCustomClaims';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type {
|
||||
CreatePermissionVariableBaseFormData,
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
} from './CreatePermissionVariableModalBase';
|
||||
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
|
||||
|
||||
export type CreatePermissionVariableFormData =
|
||||
CreatePermissionVariableBaseFormData;
|
||||
|
||||
export type CreatePermissionVariableModalProps = Pick<
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
'onClose'
|
||||
>;
|
||||
|
||||
export default function CreatePermissionVariableModal({
|
||||
onClose,
|
||||
}: CreatePermissionVariableModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const form = useForm<CreatePermissionVariableFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceContext: { appId },
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const { data: customClaims } = useCustomClaims({ appId });
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
|
||||
});
|
||||
|
||||
async function handleSubmit(permissionVariable: CustomClaim) {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (
|
||||
customClaims.some(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Permission variable with this field name already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: [...customClaims, permissionVariable]
|
||||
.filter((claim) => !claim.system)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast('Permission variable created');
|
||||
|
||||
if (!onClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CreatePermissionVariableModalBase
|
||||
title="Create Permission Variable"
|
||||
type="create"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={onClose}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { ChangeEvent, MouseEventHandler, ReactNode } from 'react';
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface CreatePermissionVariableBaseFormData {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface CreateModalBaseProps<T> {
|
||||
/**
|
||||
* Title of this modal.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Type of this modal.
|
||||
*/
|
||||
type?: 'create' | 'edit';
|
||||
/**
|
||||
* Callback to be called when the modal is closed.
|
||||
*/
|
||||
onClose?: VoidFunction;
|
||||
/**
|
||||
* Callback to be called when remove button is clicked.
|
||||
*/
|
||||
onRemove?: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Callback to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit: SubmitHandler<T>;
|
||||
/**
|
||||
* Error to be displayed.
|
||||
*/
|
||||
errorComponent?: ReactNode;
|
||||
}
|
||||
|
||||
export type CreatePermissionVariableModalBaseProps =
|
||||
CreateModalBaseProps<CreatePermissionVariableBaseFormData>;
|
||||
|
||||
export default function CreatePermissionVariableModalBase({
|
||||
title,
|
||||
type,
|
||||
onClose,
|
||||
onRemove,
|
||||
onSubmit,
|
||||
errorComponent,
|
||||
}: CreatePermissionVariableModalBaseProps) {
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
register,
|
||||
formState: { isSubmitting, errors },
|
||||
} = useFormContext<CreatePermissionVariableBaseFormData>();
|
||||
|
||||
const keyHandlers = register('key', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z-]+$/i,
|
||||
message: 'Must contain only letters and hyphens',
|
||||
},
|
||||
});
|
||||
|
||||
const valueHandlers = register('value', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9._[\]]+$/i,
|
||||
message: 'Must contain only letters, dots, brackets, and underscores',
|
||||
},
|
||||
});
|
||||
|
||||
const isComplete = !!watch('key') && !!watch('value');
|
||||
|
||||
return (
|
||||
<div className="w-modal p-6 text-left">
|
||||
<div className="grid w-full grid-flow-col items-center justify-between">
|
||||
<Text variant="h3" component="h2">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{type === 'edit' && onRemove && (
|
||||
<Button variant="borderless" color="error" onClick={onRemove}>
|
||||
Remove
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text className="mt-2 text-sm+ text-greyscaleDark">
|
||||
Enter the field name and the path you want to use in this permission
|
||||
variable.
|
||||
</Text>
|
||||
|
||||
{errorComponent}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="my-4 grid grid-flow-row divide-y-1 divide-solid divide-gray-200 border-y border-gray-200">
|
||||
<Input
|
||||
{...keyHandlers}
|
||||
value={watch('key')}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
keyHandlers.onChange(event);
|
||||
}}
|
||||
id="key"
|
||||
variant="inline"
|
||||
inlineInputProportion="66%"
|
||||
label="Field name"
|
||||
fullWidth
|
||||
startAdornment={
|
||||
<Text className="min-w-[73px] text-sm+ text-greyscaleGrey">
|
||||
X-Hasura-
|
||||
</Text>
|
||||
}
|
||||
componentsProps={{
|
||||
inputWrapper: { className: 'my-1' },
|
||||
input: {
|
||||
className: 'border-transparent focus-within:border-solid pl-2',
|
||||
},
|
||||
inputRoot: { className: '!pl-[1px]' },
|
||||
}}
|
||||
autoFocus
|
||||
error={!!errors?.key?.message}
|
||||
helperText={errors?.key?.message}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...valueHandlers}
|
||||
value={watch('value')}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.target.value &&
|
||||
!/^[a-zA-Z-]+$/gi.test(event.target.value)
|
||||
) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
valueHandlers.onChange(event);
|
||||
}}
|
||||
id="value"
|
||||
variant="inline"
|
||||
inlineInputProportion="66%"
|
||||
label="Path"
|
||||
fullWidth
|
||||
startAdornment={
|
||||
<Text className="text-sm+ text-greyscaleGrey">user.</Text>
|
||||
}
|
||||
componentsProps={{
|
||||
inputWrapper: { className: 'my-1' },
|
||||
input: {
|
||||
className: 'border-transparent focus-within:border-solid pl-2',
|
||||
},
|
||||
inputRoot: { className: '!pl-[1px]' },
|
||||
}}
|
||||
error={!!errors?.value?.message}
|
||||
helperText={errors?.value?.message}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting || !isComplete}
|
||||
>
|
||||
{type === 'create' ? 'Create Permission Variable' : 'Save Changes'}
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
import { useWorkspaceContext } from '@/context/workspace-context';
|
||||
import useCustomClaims from '@/hooks/useCustomClaims';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import {
|
||||
refetchGetAppCustomClaimsQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type {
|
||||
CreatePermissionVariableBaseFormData,
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
} from './CreatePermissionVariableModalBase';
|
||||
import CreatePermissionVariableModalBase from './CreatePermissionVariableModalBase';
|
||||
|
||||
export type EditPermissionVariableFormData =
|
||||
CreatePermissionVariableBaseFormData;
|
||||
|
||||
export type EditPermissionVariableModalProps = Pick<
|
||||
CreatePermissionVariableModalBaseProps,
|
||||
'onClose'
|
||||
> & {
|
||||
/**
|
||||
* The permission variable to edit.
|
||||
*/
|
||||
payload: CustomClaim;
|
||||
};
|
||||
|
||||
export default function EditPermissionVariableModal({
|
||||
payload: originalCustomClaim,
|
||||
...props
|
||||
}: EditPermissionVariableModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
|
||||
const form = useForm<EditPermissionVariableFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
key: originalCustomClaim.key || '',
|
||||
value: originalCustomClaim.value || '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
workspaceContext: { appId },
|
||||
} = useWorkspaceContext();
|
||||
|
||||
const { data: customClaims } = useCustomClaims({ appId });
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetAppCustomClaimsQuery({ id: appId })],
|
||||
});
|
||||
|
||||
async function handleSubmit(permissionVariable: CustomClaim) {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (
|
||||
originalCustomClaim.key.toLowerCase() !==
|
||||
permissionVariable.key.toLowerCase() &&
|
||||
customClaims.some(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === permissionVariable.key.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
'Permission variable with this field name already exists.',
|
||||
);
|
||||
}
|
||||
|
||||
// we need to preserve the original position of the permission variable
|
||||
const currentIndex = customClaims.findIndex(
|
||||
(claim) =>
|
||||
claim.key.toLowerCase() === originalCustomClaim.key.toLowerCase(),
|
||||
);
|
||||
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: customClaims
|
||||
.slice(0, currentIndex)
|
||||
.concat(permissionVariable)
|
||||
.concat(customClaims.slice(currentIndex + 1))
|
||||
.filter((claim) => !claim.system)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
triggerToast(`Permission variable updated`);
|
||||
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove() {
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: appId,
|
||||
app: {
|
||||
authJwtCustomClaims: customClaims
|
||||
.filter(
|
||||
(claim) =>
|
||||
claim.key !== originalCustomClaim.key && !claim.system,
|
||||
)
|
||||
.reduce(
|
||||
(authJwtCustomClaims, claim) => ({
|
||||
...authJwtCustomClaims,
|
||||
[claim.key]: claim.value,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setShowRemoveModal(false);
|
||||
|
||||
triggerToast('Permission variable removed');
|
||||
|
||||
if (props.onClose) {
|
||||
props.onClose();
|
||||
}
|
||||
} catch (updateError) {
|
||||
if (updateError instanceof Error) {
|
||||
setError(updateError);
|
||||
} else {
|
||||
setError(new Error(updateError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRemoveModal}
|
||||
close={() => setShowRemoveModal(false)}
|
||||
>
|
||||
<div className="grid w-96 grid-flow-row gap-2 p-6 text-left text-greyscaleDark">
|
||||
<Text variant="h3" component="h2">
|
||||
Remove {originalCustomClaim.key}?
|
||||
</Text>
|
||||
|
||||
<Text>You will not be able to use it in permissions anymore.</Text>
|
||||
|
||||
<Text>
|
||||
If you have permission checks currently using this property, they
|
||||
will never resolve to true.
|
||||
</Text>
|
||||
|
||||
<div className="mt-2 grid grid-flow-row gap-2">
|
||||
<Button color="error" onClick={handleRemove} className="w-full">
|
||||
Remove Permission Variable
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={() => setShowRemoveModal(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CreatePermissionVariableModalBase
|
||||
title="Edit Permission Variable"
|
||||
type="edit"
|
||||
onSubmit={handleSubmit}
|
||||
onRemove={() => setShowRemoveModal(true)}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Loading from '@/ui/Loading';
|
||||
import {
|
||||
refetchGetRolesQuery,
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import type {
|
||||
CreateUserRoleBaseFormData,
|
||||
CreateUserRoleModalBaseProps,
|
||||
} from './CreateUserRoleModalBase';
|
||||
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
|
||||
|
||||
export type CreateUserRoleFormData = CreateUserRoleBaseFormData;
|
||||
|
||||
export type CreateUserRoleModalProps = Pick<
|
||||
CreateUserRoleModalBaseProps,
|
||||
'onClose'
|
||||
>;
|
||||
|
||||
export function CreateUserRoleModal({ onClose }: CreateUserRoleModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
|
||||
const form = useForm<CreateUserRoleBaseFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
});
|
||||
|
||||
const [updateApp] = useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentRolesData,
|
||||
loading,
|
||||
error: getRolesError,
|
||||
} = useGetRolesQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (getRolesError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2.5xl">
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(data) {
|
||||
setError(undefined);
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = `${currentRolesData.app.authUserDefaultAllowedRoles},${data.roleName}`;
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!onClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClose();
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<CreateUserRoleModalBase
|
||||
title="Create New Role"
|
||||
type="create"
|
||||
onSubmit={handleSubmit}
|
||||
onClose={onClose}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import type { CreateModalBaseProps } from '@/components/applications/users/permissions/modal/CreatePermissionVariableModalBase';
|
||||
import { Input } from '@/ui';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface CreateUserRoleBaseFormData {
|
||||
roleName: string;
|
||||
}
|
||||
|
||||
export type CreateUserRoleModalBaseProps =
|
||||
CreateModalBaseProps<CreateUserRoleBaseFormData>;
|
||||
export type CreateUserRoleModal = Pick<CreateUserRoleModalBaseProps, 'onClose'>;
|
||||
|
||||
export function CreateUserRoleModalBase({
|
||||
title,
|
||||
type,
|
||||
onRemove,
|
||||
onSubmit,
|
||||
errorComponent,
|
||||
}: CreateUserRoleModalBaseProps) {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = useFormContext<CreateUserRoleBaseFormData>();
|
||||
|
||||
return (
|
||||
<div className="w-modal- p-6 text-left">
|
||||
<div className="mx-auto items-center justify-between">
|
||||
<Text
|
||||
variant="heading"
|
||||
className="text-center text-lg font-medium text-greyscaleDark"
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{errorComponent}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<div className="mt-3 mb-3 divide-y border-t border-b py-1">
|
||||
<div className="flex flex-row place-content-between py-2">
|
||||
<div className="flex w-full flex-row">
|
||||
<Text
|
||||
color="greyscaleDark"
|
||||
className="self-center font-medium"
|
||||
size="normal"
|
||||
>
|
||||
New Role Name
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex w-full">
|
||||
<Controller
|
||||
name="roleName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^[a-zA-Z0-9-_]+$/,
|
||||
message: 'Must contain only letters, hyphens, and numbers.',
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
{...field}
|
||||
id="roleName"
|
||||
required
|
||||
value={field.value || ''}
|
||||
onChange={(value: string) => {
|
||||
if (value && !/^[a-zA-Z0-9-_]+$/gi.test(value)) {
|
||||
// prevent the user from entering invalid characters
|
||||
return;
|
||||
}
|
||||
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{type === 'create' ? 'Create New User Role' : 'Save Changes'}
|
||||
</Button>
|
||||
{type === 'edit' && onRemove && (
|
||||
<Button variant="menu" border onClick={onRemove}>
|
||||
<Text className="text-sm+ font-medium text-red">Remove Role</Text>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import type { GetRolesQuery } from '@/generated/graphql';
|
||||
import {
|
||||
refetchGetRolesQuery,
|
||||
useGetRolesQuery,
|
||||
useUpdateAppMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import { Button } from '@/ui/Button';
|
||||
import Loading from '@/ui/Loading';
|
||||
import { Modal } from '@/ui/Modal';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import type {
|
||||
CreateUserRoleBaseFormData,
|
||||
CreateUserRoleModalBaseProps,
|
||||
} from './CreateUserRoleModalBase';
|
||||
import { CreateUserRoleModalBase } from './CreateUserRoleModalBase';
|
||||
|
||||
export type EditUserRoleFormData = CreateUserRoleBaseFormData;
|
||||
|
||||
export type EditUserRoleModalProps = Pick<
|
||||
CreateUserRoleModalBaseProps,
|
||||
'onClose'
|
||||
> & {
|
||||
/**
|
||||
* The permission variable to edit.
|
||||
*/
|
||||
payload: any;
|
||||
};
|
||||
|
||||
export function EditUserRoleModal({
|
||||
payload: originalRole,
|
||||
...props
|
||||
}: EditUserRoleModalProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [showRemoveModal, setShowRemoveModal] = useState(false);
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const form = useForm<EditUserRoleFormData>({
|
||||
reValidateMode: 'onBlur',
|
||||
defaultValues: {
|
||||
roleName: originalRole.name || '',
|
||||
},
|
||||
});
|
||||
|
||||
const [updateApp, { loading: loadingUpdateAppMutation }] =
|
||||
useUpdateAppMutation({
|
||||
refetchQueries: [refetchGetRolesQuery({ id: currentApplication.id })],
|
||||
});
|
||||
|
||||
const {
|
||||
data: currentRolesData,
|
||||
loading,
|
||||
error: getRolesError,
|
||||
} = useGetRolesQuery({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
if (getRolesError) {
|
||||
return (
|
||||
<div className="mx-auto max-w-2.5xl">
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSubmit(data: EditUserRoleFormData) {
|
||||
setError(undefined);
|
||||
|
||||
const currentUserRoles =
|
||||
currentRolesData.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
const roleBeingEdited = currentUserRoles.find(
|
||||
(role) => role === originalRole.name,
|
||||
);
|
||||
|
||||
const indexofRoleBeingEdited = currentUserRoles.indexOf(roleBeingEdited);
|
||||
const newRoleName = data.roleName;
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = currentUserRoles.slice();
|
||||
|
||||
if (data.roleName !== originalRole.name) {
|
||||
newAuthUserDefaultAllowedRoles[indexofRoleBeingEdited] = newRoleName;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles:
|
||||
newAuthUserDefaultAllowedRoles.join(','),
|
||||
},
|
||||
},
|
||||
});
|
||||
triggerToast(`Role "${data.roleName}" updated successfully`);
|
||||
props.onClose();
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(data: GetRolesQuery) {
|
||||
setError(undefined);
|
||||
|
||||
// Get the current roles of this application.
|
||||
const currentUserRoles = data.app.authUserDefaultAllowedRoles.split(',');
|
||||
|
||||
// Remove the role from the current roles.
|
||||
const filteredCurrentUserRoles = currentUserRoles.filter(
|
||||
(role) => role !== originalRole.name,
|
||||
);
|
||||
|
||||
const newAuthUserDefaultAllowedRoles = filteredCurrentUserRoles.join(',');
|
||||
|
||||
try {
|
||||
await updateApp({
|
||||
variables: {
|
||||
id: currentApplication.id,
|
||||
app: {
|
||||
authUserDefaultAllowedRoles: newAuthUserDefaultAllowedRoles,
|
||||
},
|
||||
},
|
||||
});
|
||||
props.onClose();
|
||||
triggerToast(`Role "${originalRole.name}" removed successfully`);
|
||||
} catch (updateError) {
|
||||
setError(updateError);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
showModal={showRemoveModal}
|
||||
close={() => setShowRemoveModal(false)}
|
||||
>
|
||||
<div className="px-6 pt-5 text-center text-greyscaleDark">
|
||||
<Text variant="heading" className="mb-2 text-lg font-medium">
|
||||
Remove Role "{originalRole.name}"?
|
||||
</Text>
|
||||
|
||||
<div className="my-4">
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => handleRemove(currentRolesData)}
|
||||
className="w-full"
|
||||
loading={loadingUpdateAppMutation}
|
||||
>
|
||||
Remove Role
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setShowRemoveModal(false)}
|
||||
className="w-full"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<FormProvider {...form}>
|
||||
<CreateUserRoleModalBase
|
||||
title="Edit Role"
|
||||
type="edit"
|
||||
onSubmit={handleSubmit}
|
||||
onRemove={() => setShowRemoveModal(true)}
|
||||
errorComponent={
|
||||
error && (
|
||||
<Alert className="mt-4" severity="error">
|
||||
{error.message}
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</FormProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { useState } from 'react';
|
||||
|
||||
type EnvModalProps = {
|
||||
onSubmit: (props: {
|
||||
name: string;
|
||||
prodValue: string;
|
||||
devValue: string;
|
||||
}) => Promise<void>;
|
||||
name?: string;
|
||||
prodValue?: string;
|
||||
devValue?: string;
|
||||
close: VoidFunction;
|
||||
};
|
||||
|
||||
interface AddEnvVarModalVariablesError {
|
||||
hasError: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const DISABLED_START_ENV_VARIABLES = [
|
||||
'NHOST_',
|
||||
'HASURA_',
|
||||
'AUTH_',
|
||||
'STORAGE_',
|
||||
'POSTGRES_',
|
||||
];
|
||||
|
||||
const DISABLED_ENV_VARIABLES = [
|
||||
'PATH',
|
||||
'NODE_PATH',
|
||||
'PYTHONPATH',
|
||||
'GEM_PATH',
|
||||
'HOSTNAME',
|
||||
'TERM',
|
||||
'NODE_VERSION',
|
||||
'YARN_VERSION',
|
||||
'NODE_ENV',
|
||||
'HOME',
|
||||
];
|
||||
|
||||
export default function AddEnvVarModal({
|
||||
name: externalName,
|
||||
prodValue: externalProdValue,
|
||||
devValue: externalDevValue,
|
||||
close,
|
||||
onSubmit,
|
||||
}: EnvModalProps) {
|
||||
const [name, setName] = useState(externalName || '');
|
||||
const [prodValue, setProdValue] = useState(externalProdValue || '');
|
||||
const [devValue, setDevValue] = useState(externalDevValue || '');
|
||||
const [error, setError] = useState<AddEnvVarModalVariablesError>({
|
||||
hasError: false,
|
||||
message: '',
|
||||
});
|
||||
|
||||
const noError: AddEnvVarModalVariablesError = {
|
||||
hasError: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
||||
setError({ hasError: false, message: '' });
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
DISABLED_START_ENV_VARIABLES.some((envVar) =>
|
||||
name.toUpperCase().startsWith(envVar),
|
||||
)
|
||||
) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The environment variable name cannot start with a value that is reserved for an internal environment variable.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
DISABLED_ENV_VARIABLES.some((envVar) => envVar === name.toUpperCase())
|
||||
) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The environment variable name cannot be a value that is reserved for internal use.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// only allow alphabet characters and underscores
|
||||
const onlyLettersWithNumbersStartsWithLetter = /^[a-zA-Z_]+[a-zA-Z0-9_]*$/;
|
||||
if (!onlyLettersWithNumbersStartsWithLetter.test(name)) {
|
||||
setError({
|
||||
hasError: true,
|
||||
message:
|
||||
'The name contains invalid characters. Only letters, digits, and underscores are allowed. Furthermore, the name should start with a letter.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
setError({ hasError: true, message: 'Variable name is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!prodValue) {
|
||||
setError({ hasError: true, message: 'Production value is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!devValue) {
|
||||
setError({ hasError: true, message: 'Development value is required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
await onSubmit({
|
||||
name,
|
||||
prodValue,
|
||||
devValue,
|
||||
});
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="w-modal px-6 py-6 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<Text variant="h3" component="h2">
|
||||
{name || 'EXAMPLE_NAME'}
|
||||
</Text>
|
||||
|
||||
<Text variant="subtitle2">
|
||||
The default value will be available in all environments, unless you
|
||||
override it. All values are encrypted.
|
||||
</Text>
|
||||
|
||||
<div className="my-2 grid grid-flow-row gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
fullWidth
|
||||
placeholder="EXAMPLE_NAME"
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setName(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="prodValue"
|
||||
label="Production Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={prodValue}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setProdValue(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="devValue"
|
||||
label="Development Value"
|
||||
fullWidth
|
||||
placeholder="Enter a value"
|
||||
value={devValue}
|
||||
onChange={(event) => {
|
||||
setError(noError);
|
||||
setDevValue(event.target.value);
|
||||
}}
|
||||
hideEmptyHelperText
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error.hasError && (
|
||||
<Alert severity="warning" className="mb-2">
|
||||
<Text className="font-medium">Warning</Text>
|
||||
<Text>{error.message}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button type="submit">Add</Button>
|
||||
|
||||
<Button onClick={close} variant="outlined" color="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ function AddPaymentMethodForm({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-6 pb-6 text-left w-modal2">
|
||||
<div className="w-modal2 px-6 pt-6 pb-6 text-left">
|
||||
<div className="flex flex-col">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Text
|
||||
@@ -161,11 +161,11 @@ function AddPaymentMethodForm({
|
||||
variant="body"
|
||||
color="greyscaleDark"
|
||||
size="small"
|
||||
className="font-normal text-center"
|
||||
className="text-center font-normal"
|
||||
>
|
||||
We'll store these in your workspace for future use.
|
||||
</Text>
|
||||
<div className="w-full px-2 py-2 my-2 mt-6 rounded-lg border-1">
|
||||
<div className="my-2 mt-6 w-full rounded-lg border-1 px-2 py-2">
|
||||
<CardElement
|
||||
onReady={(element) => element.focus()}
|
||||
options={{
|
||||
|
||||
@@ -53,7 +53,7 @@ function ControlledCheckbox(
|
||||
name={field.name}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, checked) => {
|
||||
setValue(controllerProps?.name || name, checked);
|
||||
setValue(controllerProps?.name || name, checked, { shouldDirty: true });
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, checked);
|
||||
|
||||
@@ -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';
|
||||
@@ -12,7 +12,13 @@ export type DialogType =
|
||||
| 'CREATE_TABLE'
|
||||
| 'EDIT_TABLE'
|
||||
| 'CREATE_FOREIGN_KEY'
|
||||
| 'EDIT_FOREIGN_KEY';
|
||||
| 'EDIT_FOREIGN_KEY'
|
||||
| 'CREATE_ROLE'
|
||||
| 'EDIT_ROLE'
|
||||
| 'CREATE_PERMISSION_VARIABLE'
|
||||
| 'EDIT_PERMISSION_VARIABLE'
|
||||
| 'CREATE_ENVIRONMENT_VARIABLE'
|
||||
| 'EDIT_ENVIRONMENT_VARIABLE';
|
||||
|
||||
export interface DialogConfig<TPayload = unknown> {
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
import { CreateForeignKeyForm } from '@/components/data-browser/CreateForeignKeyForm';
|
||||
import { EditForeignKeyForm } from '@/components/data-browser/EditForeignKeyForm';
|
||||
import CreateForeignKeyForm from '@/components/data-browser/CreateForeignKeyForm';
|
||||
import EditForeignKeyForm from '@/components/data-browser/EditForeignKeyForm';
|
||||
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
|
||||
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
|
||||
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
|
||||
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
|
||||
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
|
||||
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import AlertDialog from '@/ui/v2/AlertDialog';
|
||||
import { BaseDialog } from '@/ui/v2/Dialog';
|
||||
import Drawer from '@/ui/v2/Drawer';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
|
||||
import type {
|
||||
BaseSyntheticEvent,
|
||||
DetailedHTMLProps,
|
||||
HTMLProps,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import { useCallback, useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import DialogContext from './DialogContext';
|
||||
import {
|
||||
@@ -16,13 +28,21 @@ import {
|
||||
drawerReducer,
|
||||
} from './dialogReducers';
|
||||
|
||||
function LoadingComponent() {
|
||||
function LoadingComponent({
|
||||
className,
|
||||
...props
|
||||
}: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> = {}) {
|
||||
return (
|
||||
<div className="grid items-center justify-center px-6 py-4">
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
'grid items-center justify-center px-6 py-4',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<ActivityIndicator
|
||||
circularProgressProps={{ className: 'w-5 h-5' }}
|
||||
label="Loading form..."
|
||||
delay={500}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -30,27 +50,27 @@ function LoadingComponent() {
|
||||
|
||||
const CreateRecordForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateRecordForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateColumnForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditColumnForm = dynamic(
|
||||
() => import('@/components/data-browser/EditColumnForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const CreateTableForm = dynamic(
|
||||
() => import('@/components/data-browser/CreateTableForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
const EditTableForm = dynamic(
|
||||
() => import('@/components/data-browser/EditTableForm'),
|
||||
{ ssr: false, loading: LoadingComponent },
|
||||
{ ssr: false, loading: () => LoadingComponent() },
|
||||
);
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
@@ -209,6 +229,25 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
[closeDialog, closeDrawer, onDirtyStateChange, openDialog, openDrawer],
|
||||
);
|
||||
|
||||
const sharedDialogProps = {
|
||||
...dialogPayload,
|
||||
onSubmit: async (values: any) => {
|
||||
await dialogPayload?.onSubmit?.(values);
|
||||
|
||||
closeDialog();
|
||||
},
|
||||
onCancel: closeDialogWithDirtyGuard,
|
||||
};
|
||||
|
||||
const sharedDrawerProps = {
|
||||
onSubmit: async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
},
|
||||
onCancel: closeDrawerWithDirtyGuard,
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogContext.Provider value={contextValue}>
|
||||
<AlertDialog
|
||||
@@ -249,33 +288,47 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
open={dialogOpen}
|
||||
onClose={closeDialogWithDirtyGuard}
|
||||
TransitionProps={{ onExited: clearDialogContent, unmountOnExit: false }}
|
||||
PaperProps={{ className: 'max-w-md w-full' }}
|
||||
PaperProps={{
|
||||
...dialogProps?.PaperProps,
|
||||
className: twMerge(
|
||||
'max-w-md w-full',
|
||||
dialogProps?.PaperProps?.className,
|
||||
),
|
||||
}}
|
||||
>
|
||||
<RetryableErrorBoundary
|
||||
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
||||
>
|
||||
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
|
||||
<CreateForeignKeyForm
|
||||
{...dialogPayload}
|
||||
onSubmit={async (values) => {
|
||||
await dialogPayload?.onSubmit(values);
|
||||
|
||||
closeDialog();
|
||||
}}
|
||||
onCancel={closeDialogWithDirtyGuard}
|
||||
/>
|
||||
<CreateForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_FOREIGN_KEY' && (
|
||||
<EditForeignKeyForm
|
||||
{...dialogPayload}
|
||||
onSubmit={async (values) => {
|
||||
await dialogPayload?.onSubmit(values);
|
||||
<EditForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
closeDialog();
|
||||
}}
|
||||
onCancel={closeDialogWithDirtyGuard}
|
||||
/>
|
||||
{activeDialogType === 'CREATE_ROLE' && (
|
||||
<CreateRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_ROLE' && (
|
||||
<EditRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
|
||||
<CreatePermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_PERMISSION_VARIABLE' && (
|
||||
<EditPermissionVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_ENVIRONMENT_VARIABLE' && (
|
||||
<CreateEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
|
||||
<EditEnvironmentVariableForm {...sharedDialogProps} />
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
</BaseDialog>
|
||||
@@ -292,61 +345,34 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<RetryableErrorBoundary>
|
||||
{activeDrawerType === 'CREATE_RECORD' && (
|
||||
<CreateRecordForm
|
||||
{...sharedDrawerProps}
|
||||
columns={drawerPayload?.columns}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_COLUMN' && (
|
||||
<CreateColumnForm
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
<CreateColumnForm {...sharedDrawerProps} />
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_COLUMN' && (
|
||||
<EditColumnForm
|
||||
{...sharedDrawerProps}
|
||||
column={drawerPayload?.column}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_TABLE' && (
|
||||
<CreateTableForm
|
||||
{...sharedDrawerProps}
|
||||
schema={drawerPayload?.schema}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_TABLE' && (
|
||||
<EditTableForm
|
||||
{...sharedDrawerProps}
|
||||
table={drawerPayload?.table}
|
||||
schema={drawerPayload?.schema}
|
||||
onSubmit={async () => {
|
||||
await drawerPayload?.onSubmit();
|
||||
|
||||
closeDrawer();
|
||||
}}
|
||||
onCancel={closeDrawerWithDirtyGuard}
|
||||
/>
|
||||
)}
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
@@ -109,7 +109,7 @@ function FormFooter({
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid justify-between flex-shrink-0 grid-flow-col gap-3 p-2 border-gray-200 border-t-1">
|
||||
<div className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 border-gray-200 p-2">
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
@@ -139,14 +139,14 @@ export default function BaseTableForm({
|
||||
return (
|
||||
<Form
|
||||
onSubmit={handleExternalSubmit}
|
||||
className="flex flex-col content-between flex-auto overflow-hidden border-gray-200 border-t-1"
|
||||
className="flex flex-auto flex-col content-between overflow-hidden border-t-1 border-gray-200"
|
||||
>
|
||||
<div className="flex-auto pb-4 overflow-y-auto">
|
||||
<section className="grid grid-cols-8 px-6 py-3">
|
||||
<div className="flex-auto overflow-y-auto pb-4">
|
||||
<section className="grid grid-cols-8 py-3 px-6">
|
||||
<NameInput />
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-8 px-6 py-3 border-gray-200 border-t-1">
|
||||
<section className="grid grid-cols-8 border-t-1 border-gray-200 py-3 px-6">
|
||||
<h2 className="col-span-8 mt-3 mb-1.5 text-sm+ font-bold text-greyscaleDark">
|
||||
Columns
|
||||
</h2>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function ColumnEditorTable() {
|
||||
return (
|
||||
<>
|
||||
<div role="table" className="col-span-8">
|
||||
<div className="sticky top-0 z-10 grid w-full grid-cols-12 gap-1 pt-1 pb-2 bg-white">
|
||||
<div className="sticky top-0 z-10 grid w-full grid-cols-12 gap-1 bg-white pt-1 pb-2">
|
||||
<div role="columnheader" className="col-span-3">
|
||||
<InputLabel as="span">
|
||||
Name
|
||||
@@ -44,13 +44,13 @@ export default function ColumnEditorTable() {
|
||||
<InputLabel as="span">Default Value</InputLabel>
|
||||
</div>
|
||||
|
||||
<div role="columnheader" className="col-span-1 text-center truncate">
|
||||
<div role="columnheader" className="col-span-1 truncate text-center">
|
||||
<InputLabel as="span" className="truncate">
|
||||
Nullable
|
||||
</InputLabel>
|
||||
</div>
|
||||
|
||||
<div role="columnheader" className="col-span-1 text-center truncate">
|
||||
<div role="columnheader" className="col-span-1 truncate text-center">
|
||||
<InputLabel as="span" className="truncate">
|
||||
Unique
|
||||
</InputLabel>
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function CreateColumnForm({
|
||||
isUnique: false,
|
||||
isIdentity: false,
|
||||
},
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseColumnValidationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface CreateForeignKeyFormProps
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
||||
}
|
||||
|
||||
export function CreateForeignKeyForm({
|
||||
export default function CreateForeignKeyForm({
|
||||
onSubmit,
|
||||
selectedColumn,
|
||||
...props
|
||||
@@ -43,7 +43,7 @@ export function CreateForeignKeyForm({
|
||||
updateAction: 'RESTRICT',
|
||||
deleteAction: 'RESTRICT',
|
||||
},
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseForeignKeyValidationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './CreateForeignKeyForm';
|
||||
export { CreateForeignKeyForm as default } from './CreateForeignKeyForm';
|
||||
export { default } from './CreateForeignKeyForm';
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function CreateRecordForm({
|
||||
|
||||
return { ...defaultValues, [column.id]: null };
|
||||
}, {}),
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ function DataBrowserSidebarContent({
|
||||
const [optimisticlyRemovedTable, setOptimisticlyRemovedTable] =
|
||||
useState<string>();
|
||||
|
||||
const [selectedSchema, setSelectedSchema] = useState<string>();
|
||||
const [selectedSchema, setSelectedSchema] = useState<string>('');
|
||||
const isSelectedSchemaLocked = isSchemaLocked(selectedSchema);
|
||||
|
||||
/**
|
||||
|
||||
@@ -121,7 +121,7 @@ export default function DatabaseRecordInputGroup({
|
||||
|
||||
const InputLabel = (
|
||||
<span className="inline-grid grid-flow-col gap-1">
|
||||
<span className="inline-grid items-center grid-flow-col gap-1">
|
||||
<span className="inline-grid grid-flow-col items-center gap-1">
|
||||
{isPrimary && <KeyIcon className="text-base text-inherit" />}
|
||||
|
||||
<span>{columnId}</span>
|
||||
|
||||
@@ -79,7 +79,7 @@ export default function EditColumnForm({
|
||||
|
||||
const form = useForm<BaseColumnFormValues>({
|
||||
defaultValues: columnValues,
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseColumnValidationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface EditForeignKeyFormProps
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
||||
}
|
||||
|
||||
export function EditForeignKeyForm({
|
||||
export default function EditForeignKeyForm({
|
||||
foreignKeyRelation,
|
||||
selectedColumn,
|
||||
onSubmit,
|
||||
@@ -49,7 +49,7 @@ export function EditForeignKeyForm({
|
||||
updateAction: foreignKeyRelation.updateAction,
|
||||
deleteAction: foreignKeyRelation.deleteAction,
|
||||
},
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseForeignKeyValidationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './EditForeignKeyForm';
|
||||
export { EditForeignKeyForm as default } from './EditForeignKeyForm';
|
||||
export { default } from './EditForeignKeyForm';
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function EditTableForm({
|
||||
identityColumnIndex: null,
|
||||
foreignKeyRelations: [],
|
||||
},
|
||||
reValidateMode: 'onBlur',
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(baseTableValidationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Text } from '@/ui/Text';
|
||||
|
||||
interface ErrorComponentProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
function ErrorComponent({ message }: ErrorComponentProps) {
|
||||
return (
|
||||
<div className="my-4 rounded-md bg-warning px-4 py-2 text-dark">
|
||||
<Text className="font-medium text-textOrange">Error</Text>
|
||||
<Text className="pt-2 font-medium text-dimBlack" size="normal">
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default ErrorComponent;
|
||||
@@ -12,7 +12,7 @@ export function FeedbackReceived({ setFeedbackSent, close }: any) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid items-center grid-flow-row gap-4 text-center">
|
||||
<div className="grid grid-flow-row items-center gap-4 text-center">
|
||||
<Image
|
||||
src="/assets/FeedbackReceived.svg"
|
||||
alt="Light bulb with a checkmark"
|
||||
|
||||
@@ -38,13 +38,13 @@ export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
|
||||
</Text>
|
||||
|
||||
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
|
||||
<div className="grid grid-flow-col gap-2 place-content-between">
|
||||
<div className="grid grid-flow-col place-content-between gap-2">
|
||||
<Text className="font-medium">
|
||||
What do you think we should improve?
|
||||
</Text>
|
||||
|
||||
<Avatar
|
||||
className="w-6 h-6 rounded-full"
|
||||
className="h-6 w-6 rounded-full"
|
||||
name={user?.displayName}
|
||||
avatarUrl={user?.avatarUrl}
|
||||
/>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
function Copy({ stroke = '#21324B', ...props }: any) {
|
||||
return (
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M10.5 10.5h3v-8h-8v3"
|
||||
stroke={stroke}
|
||||
strokeWidth={1.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M10.5 5.5h-8v8h8v-8z"
|
||||
stroke={stroke}
|
||||
strokeWidth={1.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Copy;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function Eye(props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Eye;
|
||||
@@ -1,25 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function EyeOff(
|
||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default EyeOff;
|
||||
@@ -1,14 +0,0 @@
|
||||
export default function Lock(
|
||||
props: JSX.IntrinsicAttributes & React.SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M8 1.75a1.5 1.5 0 0 0-1.5 1.5v1.5h3v-1.5A1.5 1.5 0 0 0 8 1.75Zm-3 1.5v1.5H3c-.69 0-1.25.56-1.25 1.25v7c0 .69.56 1.25 1.25 1.25h10c.69 0 1.25-.56 1.25-1.25V6c0-.69-.56-1.25-1.25-1.25h-2v-1.5a3 3 0 0 0-6 0Zm-1.75 3h9.5v6.5h-9.5v-6.5ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
fill="#21324B"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function Plus(props: any) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
stroke="currentColor"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M16 4.75C9.787 4.75 4.75 9.787 4.75 16S9.787 27.25 16 27.25 27.25 22.213 27.25 16 22.213 4.75 16 4.75zM3.25 16C3.25 8.958 8.958 3.25 16 3.25S28.75 8.958 28.75 16 23.042 28.75 16 28.75 3.25 23.042 3.25 16zm12 .75H10v-1.5h5.25V10h1.5v5.25H22v1.5h-5.25V22h-1.5v-5.25z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Plus;
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function TwilioIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={108}
|
||||
height={32}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M16.864 0c8.844 0 15.984 7.147 15.984 16s-7.14 16-15.984 16C8.019 32 .88 24.853.88 16S8.02 0 16.864 0Zm0 4.267c-6.5 0-11.722 5.226-11.722 11.733a11.694 11.694 0 0 0 11.722 11.733c6.5 0 11.721-5.226 11.721-11.733A11.694 11.694 0 0 0 16.864 4.267Zm27.173 2.026c.213-.106.426.107.426.214v4.586h8.525c.106 0 .32.214.32.32l.639 2.667.639 2.667.107.32.106-.32 1.599-5.334c0-.213.213-.32.32-.32h4.262c.106 0 .32.214.32.32l1.704 5.654.107-.32 1.385-5.334c0-.213.213-.32.32-.32h10.869c.106 0 .213.214.213.32V25.6c0 .213-.213.32-.32.32h-5.434c-.213 0-.32-.213-.32-.32V12.16l-4.05 13.44c0 .187-.162.292-.275.315l-.044.005H60.98c-.107 0-.32-.213-.32-.32l-.853-2.88-.959-3.093L56.93 25.6c0 .213-.213.32-.32.32h-4.475c-.106 0-.32-.213-.32-.32l-4.049-13.44v3.413c0 .214-.213.32-.32.32h-3.09v3.627c0 1.067.533 1.493 1.492 1.493.426 0 .96-.106 1.492-.32.106-.106.32 0 .32.214v4.266c-.96.534-2.345.854-3.836.854-3.517 0-5.435-1.6-5.435-5.12v-5.014h-1.385c-.213 0-.32-.213-.32-.32V11.52c0-.213.213-.32.32-.32h1.385V8.32c0-.213.106-.32.32-.32l5.328-1.707Zm54.984 4.48c4.689 0 8.099 3.52 8.099 7.574 0 4.053-3.41 7.573-8.205 7.573-4.689 0-8.099-3.52-8.099-7.573 0-4.054 3.41-7.574 8.205-7.574Zm-16.197-4.48c.213 0 .32.107.32.214v18.986c0 .214-.213.32-.32.32H77.39c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.434Zm7.14 4.8c.213 0 .32.214.32.32v13.974c0 .213-.214.32-.32.32h-5.435c-.213 0-.32-.214-.32-.32V11.413c0-.213.214-.32.32-.32h5.435ZM12.92 16.64a3.322 3.322 0 0 1 3.303 3.307 3.322 3.322 0 0 1-3.303 3.306 3.322 3.322 0 0 1-3.303-3.306 3.322 3.322 0 0 1 3.303-3.307Zm7.886 0a3.322 3.322 0 0 1 3.303 3.307 3.322 3.322 0 0 1-3.303 3.306 3.322 3.322 0 0 1-3.304-3.306 3.322 3.322 0 0 1 3.304-3.307Zm78.214-.747c-1.385 0-2.344 1.174-2.344 2.56 0 1.387 1.066 2.56 2.344 2.56 1.386 0 2.345-1.173 2.345-2.56 0-1.493-1.066-2.666-2.345-2.56ZM20.807 8.747a3.322 3.322 0 0 1 3.303 3.306 3.322 3.322 0 0 1-3.303 3.307 3.322 3.322 0 0 1-3.304-3.307 3.322 3.322 0 0 1 3.304-3.306Zm-7.886 0a3.322 3.322 0 0 1 3.303 3.306 3.322 3.322 0 0 1-3.303 3.307 3.322 3.322 0 0 1-3.303-3.307 3.322 3.322 0 0 1 3.303-3.306Zm62.87-2.454c.107 0 .213.107.32.214V9.92c0 .213-.213.32-.32.32h-5.647c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.647Zm14.28 0c.213 0 .319.107.319.214V9.92c0 .213-.213.32-.32.32h-5.647c-.213 0-.32-.213-.32-.32V6.613c0-.213.213-.32.32-.32h5.647Z"
|
||||
fill="#F22F46"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default TwilioIcon;
|
||||
@@ -4,19 +4,19 @@ import { twMerge } from 'tailwind-merge';
|
||||
export interface ContainerProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {
|
||||
/**
|
||||
* Class name passed to the wrapper element.
|
||||
* Class name passed to the root element.
|
||||
*/
|
||||
wrapperClassName?: string;
|
||||
rootClassName?: string;
|
||||
}
|
||||
|
||||
export default function Container({
|
||||
children,
|
||||
className,
|
||||
wrapperClassName,
|
||||
rootClassName,
|
||||
...props
|
||||
}: ContainerProps) {
|
||||
return (
|
||||
<div className={twMerge('mx-auto w-full bg-white', wrapperClassName)}>
|
||||
<div className={twMerge('mx-auto w-full bg-white', rootClassName)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
'mx-auto max-w-7xl bg-white px-5 pt-6 pb-20',
|
||||
|
||||
@@ -55,7 +55,7 @@ function LogsTimePicker({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid items-center self-center grid-flow-row mx-auto">
|
||||
<div className="mx-auto grid grid-flow-row items-center self-center">
|
||||
<div className="border border-[#EAEDF0] px-4 py-2">
|
||||
<Input
|
||||
value={format(selectedDate, 'HH:mm:ss')}
|
||||
@@ -85,7 +85,7 @@ function LogsTimePicker({
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid justify-end grid-flow-col px-4 py-2 gap-x-4">
|
||||
<div className="grid grid-flow-col justify-end gap-x-4 px-4 py-2">
|
||||
<Button variant="outlined" color="secondary" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -42,7 +42,7 @@ export default function OverviewMigration() {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row mt-6 rounded-lg place-content-between">
|
||||
<div className="mt-6 flex flex-row place-content-between rounded-lg">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
@@ -64,9 +64,9 @@ export default function OverviewMigration() {
|
||||
<div className="grid grid-rows-3 gap-4">
|
||||
{migrationSteps.map((step, index) => (
|
||||
<div key={step.title} className="col-span-1">
|
||||
<div className="flex flex-row gap-3 h-11">
|
||||
<div className="flex h-11 flex-row gap-3">
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-col items-center self-center justify-center w-8 h-8 font-semibold align-middle rounded-md bg-veryLightGray">
|
||||
<div className="flex h-8 w-8 flex-col items-center justify-center self-center rounded-md bg-veryLightGray align-middle font-semibold">
|
||||
<span className="text-[15px] font-semibold leading-[22px] text-greyscaleGreyDark">
|
||||
{index + 1}
|
||||
</span>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user