Compare commits

..

117 Commits

Author SHA1 Message Date
github-actions[bot]
6cec04bd6f chore: update versions (#2680)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.14.0

### Minor Changes

-   a448d7d: feat: allow configuring postmark and delete SMTP settings

## @nhost/docs@2.10.2

### Patch Changes

-   9480489: fix: update docs performance info

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-25 13:13:32 +01:00
David Barroso
a448d7d182 feat (dashboard): allow configuring postmark and deleting SMTP settings (#2678)
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-04-25 12:58:47 +01:00
David Barroso
948048940e fix (docs): update docs performance info (#2679)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-04-25 13:50:45 +02:00
github-actions[bot]
5e91221d5a chore: update versions (#2672)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.3

### Patch Changes

-   5924bc3: fix: include password in `GetSmtpSettings` query
- c5ad634: fix: resolved an issue where one-click install links were
broken on Safari
- 7278991: fix: update graphql auto-embeddings configuration to use
String type for model field

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-18 15:49:01 +01:00
Hassan Ben Jobrane
7278991a59 fix: dashboard: update graphql auto-embeddings configuration to use String type for model field (#2674) 2024-04-18 15:22:32 +01:00
Hassan Ben Jobrane
5924bc3248 fix: dashboard: include password in GetSmtpSettings query (#2673) 2024-04-18 15:14:02 +01:00
Hassan Ben Jobrane
c5ad634799 fix: dashboard: check for undefined router query on Safari when accessing base64config (#2671) 2024-04-18 10:04:18 +01:00
github-actions[bot]
426b93a19f chore: update versions (#2670)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.2

### Patch Changes

-   026f84f: fix: use configuration server URL from environment variable

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-17 14:15:04 +01:00
Hassan Ben Jobrane
026f84f466 fix: dashboard: use config server url from env variable (#2669)
related to https://github.com/nhost/cli/issues/862
2024-04-17 14:02:11 +01:00
github-actions[bot]
384fac00b1 chore: update versions (#2664)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.1

### Patch Changes

-   7e9a2ce: fix: resolve issue where run services form fails to open

## @nhost/docs@2.10.1

### Patch Changes

-   9525fd7: fix: update AI docs for 0.5.0
-   076fd4a: fix: update permissions page to indicate we use jsonpath

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-16 10:53:36 +01:00
Hassan Ben Jobrane
7e9a2ce136 fix: dashboard: fix isPlatform check when opening run services form (#2666) 2024-04-16 10:38:28 +01:00
David Barroso
076fd4a7c0 fix (docs): update permissions page to indicate we use jsonpath (#2663) 2024-04-15 14:50:21 +02:00
David Barroso
9525fd74b3 fix (docs): update AI docs for 0.5.0 (#2662) 2024-04-15 14:33:59 +02:00
github-actions[bot]
8a2bc98214 chore: update versions (#2648)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.0

### Minor Changes

-   dd5d262: feat: add model field to the auto-embeddings form
- 09962be: feat: enable settings and run services when running the
dashboard locally
- 9cdecb6: feat: enable users to update their email address from the
account settings page

## @nhost/docs@2.10.0

### Minor Changes

-   87ae23b: feat: added "advanced graphql" documentation

### Patch Changes

-   b2be364: feat: added postmark native integration

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-15 13:12:24 +01:00
Hassan Ben Jobrane
dd5d262062 feat: dashboard: add model field to auto-embeddings form (#2661)
fixes https://github.com/nhost/nhost/issues/2660
2024-04-15 12:51:57 +01:00
Hassan Ben Jobrane
09962bef37 feat: dashboard: enable local settings (#2647)
fixes https://github.com/nhost/projects/issues/66
2024-04-15 12:49:20 +01:00
Hassan Ben Jobrane
9cdecb6b23 feat: dashboard: add email field to account settings (#2612)
fixes https://github.com/nhost/nhost/issues/2561
2024-04-11 10:47:03 +01:00
David Barroso
e7eb90318e fix: observability: amend export of graphql dashboard (#2655) 2024-04-11 11:44:56 +02:00
Hassan Ben Jobrane
f67f22d321 chore: update @google-cloud/translate to 8.2.0 (#2654) 2024-04-11 10:00:13 +01:00
David Barroso
87ae23ba05 feat (docs/observability): added docs and observability dashboard for "advanced graphql" (#2653)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-04-11 10:22:54 +02:00
David Barroso
b2be3642aa feat (docs): added postmark native integration (#2636) 2024-04-09 09:36:48 +02:00
github-actions[bot]
1230081ce6 chore: update versions (#2640)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.12.2

### Patch Changes

-   c195c51: fix: send email upon signin for unverified users

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-05 12:48:55 +01:00
Hassan Ben Jobrane
c195c517de fix: send email upon signin for unverified users (#2639) 2024-04-05 11:48:14 +01:00
github-actions[bot]
6f419be2c1 chore: update versions (#2634)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost/react-apollo@11.0.1

### Patch Changes

-   @nhost/apollo@6.2.1
-   @nhost/react@3.4.1

## @nhost/react-urql@8.0.1

### Patch Changes

-   @nhost/react@3.4.1

## @nhost/hasura-auth-js@2.4.1

### Patch Changes

-   bcd889b: fix: change expiresAt format to RFC3339 in createPATPromise

## @nhost/nextjs@2.1.10

### Patch Changes

-   @nhost/react@3.4.1

## @nhost/nhost-js@3.0.11

### Patch Changes

-   Updated dependencies [bcd889b]
    -   @nhost/hasura-auth-js@2.4.1

## @nhost/react@3.4.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost/vue@2.5.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost/docs@2.9.0

### Minor Changes

-   3c31657: chore: update docs with provider connect

### Patch Changes

-   992939c: feat: added social connect docs

## @nhost/dashboard@1.12.1

### Patch Changes

- 93ebdf8: fix: use service urls when initilizaing NhostClient running
local dashboard
    -   @nhost/react-apollo@11.0.1
    -   @nhost/nextjs@2.1.10

## @nhost-examples/cli@0.3.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost-examples/codegen-react-apollo@0.4.1

### Patch Changes

-   @nhost/react@3.4.1
-   @nhost/react-apollo@11.0.1

## @nhost-examples/codegen-react-query@0.4.1

### Patch Changes

-   @nhost/react@3.4.1

## @nhost-examples/codegen-react-urql@0.3.1

### Patch Changes

-   @nhost/react@3.4.1
-   @nhost/react-urql@8.0.1

## @nhost-examples/multi-tenant-one-to-many@2.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost-examples/nextjs@0.3.1

### Patch Changes

-   @nhost/react@3.4.1
-   @nhost/react-apollo@11.0.1
-   @nhost/nextjs@2.1.10

## @nhost-examples/node-storage@0.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost-examples/nextjs-server-components@0.4.1

### Patch Changes

-   @nhost/nhost-js@3.0.11

## @nhost-examples/react-apollo@0.8.1

### Patch Changes

-   @nhost/react@3.4.1
-   @nhost/react-apollo@11.0.1

## @nhost-examples/react-gqty@1.2.1

### Patch Changes

-   @nhost/react@3.4.1

## @nhost-examples/vue-apollo@0.6.1

### Patch Changes

-   @nhost/nhost-js@3.0.11
-   @nhost/apollo@6.2.1
-   @nhost/vue@2.5.1

## @nhost-examples/vue-quickstart@0.2.1

### Patch Changes

-   @nhost/apollo@6.2.1
-   @nhost/vue@2.5.1

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-04 10:49:02 +01:00
Hassan Ben Jobrane
93ebdf844f fix: dashboard: use service urls when running locally with the cli (#2622) 2024-04-04 10:28:38 +01:00
Hassan Ben Jobrane
bcd889b53a fix: hasura-auth-js: use RFC3339 format for expiresAt when creating a PAT (#2637) 2024-04-03 21:40:02 +01:00
David Barroso
992939cdcd feat (docs): added social connect docs (#2633)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-04-03 16:37:07 +02:00
Nuno Pato
3c31657c50 chore: update docs with provider connect (#2632)
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2024-04-03 14:27:49 +00:00
github-actions[bot]
a654d536e0 chore: update versions (#2618)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.2.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost/google-translation@0.2.0

### Minor Changes

-   768ca17: chore: update dependencies

## @nhost/react-apollo@11.0.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/apollo@6.2.0
    -   @nhost/react@3.4.0

## @nhost/react-urql@8.0.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react@3.4.0

## @nhost/stripe-graphql-js@1.2.0

### Minor Changes

-   768ca17: chore: update dependencies

## @nhost/react@3.4.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost/vue@2.5.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost/nextjs@2.1.9

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react@3.4.0

## @nhost/dashboard@1.12.0

### Minor Changes

- f242e4b: feat: add connect with github to the user's account settings
-   768ca17: chore: update dependencies
- d62bd0f: fix: "Track this" option within the SQL editor now correctly
updates the metadata
- 91c2bb6: feat: refactor sign-in and sign-up pages to enforce email
verification

### Patch Changes

-   943831f: fix: resolve an error toast issue when unpausing a project
-   Updated dependencies [768ca17]
    -   @nhost/react-apollo@11.0.0
    -   @nhost/nextjs@2.1.9

## @nhost/docs@2.8.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   9f2bf9e: chore: added hasura's authHook settings

## @nhost-examples/cli@0.3.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/codegen-react-apollo@0.4.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react-apollo@11.0.0
    -   @nhost/react@3.4.0

## @nhost-examples/codegen-react-query@0.4.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react@3.4.0

## @nhost-examples/codegen-react-urql@0.3.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react-urql@8.0.0
    -   @nhost/react@3.4.0

## @nhost-examples/docker-compose@0.4.0

### Minor Changes

-   768ca17: chore: update dependencies

## @nhost-examples/multi-tenant-one-to-many@2.2.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/nextjs@0.3.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react-apollo@11.0.0
    -   @nhost/react@3.4.0
    -   @nhost/nextjs@2.1.9

## @nhost-examples/node-storage@0.2.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/nextjs-server-components@0.4.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/sveltekit@0.4.0

### Minor Changes

-   768ca17: chore: update dependencies

## @nhost-examples/react-apollo@0.8.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   863b37d: chore: bump hasura-auth
-   Updated dependencies [768ca17]
    -   @nhost/react-apollo@11.0.0
    -   @nhost/react@3.4.0

## @nhost-examples/react-gqty@1.2.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/react@3.4.0

## @nhost-examples/serverless-functions@0.2.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/stripe-graphql-js@1.2.0

## @nhost-examples/vue-apollo@0.6.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/apollo@6.2.0
    -   @nhost/vue@2.5.0
    -   @nhost/nhost-js@3.0.10

## @nhost-examples/vue-quickstart@0.2.0

### Minor Changes

-   768ca17: chore: update dependencies

### Patch Changes

-   Updated dependencies [768ca17]
    -   @nhost/apollo@6.2.0
    -   @nhost/vue@2.5.0

## @nhost/docgen@0.3.0

### Minor Changes

-   768ca17: chore: update dependencies

## @nhost/sync-versions@0.2.0

### Minor Changes

-   768ca17: chore: update dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-04-03 13:29:25 +01:00
Hassan Ben Jobrane
91c2bb6f53 feat: dashboard: restrict signup/signin to verified emails only (#2610)
resolves https://github.com/nhost/nhost/issues/2585
2024-04-03 12:52:47 +01:00
David Barroso
9f2bf9ec2b chore (docs): added hasura's authHook settings (#2630) 2024-04-03 09:01:28 +02:00
Nuno Pato
d62bd0fc9a fix: dashboard: SQL editor "track this" (#2626)
fixes https://github.com/nhost/nhost/issues/2625

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-04-02 16:50:26 +00:00
github-actions[bot]
768ca17494 [Scheduled] Update dependencies (#2627)
Dependencies updated

Note - If you see this PR and the checks haven't run, close and reopen
the PR. See
https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs

---------

Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-04-02 16:44:24 +01:00
Hassan Ben Jobrane
943831fe2e fix: await unpauseApplication promise (#2621)
fixes https://github.com/nhost/nhost/issues/2620
2024-03-26 09:29:10 +01:00
Hassan Ben Jobrane
f242e4b92f feat: connect with github (#2616)
fixes https://github.com/nhost/nhost/issues/2581
2024-03-25 13:22:15 +01:00
David Barroso
863b37d313 chore (examples/apollo): bump hasura-auth (#2617) 2024-03-23 12:07:22 +01:00
github-actions[bot]
c8a8d4fca3 chore: update versions (#2609)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/hasura-auth-js@2.4.0

### Minor Changes

- 311374e: fix: ensure that the user remains signed in even after being
redirected with an error following an attempt to connect with a social
provider

### Patch Changes

-   1623e9b: chore: update `@simplewebauthn/browser` to `9.0.1`

## @nhost/vue@2.4.0

### Minor Changes

-   311374e: feat: add `connect` param to `useProviderLink` hook

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost/apollo@6.1.2

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost/react-apollo@10.0.2

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2
    -   @nhost/apollo@6.1.2

## @nhost/react-urql@7.0.2

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2

## @nhost/nextjs@2.1.8

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2

## @nhost/nhost-js@3.0.10

### Patch Changes

-   Updated dependencies [1623e9b]
-   Updated dependencies [311374e]
    -   @nhost/hasura-auth-js@2.4.0

## @nhost/react@3.3.2

### Patch Changes

-   311374e: feat: add `connect` param to `useProviderLink` hook
    -   @nhost/nhost-js@3.0.10

## @nhost-examples/docker-compose@0.3.0

### Minor Changes

- e40a452: chore: clarification on greyed-out options in the dashboard
when self-hosting

## @nhost-examples/react-apollo@0.7.0

### Minor Changes

- 311374e: feat: add example of how to connect a social auth provider to
an existing account

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2
    -   @nhost/react-apollo@10.0.2

## @nhost-examples/vue-apollo@0.5.0

### Minor Changes

- 311374e: feat: add example of how to connect a social auth provider to
an existing account

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/vue@2.4.0
    -   @nhost/nhost-js@3.0.10
    -   @nhost/apollo@6.1.2

## @nhost/dashboard@1.11.2

### Patch Changes

-   @nhost/react-apollo@10.0.2
-   @nhost/nextjs@2.1.8

## @nhost/docs@2.7.2

### Patch Changes

-   5c47e8e: feat: added hasura's stringifyNumericTypes setting

## @nhost-examples/cli@0.2.2

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/codegen-react-apollo@0.3.1

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2
    -   @nhost/react-apollo@10.0.2

## @nhost-examples/codegen-react-query@0.3.1

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2

## @nhost-examples/codegen-react-urql@0.2.1

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2
    -   @nhost/react-urql@7.0.2

## @nhost-examples/multi-tenant-one-to-many@2.1.2

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/nextjs@0.2.2

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2
    -   @nhost/react-apollo@10.0.2
    -   @nhost/nextjs@2.1.8

## @nhost-examples/node-storage@0.1.2

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/nextjs-server-components@0.3.2

### Patch Changes

-   @nhost/nhost-js@3.0.10

## @nhost-examples/react-gqty@1.1.2

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/react@3.3.2

## @nhost-examples/vue-quickstart@0.1.2

### Patch Changes

-   Updated dependencies [311374e]
    -   @nhost/vue@2.4.0
    -   @nhost/apollo@6.1.2

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-21 17:54:02 +01:00
Hassan Ben Jobrane
311374e3fb feat: react-apollo: add example of how to connect github to an existing account (#2615)
fixes https://github.com/nhost/nhost/issues/2582
2024-03-21 17:38:54 +01:00
Seth Deegan
e40a4529b4 chore (examples/docker-compose): clarification on greyed-out options in the dashboard when self-hosting (#2564) 2024-03-18 11:03:10 +01:00
Hassan Ben Jobrane
1623e9bd20 chore: hasura-auth-js: upgrade @simplewebauthn/browser to 9.0.1 (#2611)
fixes https://github.com/nhost/nhost/issues/2597
2024-03-13 17:25:29 +01:00
David Barroso
5c47e8e675 feat (docs): added hasura's stringifyNumericTypes setting (#2608) 2024-03-12 11:29:53 +01:00
github-actions[bot]
9f9f1c64f4 chore: update versions (#2605)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.11.1

### Patch Changes

-   981404f: fix: set default value for healthCheck field validation

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-11 11:02:17 +01:00
Hassan Ben Jobrane
981404f0b9 fix(dashboard): set default value for healthCheck field validation (#2604) 2024-03-11 10:45:33 +01:00
github-actions[bot]
4ad27e9d72 chore: update versions (#2599)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.1.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost/react-apollo@10.0.1

### Patch Changes

-   @nhost/apollo@6.1.1
-   @nhost/react@3.3.1

## @nhost/react-urql@7.0.1

### Patch Changes

-   @nhost/react@3.3.1

## @nhost/stripe-graphql-js@1.1.1

### Patch Changes

-   7789469: chore: address linter errors and remove unnecessary imports

## @nhost/graphql-js@0.1.9

### Patch Changes

- 7789469: fix: resolve process is undefined error when running with
vitejs

## @nhost/nextjs@2.1.7

### Patch Changes

-   @nhost/react@3.3.1

## @nhost/nhost-js@3.0.9

### Patch Changes

-   Updated dependencies [7789469]
    -   @nhost/graphql-js@0.1.9

## @nhost/react@3.3.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost/vue@2.3.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost/dashboard@1.11.0

### Minor Changes

- 7789469: chore: upgrade dependency `@graphql-codegen/cli` to `5.0.2`
to address vulnerability
- 6c11b75: feat: add update user displayName section in account settings

### Patch Changes

-   @nhost/react-apollo@10.0.1
-   @nhost/nextjs@2.1.7

## @nhost-examples/codegen-react-apollo@0.3.0

### Minor Changes

- 7789469: chore: upgrade dependency `@graphql-codegen/cli` to `5.0.2`
to address vulnerability

### Patch Changes

-   @nhost/react@3.3.1
-   @nhost/react-apollo@10.0.1

## @nhost-examples/codegen-react-query@0.3.0

### Minor Changes

- 7789469: chore: upgrade dependency `@graphql-codegen/cli` to `5.0.2`
to address vulnerability

### Patch Changes

-   @nhost/react@3.3.1

## @nhost-examples/codegen-react-urql@0.2.0

### Minor Changes

- 7789469: chore: upgrade dependency `@graphql-codegen/cli` to `5.0.2`
to address vulnerability

### Patch Changes

-   @nhost/react@3.3.1
-   @nhost/react-urql@7.0.1

## @nhost-examples/react-apollo@0.6.0

### Minor Changes

- 7789469: chore: upgrade dependency `@graphql-codegen/cli` to `5.0.2`
to address vulnerability

### Patch Changes

-   @nhost/react@3.3.1
-   @nhost/react-apollo@10.0.1

## @nhost-examples/cli@0.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost-examples/multi-tenant-one-to-many@2.1.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost-examples/nextjs@0.2.1

### Patch Changes

-   @nhost/react@3.3.1
-   @nhost/react-apollo@10.0.1
-   @nhost/nextjs@2.1.7

## @nhost-examples/node-storage@0.1.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost-examples/nextjs-server-components@0.3.1

### Patch Changes

-   @nhost/nhost-js@3.0.9

## @nhost-examples/react-gqty@1.1.1

### Patch Changes

-   @nhost/react@3.3.1

## @nhost-examples/serverless-functions@0.1.1

### Patch Changes

-   Updated dependencies [7789469]
    -   @nhost/stripe-graphql-js@1.1.1

## @nhost-examples/vue-apollo@0.4.1

### Patch Changes

-   7789469: chore: address linter errors and remove unnecessary imports
    -   @nhost/nhost-js@3.0.9
    -   @nhost/apollo@6.1.1
    -   @nhost/vue@2.3.1

## @nhost-examples/vue-quickstart@0.1.1

### Patch Changes

-   @nhost/apollo@6.1.1
-   @nhost/vue@2.3.1

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-08 15:51:16 +01:00
Hassan Ben Jobrane
778946998a fix: graphql-js: resolve process is undefined error when running with vitejs (#2601)
fixes https://github.com/nhost/nhost/issues/2600
2024-03-08 15:23:13 +01:00
Hassan Ben Jobrane
6c11b75807 feat: dashboard: add update user displayName section in account settings (#2598)
fixes https://github.com/nhost/nhost/issues/1489
2024-03-07 17:23:35 +01:00
github-actions[bot]
2dc031d16c chore: update versions (#2592)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost-examples/react-apollo@0.5.0

### Minor Changes

- 08a7dd9: feat: add example workaround to the reset password ticket
expired issue

### Patch Changes

- f0a994a: fix: update allowedUrls and redirectTo to point to the
profile page

## @nhost-examples/vue-apollo@0.4.0

### Minor Changes

- 08a7dd9: feat: add example workaround to the reset password ticket
expired issue

### Patch Changes

- f0a994a: fix: update allowedUrls and redirectTo to point to the
profile page

## @nhost/docs@2.7.1

### Patch Changes

-   6cb2b63: feat: added nhost run env documentation
-   40bd3e4: fix: fixed wrong links in documentation

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-06 16:56:26 +01:00
Nicolas Bourdin
40bd3e4572 doc(link): fix wrong links in documentation (#2596)
Co-authored-by: Nicolas Bourdin <nicolas@epeak.co>
Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2024-03-06 14:50:32 +01:00
David Barroso
6cb2b6331a feat (docs): added nhost run env documentation (#2594) 2024-03-06 14:13:02 +01:00
Hassan Ben Jobrane
08a7dd9894 feat(examples): add reset password ticket expired workarounds in the examples (#2590)
fixes https://github.com/nhost/nhost/issues/2314
2024-03-05 15:48:46 +01:00
Hassan Ben Jobrane
f0a994a26e fix(examples): update allowedUrls and redirectTo to point to /profile (#2591) 2024-03-05 14:17:01 +01:00
Hassan Ben Jobrane
4fbd6bd4fa chore: fix release with missing changeset (#2588)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-04 16:51:57 +01:00
github-actions[bot]
67fc77486c chore: update versions (#2578)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost/google-translation@0.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

## @nhost/react-apollo@10.0.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/apollo@6.1.0
    -   @nhost/react@3.3.0

## @nhost/react-urql@7.0.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react@3.3.0

## @nhost/stripe-graphql-js@1.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

## @nhost/react@3.3.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost/vue@2.3.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost/nextjs@2.1.6

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react@3.3.0

## @nhost/dashboard@1.10.0

### Minor Changes

-   49a80c2: chore: update dependencies
-   150c04a: feat: add healthcheck config to run services

### Patch Changes

- e03f141: fix: allow insert, update and delete on tables in `auth` and
`storage` schemas
- 28676f4: feat: add min postgres version check to enable the ai service
-   Updated dependencies [49a80c2]
    -   @nhost/react-apollo@10.0.0
    -   @nhost/nextjs@2.1.6

## @nhost/docs@2.7.0

### Minor Changes

-   49a80c2: chore: update dependencies

## @nhost-examples/cli@0.2.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/codegen-react-apollo@0.2.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react-apollo@10.0.0
    -   @nhost/react@3.3.0

## @nhost-examples/codegen-react-query@0.2.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react@3.3.0

## @nhost-examples/codegen-react-urql@0.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react-urql@7.0.0
    -   @nhost/react@3.3.0

## @nhost-examples/docker-compose@0.2.0

### Minor Changes

-   49a80c2: chore: update dependencies

## @nhost-examples/multi-tenant-one-to-many@2.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/nextjs@0.2.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react-apollo@10.0.0
    -   @nhost/react@3.3.0
    -   @nhost/nextjs@2.1.6

## @nhost-examples/node-storage@0.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/nextjs-server-components@0.3.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/sveltekit@0.3.0

### Minor Changes

-   49a80c2: chore: update dependencies

## @nhost-examples/react-apollo@0.4.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

- 4f3fb34: fix: set redirectTo when doing sign in with github and
include vercel previews in allowed redirect URLs
-   Updated dependencies [49a80c2]
    -   @nhost/react-apollo@10.0.0
    -   @nhost/react@3.3.0

## @nhost-examples/react-gqty@1.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/react@3.3.0

## @nhost-examples/serverless-functions@0.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/stripe-graphql-js@1.1.0

## @nhost-examples/vue-apollo@0.3.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

- 4f3fb34: fix: set redirectTo when doing sign in with github and
include vercel previews in allowed redirect URLs
-   Updated dependencies [49a80c2]
    -   @nhost/apollo@6.1.0
    -   @nhost/vue@2.3.0
    -   @nhost/nhost-js@3.0.8

## @nhost-examples/vue-quickstart@0.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

### Patch Changes

-   Updated dependencies [49a80c2]
    -   @nhost/apollo@6.1.0
    -   @nhost/vue@2.3.0

## @nhost/docgen@0.2.0

### Minor Changes

-   49a80c2: chore: update dependencies

## @nhost/sync-versions@0.1.0

### Minor Changes

-   49a80c2: chore: update dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-03-04 16:16:06 +01:00
Hassan Ben Jobrane
4f3fb3446e fix: set redirectTo in oauth examples (#2586) 2024-03-04 15:33:24 +01:00
Hassan Ben Jobrane
49a80c22be chore: add changeset (#2584)
adds missing changeset for this PR
https://github.com/nhost/nhost/pull/2574
2024-03-04 13:52:38 +01:00
Hassan Ben Jobrane
28676f4cdc feat: dashboard: add min postgres version check to enable the ai service (#2576)
fixes https://github.com/nhost/nhost/issues/2439
2024-03-02 22:33:49 +01:00
Hassan Ben Jobrane
e03f14133c fix: dashboard: refactor database datagrid to allow insert/update/delete for tables auth and storage (#2577)
fixes https://github.com/nhost/nhost/issues/2476
2024-03-02 22:04:23 +01:00
Hassan Ben Jobrane
150c04a4f4 feat: dashboard: add healthcheck config to run services (#2575)
fixes https://github.com/nhost/nhost/issues/2410
2024-03-02 19:28:13 +01:00
github-actions[bot]
bccd67b1b1 [Scheduled] Update dependencies (#2574)
Dependencies updated

Note - If you see this PR and the checks haven't run, close and reopen
the PR. See
https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-03-02 18:40:03 +01:00
David Barroso
b14fd2f14c chore: fix role to assume in gen update dependencies (#2573) 2024-03-01 12:21:38 +01:00
github-actions[bot]
68b3d23cd9 chore: update versions (#2572)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.9.0

### Minor Changes

-   d86e5c9: feat: add support for filtering the logs using a RegExp

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-29 17:54:02 +01:00
Hassan Ben Jobrane
d86e5c9c16 feat(dashboard): query services list from be and filter the logs using a regex (#2552)
fixes: https://github.com/nhost/nhost/issues/2391
2024-02-29 17:38:38 +01:00
github-actions[bot]
b2cc1411d7 chore: update versions (#2571)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.0.8

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost/react-apollo@9.0.3

### Patch Changes

-   @nhost/apollo@6.0.8
-   @nhost/react@3.2.3

## @nhost/react-urql@6.0.3

### Patch Changes

-   @nhost/react@3.2.3

## @nhost/graphql-js@0.1.8

### Patch Changes

- 407feea: fix: replace `jwt-decode` with `jose` to decode access tokens
in a non browser environment

## @nhost/nextjs@2.1.5

### Patch Changes

-   @nhost/react@3.2.3

## @nhost/nhost-js@3.0.8

### Patch Changes

-   Updated dependencies [407feea]
    -   @nhost/graphql-js@0.1.8

## @nhost/react@3.2.3

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost/vue@2.2.3

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost/dashboard@1.8.3

### Patch Changes

-   @nhost/react-apollo@9.0.3
-   @nhost/nextjs@2.1.5

## @nhost-examples/cli@0.1.9

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/codegen-react-apollo@0.1.17

### Patch Changes

-   @nhost/react@3.2.3
-   @nhost/react-apollo@9.0.3

## @nhost-examples/codegen-react-query@0.1.18

### Patch Changes

-   @nhost/react@3.2.3

## @nhost-examples/codegen-react-urql@0.0.14

### Patch Changes

-   @nhost/react@3.2.3
-   @nhost/react-urql@6.0.3

## @nhost-examples/multi-tenant-one-to-many@2.0.7

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/nextjs@0.1.19

### Patch Changes

-   @nhost/react@3.2.3
-   @nhost/react-apollo@9.0.3
-   @nhost/nextjs@2.1.5

## @nhost-examples/node-storage@0.0.11

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/nextjs-server-components@0.2.5

### Patch Changes

-   @nhost/nhost-js@3.0.8

## @nhost-examples/react-apollo@0.3.3

### Patch Changes

-   @nhost/react@3.2.3
-   @nhost/react-apollo@9.0.3

## @nhost-examples/react-gqty@1.0.7

### Patch Changes

-   @nhost/react@3.2.3

## @nhost-examples/vue-apollo@0.2.4

### Patch Changes

-   @nhost/nhost-js@3.0.8
-   @nhost/apollo@6.0.8
-   @nhost/vue@2.2.3

## @nhost-examples/vue-quickstart@0.0.16

### Patch Changes

-   @nhost/apollo@6.0.8
-   @nhost/vue@2.2.3

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-29 15:43:48 +01:00
Hassan Ben Jobrane
407feeac37 fix: sdk: graphql-js: replace jwt-decode with jose to decode access tokens in both node and the browser (#2570) 2024-02-29 14:51:33 +01:00
github-actions[bot]
7b25c37c26 chore: update versions (#2569)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.8.2

### Patch Changes

- 6df4f02: fix: use custom error toast and show correct message when
sending an invite

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-29 13:10:09 +01:00
Hassan Ben Jobrane
6df4f02e95 fix(dashboard): show correct message when sending invite fails (#2567) 2024-02-29 12:52:25 +01:00
github-actions[bot]
aaae98f019 chore: update versions (#2559)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/docs@2.6.0

### Minor Changes

-   dc23dc0: fix: docs run references

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-25 23:02:36 -01:00
Nuno Pato
dc23dc0f49 fix: docs run references (#2558) 2024-02-25 22:47:38 -01:00
github-actions[bot]
82728da100 chore: update versions (#2556)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.0.7

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost/react-apollo@9.0.2

### Patch Changes

-   @nhost/apollo@6.0.7
-   @nhost/react@3.2.2

## @nhost/react-urql@6.0.2

### Patch Changes

-   @nhost/react@3.2.2

## @nhost/graphql-js@0.1.7

### Patch Changes

- 2d68fee: fix: resolve an issue where unauthenticated graphql requests
are not sent

## @nhost/nextjs@2.1.4

### Patch Changes

-   @nhost/react@3.2.2

## @nhost/nhost-js@3.0.7

### Patch Changes

-   Updated dependencies [2d68fee]
    -   @nhost/graphql-js@0.1.7

## @nhost/react@3.2.2

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost/vue@2.2.2

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost/dashboard@1.8.1

### Patch Changes

-   @nhost/react-apollo@9.0.2
-   @nhost/nextjs@2.1.4

## @nhost-examples/cli@0.1.8

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost-examples/codegen-react-apollo@0.1.16

### Patch Changes

-   @nhost/react@3.2.2
-   @nhost/react-apollo@9.0.2

## @nhost-examples/codegen-react-query@0.1.17

### Patch Changes

-   @nhost/react@3.2.2

## @nhost-examples/codegen-react-urql@0.0.13

### Patch Changes

-   @nhost/react@3.2.2
-   @nhost/react-urql@6.0.2

## @nhost-examples/multi-tenant-one-to-many@2.0.6

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost-examples/nextjs@0.1.18

### Patch Changes

-   @nhost/react@3.2.2
-   @nhost/react-apollo@9.0.2
-   @nhost/nextjs@2.1.4

## @nhost-examples/node-storage@0.0.10

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost-examples/nextjs-server-components@0.2.4

### Patch Changes

-   @nhost/nhost-js@3.0.7

## @nhost-examples/react-apollo@0.3.2

### Patch Changes

-   @nhost/react@3.2.2
-   @nhost/react-apollo@9.0.2

## @nhost-examples/react-gqty@1.0.6

### Patch Changes

-   @nhost/react@3.2.2

## @nhost-examples/vue-apollo@0.2.3

### Patch Changes

-   @nhost/nhost-js@3.0.7
-   @nhost/apollo@6.0.7
-   @nhost/vue@2.2.2

## @nhost-examples/vue-quickstart@0.0.15

### Patch Changes

-   @nhost/apollo@6.0.7
-   @nhost/vue@2.2.2

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-23 21:14:11 +01:00
Hassan Ben Jobrane
2d68fee54c fix(graphql-js): allow graphql requests with no access token (#2555) 2024-02-23 21:09:45 +01:00
github-actions[bot]
35010353c7 chore: update versions (#2547)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/apollo@6.0.6

### Patch Changes

-   e0ab6d9: fix: add extra logic to check and wait for a valid JWT
    -   @nhost/nhost-js@3.0.6

## @nhost/react-apollo@9.0.1

### Patch Changes

-   Updated dependencies [e0ab6d9]
    -   @nhost/apollo@6.0.6
    -   @nhost/react@3.2.1

## @nhost/react-urql@6.0.1

### Patch Changes

-   @nhost/react@3.2.1

## @nhost/graphql-js@0.1.6

### Patch Changes

-   e0ab6d9: fix: add extra logic to check and wait for a valid JWT

## @nhost/hasura-auth-js@2.3.1

### Patch Changes

- 7baee8a: fix(hasura-auth-js): replace `jwt-decode` with `jose` for
decoding access tokens that works on both the browser and Node.js
-   e0ab6d9: fix: add extra logic to check and wait for a valid JWT

## @nhost/nextjs@2.1.3

### Patch Changes

-   @nhost/react@3.2.1

## @nhost/nhost-js@3.0.6

### Patch Changes

-   Updated dependencies [7baee8a]
-   Updated dependencies [e0ab6d9]
    -   @nhost/hasura-auth-js@2.3.1
    -   @nhost/graphql-js@0.1.6

## @nhost/react@3.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.6

## @nhost/vue@2.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.6

## @nhost/dashboard@1.8.0

### Minor Changes

- 713d53c: feat: add catch-all route for workspace/project - useful for
documentation

### Patch Changes

-   3db2999: fix: refresh table list after running SQL using the editor
- 3c4dd55: fix: handle `Error` objects properly in the `ErrorToast`
component
- 92b434e: fix: resolve an issue where the checkbox in the data-grid
header did not select all rows
    -   @nhost/react-apollo@9.0.1
    -   @nhost/nextjs@2.1.3

## @nhost-examples/cli@0.1.7

### Patch Changes

-   @nhost/nhost-js@3.0.6

## @nhost-examples/codegen-react-apollo@0.1.15

### Patch Changes

-   @nhost/react-apollo@9.0.1
-   @nhost/react@3.2.1

## @nhost-examples/codegen-react-query@0.1.16

### Patch Changes

-   @nhost/react@3.2.1

## @nhost-examples/codegen-react-urql@0.0.12

### Patch Changes

-   @nhost/react@3.2.1
-   @nhost/react-urql@6.0.1

## @nhost-examples/docker-compose@0.1.1

### Patch Changes

-   aff059e: fix: timers

## @nhost-examples/multi-tenant-one-to-many@2.0.5

### Patch Changes

-   @nhost/nhost-js@3.0.6

## @nhost-examples/nextjs@0.1.17

### Patch Changes

-   @nhost/react-apollo@9.0.1
-   @nhost/react@3.2.1
-   @nhost/nextjs@2.1.3

## @nhost-examples/node-storage@0.0.9

### Patch Changes

-   @nhost/nhost-js@3.0.6

## @nhost-examples/nextjs-server-components@0.2.3

### Patch Changes

-   @nhost/nhost-js@3.0.6

## @nhost-examples/react-apollo@0.3.1

### Patch Changes

-   @nhost/react-apollo@9.0.1
-   @nhost/react@3.2.1

## @nhost-examples/react-gqty@1.0.5

### Patch Changes

-   @nhost/react@3.2.1

## @nhost-examples/vue-apollo@0.2.2

### Patch Changes

-   Updated dependencies [e0ab6d9]
    -   @nhost/apollo@6.0.6
    -   @nhost/nhost-js@3.0.6
    -   @nhost/vue@2.2.1

## @nhost-examples/vue-quickstart@0.0.14

### Patch Changes

-   Updated dependencies [e0ab6d9]
    -   @nhost/apollo@6.0.6
    -   @nhost/vue@2.2.1

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-23 10:07:53 +01:00
David Barroso
aff059ec71 fix (examples/docker-compose): timers (#2553) 2024-02-23 10:04:20 +01:00
Nuno Pato
713d53cfc0 feat: dashboard: add catch-all route (#2545) 2024-02-22 17:59:24 -01:00
Hassan Ben Jobrane
e0ab6d9a37 fix: JWT expired bug (#2533)
fixes: https://github.com/nhost/nhost/issues/2348

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2024-02-22 18:44:43 +01:00
Hassan Ben Jobrane
7baee8a9cc fix(hasura-auth-js): use jose instead of jwt-decode to decode the accessToken and get the Hasura claims (#2550)
fixes https://github.com/nhost/nhost/issues/2513
2024-02-21 11:17:03 +01:00
Hassan Ben Jobrane
3db2999f60 fix(dashboard): refresh table list after running a SQL stmt using the editor (#2549)
fixes https://github.com/nhost/projects/issues/52
2024-02-20 13:40:46 +01:00
Hassan Ben Jobrane
3c4dd55045 fix(dashboard): handle Error alongside ApolloError properly in the ErrorToast component (#2548)
fixes https://github.com/nhost/nhost/issues/2525
2024-02-20 12:16:26 +01:00
Hassan Ben Jobrane
92b434e840 fix: refactor DataGridHeader component to allow for selecting all rows (#2546)
fixes https://github.com/nhost/nhost/issues/2526
2024-02-19 17:30:59 +01:00
github-actions[bot]
13d359602f chore: update versions (#2540)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.7.0

### Minor Changes

-   0d8d0eb: Update docs and dashboard references

## @nhost/docs@2.5.0

### Minor Changes

-   0d8d0eb: Update docs and dashboard references

### Patch Changes

-   41617b9: feat: added elevated permissions docs
- 7db095f: chore: added a note about disk performance and CDN
information

## @nhost-examples/docker-compose@0.1.0

### Minor Changes

-   ed9df85: updated docker-compose.yaml and .env-example

## @nhost-examples/vue-apollo@0.2.1

### Patch Changes

-   c5c904b: fix: update signin methods settings

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-16 20:44:28 -01:00
Nuno Pato
0d8d0eb10f chore: update docs and fix dashboard references (#2543) 2024-02-16 18:21:30 -01:00
Seth Deegan
ed9df85778 Update docker-compose and .env-example (#2397)
The docker-compose example is severely outdated and the past
configuration used a docker image for the dashboard that did not allow
you to configure the URLs to the various API endpoints if you
self-hosted your own dashboard publicly.

The newest dashboard image allows you to do this so the docker-compose
has been updated to use this image and the env variables have been
updated accordingly.

Other variables have been updated in the docker-compose to support
self-hosting a public instance.

A commented traefik configuration in the docker-compose for the
dashboard service also allows the user to configure basic auth to
protect a publicly-facing dashboard.

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-02-16 08:52:58 +01:00
David Barroso
41617b970a feat (docs): added elevated permissions docs (#2519)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-02-15 16:00:44 +01:00
Hassan Ben Jobrane
c5c904b716 fix(vue-apollo): update signin methods settings (#2541) 2024-02-15 13:48:41 +01:00
David Barroso
7db095fe92 chore: docs: added a note about disk performance and CDN information (#2539)
Fixes #2504
2024-02-15 11:47:33 +01:00
github-actions[bot]
f33e07b191 chore: update versions (#2538)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/hasura-auth-js@2.3.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

## @nhost/react@3.2.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost/vue@2.2.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost/apollo@6.0.5

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost/react-apollo@9.0.0

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/apollo@6.0.5

## @nhost/react-urql@6.0.0

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost/nextjs@2.1.2

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost/nhost-js@3.0.5

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/hasura-auth-js@2.3.0

## @nhost-examples/react-apollo@0.3.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-apollo@9.0.0

## @nhost-examples/vue-apollo@0.2.0

### Minor Changes

-   017f1a6: feat: add elevated permission examples

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/vue@2.2.0
    -   @nhost/nhost-js@3.0.5
    -   @nhost/apollo@6.0.5

## @nhost/dashboard@1.6.9

### Patch Changes

-   @nhost/react-apollo@9.0.0
-   @nhost/nextjs@2.1.2

## @nhost-examples/cli@0.1.6

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/codegen-react-apollo@0.1.14

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-apollo@9.0.0

## @nhost-examples/codegen-react-query@0.1.15

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost-examples/codegen-react-urql@0.0.11

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-urql@6.0.0

## @nhost-examples/multi-tenant-one-to-many@2.0.4

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/nextjs@0.1.16

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0
    -   @nhost/react-apollo@9.0.0
    -   @nhost/nextjs@2.1.2

## @nhost-examples/node-storage@0.0.8

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/nextjs-server-components@0.2.2

### Patch Changes

-   @nhost/nhost-js@3.0.5

## @nhost-examples/react-gqty@1.0.4

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/react@3.2.0

## @nhost-examples/vue-quickstart@0.0.13

### Patch Changes

-   Updated dependencies [017f1a6]
    -   @nhost/vue@2.2.0
    -   @nhost/apollo@6.0.5

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-14 15:12:34 +01:00
Hassan Ben Jobrane
017f1a6c7b feat: add elevate workflow to react-apollo and vue-apollo example projects (#2521)
part-2 of https://github.com/nhost/nhost/issues/2394

---------

Co-authored-by: David Barroso <dbarrosop@dravetech.com>
2024-02-14 14:52:43 +01:00
github-actions[bot]
93957c8af3 chore: update versions (#2537)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/hasura-storage-js@2.4.0

### Minor Changes

-   2505b2e: fix: fix headers sent with getPresignedUrl

## @nhost/apollo@6.0.4

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost/react-apollo@8.0.1

### Patch Changes

-   @nhost/apollo@6.0.4
-   @nhost/react@3.1.1

## @nhost/react-urql@5.0.1

### Patch Changes

-   @nhost/react@3.1.1

## @nhost/nextjs@2.1.1

### Patch Changes

-   @nhost/react@3.1.1

## @nhost/nhost-js@3.0.4

### Patch Changes

-   Updated dependencies [2505b2e]
    -   @nhost/hasura-storage-js@2.4.0

## @nhost/react@3.1.1

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost/vue@2.1.1

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost/dashboard@1.6.8

### Patch Changes

-   @nhost/react-apollo@8.0.1
-   @nhost/nextjs@2.1.1

## @nhost-examples/cli@0.1.5

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost-examples/codegen-react-apollo@0.1.13

### Patch Changes

-   @nhost/react@3.1.1
-   @nhost/react-apollo@8.0.1

## @nhost-examples/codegen-react-query@0.1.14

### Patch Changes

-   @nhost/react@3.1.1

## @nhost-examples/codegen-react-urql@0.0.10

### Patch Changes

-   @nhost/react@3.1.1
-   @nhost/react-urql@5.0.1

## @nhost-examples/multi-tenant-one-to-many@2.0.3

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost-examples/nextjs@0.1.15

### Patch Changes

-   @nhost/react@3.1.1
-   @nhost/react-apollo@8.0.1
-   @nhost/nextjs@2.1.1

## @nhost-examples/node-storage@0.0.7

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost-examples/nextjs-server-components@0.2.1

### Patch Changes

-   @nhost/nhost-js@3.0.4

## @nhost-examples/react-apollo@0.2.1

### Patch Changes

-   @nhost/react@3.1.1
-   @nhost/react-apollo@8.0.1

## @nhost-examples/react-gqty@1.0.3

### Patch Changes

-   @nhost/react@3.1.1

## @nhost-examples/vue-apollo@0.1.1

### Patch Changes

-   @nhost/nhost-js@3.0.4
-   @nhost/apollo@6.0.4
-   @nhost/vue@2.1.1

## @nhost-examples/vue-quickstart@0.0.12

### Patch Changes

-   @nhost/apollo@6.0.4
-   @nhost/vue@2.1.1

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-14 10:29:57 +01:00
Nuno Pato
2505b2e26b fix: fix headers sent with getPresignedUrl (#2535) 2024-02-13 23:33:09 -01:00
Nuno Pato
5f4b4d2acc chore: update dependencies (#2536)
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-02-13 20:29:47 -01:00
github-actions[bot]
71a8ce4446 chore: update versions (#2524)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/docs@2.4.0

### Minor Changes

-   791b729: fix: remove auth method

## @nhost/dashboard@1.6.7

### Patch Changes

-   5ef5189: fix: update `@apollo/client` to `3.9.4` to fix a cache bug

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-02-12 16:33:16 +01:00
Hassan Ben Jobrane
5ef5189898 fix(dashboard): resolve change plan modal cache issue (#2532)
related to https://github.com/nhost/nhost/issues/2530
2024-02-12 16:08:22 +01:00
Nuno Pato
791b7295fb fix: docs: remove auth method (#2475)
- https://github.com/nhost/nhost/issues/2474
2024-02-08 10:36:51 -01:00
github-actions[bot]
25bc4b7fd6 chore: update versions (#2501)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/hasura-auth-js@2.2.0

### Minor Changes

- 1a61c65: feat: add 'elevateEmailSecurityKey' to the SDKs along with
integration into react-apollo and vue-apollo examples

## @nhost/hasura-storage-js@2.3.0

### Minor Changes

-   d3d1424: feat: Add support for authenticated download of files

### Patch Changes

-   e5bab6a: chore: update dependencies

## @nhost/nextjs@2.1.0

### Minor Changes

-   b19ffed: chore: update peerDependency to support nextjs 14

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0

## @nhost/react@3.1.0

### Minor Changes

- 1a61c65: feat: add 'elevateEmailSecurityKey' to the SDKs along with
integration into react-apollo and vue-apollo examples

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost/vue@2.1.0

### Minor Changes

- 1a61c65: feat: add 'elevateEmailSecurityKey' to the SDKs along with
integration into react-apollo and vue-apollo examples

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost/apollo@6.0.3

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost/google-translation@0.0.8

### Patch Changes

-   e5bab6a: chore: update dependencies

## @nhost/react-apollo@8.0.0

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0
    -   @nhost/apollo@6.0.3

## @nhost/react-urql@5.0.0

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0

## @nhost/stripe-graphql-js@1.0.7

### Patch Changes

-   e5bab6a: chore: update dependencies

## @nhost/nhost-js@3.0.3

### Patch Changes

-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
-   Updated dependencies [d3d1424]
    -   @nhost/hasura-auth-js@2.2.0
    -   @nhost/hasura-storage-js@2.3.0
    -   @nhost/graphql-js@0.1.5

## @nhost/docs@2.3.0

### Minor Changes

-   d3d1424: feat: Add support for authenticated download of files

### Patch Changes

-   e5bab6a: chore: update dependencies
- 2ae5ea8: fix: indicate that custom domains for postgres doesn't
require configuration

## @nhost-examples/nextjs-server-components@0.2.0

### Minor Changes

-   d3d1424: feat: Add support for authenticated download of files

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost-examples/react-apollo@0.2.0

### Minor Changes

- 1a61c65: feat: add 'elevateEmailSecurityKey' to the SDKs along with
integration into react-apollo and vue-apollo examples

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0
    -   @nhost/react-apollo@8.0.0

## @nhost-examples/vue-apollo@0.1.0

### Minor Changes

- 1a61c65: feat: add 'elevateEmailSecurityKey' to the SDKs along with
integration into react-apollo and vue-apollo examples

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/vue@2.1.0
    -   @nhost/apollo@6.0.3
    -   @nhost/nhost-js@3.0.3

## @nhost/dashboard@1.6.6

### Patch Changes

-   3ba485e: fix: added discord.com to connect-src
-   e5bab6a: chore: update dependencies
-   Updated dependencies [b19ffed]
-   Updated dependencies [e5bab6a]
    -   @nhost/nextjs@2.1.0
    -   @nhost/react-apollo@8.0.0

## @nhost-examples/cli@0.1.4

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost-examples/codegen-react-apollo@0.1.12

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0
    -   @nhost/react-apollo@8.0.0

## @nhost-examples/codegen-react-query@0.1.13

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0

## @nhost-examples/codegen-react-urql@0.0.9

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0
    -   @nhost/react-urql@5.0.0

## @nhost-examples/multi-tenant-one-to-many@2.0.2

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost-examples/nextjs@0.1.14

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [b19ffed]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0
    -   @nhost/nextjs@2.1.0
    -   @nhost/react-apollo@8.0.0

## @nhost-examples/node-storage@0.0.6

### Patch Changes

-   e5bab6a: chore: update dependencies
    -   @nhost/nhost-js@3.0.3

## @nhost-examples/sveltekit@0.2.3

### Patch Changes

-   e5bab6a: chore: update dependencies

## @nhost-examples/react-gqty@1.0.2

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/react@3.1.0

## @nhost-examples/serverless-functions@0.0.11

### Patch Changes

-   8c34c69: chore: update nodemailer version to `6.9.9`
-   Updated dependencies [e5bab6a]
    -   @nhost/stripe-graphql-js@1.0.7

## @nhost-examples/vue-quickstart@0.0.11

### Patch Changes

-   e5bab6a: chore: update dependencies
-   Updated dependencies [1a61c65]
-   Updated dependencies [e5bab6a]
    -   @nhost/vue@2.1.0
    -   @nhost/apollo@6.0.3

## @nhost/docgen@0.1.13

### Patch Changes

-   e5bab6a: chore: update dependencies

## @nhost/sync-versions@0.0.10

### Patch Changes

-   e5bab6a: chore: update dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-02-06 14:41:47 +01:00
Hassan Ben Jobrane
da20159ec5 chore(examples): update auth version to 0.25.0 in nhost.toml (#2518) 2024-02-06 14:09:30 +01:00
David Barroso
2ae5ea8bc1 fix (docs): indicate that custom domains for postgres doesn't require configuration (#2506) 2024-02-06 11:54:18 +01:00
David Barroso
3ba485e582 fix (dashboard): added discord.com to connect-src (#2516) 2024-02-06 11:29:27 +01:00
David Barroso
e5bab6a061 chore: update dependencies (#2505)
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-02-06 10:25:48 +01:00
David Barroso
be64353145 Create SECURITY.md (#2515) 2024-02-06 10:00:38 +01:00
Hassan Ben Jobrane
2f5913c78d fix(examples): revert back auth version to 0.24.1 (#2512) 2024-02-05 17:18:52 +01:00
Nuno Pato
757ddd901c chore: docs: use hyphen instead of underscore (#2511) 2024-02-05 13:46:13 -01:00
Hassan Ben Jobrane
1a61c658a7 feat: add elevate func to the SDKs and the react-apollo example project (#2500)
part-1 of https://github.com/nhost/nhost/issues/2394
2024-02-05 13:25:14 +01:00
Nuno Pato
d3d14245c7 feat: hasura-storage-js: add authenticated download of files (#2507) 2024-02-05 11:24:34 -01:00
Alexander Mart
53d2f9d3e0 doc: fix method name in example code at refresh-session.mdx doc page (#2468)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-02-03 15:54:08 +01:00
Hassan Ben Jobrane
8c34c69e79 chore: update nodemailer (#2510) 2024-02-02 20:31:29 +01:00
Jared Prather
b19ffed273 chore: Update peerDependency next to ^14.0.0 (#2354)
Fixes: #2351 

Summary of next 14 changes
[here](https://nextjs.org/docs/pages/building-your-application/upgrading/version-14#v14-summary)

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-01-30 18:25:02 +01:00
Nuno Pato
859efa988a fix: docs: small fixes (#2486) 2024-01-26 17:15:06 -01:00
github-actions[bot]
3202b6b897 chore: update versions (#2495)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.6.5

### Patch Changes

- ba73bb4: fix: update ErrorToast component to show the internal graphql
error
- d5337ff: fix: utilize accumulator in the creation of validation schema
within data grid utils

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-26 18:32:57 +01:00
Hassan Ben Jobrane
ba73bb4003 fix(dashboard): show internal error in toast message (#2496) 2024-01-26 17:42:18 +01:00
Hassan Ben Jobrane
d5337ff5bd fix(dashboard): fix bug with validation schema for create record form (#2494) 2024-01-26 16:50:28 +01:00
github-actions[bot]
511ab19755 chore: update versions (#2492)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/docs@2.2.0

### Minor Changes

- 5c9b8f0: feat: added docs regarding local development for Run services

## @nhost/dashboard@1.6.4

### Patch Changes

-   7c2a1c2: feat: show error and debug info in the error toast

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-25 17:23:36 +01:00
Hassan Ben Jobrane
7c2a1c29fd feat(dashboard): add custom error toast (#2463)
fixes https://github.com/nhost/nhost/issues/2435
2024-01-25 16:48:54 +01:00
David Barroso
5c9b8f0a3f feat (docs): added docs regarding local development for Run services (#2488) 2024-01-25 12:40:04 +01:00
github-actions[bot]
b3f1f5f6ea chore: update versions (#2491)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.6.3

### Patch Changes

-   6b8aad5: fix: add bare nhost.run to CSP

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-25 12:14:56 +01:00
David Barroso
6b8aad5c84 fix(dashboard): add bare nhost.run to CSP (#2490) 2024-01-25 11:28:15 +01:00
github-actions[bot]
c36132c9bb chore: update versions (#2489)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.6.2

### Patch Changes

-   b18edc0: feat: added CSP and X-Frame-Options

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-01-25 11:00:06 +01:00
David Barroso
b18edc0532 feat (dashboard): added CSP and X-Frame-Options (#2479) 2024-01-25 10:46:41 +01:00
David Barroso
1d55d3ea38 chore (general): added mintlify to docs devDependencies and manage node_modules with nix (#2487)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-01-24 10:43:59 +01:00
534 changed files with 23950 additions and 18754 deletions

View File

@@ -0,0 +1,82 @@
---
name: "gen: update depenendencies"
on:
schedule:
- cron: '0 2 1 * *'
jobs:
run:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
pull-requests: write
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure aws
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_PRODUCTION_CORE_ACCOUNT_ID }}:role/github-actions-nhost-${{ github.event.repository.name }}
aws-region: eu-central-1
- uses: nixbuild/nix-quick-install-action@v26
with:
nix_version: 2.16.2
nix_conf: |
experimental-features = nix-command flakes
sandbox = false
access-tokens = github.com=${{ secrets.GITHUB_TOKEN }}
substituters = https://cache.nixos.org/?priority=40 s3://nhost-nix-cache?region=eu-central-1&priority=50
trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ${{ secrets.NIX_CACHE_PUB_KEY }}
- name: Cache nix store
uses: actions/cache@v4
with:
path: /nix
key: nix-update-deps-${{ hashFiles('flakes.nix', 'flake.lock') }}
- name: Update nix flakes
run: nix flake update
- name: Update dependencies
run: |
nix develop -c bash -c "
pnpm dedupe
pnpm update -r
pnpm dedupe
"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update dependencies
committer: GitHub <noreply@github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
signoff: false
branch: automated/update-deps
delete-branch: true
title: '[Scheduled] Update dependencies'
body: |
Dependencies updated
Note - If you see this PR and the checks haven't run, close and reopen the PR. See https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs
labels: |
dependencies
draft: false
- name: "Cache nix store on s3"
run: |
echo ${{ secrets.NIX_CACHE_PRIV_KEY }} > cache-priv-key.pem
nix build .\#devShells.x86_64-linux.default
nix store sign --key-file cache-priv-key.pem --all
nix copy --to s3://nhost-nix-cache\?region=eu-central-1 .\#devShells.x86_64-linux.default
- run: rm cache-priv-key.pem
if: always()

2
.gitignore vendored
View File

@@ -19,7 +19,7 @@ logs/
coverage/ coverage/
dist/ dist/
umd/ umd/
node_modules/ node_modules
tmp/ tmp/
.pnpm-store .pnpm-store
.turbo .turbo

7
SECURITY.md Normal file
View File

@@ -0,0 +1,7 @@
# Security Policy
## Reporting a Vulnerability
At Nhost, we take security vulnerabilities seriously and appreciate the assistance of the community in bringing any issues to our attention. If you discover a security vulnerability, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/nhost/nhost/security/advisories/new) tab.
Once you have submitted the report, we will promptly conduct a thorough investigation within 72 hours. In case we need further information, we may contact you for additional details. Rest assured that addressing the reported vulnerability in a timely manner is our top priority.

3
config/.husky/pre-commit Executable file → Normal file
View File

@@ -1,4 +1,7 @@
#!/bin/sh #!/bin/sh
#
[ -n "$CI" ] && exit 0
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
pnpm dlx lint-staged --config config/.lintstagedrc.js pnpm dlx lint-staged --config config/.lintstagedrc.js

View File

@@ -1,5 +1,211 @@
# @nhost/dashboard # @nhost/dashboard
## 1.14.0
### Minor Changes
- a448d7d: feat: allow configuring postmark and delete SMTP settings
## 1.13.3
### Patch Changes
- 5924bc3: fix: include password in `GetSmtpSettings` query
- c5ad634: fix: resolved an issue where one-click install links were broken on Safari
- 7278991: fix: update graphql auto-embeddings configuration to use String type for model field
## 1.13.2
### Patch Changes
- 026f84f: fix: use configuration server URL from environment variable
## 1.13.1
### Patch Changes
- 7e9a2ce: fix: resolve issue where run services form fails to open
## 1.13.0
### Minor Changes
- dd5d262: feat: add model field to the auto-embeddings form
- 09962be: feat: enable settings and run services when running the dashboard locally
- 9cdecb6: feat: enable users to update their email address from the account settings page
## 1.12.2
### Patch Changes
- c195c51: fix: send email upon signin for unverified users
## 1.12.1
### Patch Changes
- 93ebdf8: fix: use service urls when initilizaing NhostClient running local dashboard
- @nhost/react-apollo@11.0.1
- @nhost/nextjs@2.1.10
## 1.12.0
### Minor Changes
- f242e4b: feat: add connect with github to the user's account settings
- 768ca17: chore: update dependencies
- d62bd0f: fix: "Track this" option within the SQL editor now correctly updates the metadata
- 91c2bb6: feat: refactor sign-in and sign-up pages to enforce email verification
### Patch Changes
- 943831f: fix: resolve an error toast issue when unpausing a project
- Updated dependencies [768ca17]
- @nhost/react-apollo@11.0.0
- @nhost/nextjs@2.1.9
## 1.11.2
### Patch Changes
- @nhost/react-apollo@10.0.2
- @nhost/nextjs@2.1.8
## 1.11.1
### Patch Changes
- 981404f: fix: set default value for healthCheck field validation
## 1.11.0
### Minor Changes
- 7789469: chore: upgrade dependency `@graphql-codegen/cli` to `5.0.2` to address vulnerability
- 6c11b75: feat: add update user displayName section in account settings
### Patch Changes
- @nhost/react-apollo@10.0.1
- @nhost/nextjs@2.1.7
## 1.10.0
### Minor Changes
- 49a80c2: chore: update dependencies
- 150c04a: feat: add healthcheck config to run services
### Patch Changes
- e03f141: fix: allow insert, update and delete on tables in `auth` and `storage` schemas
- 28676f4: feat: add min postgres version check to enable the ai service
- Updated dependencies [49a80c2]
- @nhost/react-apollo@10.0.0
- @nhost/nextjs@2.1.6
## 1.9.0
### Minor Changes
- d86e5c9: feat: add support for filtering the logs using a RegExp
## 1.8.3
### Patch Changes
- @nhost/react-apollo@9.0.3
- @nhost/nextjs@2.1.5
## 1.8.2
### Patch Changes
- 6df4f02: fix: use custom error toast and show correct message when sending an invite
## 1.8.1
### Patch Changes
- @nhost/react-apollo@9.0.2
- @nhost/nextjs@2.1.4
## 1.8.0
### Minor Changes
- 713d53c: feat: add catch-all route for workspace/project - useful for documentation
### Patch Changes
- 3db2999: fix: refresh table list after running SQL using the editor
- 3c4dd55: fix: handle `Error` objects properly in the `ErrorToast` component
- 92b434e: fix: resolve an issue where the checkbox in the data-grid header did not select all rows
- @nhost/react-apollo@9.0.1
- @nhost/nextjs@2.1.3
## 1.7.0
### Minor Changes
- 0d8d0eb: Update docs and dashboard references
## 1.6.9
### Patch Changes
- @nhost/react-apollo@9.0.0
- @nhost/nextjs@2.1.2
## 1.6.8
### Patch Changes
- @nhost/react-apollo@8.0.1
- @nhost/nextjs@2.1.1
## 1.6.7
### Patch Changes
- 5ef5189: fix: update `@apollo/client` to `3.9.4` to fix a cache bug
## 1.6.6
### Patch Changes
- 3ba485e: fix: added discord.com to connect-src
- e5bab6a: chore: update dependencies
- Updated dependencies [b19ffed]
- Updated dependencies [e5bab6a]
- @nhost/nextjs@2.1.0
- @nhost/react-apollo@8.0.0
## 1.6.5
### Patch Changes
- ba73bb4: fix: update ErrorToast component to show the internal graphql error
- d5337ff: fix: utilize accumulator in the creation of validation schema within data grid utils
## 1.6.4
### Patch Changes
- 7c2a1c2: feat: show error and debug info in the error toast
## 1.6.3
### Patch Changes
- 6b8aad5: fix: add bare nhost.run to CSP
## 1.6.2
### Patch Changes
- b18edc0: feat: added CSP and X-Frame-Options
## 1.6.1 ## 1.6.1
### Patch Changes ### Patch Changes

View File

@@ -28,6 +28,7 @@ ENV NEXT_PUBLIC_NHOST_STORAGE_URL __NEXT_PUBLIC_NHOST_STORAGE_URL__
ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__ ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__ ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__ ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
ENV NEXT_PUBLIC_NHOST_CONFIGSERVER_URL __NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__
RUN yarn global add pnpm@8.10.5 RUN yarn global add pnpm@8.10.5
COPY .gitignore .gitignore COPY .gitignore .gitignore

View File

@@ -21,5 +21,6 @@ find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} + find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} + find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} + find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL}~g" {} +
exec "$@" exec "$@"

View File

@@ -4,6 +4,20 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
}); });
const { version } = require('./package.json'); const { version } = require('./package.json');
const cspHeader = `
default-src 'self' *.nhost.run ws://*.nhost.run nhost.run ws://nhost.run;
script-src 'self' 'unsafe-eval' 'unsafe-inline' cdn.segment.com js.stripe.com;
connect-src 'self' *.nhost.run ws://*.nhost.run nhost.run ws://nhost.run discord.com;
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: avatars.githubusercontent.com s.gravatar.com *.nhost.run nhost.run;
font-src 'self' data:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
frame-src 'self' js.stripe.com;
`;
module.exports = withBundleAnalyzer({ module.exports = withBundleAnalyzer({
reactStrictMode: true, reactStrictMode: true,
swcMinify: false, swcMinify: false,
@@ -17,6 +31,19 @@ module.exports = withBundleAnalyzer({
eslint: { eslint: {
dirs: ['src'], dirs: ['src'],
}, },
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
],
},
];
},
async redirects() { async redirects() {
return [ return [
{ {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/dashboard", "name": "@nhost/dashboard",
"version": "1.6.1", "version": "1.14.0",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -19,57 +19,58 @@
"e2e": "pnpm install-browsers && pnpm playwright test" "e2e": "pnpm install-browsers && pnpm playwright test"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.8.9", "@apollo/client": "^3.9.9",
"@codemirror/lang-sql": "^6.5.5", "@codemirror/lang-sql": "^6.6.2",
"@emotion/cache": "^11.11.0", "@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.4",
"@emotion/server": "^11.11.0", "@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.5",
"@fontsource/inter": "^5.0.16", "@fontsource/inter": "^5.0.17",
"@fontsource/roboto-mono": "^5.0.16", "@fontsource/roboto-mono": "^5.0.17",
"@graphiql/react": "^0.20.2", "@graphiql/react": "^0.20.3",
"@graphiql/toolkit": "^0.9.1", "@graphiql/toolkit": "^0.9.1",
"@headlessui/react": "^1.7.18", "@headlessui/react": "^1.7.18",
"@heroicons/react": "^1.0.6", "@heroicons/react": "^1.0.6",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@mui/base": "5.0.0-beta.31", "@mui/base": "5.0.0-beta.31",
"@mui/material": "^5.15.4", "@mui/material": "^5.15.14",
"@mui/system": "^5.15.4", "@mui/system": "^5.15.14",
"@mui/x-date-pickers": "^5.0.20", "@mui/x-date-pickers": "^5.0.20",
"@nhost/nextjs": "workspace:*", "@nhost/nextjs": "workspace:*",
"@nhost/react-apollo": "workspace:*", "@nhost/react-apollo": "workspace:*",
"@segment/snippet": "^4.16.2", "@segment/snippet": "^4.16.2",
"@stripe/react-stripe-js": "^2.4.0", "@stripe/react-stripe-js": "^2.6.2",
"@stripe/stripe-js": "^1.54.2", "@stripe/stripe-js": "^1.54.2",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.11.6", "@tanstack/react-table": "^8.15.3",
"@tanstack/react-virtual": "^3.0.1", "@tanstack/react-virtual": "^3.2.0",
"@uiw/codemirror-theme-github": "^4.21.21", "@uiw/codemirror-theme-github": "^4.21.25",
"@uiw/react-codemirror": "^4.21.21", "@uiw/react-codemirror": "^4.21.25",
"analytics-node": "^6.2.0", "analytics-node": "^6.2.0",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"framer-motion": "^10.18.0",
"generate-password": "^1.7.1", "generate-password": "^1.7.1",
"graphiql": "^3.1.0", "graphiql": "^3.1.1",
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"graphql-ws": "^5.14.3", "graphql-ws": "^5.16.0",
"just-kebab-case": "^4.2.0", "just-kebab-case": "^4.2.0",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "^14.0.4", "next": "^14.1.4",
"next-seo": "^6.4.0", "next-seo": "^6.5.0",
"node-pg-format": "^1.3.5", "node-pg-format": "^1.3.5",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-children-utilities": "^2.10.0", "react-children-utilities": "^2.10.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-error-boundary": "^4.0.12", "react-error-boundary": "^4.0.13",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.51.2",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.5.3", "react-intersection-observer": "^9.8.1",
"react-is": "18.2.0", "react-is": "18.2.0",
"react-loading-skeleton": "^2.2.0", "react-loading-skeleton": "^2.2.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
@@ -84,15 +85,15 @@
"slugify": "^1.6.6", "slugify": "^1.6.6",
"stripe": "^10.17.0", "stripe": "^10.17.0",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"utility-types": "^3.10.0", "utility-types": "^3.11.0",
"validator": "^13.11.0", "validator": "^13.11.0",
"yup": "^1.3.3", "yup": "^1.4.0",
"yup-password": "^0.2.2" "yup-password": "^0.2.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.23.7", "@babel/core": "^7.24.3",
"@faker-js/faker": "^7.6.0", "@faker-js/faker": "^7.6.0",
"@graphql-codegen/cli": "^3.3.1", "@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/typescript": "^3.0.4", "@graphql-codegen/typescript": "^3.0.4",
"@graphql-codegen/typescript-operations": "^3.0.4", "@graphql-codegen/typescript-operations": "^3.0.4",
"@graphql-codegen/typescript-react-apollo": "^3.3.7", "@graphql-codegen/typescript-react-apollo": "^3.3.7",
@@ -105,50 +106,50 @@
"@storybook/addon-postcss": "^2.0.0", "@storybook/addon-postcss": "^2.0.0",
"@storybook/builder-webpack5": "^6.5.16", "@storybook/builder-webpack5": "^6.5.16",
"@storybook/manager-webpack5": "^6.5.16", "@storybook/manager-webpack5": "^6.5.16",
"@storybook/react": "^6.5.16", "@storybook/react": "^7.6.17",
"@storybook/testing-library": "^0.2.2", "@storybook/testing-library": "^0.2.2",
"@tailwindcss/typography": "^0.5.10", "@tailwindcss/typography": "^0.5.12",
"@testing-library/dom": "^9.3.4", "@testing-library/dom": "^9.3.4",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^14.1.2", "@testing-library/react": "^14.2.2",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/ace": "^0.0.48", "@types/ace": "^0.0.48",
"@types/bcryptjs": "^2.4.6", "@types/bcryptjs": "^2.4.6",
"@types/jest": "^29.5.11", "@types/jest": "^29.5.12",
"@types/lodash.debounce": "^4.0.9", "@types/lodash.debounce": "^4.0.9",
"@types/node": "^16.18.70", "@types/node": "^16.18.93",
"@types/pluralize": "^0.0.30", "@types/pluralize": "^0.0.30",
"@types/react": "^18.2.47", "@types/react": "^18.2.73",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.23",
"@types/react-table": "^7.7.19", "@types/react-table": "^7.7.20",
"@types/shell-quote": "^1.7.5", "@types/shell-quote": "^1.7.5",
"@types/testing-library__jest-dom": "^5.14.9", "@types/testing-library__jest-dom": "^5.14.9",
"@types/validator": "^13.11.8", "@types/validator": "^13.11.9",
"@typescript-eslint/eslint-plugin": "^6.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.18.1", "@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^0.32.4", "@vitest/coverage-v8": "^0.32.4",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.19",
"babel-loader": "^8.3.0", "babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-console": "^6.9.4",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"dotenv": "^16.3.1", "dotenv": "^16.4.5",
"encoding": "^0.1.13", "encoding": "^0.1.13",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-airbnb": "19.0.4", "eslint-config-airbnb": "19.0.4",
"eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-next": "^13.5.6", "eslint-config-next": "^13.5.6",
"eslint-config-prettier": "^8.10.0", "eslint-config-prettier": "^8.10.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"lint-staged": "^15.2.0", "lint-staged": "^15.2.2",
"msw": "^1.3.2", "msw": "^1.3.3",
"msw-storybook-addon": "^1.10.0", "msw-storybook-addon": "^1.10.0",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"postcss": "^8.4.33", "postcss": "^8.4.38",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.4.1", "prettier-plugin-tailwindcss": "^0.4.1",
@@ -156,11 +157,11 @@
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2",
"snake-case": "^3.0.4", "snake-case": "^3.0.4",
"storybook-addon-next-router": "^4.0.2", "storybook-addon-next-router": "^4.0.2",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.3",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths-webpack-plugin": "^4.1.0", "tsconfig-paths-webpack-plugin": "^4.1.0",
"vite": "^5.0.12", "vite": "^5.2.7",
"vite-tsconfig-paths": "^4.2.3", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^0.32.4" "vitest": "^0.32.4"
}, },
"browserslist": { "browserslist": {

View File

@@ -0,0 +1,30 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
export default function ApplyLocalSettingsDialog() {
const { closeDialog } = useDialog();
return (
<div className="flex flex-col gap-4 px-6 pb-6">
<div className="flex flex-col gap-2">
<Text color="secondary">
Run{' '}
<code className="px-1 py-px mx-1 rounded-md bg-slate-500 text-slate-100">
$ nhost up
</code>{' '}
using the cli to apply your changes
</Text>
</div>
<Button
className="w-full"
color="primary"
onClick={() => closeDialog()}
autoFocus
>
OK
</Button>
</div>
);
}

View File

@@ -0,0 +1 @@
export { default as ApplyLocalSettingsDialog } from './ApplyLocalSettingsDialog';

View File

@@ -35,7 +35,7 @@ function InsertPlaceholderTableRow({
...props ...props
}: InsertPlaceholderTableRowProps) { }: InsertPlaceholderTableRowProps) {
return ( return (
<Box className="h-12 border-r-1 border-b-1" {...props}> <Box className="h-12 border-b-1 border-r-1" {...props}>
<Button <Button
onClick={onInsertRow} onClick={onInsertRow}
variant="borderless" variant="borderless"
@@ -209,7 +209,7 @@ export default function DataGridBody<T extends object>({
/> />
) : ( ) : (
<Box <Box
className="inline-flex h-12 items-center border-b-1 border-r-1 py-1.5 px-2 text-xs" className="inline-flex h-12 items-center border-b-1 border-r-1 px-2 py-1.5 text-xs"
sx={{ color: 'text.secondary' }} sx={{ color: 'text.secondary' }}
style={{ style={{
width: allowInsertColumn width: allowInsertColumn
@@ -281,8 +281,8 @@ export default function DataGridBody<T extends object>({
}} }}
className={twMerge( className={twMerge(
'h-12 font-display text-xs motion-safe:transition-colors', 'h-12 font-display text-xs motion-safe:transition-colors',
'border-r-1 border-b-1', 'border-b-1 border-r-1',
'scroll-mt-[57px] scroll-ml-8', 'scroll-ml-8 scroll-mt-[57px]',
column.id === 'selection' && column.id === 'selection' &&
'sticky left-0 z-20 justify-center px-0', 'sticky left-0 z-20 justify-center px-0',
)} )}
@@ -296,7 +296,7 @@ export default function DataGridBody<T extends object>({
})} })}
{allowInsertColumn && ( {allowInsertColumn && (
<Box className="h-12 w-25 border-r-1 border-b-1" /> <Box className="h-12 w-25 border-b-1 border-r-1" />
)} )}
</div> </div>

View File

@@ -8,7 +8,15 @@ import type {
DataBrowserGridCellProps, DataBrowserGridCellProps,
} from '@/features/database/dataGrid/types/dataBrowser'; } from '@/features/database/dataGrid/types/dataBrowser';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
import type { FocusEvent, JSXElementConstructor, KeyboardEvent, MouseEvent, ReactElement, ReactNode, ReactPortal } from 'react'; import type {
FocusEvent,
JSXElementConstructor,
KeyboardEvent,
MouseEvent,
ReactElement,
ReactNode,
ReactPortal,
} from 'react';
import { import {
Children, Children,
cloneElement, cloneElement,
@@ -308,7 +316,7 @@ function DataGridCellContent<TData extends object = {}, TValue = unknown>({
isEditable && isEditable &&
'focus-within:outline-none focus-within:ring-0 focus:ring-0', 'focus-within:outline-none focus-within:ring-0 focus:ring-0',
isSelected && 'shadow-outline', isSelected && 'shadow-outline',
isEditing ? 'p-0.5 shadow-outline-dark' : 'py-1.5 px-2', isEditing ? 'p-0.5 shadow-outline-dark' : 'px-2 py-1.5',
className, className,
)} )}
onFocus={handleFocus} onFocus={handleFocus}
@@ -320,20 +328,28 @@ function DataGridCellContent<TData extends object = {}, TValue = unknown>({
sx={{ backgroundColor: 'transparent' }} sx={{ backgroundColor: 'transparent' }}
{...props} {...props}
> >
{Children.map(children, (child: ReactNode | ReactPortal | ReactElement<unknown, string | JSXElementConstructor<any>>) => { {Children.map(
if (!isValidElement(child)) { children,
return null; (
} child:
| ReactNode
| ReactPortal
| ReactElement<unknown, string | JSXElementConstructor<any>>,
) => {
if (!isValidElement(child)) {
return null;
}
return cloneElement(child, { return cloneElement(child, {
...child.props, ...child.props,
onSave: handleSave, onSave: handleSave,
optimisticValue, optimisticValue,
onOptimisticValueChange: setOptimisticValue, onOptimisticValueChange: setOptimisticValue,
temporaryValue, temporaryValue,
onTemporaryValueChange: setTemporaryValue, onTemporaryValueChange: setTemporaryValue,
}); });
})} },
)}
</Box> </Box>
); );

View File

@@ -96,45 +96,52 @@ export default function DataGridHeader<T extends object>({
}} }}
key={column.id} key={column.id}
> >
<Dropdown.Trigger {column.id === 'selection' ? (
className={twMerge(
'focus:outline-none motion-safe:transition-colors',
)}
disabled={
column.isDisabled ||
column.id === 'selection' ||
(column.disableSortBy && !onRemoveColumn)
}
hideChevron
>
<span <span
{...headerProps} {...headerProps}
className="relative grid w-full grid-flow-col items-center justify-between p-2" className="relative grid w-full grid-flow-col items-center justify-between p-2"
> >
{column.render('Header')} {column.render('Header')}
{allowSort && (
<Box component="span" sx={{ color: 'text.primary' }}>
{column.isSorted && !column.isSortedDesc && (
<ArrowUpIcon className="h-3 w-3" />
)}
{column.isSorted && column.isSortedDesc && (
<ArrowDownIcon className="h-3 w-3" />
)}
</Box>
)}
</span> </span>
) : (
{allowResize && !column.disableResizing && ( <Dropdown.Trigger
className={twMerge(
'focus:outline-none motion-safe:transition-colors',
)}
disabled={
column.isDisabled || (column.disableSortBy && !onRemoveColumn)
}
hideChevron
>
<span <span
{...column.getResizerProps({ {...headerProps}
onClick: (event: Event) => event.stopPropagation(), className="relative grid w-full grid-flow-col items-center justify-between p-2"
})} >
className="absolute top-0 bottom-0 -right-0.5 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors" {column.render('Header')}
/>
)} {allowSort && (
</Dropdown.Trigger> <Box component="span" sx={{ color: 'text.primary' }}>
{column.isSorted && !column.isSortedDesc && (
<ArrowUpIcon className="h-3 w-3" />
)}
{column.isSorted && column.isSortedDesc && (
<ArrowDownIcon className="h-3 w-3" />
)}
</Box>
)}
</span>
{allowResize && !column.disableResizing && (
<span
{...column.getResizerProps({
onClick: (event: Event) => event.stopPropagation(),
})}
className="absolute -right-0.5 bottom-0 top-0 z-10 h-full w-1.5 group-hover:bg-slate-900 group-hover:bg-opacity-20 group-active:bg-slate-900 group-active:bg-opacity-20 motion-safe:transition-colors"
/>
)}
</Dropdown.Trigger>
)}
<Dropdown.Content <Dropdown.Content
menu menu

View File

@@ -11,7 +11,6 @@ import { useNotFoundRedirect } from '@/features/projects/common/hooks/useNotFoun
import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes'; import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes';
import { NextSeo } from 'next-seo'; import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export interface ProjectLayoutProps extends AuthenticatedLayoutProps { export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
@@ -48,15 +47,16 @@ function ProjectLayoutContent({
useNotFoundRedirect(); useNotFoundRedirect();
useEffect(() => { // useEffect(() => {
if (isPlatform || !router.isReady) { // if (isPlatform || !router.isReady) {
return; // return;
} // }
if (isRestrictedPath) { // TODO // Double check what restricted path means here
router.push('/local/local'); // if (isRestrictedPath) {
} // router.push('/local/local');
}, [isPlatform, isRestrictedPath, router]); // }
// }, [isPlatform, isRestrictedPath, router]);
if (isRestrictedPath || loading) { if (isRestrictedPath || loading) {
return <LoadingScreen />; return <LoadingScreen />;

View File

@@ -7,6 +7,7 @@ import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem'; import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem'; import { ListItem } from '@/components/ui/v2/ListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -61,6 +62,7 @@ export default function SettingsSidebar({
className, className,
...props ...props
}: SettingsSidebarProps) { }: SettingsSidebarProps) {
const isPlatform = useIsPlatform();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
@@ -95,7 +97,7 @@ export default function SettingsSidebar({
<> <>
<Backdrop <Backdrop
open={expanded} open={expanded}
className="absolute top-0 left-0 bottom-0 right-0 z-[34] md:hidden" className="absolute bottom-0 left-0 right-0 top-0 z-[34] md:hidden"
role="button" role="button"
tabIndex={-1} tabIndex={-1}
onClick={() => setExpanded(false)} onClick={() => setExpanded(false)}
@@ -112,7 +114,7 @@ export default function SettingsSidebar({
<Box <Box
component="aside" component="aside"
className={twMerge( className={twMerge(
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pt-2 pb-17 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none', 'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0', expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
className, className,
)} )}
@@ -181,7 +183,12 @@ export default function SettingsSidebar({
SMTP SMTP
</SettingsNavLink> </SettingsNavLink>
<SettingsNavLink href="/git" exact={false} onClick={handleSelect}> <SettingsNavLink
href="/git"
exact={false}
onClick={handleSelect}
disabled={!isPlatform}
>
Git Git
</SettingsNavLink> </SettingsNavLink>

View File

@@ -1,6 +1,6 @@
import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon'; import { PlusCircleIcon } from '@/components/ui/v2/icons/PlusCircleIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon'; import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import type { ComponentMeta, ComponentStory } from '@storybook/react'; import type { Meta, StoryFn } from '@storybook/react';
import type { ButtonProps } from './Button'; import type { ButtonProps } from './Button';
import Button from './Button'; import Button from './Button';
@@ -24,9 +24,9 @@ export default {
control: { type: 'radio' }, control: { type: 'radio' },
}, },
}, },
} as ComponentMeta<typeof Button>; } as Meta<typeof Button>;
const Template: ComponentStory<typeof Button> = function Template( const Template: StoryFn<ButtonProps> = function TemplateFunction(
args: ButtonProps, args: ButtonProps,
) { ) {
return <Button {...args} />; return <Button {...args} />;

View File

@@ -0,0 +1,164 @@
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
import { ChevronUpIcon } from '@/components/ui/v2/icons/ChevronUpIcon';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastBackgroundColor } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import type { ApolloError } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import { AnimatePresence, motion } from 'framer-motion';
import { useRouter } from 'next/router';
import { useState } from 'react';
interface ErrorDetails {
info: {
projectId: string;
userId: string;
url?: string;
};
error: any;
}
const getInternalErrorMessage = (
error: Error | ApolloError | undefined,
): string | null => {
if (!error) {
return null;
}
if (error.name === 'ApolloError') {
// @ts-ignore
const internalError = error.graphQLErrors?.[0]?.extensions?.internal as {
error: { message: string };
};
return internalError?.error?.message || null;
}
if (error instanceof Error) {
return error.message;
}
return null;
};
const errorToObject = (error: ApolloError | Error) => {
if (error.name === 'ApolloError') {
return error;
}
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
};
}
return {};
};
export default function ErrorToast({
isVisible,
errorMessage,
error,
close,
}: {
isVisible: boolean;
errorMessage: string;
error: ApolloError | Error;
close: () => void;
}) {
const userData = useUserData();
const { asPath } = useRouter();
const [showInfo, setShowInfo] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const errorDetails: ErrorDetails = {
info: {
projectId: currentProject?.id,
userId: userData?.id || 'local',
url: asPath,
},
error: errorToObject(error),
};
const msg = getInternalErrorMessage(error) || errorMessage;
return (
<AnimatePresence>
{isVisible && (
<motion.div
style={{
backgroundColor: getToastBackgroundColor(),
}}
className="flex w-full max-w-xl flex-col space-y-4 rounded-lg p-4 text-white"
initial={{
opacity: 0,
y: 100,
}}
animate={{
opacity: 1,
scale: 1,
y: 0,
}}
exit={{
opacity: 0,
scale: 0,
y: 100,
}}
transition={{
bounce: 0.1,
}}
>
<div className="flex w-full flex-row items-center justify-between space-x-4">
<button onClick={close} type="button" aria-label="Close">
<XIcon className="h-4 w-4 text-white" />
</button>
<span>
{msg ?? 'An unkown error has occured, please try again later!'}
</span>
<button
type="button"
onClick={() => setShowInfo(!showInfo)}
className="flex flex-row items-center justify-center space-x-2 text-white"
>
<span>Info</span>
{showInfo ? (
<ChevronUpIcon className="h-3 w-3 text-white" />
) : (
<ChevronDownIcon className="h-3 w-3 text-white" />
)}
</button>
</div>
{showInfo && (
<div className="flex flex-col space-y-4">
<div className="relative flex flex-col">
<div className="relative flex max-h-[400px] w-full max-w-xl flex-row justify-between overflow-x-auto rounded-lg bg-black p-4">
<pre>{JSON.stringify(errorDetails, null, 2)}</pre>
</div>
<button
type="button"
aria-label="Copy error details"
className="absolute right-2 top-2"
onClick={(event) => {
event.stopPropagation();
copy(
JSON.stringify(errorDetails, null, 2),
'Error details',
);
}}
>
<CopyIcon className="h-4 w-4" />
</button>
</div>
</div>
)}
</motion.div>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1 @@
export { default as ErrorToast } from './ErrorToast';

View File

@@ -1,5 +1,5 @@
import { Option } from '@/components/ui/v2/Option'; import { Option } from '@/components/ui/v2/Option';
import type { ComponentMeta, ComponentStory } from '@storybook/react'; import type { Meta, StoryFn } from '@storybook/react';
import type { SelectProps } from './Select'; import type { SelectProps } from './Select';
import Select from './Select'; import Select from './Select';
@@ -7,11 +7,9 @@ export default {
title: 'UI Library / Select', title: 'UI Library / Select',
component: Select, component: Select,
argTypes: {}, argTypes: {},
} as ComponentMeta<typeof Select>; } as Meta<typeof Select>;
const Template: ComponentStory<typeof Select> = function Template( const Template: StoryFn<SelectProps<any>> = function TemplateFunction(args) {
args: SelectProps<any>,
) {
return ( return (
<Select className="w-64" {...args}> <Select className="w-64" {...args}>
<Option value="value1">Value 1</Option> <Option value="value1">Value 1</Option>

View File

@@ -1,4 +1,4 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react'; import type { Meta, StoryFn } from '@storybook/react';
import type { SwitchProps } from './Switch'; import type { SwitchProps } from './Switch';
import Switch from './Switch'; import Switch from './Switch';
@@ -6,9 +6,9 @@ export default {
title: 'UI Library / Switch', title: 'UI Library / Switch',
component: Switch, component: Switch,
argTypes: {}, argTypes: {},
} as ComponentMeta<typeof Switch>; } as Meta<typeof Switch>;
const Template: ComponentStory<typeof Switch> = function Template( const Template: StoryFn<SwitchProps> = function TemplateFunction(
args: SwitchProps, args: SwitchProps,
) { ) {
return <Switch label="Accept Rules" {...args} />; return <Switch label="Accept Rules" {...args} />;

View File

@@ -4,16 +4,14 @@ import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox'; import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { import {
useDeleteUserAccountMutation, useDeleteUserAccountMutation,
useGetAllWorkspacesAndProjectsQuery, useGetAllWorkspacesAndProjectsQuery,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import { type ApolloError } from '@apollo/client';
import { useSignOut, useUserData } from '@nhost/nextjs'; import { useSignOut, useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
function ConfirmDeleteAccountModal({ function ConfirmDeleteAccountModal({
@@ -44,30 +42,19 @@ function ConfirmDeleteAccountModal({
const onClickConfirm = async () => { const onClickConfirm = async () => {
setLoadingRemove(true); setLoadingRemove(true);
await toast.promise( await execPromiseWithErrorToast(
deleteUserAccount(), async () => {
{ await deleteUserAccount();
loading: 'Deleting your account...', onDelete?.();
success: `The account has been deleted successfully.`, close();
error: (arg: ApolloError) => { },
// we need to get the internal error message from the GraphQL error {
const { internal } = arg.graphQLErrors[0]?.extensions || {}; loadingMessage: 'Deleting your account...',
const { message } = (internal as Record<string, any>)?.error || {}; successMessage: 'The account has been deleted successfully.',
errorMessage:
// we use the default Apollo error message if we can't find the 'An error occurred while deleting your account. Please try again.',
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting your account. Please try again.'
);
},
}, },
getToastStyleProps(),
); );
onDelete?.();
close();
}; };
return ( return (

View File

@@ -0,0 +1,85 @@
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUpdateUserDisplayNameMutation } from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
displayName: Yup.string()
.label('Display Name')
.required('This field is required.'),
});
export type DisplayNameSettingFormValues = Yup.InferType<
typeof validationSchema
>;
export default function DisplayNameSetting() {
const { id: userID, displayName } = useUserData();
const [updateUserDisplayName] = useUpdateUserDisplayNameMutation();
const form = useForm<DisplayNameSettingFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
displayName,
},
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
async function handleSubmit(formValues: DisplayNameSettingFormValues) {
await execPromiseWithErrorToast(
async () => {
await updateUserDisplayName({
variables: {
id: userID,
displayName: formValues.displayName,
},
});
form.reset({ displayName: formValues.displayName });
},
{
loadingMessage: 'Updating your display name...',
successMessage: 'Your display name has been updated successfully.',
errorMessage:
'An error occurred while trying to update your display name. Please try again.',
},
);
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Update your display name"
slotProps={{
submitButton: {
disabled: !isDirty,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row lg:grid-cols-5"
>
<Input
{...register('displayName')}
className="col-span-2"
type="text"
id="display-name"
label="Display Name"
fullWidth
helperText={formState.errors.displayName?.message}
error={Boolean(formState.errors.displayName)}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,80 @@
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useNhostClient, useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
});
export type EmailSettingFormValues = Yup.InferType<typeof validationSchema>;
export default function EmailSetting() {
const nhost = useNhostClient();
const { email } = useUserData();
const form = useForm<EmailSettingFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { email },
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
async function handleSubmit(formValues: EmailSettingFormValues) {
await execPromiseWithErrorToast(
async () => {
await nhost.auth.changeEmail({
newEmail: formValues.email,
options: {
redirectTo: `${window.location.origin}/account`,
},
});
form.reset({ email: formValues.email });
},
{
loadingMessage: 'Updating your email...',
successMessage:
'Please check your inbox. Follow the link to finalize changing your email.',
errorMessage:
'An error occurred while trying to update your email. Please try again.',
},
);
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Update your email"
slotProps={{
submitButton: {
disabled: !isDirty,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row lg:grid-cols-5"
>
<Input
{...register('email')}
className="col-span-2"
id="email"
spellCheck="false"
autoCapitalize="none"
type="email"
label="Email"
hideEmptyHelperText
fullWidth
helperText={formState.errors.email?.message}
error={Boolean(formState.errors.email)}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -15,15 +15,13 @@ import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip'; import { Tooltip } from '@/components/ui/v2/Tooltip';
import { CreatePATForm } from '@/features/account/settings/components/CreatePATForm'; import { CreatePATForm } from '@/features/account/settings/components/CreatePATForm';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getServerError } from '@/utils/getServerError';
import { import {
GetPersonalAccessTokensDocument, GetPersonalAccessTokensDocument,
useDeletePersonalAccessTokenMutation, useDeletePersonalAccessTokenMutation,
useGetPersonalAccessTokensQuery, useGetPersonalAccessTokensQuery,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function PATSettings() { export default function PATSettings() {
@@ -59,28 +57,20 @@ export default function PATSettings() {
async function handleDeletePAT({ async function handleDeletePAT({
id, id,
}: typeof availablePersonalAccessTokens[0]) { }: (typeof availablePersonalAccessTokens)[0]) {
const deletePATPromise = deletePAT({ variables: { patId: id } }); await execPromiseWithErrorToast(
() => deletePAT({ variables: { patId: id } }),
try { {
await toast.promise( loadingMessage: 'Deleting personal access token...',
deletePATPromise, successMessage: 'Personal access token has been deleted successfully.',
{ errorMessage:
loading: 'Deleting personal access token...', 'An error occurred while deleting the personal access token.',
success: 'Personal access token has been deleted successfully.', },
error: getServerError( );
'An error occurred while deleting the personal access token.',
),
},
getToastStyleProps(),
);
} catch {
// Note: The toast will handle the error.
}
} }
function handleConfirmDelete( function handleConfirmDelete(
originalPAT: typeof availablePersonalAccessTokens[0], originalPAT: (typeof availablePersonalAccessTokens)[0],
) { ) {
openAlertDialog({ openAlertDialog({
title: 'Delete Personal Access Token', title: 'Delete Personal Access Token',

View File

@@ -1,12 +1,10 @@
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useChangePassword } from '@nhost/nextjs'; import { useChangePassword } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -38,25 +36,19 @@ export default function PasswordSettings() {
const isDirty = Object.keys(formState.dirtyFields).length > 0; const isDirty = Object.keys(formState.dirtyFields).length > 0;
async function handleSubmit(formValues: PasswordSettingsFormValues) { async function handleSubmit(formValues: PasswordSettingsFormValues) {
try { await execPromiseWithErrorToast(
const changePasswordPromise = changePassword(formValues.newPassword); async () => {
// TODO fix changePassword should throw an error if something happens
await toast.promise( await changePassword(formValues.newPassword);
changePasswordPromise, form.reset();
{ },
loading: 'Changing password...', {
success: 'The password has been changed successfully.', loadingMessage: 'Changing password...',
error: getServerError( successMessage: 'The password has been changed successfully.',
'An error occurred while trying to update the password. Please try again.', errorMessage:
), 'An error occurred while trying to update the password. Please try again.',
}, },
getToastStyleProps(), );
);
form.reset();
} catch {
// Note: The error is handled by the toast.
}
} }
return ( return (

View File

@@ -0,0 +1,76 @@
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { GitHubIcon } from '@/components/ui/v2/icons/GitHubIcon';
import { Text } from '@/components/ui/v2/Text';
import { useGetAuthUserProvidersQuery } from '@/utils/__generated__/graphql';
import { useProviderLink } from '@nhost/nextjs';
import NavLink from 'next/link';
export default function SocialProvidersSettings() {
const { data, loading, error } = useGetAuthUserProvidersQuery();
const isGithubConnected = data?.authUserProviders?.some(
(item) => item.providerId === 'github',
);
const { github } = useProviderLink({
connect: true,
redirectTo: `${window.location.origin}/account`,
});
if (!data && loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading personal access tokens..."
/>
);
}
if (error) {
throw error;
}
return (
<SettingsContainer
title="Authentication providers"
description=""
rootClassName="gap-0 flex flex-col items-start"
className="my-2"
slotProps={{
submitButton: { className: 'hidden' },
footer: { className: 'hidden' },
}}
>
{isGithubConnected ? (
<Box
sx={{ backgroundColor: 'grey.200' }}
className="flex flex-row items-center justify-start space-x-2 rounded-md p-2"
>
<GitHubIcon />
<Text className="font-medium ">Connected</Text>
</Box>
) : (
<Box>
<NavLink
href={github}
passHref
target="_blank"
rel="noreferrer noopener"
legacyBehavior
>
<Button
className=""
variant="outlined"
color="secondary"
startIcon={<GitHubIcon />}
>
Connect with GitHub
</Button>
</NavLink>
</Box>
)}
</SettingsContainer>
);
}

View File

@@ -0,0 +1 @@
export { default as SocialProvidersSettings } from './SocialProvidersSettings';

View File

@@ -0,0 +1,6 @@
query getAuthUserProviders {
authUserProviders {
id
providerId
}
}

View File

@@ -0,0 +1,6 @@
mutation updateUserDisplayName($id: uuid!, $displayName: String!) {
updateUser(pk_columns: { id: $id }, _set: { displayName: $displayName }) {
id
displayName
}
}

View File

@@ -10,26 +10,17 @@ import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip'; import { Tooltip } from '@/components/ui/v2/Tooltip';
import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection'; import { GraphqlDataSourcesFormSection } from '@/features/ai/AssistantForm/components/GraphqlDataSourcesFormSection';
import { WebhooksDataSourcesFormSection } from '@/features/ai/AssistantForm/components/WebhooksDataSourcesFormSection'; import { WebhooksDataSourcesFormSection } from '@/features/ai/AssistantForm/components/WebhooksDataSourcesFormSection';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getHasuraAdminSecret } from '@/utils/env';
import { removeTypename, type DeepRequired } from '@/utils/helpers'; import { removeTypename, type DeepRequired } from '@/utils/helpers';
import { import {
useInsertAssistantMutation, useInsertAssistantMutation,
useUpdateAssistantMutation, useUpdateAssistantMutation,
} from '@/utils/__generated__/graphite.graphql'; } from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export const validationSchema = Yup.object({ export const validationSchema = Yup.object({
@@ -101,32 +92,15 @@ export default function AssistantForm({
}: AssistantFormProps) { }: AssistantFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject(); const { adminClient } = useAdminApolloClient();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [insertAssistantMutation] = useInsertAssistantMutation({ const [insertAssistantMutation] = useInsertAssistantMutation({
client, client: adminClient,
}); });
const [updateAssistantMutation] = useUpdateAssistantMutation({ client }); const [updateAssistantMutation] = useUpdateAssistantMutation({
client: adminClient,
});
const form = useForm<AssistantFormValues>({ const form = useForm<AssistantFormValues>({
defaultValues: initialData, defaultValues: initialData,
@@ -186,33 +160,18 @@ export default function AssistantForm({
const handleSubmit = async ( const handleSubmit = async (
values: DeepRequired<AssistantFormValues> & { assistantID: string }, values: DeepRequired<AssistantFormValues> & { assistantID: string },
) => { ) => {
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
createOrUpdateAutoEmbeddings(values), await createOrUpdateAutoEmbeddings(values);
{ onSubmit?.();
loading: 'Configuring the Assistant...', },
success: `The Assistant has been configured successfully.`, {
error: (arg: ApolloError) => { loadingMessage: 'Configuring the Assistant...',
// we need to get the internal error message from the GraphQL error successMessage: 'The Assistant has been configured successfully.',
const { internal } = arg.graphQLErrors[0]?.extensions || {}; errorMessage:
const { message } = (internal as Record<string, any>)?.error || {}; 'An error occurred while configuring the Assistant. Please try again.',
},
// we use the default Apollo error message if we can't find the );
// internal error message
return (
message ||
arg.message ||
'An error occurred while configuring the Assistant. Please try again.'
);
},
},
getToastStyleProps(),
);
onSubmit?.();
} catch {
// Note: The toast will handle the error.
}
}; };
return ( return (

View File

@@ -1,4 +1,5 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { Box } from '@/components/ui/v2/Box'; import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
@@ -6,34 +7,33 @@ import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon'; import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon'; import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip'; import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getHasuraAdminSecret } from '@/utils/env';
import { import {
useInsertGraphiteAutoEmbeddingsConfigurationMutation, useInsertGraphiteAutoEmbeddingsConfigurationMutation,
useUpdateGraphiteAutoEmbeddingsConfigurationMutation, useUpdateGraphiteAutoEmbeddingsConfigurationMutation,
} from '@/utils/__generated__/graphite.graphql'; } from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const AUTO_EMBEDDINGS_MODELS = [
'text-embedding-ada-002',
'text-embedding-3-small',
'text-embedding-3-large',
];
export const validationSchema = Yup.object({ export const validationSchema = Yup.object({
name: Yup.string().required('The name is required.'), name: Yup.string().required('The name field is required.'),
schemaName: Yup.string().required('The schema is required'), model: Yup.string().oneOf(AUTO_EMBEDDINGS_MODELS),
tableName: Yup.string().required('The table is required'), schemaName: Yup.string().required('The schema field is required'),
columnName: Yup.string().required('The column is required'), tableName: Yup.string().required('The table field is required'),
columnName: Yup.string().required('The column field is required'),
query: Yup.string(), query: Yup.string(),
mutation: Yup.string(), mutation: Yup.string(),
}); });
@@ -49,7 +49,7 @@ export interface AutoEmbeddingsFormProps extends DialogFormProps {
/** /**
* if there is initialData then it's an update operation * if there is initialData then it's an update operation
*/ */
initialData?: AutoEmbeddingsFormValues; initialData?: AutoEmbeddingsFormValues & { model: string };
/** /**
* Function to be called when the operation is cancelled. * Function to be called when the operation is cancelled.
@@ -70,37 +70,23 @@ export default function AutoEmbeddingsForm({
}: AutoEmbeddingsFormProps) { }: AutoEmbeddingsFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject(); const { adminClient } = useAdminApolloClient();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [insertGraphiteAutoEmbeddingsConfiguration] = const [insertGraphiteAutoEmbeddingsConfiguration] =
useInsertGraphiteAutoEmbeddingsConfigurationMutation({ useInsertGraphiteAutoEmbeddingsConfigurationMutation({
client, client: adminClient,
}); });
const [updateGraphiteAutoEmbeddingsConfiguration] = const [updateGraphiteAutoEmbeddingsConfiguration] =
useUpdateGraphiteAutoEmbeddingsConfigurationMutation({ client }); useUpdateGraphiteAutoEmbeddingsConfigurationMutation({
client: adminClient,
});
const form = useForm<AutoEmbeddingsFormValues>({ const form = useForm<AutoEmbeddingsFormValues>({
defaultValues: initialData, defaultValues: {
...initialData,
model: initialData?.model ?? 'text-embedding-ada-002',
},
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
@@ -132,47 +118,34 @@ export default function AutoEmbeddingsForm({
} }
await insertGraphiteAutoEmbeddingsConfiguration({ await insertGraphiteAutoEmbeddingsConfiguration({
variables: values, variables: {
...values,
},
}); });
}; };
const handleSubmit = async (values: AutoEmbeddingsFormValues) => { const handleSubmit = async (values: AutoEmbeddingsFormValues) => {
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
createOrUpdateAutoEmbeddings(values), await createOrUpdateAutoEmbeddings(values);
{ onSubmit?.();
loading: 'Configuring the Auto-Embeddings...', },
success: `The Auto-Embeddings has been configured successfully.`, {
error: (arg: ApolloError) => { loadingMessage: 'Configuring the Auto-Embeddings...',
// we need to get the internal error message from the GraphQL error successMessage: 'The Auto-Embeddings has been configured successfully.',
const { internal } = arg.graphQLErrors[0]?.extensions || {}; errorMessage:
const { message } = (internal as Record<string, any>)?.error || {}; 'An error occurred while configuring the Auto-Embeddings. Please try again.',
},
// we use the default Apollo error message if we can't find the );
// internal error message
return (
message ||
arg.message ||
'An error occurred while configuring the Auto-Embeddings. Please try again.'
);
},
},
getToastStyleProps(),
);
onSubmit?.();
} catch {
// Note: The toast will handle the error.
}
}; };
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex h-full flex-col gap-4 overflow-hidden" className="flex flex-col h-full gap-4 overflow-hidden"
> >
<div className="flex flex-1 flex-col space-y-6 overflow-auto px-6"> <div className="flex flex-col flex-1 px-6 space-y-6 overflow-auto">
<Input <Input
{...register('name')} {...register('name')}
id="name" id="name"
@@ -182,7 +155,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Name of the Auto-Embeddings"> <Tooltip title="Name of the Auto-Embeddings">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -196,6 +169,36 @@ export default function AutoEmbeddingsForm({
autoComplete="off" autoComplete="off"
autoFocus autoFocus
/> />
<ControlledSelect
slotProps={{
popper: { disablePortal: false, className: 'z-[10000]' },
}}
id="model"
name="model"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Model</Text>
<Tooltip title="Auto-Embeddings Model">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
</Box>
}
fullWidth
error={!!errors?.model?.message}
helperText={errors?.model?.message}
>
{AUTO_EMBEDDINGS_MODELS.map((model) => (
<Option key={model} value={model}>
{model}
</Option>
))}
</ControlledSelect>
<Input <Input
{...register('schemaName')} {...register('schemaName')}
id="schemaName" id="schemaName"
@@ -205,7 +208,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title={<span>Schema where the table belongs to</span>}> <Tooltip title={<span>Schema where the table belongs to</span>}>
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -227,7 +230,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Table Name"> <Tooltip title="Table Name">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -249,7 +252,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Column name"> <Tooltip title="Column name">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -271,7 +274,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Query"> <Tooltip title="Query">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -295,7 +298,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Mutation"> <Tooltip title="Mutation">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -312,7 +315,7 @@ export default function AutoEmbeddingsForm({
/> />
</div> </div>
<Box className="flex w-full flex-row justify-between rounded border-t px-6 py-4"> <Box className="flex flex-row justify-between w-full px-6 py-4 border-t rounded">
<Button variant="outlined" color="secondary" onClick={onCancel}> <Button variant="outlined" color="secondary" onClick={onCancel}>
Cancel Cancel
</Button> </Button>

View File

@@ -2,20 +2,11 @@ import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox'; import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env';
import { useDeleteAssistantMutation } from '@/utils/__generated__/graphite.graphql'; import { useDeleteAssistantMutation } from '@/utils/__generated__/graphite.graphql';
import {
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants'; import { type Assistant } from 'pages/[workspaceSlug]/[appSlug]/ai/assistants';
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export interface DeleteAssistantModalProps { export interface DeleteAssistantModalProps {
@@ -32,29 +23,10 @@ export default function DeleteAssistantModal({
const [remove, setRemove] = useState(false); const [remove, setRemove] = useState(false);
const [loadingRemove, setLoadingRemove] = useState(false); const [loadingRemove, setLoadingRemove] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject(); const { adminClient } = useAdminApolloClient();
const serviceUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'graphql',
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: serviceUrl,
headers: {
'x-hasura-admin-secret':
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentProject?.config?.hasura.adminSecret,
},
}),
});
const [deleteAssistantMutation] = useDeleteAssistantMutation({ const [deleteAssistantMutation] = useDeleteAssistantMutation({
client, client: adminClient,
}); });
const deleteAssistant = async () => { const deleteAssistant = async () => {
@@ -70,27 +42,12 @@ export default function DeleteAssistantModal({
async function handleClick() { async function handleClick() {
setLoadingRemove(true); setLoadingRemove(true);
await toast.promise( await execPromiseWithErrorToast(deleteAssistant, {
deleteAssistant(), loadingMessage: 'Deleting the assistant...',
{ successMessage: 'The Assistant has been deleted successfully.',
loading: 'Deleting the assistant...', errorMessage:
success: `The Assistant has been deleted successfully.`, 'An error occurred while deleting the Assistant. Please try again.',
error: (arg: ApolloError) => { });
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting the Assistant. Please try again.'
);
},
},
getToastStyleProps(),
);
} }
return ( return (

View File

@@ -4,18 +4,12 @@ import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env'; import { getHasuraAdminSecret } from '@/utils/env';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useDeleteGraphiteAutoEmbeddingsConfigurationMutation } from '@/utils/__generated__/graphite.graphql'; import { useDeleteGraphiteAutoEmbeddingsConfigurationMutation } from '@/utils/__generated__/graphite.graphql';
import { import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
ApolloClient,
HttpLink,
InMemoryCache,
type ApolloError,
} from '@apollo/client';
import { type AutoEmbeddingsConfiguration } from 'pages/[workspaceSlug]/[appSlug]/ai/auto-embeddings'; import { type AutoEmbeddingsConfiguration } from 'pages/[workspaceSlug]/[appSlug]/ai/auto-embeddings';
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export interface DeleteAutoEmbeddingsModalProps { export interface DeleteAutoEmbeddingsModalProps {
@@ -71,27 +65,13 @@ export default function DeleteAutoEmbeddingsModal({
async function handleClick() { async function handleClick() {
setLoadingRemove(true); setLoadingRemove(true);
await toast.promise( await execPromiseWithErrorToast(deleteAutoEmbeddingsConfig, {
deleteAutoEmbeddingsConfig(), loadingMessage: 'Deleting Auto-Embeddings Configuration...',
{ successMessage:
loading: 'Deleting Auto-Embeddings Configuration...', 'The Auto-Embeddings Configuration has been deleted successfully.',
success: `The Auto-Embeddings Configuration has been deleted successfully.`, errorMessage:
error: (arg: ApolloError) => { 'An error occurred while deleting the Auto-Embeddings Configuration. Please try again.',
// we need to get the internal error message from the GraphQL error });
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting the Auto-Embeddings Configuration. Please try again.'
);
},
},
getToastStyleProps(),
);
} }
return ( return (

View File

@@ -2,6 +2,7 @@ import { UpgradeToProBanner } from '@/components/common/UpgradeToProBanner';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert'; import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box'; import { Box } from '@/components/ui/v2/Box';
import { ErrorToast } from '@/components/ui/v2/ErrorToast';
import { IconButton } from '@/components/ui/v2/IconButton'; import { IconButton } from '@/components/ui/v2/IconButton';
import { ArrowUpIcon } from '@/components/ui/v2/icons/ArrowUpIcon'; import { ArrowUpIcon } from '@/components/ui/v2/icons/ArrowUpIcon';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
@@ -17,7 +18,6 @@ import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminA
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsGraphiteEnabled } from '@/features/projects/common/hooks/useIsGraphiteEnabled'; import { useIsGraphiteEnabled } from '@/features/projects/common/hooks/useIsGraphiteEnabled';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform'; import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { getToastStyleProps } from '@/utils/constants/settings';
import { import {
useSendDevMessageMutation, useSendDevMessageMutation,
useStartDevSessionMutation, useStartDevSessionMutation,
@@ -122,11 +122,17 @@ export default function DevAssistant() {
setMessages(thread); setMessages(thread);
} catch (error) { } catch (error) {
toast.error( toast.custom(
'Failed to send the message to graphite. Please try again later.', (t) => (
<ErrorToast
isVisible={t.visible}
errorMessage="Failed to send the message. Please try again later."
error={error}
close={() => toast.dismiss()}
/>
),
{ {
style: getToastStyleProps().style, duration: Number.POSITIVE_INFINITY,
...getToastStyleProps().error,
}, },
); );
} finally { } finally {

View File

@@ -24,7 +24,7 @@ function PreComponent(
const { children } = props; const { children } = props;
return ( return (
<div className="group relative"> <div className="relative group">
<pre>{children}</pre> <pre>{children}</pre>
<IconButton <IconButton
sx={{ sx={{
@@ -34,13 +34,13 @@ function PreComponent(
}} }}
color="warning" color="warning"
variant="contained" variant="contained"
className="absolute top-2 right-2 hidden group-hover:flex" className="absolute hidden top-2 right-2 group-hover:flex"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
copy(onlyText(children), 'Snippet'); copy(onlyText(children), 'Snippet');
}} }}
> >
<CopyIcon className="h-5 w-5" /> <CopyIcon className="w-5 h-5" />
</IconButton> </IconButton>
</div> </div>
); );
@@ -53,7 +53,7 @@ export default function MessageBox({ message }: { message: Message }) {
return ( return (
<Box <Box
className="flex flex-col space-y-4 border-t p-4 first:border-t-0" className="flex flex-col p-4 space-y-4 border-t first:border-t-0"
sx={{ sx={{
backgroundColor: isUserMessage && 'background.default', backgroundColor: isUserMessage && 'background.default',
}} }}
@@ -67,7 +67,7 @@ export default function MessageBox({ message }: { message: Message }) {
) : ( ) : (
<> <>
<Avatar <Avatar
className="h-7 w-7 rounded-full" className="rounded-full h-7 w-7"
alt={user?.displayName} alt={user?.displayName}
src={user?.avatarUrl} src={user?.avatarUrl}
> >

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete'; import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
@@ -12,6 +13,7 @@ import { Switch } from '@/components/ui/v2/Switch';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip'; import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema'; import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { ComputeFormSection } from '@/features/services/components/ServiceForm/components/ComputeFormSection'; import { ComputeFormSection } from '@/features/services/components/ServiceForm/components/ComputeFormSection';
import { import {
@@ -20,9 +22,10 @@ import {
useGetSoftwareVersionsQuery, useGetSoftwareVersionsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common'; import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
@@ -30,6 +33,8 @@ import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { DisableAIServiceConfirmationDialog } from './DisableAIServiceConfirmationDialog'; import { DisableAIServiceConfirmationDialog } from './DisableAIServiceConfirmationDialog';
const MIN_POSTGRES_VERSION_SUPPORTING_AI = '14.6-20231018-1';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
version: Yup.object({ version: Yup.object({
label: Yup.string().required(), label: Yup.string().required(),
@@ -48,21 +53,28 @@ const validationSchema = Yup.object({
export type AISettingsFormValues = Yup.InferType<typeof validationSchema>; export type AISettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function AISettings() { export default function AISettings() {
const { maintenanceActive } = useUI(); const isPlatform = useIsPlatform();
const { openDialog } = useDialog(); const { openDialog } = useDialog();
const [updateConfig] = useUpdateConfigMutation(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [aiServiceEnabled, setAIServiceEnabled] = useState(true); const [aiServiceEnabled, setAIServiceEnabled] = useState(true);
const { const {
data: { config: { ai } = {} } = {}, data: {
config: { ai, postgres: { version: postgresVersion } = {} } = {},
} = {},
loading: loadingAiSettings, loading: loadingAiSettings,
error: errorGettingAiSettings, error: errorGettingAiSettings,
} = useGetAiSettingsQuery({ } = useGetAiSettingsQuery({
variables: { variables: {
appId: currentProject.id, appId: currentProject.id,
}, },
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data: graphiteVersionsData, loading: loadingGraphiteVersionsData } = const { data: graphiteVersionsData, loading: loadingGraphiteVersionsData } =
@@ -70,6 +82,7 @@ export default function AISettings() {
variables: { variables: {
software: Software_Type_Enum.Graphite, software: Software_Type_Enum.Graphite,
}, },
skip: !isPlatform,
}); });
const graphiteVersions = graphiteVersionsData?.softwareVersions || []; const graphiteVersions = graphiteVersionsData?.softwareVersions || [];
@@ -94,8 +107,8 @@ export default function AISettings() {
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
defaultValues: { defaultValues: {
version: { version: {
label: ai?.version ?? availableVersions?.at(0)?.label, label: ai?.version || availableVersions?.at(0)?.label || '',
value: ai?.version ?? availableVersions?.at(0)?.value, value: ai?.version || availableVersions?.at(0)?.value || '',
}, },
webhookSecret: '', webhookSecret: '',
organization: '', organization: '',
@@ -152,6 +165,17 @@ export default function AISettings() {
]); ]);
const toggleAIService = async (enabled: boolean) => { const toggleAIService = async (enabled: boolean) => {
if (postgresVersion < MIN_POSTGRES_VERSION_SUPPORTING_AI) {
toast.error(
'In order to enable the AI service you need to update your database version to 14.6-20231018-1 or newer.',
{
style: getToastStyleProps().style,
...getToastStyleProps().error,
},
);
return;
}
setAIServiceEnabled(enabled); setAIServiceEnabled(enabled);
if (!enabled && ai) { if (!enabled && ai) {
@@ -182,9 +206,9 @@ export default function AISettings() {
} }
async function handleSubmit(formValues: AISettingsFormValues) { async function handleSubmit(formValues: AISettingsFormValues) {
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfig({ await updateConfig({
variables: { variables: {
appId: currentProject.id, appId: currentProject.id,
config: { config: {
@@ -207,21 +231,29 @@ export default function AISettings() {
}, },
}, },
}, },
}), });
{
loading: `AI settings are being updated...`,
success: `AI settings has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the AI settings!`,
),
},
getToastStyleProps(),
);
form.reset(formValues); form.reset(formValues);
} catch {
// Note: The toast will handle the error. if (!isPlatform) {
} openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'AI settings are being updated...',
successMessage: 'AI settings has been updated successfully.',
errorMessage:
'An error occurred while trying to update the AI settings!',
},
);
} }
const getAIResourcesCost = () => { const getAIResourcesCost = () => {
@@ -236,7 +268,7 @@ export default function AISettings() {
return ( return (
<Box className="space-y-4" sx={{ backgroundColor: 'background.default' }}> <Box className="space-y-4" sx={{ backgroundColor: 'background.default' }}>
<Box className="flex flex-row items-center justify-between rounded-lg border-1 p-4"> <Box className="flex flex-row items-center justify-between p-4 rounded-lg border-1">
<Text className="text-lg font-semibold">Enable AI service</Text> <Text className="text-lg font-semibold">Enable AI service</Text>
<Switch <Switch
checked={aiServiceEnabled} checked={aiServiceEnabled}
@@ -259,54 +291,58 @@ export default function AISettings() {
className="flex flex-col" className="flex flex-col"
> >
<Box className="space-y-4"> <Box className="space-y-4">
{availableVersions.length > 0 && ( <Box className="space-y-2">
<Box className="space-y-2"> <Box className="flex flex-row items-center space-x-2">
<Box className="flex flex-row items-center space-x-2"> <Text className="text-lg font-semibold">Version</Text>
<Text className="text-lg font-semibold">Version</Text> <Tooltip title="Version of the service to use.">
<Tooltip title="Version of the service to use."> <InfoIcon
<InfoIcon aria-label="Info"
aria-label="Info" className="w-4 h-4"
className="h-4 w-4" color="primary"
color="primary" />
/> </Tooltip>
</Tooltip>
</Box>
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
}
});
const result = [...matched, ...otherOptions];
return result;
}}
fullWidth
className="col-span-4"
options={availableVersions}
error={!!formState.errors?.version?.message}
helperText={formState.errors?.version?.message}
showCustomOption="auto"
customOptionLabel={(value) =>
`Use custom value: "${value}"`
}
/>
</Box> </Box>
)} <ControlledAutocomplete
id="version"
name="version"
autoHighlight
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
}
});
const result = [...matched, ...otherOptions];
return result;
}}
fullWidth
className="col-span-4"
options={availableVersions}
error={
!!formState.errors?.version?.message ||
!!formState.errors?.version?.value?.message
}
helperText={
formState.errors?.version?.message ||
formState.errors?.version?.value?.message
}
showCustomOption="auto"
customOptionLabel={(value) =>
`Use custom value: "${value}"`
}
/>
</Box>
<Box className="space-y-2"> <Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2"> <Box className="flex flex-row items-center space-x-2">
@@ -316,7 +352,7 @@ export default function AISettings() {
<Tooltip title="Used to validate requests between postgres and the AI service. The AI service will also include the header X-Graphite-Webhook-Secret with this value set when calling external webhooks so the source of the request can be validated."> <Tooltip title="Used to validate requests between postgres and the AI service. The AI service will also include the header X-Graphite-Webhook-Secret with this value set when calling external webhooks so the source of the request can be validated.">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -340,26 +376,28 @@ export default function AISettings() {
<Tooltip title="Dedicated resources allocated for the service."> <Tooltip title="Dedicated resources allocated for the service.">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
</Box> </Box>
<Alert {isPlatform ? (
severity="info" <Alert
className="flex items-center justify-between space-x-2" severity="info"
> className="flex items-center justify-between space-x-2"
<span>{getAIResourcesCost()}</span> >
<b> <span>{getAIResourcesCost()}</span>
$ <b>
{parseFloat( $
( {parseFloat(
aiSettingsFormValues.compute.cpu * COST_PER_VCPU (
).toFixed(2), aiSettingsFormValues.compute.cpu * COST_PER_VCPU
)} ).toFixed(2),
</b> )}
</Alert> </b>
</Alert>
) : null}
<ComputeFormSection /> <ComputeFormSection />
</Box> </Box>
@@ -378,7 +416,7 @@ export default function AISettings() {
<Tooltip title="Key to use for authenticating API requests to OpenAI"> <Tooltip title="Key to use for authenticating API requests to OpenAI">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -401,7 +439,7 @@ export default function AISettings() {
<Tooltip title="Optional. OpenAI organization to use."> <Tooltip title="Optional. OpenAI organization to use.">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>
@@ -429,7 +467,7 @@ export default function AISettings() {
<Tooltip title="How often to run the job that keeps embeddings up to date."> <Tooltip title="How often to run the job that keeps embeddings up to date.">
<InfoIcon <InfoIcon
aria-label="Info" aria-label="Info"
className="h-4 w-4" className="w-4 h-4"
color="primary" color="primary"
/> />
</Tooltip> </Tooltip>

View File

@@ -1,13 +1,14 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box'; import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button'; import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useUpdateConfigMutation } from '@/utils/__generated__/graphql'; import { useUpdateConfigMutation } from '@/utils/__generated__/graphql';
import type { ApolloError } from '@apollo/client';
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export interface DisableAIServiceConfirmationDialogProps { export interface DisableAIServiceConfirmationDialogProps {
@@ -25,46 +26,50 @@ export default function DisableAIServiceConfirmationDialog({
onCancel, onCancel,
onServiceDisabled, onServiceDisabled,
}: DisableAIServiceConfirmationDialogProps) { }: DisableAIServiceConfirmationDialogProps) {
const { closeDialog } = useDialog(); const isPlatform = useIsPlatform();
const { openDialog, closeDialog } = useDialog();
const localMimirClient = useLocalMimirClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
const [updateConfig] = useUpdateConfigMutation(); ...(!isPlatform ? { client: localMimirClient } : {}),
});
async function handleClick() { async function handleClick() {
setLoading(true); setLoading(true);
await toast.promise( await execPromiseWithErrorToast(
updateConfig({ async () => {
variables: { await updateConfig({
appId: currentProject.id, variables: {
config: { appId: currentProject.id,
ai: null, config: {
ai: null,
},
}, },
}, });
}),
{
loading: 'Disabling the AI service...',
success: () => {
onServiceDisabled();
closeDialog();
return `The service has been disabled.`;
},
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the onServiceDisabled();
// internal error message closeDialog();
return (
message || if (!isPlatform) {
arg.message || openDialog({
'An error occurred while disabling the AI service. Please try again later.' title: 'Apply your changes',
); component: <ApplyLocalSettingsDialog />,
}, props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Disabling the AI service...',
successMessage: 'The service has been disabled.',
errorMessage:
'An error occurred while disabling the AI service. Please try again later.',
}, },
getToastStyleProps(),
); );
} }

View File

@@ -1,5 +1,8 @@
query GetAISettings($appId: uuid!) { query GetAISettings($appId: uuid!) {
config(appID: $appId, resolve: false) { config(appID: $appId, resolve: false) {
postgres {
version
}
ai { ai {
version version
webhookSecret webhookSecret

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -28,15 +31,19 @@ export type AllowedEmailSettingsFormValues = Yup.InferType<
>; >;
export default function AllowedEmailDomainsSettings() { export default function AllowedEmailDomainsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { email, emailDomains } = data?.config?.auth?.user || {}; const { email, emailDomains } = data?.config?.auth?.user || {};
@@ -56,6 +63,17 @@ export default function AllowedEmailDomainsSettings() {
const isDirty = Object.keys(formState.dirtyFields).length > 0; const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (!loading && email && emailDomains) {
form.reset({
enabled:
email?.allowed?.length > 0 || emailDomains?.allowed?.length > 0,
allowedEmails: email?.allowed?.join(', ') || '',
allowedEmailDomains: emailDomains?.allowed?.join(', ') || '',
});
}
}, [loading, form, email, emailDomains]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -103,21 +121,31 @@ export default function AllowedEmailDomainsSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Allowed email settings are being updated...`,
success: `Allowed email settings have been updated successfully.`, if (!isPlatform) {
error: getServerError( openDialog({
`An error occurred while trying to update the project's allowed email settings.`, title: 'Apply your changes',
), component: <ApplyLocalSettingsDialog />,
}, props: {
getToastStyleProps(), PaperProps: {
); className: 'max-w-2xl',
} catch { },
// Note: The toast will handle the error },
} });
}
},
{
loadingMessage: 'Allowed email settings are being updated...',
successMessage:
'Allowed email settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's allowed email settings.",
},
);
form.reset(values); form.reset(values);
}; };
@@ -134,7 +162,7 @@ export default function AllowedEmailDomainsSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication#allowed-emails-and-domains" docsLink="https://docs.nhost.io/guides/auth/overview#allowed-emails-and-domains"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -25,15 +28,19 @@ export type AllowedRedirectURLFormValues = Yup.InferType<
>; >;
export default function AllowedRedirectURLsSettings() { export default function AllowedRedirectURLsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { allowedUrls } = data?.config?.auth?.redirections || {}; const { allowedUrls } = data?.config?.auth?.redirections || {};
@@ -46,6 +53,14 @@ export default function AllowedRedirectURLsSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && allowedUrls) {
form.reset({
allowedUrls: allowedUrls?.join(', ') || '',
});
}
}, [loading, allowedUrls, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -80,23 +95,31 @@ export default function AllowedRedirectURLsSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Allowed redirect URL settings are being updated...`,
success: `Allowed redirect URL settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's allowed redirect URL settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Allowed redirect URL settings are being updated...',
successMessage:
'Allowed redirect URL settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's allowed redirect URL settings.",
},
);
}; };
return ( return (
@@ -111,7 +134,7 @@ export default function AllowedRedirectURLsSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication#allowed-redirect-urls" docsLink="https://docs.nhost.io/guides/auth/overview#allowed-redirect-urls"
className="grid grid-flow-row px-4 lg:grid-cols-5" className="grid grid-flow-row px-4 lg:grid-cols-5"
> >
<Input <Input

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -22,15 +25,19 @@ const validationSchema = Yup.object({
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>; export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
export default function AnonymousSignInSettings() { export default function AnonymousSignInSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enabled } = data?.config?.auth?.method?.anonymous || {}; const { enabled } = data?.config?.auth?.method?.anonymous || {};
@@ -43,6 +50,12 @@ export default function AnonymousSignInSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({ enabled });
}
}, [loading, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -73,23 +86,31 @@ export default function AnonymousSignInSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Anonymous sign-in settings are being updated...`,
success: `Anonymous sign-in settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Anonymous sign-in settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Anonymous sign-in settings are being updated...',
successMessage:
'Anonymous sign-in settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update Anonymous sign-in settings.',
},
);
}; };
return ( return (

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -7,19 +9,20 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment'; import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -55,15 +58,19 @@ export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function AppleProviderSettings() { export default function AppleProviderSettings() {
const theme = useTheme(); const theme = useTheme();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, enabled, keyId, privateKey, teamId } = const { clientId, enabled, keyId, privateKey, teamId } =
@@ -81,6 +88,18 @@ export default function AppleProviderSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
teamId: teamId || '',
keyId: keyId || '',
clientId: clientId || '',
privateKey: privateKey || '',
enabled: enabled || false,
});
}
}, [loading, teamId, keyId, clientId, privateKey, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -117,23 +136,30 @@ export default function AppleProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Apple settings are being updated...`,
success: `Apple settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Apple settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Apple settings are being updated...',
successMessage: 'Apple settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Apple settings.",
},
);
} }
return ( return (
@@ -148,7 +174,7 @@ export default function AppleProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication/sign-in-with-apple" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-apple"
docsTitle="how to sign in users with Apple" docsTitle="how to sign in users with Apple"
icon={ icon={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
@@ -158,7 +184,7 @@ export default function AppleProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -243,7 +269,7 @@ export default function AppleProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete'; import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
Software_Type_Enum, Software_Type_Enum,
@@ -11,11 +14,11 @@ import {
useGetSoftwareVersionsQuery, useGetSoftwareVersionsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -30,21 +33,26 @@ export type AuthServiceVersionFormValues = Yup.InferType<
>; >;
export default function AuthServiceVersionSettings() { export default function AuthServiceVersionSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data: authVersionsData } = useGetSoftwareVersionsQuery({ const { data: authVersionsData } = useGetSoftwareVersionsQuery({
variables: { variables: {
software: Software_Type_Enum.Auth, software: Software_Type_Enum.Auth,
}, },
skip: !isPlatform,
}); });
const { version } = data?.config?.auth || {}; const { version } = data?.config?.auth || {};
@@ -61,10 +69,21 @@ export default function AuthServiceVersionSettings() {
const form = useForm<AuthServiceVersionFormValues>({ const form = useForm<AuthServiceVersionFormValues>({
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } }, defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && version) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -95,23 +114,29 @@ export default function AuthServiceVersionSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Auth version is being updated...`,
success: `Auth version has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Auth version.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Auth version is being updated...',
successMessage: 'Auth version has been updated successfully.',
errorMessage: 'An error occurred while trying to update Auth version.',
},
);
}; };
return ( return (
@@ -128,12 +153,20 @@ export default function AuthServiceVersionSettings() {
}} }}
docsLink="https://github.com/nhost/hasura-auth/releases" docsLink="https://github.com/nhost/hasura-auth/releases"
docsTitle="the latest releases" docsTitle="the latest releases"
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5" className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
> >
<ControlledAutocomplete <ControlledAutocomplete
id="version" id="version"
name="version" name="version"
autoHighlight autoHighlight
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false} isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase(); const inputValueLower = inputValue.toLowerCase();

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -8,18 +10,19 @@ import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment'; import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { BaseProviderSettings } from '@/features/authentication/settings/components/BaseProviderSettings'; import { BaseProviderSettings } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -48,15 +51,19 @@ const validationSchema = Yup.object({
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>; export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function AzureADProviderSettings() { export default function AzureADProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, tenant, enabled } = const { clientId, clientSecret, tenant, enabled } =
@@ -73,6 +80,17 @@ export default function AzureADProviderSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
tenant: tenant || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, tenant, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -106,23 +124,30 @@ export default function AzureADProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Azure AD settings are being updated...`,
success: `Azure AD settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Azure AD settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Azure AD settings are being updated...',
successMessage: 'Azure AD settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Azure AD settings.",
},
);
} }
return ( return (
@@ -141,7 +166,7 @@ export default function AzureADProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid grid-flow-row grid-cols-2 gap-y-4 gap-x-3 px-4 py-2', 'grid grid-flow-row grid-cols-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -189,7 +214,7 @@ export default function AzureADProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -18,12 +18,10 @@ import {
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function BitbucketProviderSettings() { export default function BitbucketProviderSettings() {
@@ -84,23 +82,18 @@ export default function BitbucketProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Bitbucket settings are being updated...`, },
success: `Bitbucket settings have been updated successfully.`, {
error: getServerError( loadingMessage: 'Bitbucket settings are being updated...',
`An error occurred while trying to update the project's Bitbucket settings.`, successMessage: 'Bitbucket settings have been updated successfully.',
), errorMessage:
}, "An error occurred while trying to update the project's Bitbucket settings.",
getToastStyleProps(), },
); );
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
} }
return ( return (
@@ -119,7 +112,7 @@ export default function BitbucketProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -26,15 +29,19 @@ const validationSchema = Yup.object({
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>; export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
export default function BlockedEmailSettings() { export default function BlockedEmailSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { email, emailDomains } = data?.config?.auth?.user || {}; const { email, emailDomains } = data?.config?.auth?.user || {};
@@ -53,6 +60,17 @@ export default function BlockedEmailSettings() {
const enabled = watch('enabled'); const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0; const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (!loading && email && emailDomains) {
form.reset({
enabled:
email?.blocked?.length > 0 || emailDomains?.blocked?.length > 0,
blockedEmails: email?.blocked?.join(', ') || '',
blockedEmailDomains: emailDomains?.blocked?.join(', ') || '',
});
}
}, [loading, email, emailDomains, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -110,23 +128,32 @@ export default function BlockedEmailSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Blocked email and domain settings are being updated...`,
success: `Blocked email and domain settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's blocked email and domain settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage:
'Blocked email and domain settings are being updated...',
successMessage:
'Blocked email and domain settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's blocked email and domain settings.",
},
);
}; };
return ( return (
@@ -141,7 +168,7 @@ export default function BlockedEmailSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication#blocked-emails-and-domains" docsLink="https://docs.nhost.io/guides/auth/overview#allowed-emails-and-domains"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -23,15 +26,19 @@ const validationSchema = Yup.object({
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>; export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
export default function ClientURLSettings() { export default function ClientURLSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientUrl, allowedUrls } = data?.config?.auth?.redirections || {}; const { clientUrl, allowedUrls } = data?.config?.auth?.redirections || {};
@@ -44,6 +51,12 @@ export default function ClientURLSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && clientUrl) {
form.reset({ clientUrl });
}
}, [loading, clientUrl, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -75,23 +88,30 @@ export default function ClientURLSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Client URL is being updated...`,
success: `Client URL has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Client URL.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Client URL is being updated...',
successMessage: 'Client URL has been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Client URL.",
},
);
}; };
return ( return (
@@ -106,7 +126,7 @@ export default function ClientURLSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication#client-url" docsLink="https://docs.nhost.io/guides/auth/overview#client-url"
className="grid grid-flow-row lg:grid-cols-5" className="grid grid-flow-row lg:grid-cols-5"
> >
<Input <Input

View File

@@ -0,0 +1,143 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetSmtpSettingsDocument,
useUpdateConfigMutation,
} from '@/utils/__generated__/graphql';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
function ConfirmDeleteSMTPSettingsModal({
close,
onDelete,
}: {
onDelete?: () => Promise<any>;
close: () => void;
}) {
const onClickDelete = async () => {
await onDelete();
close();
};
return (
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
<div className="grid grid-flow-row gap-4">
<Text variant="h3" component="h2">
Delete SMTP Settings?
</Text>
<Text>This will reset all your SMTP and Postmark settings.</Text>
<div className="grid grid-flow-row gap-2">
<Button color="error" onClick={onClickDelete}>
Delete
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</div>
</div>
</Box>
);
}
export default function DeleteSMTPSettings() {
const { openDialog, closeDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const [loading, setLoading] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSmtpSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const deleteSMTPSettings = async () => {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
provider: {
smtp: null,
},
},
},
});
setLoading(true);
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'SMTP settings are being deleted...',
successMessage: 'SMTP settings have been deleted successfully.',
errorMessage:
'An error occurred while trying to delete the SMTP settings.',
},
);
setLoading(false);
};
const confirmDeleteSMTPSettings = async () => {
openDialog({
component: (
<ConfirmDeleteSMTPSettingsModal
close={closeDialog}
onDelete={deleteSMTPSettings}
/>
),
});
};
return (
<SettingsContainer
title="Delete SMTP Settings"
description="Delete SMTP settings and revert to default values"
className="px-0"
slotProps={{
submitButton: { className: 'hidden' },
footer: { className: 'hidden' },
}}
>
<Box className="grid grid-flow-row border-t-1">
<Button
color="error"
className="mx-4 mt-4 justify-self-end"
onClick={confirmDeleteSMTPSettings}
disabled={loading || maintenanceActive}
loading={loading}
>
Delete
</Button>
</Box>
</SettingsContainer>
);
}

View File

@@ -0,0 +1 @@
export { default as DeleteSTMPSettings } from './DeleteSMTPSettings';

View File

@@ -1,17 +1,20 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { getServerError } from '@/utils/getServerError'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -21,15 +24,19 @@ const validationSchema = Yup.object({
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>; export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
export default function DisableNewUsersSettings() { export default function DisableNewUsersSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const form = useForm<DisableNewUsersFormValues>({ const form = useForm<DisableNewUsersFormValues>({
@@ -39,6 +46,14 @@ export default function DisableNewUsersSettings() {
}, },
}); });
useEffect(() => {
if (!loading) {
form.reset({
disabled: !data?.config?.auth?.signUp?.enabled,
});
}
}, [loading, data, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -71,23 +86,30 @@ export default function DisableNewUsersSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Disabling new user sign ups...`,
success: `New user sign ups have been disabled successfully.`,
error: getServerError(
`An error occurred while trying to disable new user sign ups.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Disabling new user sign ups...',
successMessage: 'New user sign ups have been disabled successfully.',
errorMessage:
'An error occurred while trying to disable new user sign ups.',
},
);
}; };
return ( return (
@@ -96,7 +118,7 @@ export default function DisableNewUsersSettings() {
<SettingsContainer <SettingsContainer
title="Disable New Users" title="Disable New Users"
description="If set, newly registered users are disabled and won't be able to sign in." description="If set, newly registered users are disabled and won't be able to sign in."
docsLink="https://docs.nhost.io/authentication#disable-new-users" docsLink="https://docs.nhost.io/guides/auth/overview#disable-new-users"
switchId="disabled" switchId="disabled"
showSwitch showSwitch
slotProps={{ slotProps={{

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,30 +14,35 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function DiscordProviderSettings() { export default function DiscordProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -51,6 +58,16 @@ export default function DiscordProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -87,23 +104,30 @@ export default function DiscordProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Discord settings are being updated...`,
success: `Discord settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Discrod settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Discord settings are being updated...',
successMessage: 'Discord settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Discrod settings.",
},
);
} }
return ( return (
@@ -118,13 +142,13 @@ export default function DiscordProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-discord" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-discord"
docsTitle="how to sign in users with Discord" docsTitle="how to sign in users with Discord"
icon="/assets/brands/discord.svg" icon="/assets/brands/discord.svg"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -160,7 +184,7 @@ export default function DiscordProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledCheckbox } from '@/components/form/ControlledCheckbox'; import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
@@ -6,16 +8,17 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -31,15 +34,19 @@ const validationSchema = Yup.object({
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>; export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
export default function EmailAndPasswordSettings() { export default function EmailAndPasswordSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, error, loading } = useGetSignInMethodsQuery({ const { data, error, loading } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { hibpEnabled, emailVerificationRequired, passwordMinLength } = const { hibpEnabled, emailVerificationRequired, passwordMinLength } =
@@ -55,6 +62,22 @@ export default function EmailAndPasswordSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
hibpEnabled,
emailVerificationRequired,
passwordMinLength,
});
}
}, [
loading,
hibpEnabled,
emailVerificationRequired,
passwordMinLength,
form,
]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -85,23 +108,31 @@ export default function EmailAndPasswordSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Email and password sign-in settings are being updated...`,
success: `Email and password sign-in settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update email sign-in settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: `Email and password sign-in settings are being updated...`,
successMessage:
'Email and password sign-in settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update email sign-in settings.',
},
);
} }
return ( return (
@@ -110,7 +141,7 @@ export default function EmailAndPasswordSettings() {
<SettingsContainer <SettingsContainer
title="Email and Password" title="Email and Password"
description="Allow users to sign in with email and password." description="Allow users to sign in with email and password."
docsLink="https://docs.nhost.io/authentication/sign-in-with-email-and-password" docsLink="https://docs.nhost.io/guides/auth/sign-in-email-password"
docsTitle="how to sign in users with email and password" docsTitle="how to sign in users with email and password"
className="grid grid-flow-row" className="grid grid-flow-row"
showSwitch showSwitch

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,30 +14,35 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function FacebookProviderSettings() { export default function FacebookProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -51,6 +58,16 @@ export default function FacebookProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -87,23 +104,30 @@ export default function FacebookProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Facebook settings are being updated...`,
success: `Facebook settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Facebook settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Facebook settings are being updated...',
successMessage: 'Facebook settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Facebook settings.",
},
);
} }
return ( return (
@@ -118,13 +142,13 @@ export default function FacebookProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-facebook" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-facebook"
docsTitle="how to sign in users with Facebook" docsTitle="how to sign in users with Facebook"
icon="/assets/brands/facebook.svg" icon="/assets/brands/facebook.svg"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -160,7 +184,7 @@ export default function FacebookProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,32 +14,37 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function GitHubProviderSettings() { export default function GitHubProviderSettings() {
const theme = useTheme(); const theme = useTheme();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -53,6 +60,16 @@ export default function GitHubProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -89,23 +106,30 @@ export default function GitHubProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `GitHub settings are being updated...`,
success: `GitHub settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's GitHub settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'GitHub settings are being updated...',
successMessage: 'GitHub settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's GitHub settings.",
},
);
} }
return ( return (
@@ -120,7 +144,7 @@ export default function GitHubProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-github" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-github"
docsTitle="how to sign in users with GitHub" docsTitle="how to sign in users with GitHub"
icon={ icon={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
@@ -130,7 +154,7 @@ export default function GitHubProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -166,7 +190,7 @@ export default function GitHubProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -18,12 +18,10 @@ import {
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function GitLabProviderSettings() { export default function GitLabProviderSettings() {
@@ -87,23 +85,18 @@ export default function GitLabProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `GitLab settings are being updated...`, },
success: `GitLab settings have been updated successfully.`, {
error: getServerError( loadingMessage: 'GitLab settings are being updated...',
`An error occurred while trying to update the project's GitLab settings.`, successMessage: 'GitLab settings have been updated successfully.',
), errorMessage:
}, "An error occurred while trying to update the project's GitLab settings.",
getToastStyleProps(), },
); );
form.reset(values);
} catch {
// Note: The toast will handle the error.
}
} }
return ( return (
@@ -122,7 +115,7 @@ export default function GitLabProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,30 +14,35 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function GoogleProviderSettings() { export default function GoogleProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -51,6 +58,16 @@ export default function GoogleProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -87,23 +104,30 @@ export default function GoogleProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Google settings are being updated...`,
success: `Google settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Google settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Google settings are being updated...',
successMessage: 'Google settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Google settings.",
},
);
} }
return ( return (
@@ -118,13 +142,13 @@ export default function GoogleProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-google" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-google"
docsTitle="how to sign in users with Google" docsTitle="how to sign in users with Google"
icon="/assets/brands/google.svg" icon="/assets/brands/google.svg"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -160,7 +184,7 @@ export default function GoogleProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledSelect } from '@/components/form/ControlledSelect'; import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
@@ -5,20 +7,21 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Option } from '@/components/ui/v2/Option'; import { Option } from '@/components/ui/v2/Option';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { import {
AUTH_GRAVATAR_DEFAULT, AUTH_GRAVATAR_DEFAULT,
AUTH_GRAVATAR_RATING, AUTH_GRAVATAR_RATING,
getToastStyleProps,
} from '@/utils/constants/settings'; } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -31,15 +34,19 @@ const validationSchema = Yup.object({
export type GravatarFormValues = Yup.InferType<typeof validationSchema>; export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
export default function GravatarSettings() { export default function GravatarSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { const {
@@ -58,6 +65,16 @@ export default function GravatarSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
default: defaultGravatar || '',
rating: rating || '',
enabled: enabled || false,
});
}
}, [loading, defaultGravatar, rating, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -89,23 +106,30 @@ export default function GravatarSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Gravatar settings are being updated...`,
success: `Gravatar settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Gravatar settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Gravatar settings are being updated...',
successMessage: 'Gravatar settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Gravatar settings.",
},
);
}; };
return ( return (
@@ -120,7 +144,7 @@ export default function GravatarSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication#gravatar" docsLink="https://docs.nhost.io/guides/auth/overview#gravatar"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,30 +14,35 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function LinkedInProviderSettings() { export default function LinkedInProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -51,6 +58,16 @@ export default function LinkedInProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -87,23 +104,30 @@ export default function LinkedInProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `LinkedIn settings are being updated...`,
success: `LinkedIn settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's LinkedIn settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'LinkedIn settings are being updated...',
successMessage: 'LinkedIn settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's LinkedIn settings.",
},
);
} }
return ( return (
@@ -118,13 +142,13 @@ export default function LinkedInProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-linkedin" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-linkedin"
docsTitle="how to sign in users with LinkedIn" docsTitle="how to sign in users with LinkedIn"
icon="/assets/brands/linkedin.svg" icon="/assets/brands/linkedin.svg"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -160,7 +184,7 @@ export default function LinkedInProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -25,15 +28,19 @@ const validationSchema = Yup.object({
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>; export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function MFASettings() { export default function MFASettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enabled, issuer } = data?.config?.auth?.totp || {}; const { enabled, issuer } = data?.config?.auth?.totp || {};
@@ -47,6 +54,15 @@ export default function MFASettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && issuer && enabled) {
form.reset({
issuer,
enabled,
});
}
}, [loading, issuer, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -76,23 +92,32 @@ export default function MFASettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Multi-factor authentication settings are being updated...`,
success: `Multi-factor authentication settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's multi-factor authentication settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage:
'Multi-factor authentication settings are being updated...',
successMessage:
'Multi-factor authentication settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's multi-factor authentication settings.",
},
);
}; };
return ( return (
@@ -107,7 +132,7 @@ export default function MFASettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication#multi-factor-authentication" docsLink="https://docs.nhost.io/guides/auth/overview#multi-factor-authentication"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -22,15 +25,19 @@ const validationSchema = Yup.object({
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>; export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
export default function MagicLinkSettings() { export default function MagicLinkSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enabled } = data?.config?.auth?.method?.emailPasswordless || {}; const { enabled } = data?.config?.auth?.method?.emailPasswordless || {};
@@ -43,6 +50,12 @@ export default function MagicLinkSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({ enabled });
}
}, [loading, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -73,23 +86,30 @@ export default function MagicLinkSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Magic Link settings are being updated...`,
success: `Magic Link settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Magic Link settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Magic Link settings are being updated...',
successMessage: 'Magic Link settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Magic Link settings.",
},
);
}; };
return ( return (
@@ -104,7 +124,7 @@ export default function MagicLinkSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication/sign-in-with-magic-link" docsLink="https://docs.nhost.io/guides/auth/sign-in-magic-link"
docsTitle="how to sign in users with Magic Link" docsTitle="how to sign in users with Magic Link"
switchId="enabled" switchId="enabled"
showSwitch showSwitch

View File

@@ -0,0 +1,149 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetSmtpSettingsDocument,
useGetSmtpSettingsQuery,
useUpdateConfigMutation,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import * as yup from 'yup';
const validationSchema = yup
.object({
sender: yup.string().label('SMTP Sender').email().required(),
password: yup.string().label('Password').required(),
})
.required();
export type PostmarkFormValues = yup.InferType<typeof validationSchema>;
export default function PostmarkSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data } = useGetSmtpSettingsQuery({
variables: { appId: currentProject?.id },
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { sender, password } = data?.config?.provider?.smtp || {};
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSmtpSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<PostmarkFormValues>({
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
defaultValues: {
password: '',
sender: '',
},
values: {
password: password || '',
sender: sender || '',
},
mode: 'onSubmit',
criteriaMode: 'all',
});
const {
register,
formState: { errors, isDirty, isSubmitting },
} = form;
const handleEditPostmarkSettings = async (values: PostmarkFormValues) => {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
provider: {
smtp: { method: 'LOGIN', host: 'postmark', ...values },
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Postmark settings are being updated...',
successMessage: 'Postmark settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update your Postmark settings.',
},
);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleEditPostmarkSettings}>
<SettingsContainer
title="Postmark Settings"
description="Configure postmark's native integration to send emails from your email domain."
submitButtonText="Save"
className="grid grid-cols-9 gap-4"
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: isSubmitting,
},
}}
>
<Input
{...register('sender')}
id="sender"
name="sender"
label="From Email"
placeholder="noreply@nhost.app"
className="lg:col-span-4"
hideEmptyHelperText
fullWidth
error={Boolean(errors.sender)}
helperText={errors.sender?.message}
/>
<Input
{...register('password')}
id="password"
label="Password"
type="password"
placeholder="Enter password"
className="lg:col-span-5"
hideEmptyHelperText
fullWidth
error={Boolean(errors.password)}
helperText={errors.password?.message}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -7,17 +9,18 @@ import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select'; import { Select } from '@/components/ui/v2/Select';
import { Text } from '@/components/ui/v2/Text'; import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import Image from 'next/image'; import Image from 'next/image';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -46,15 +49,19 @@ const validationSchema = Yup.object({
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>; export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function SMSSettings() { export default function SMSSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, error, loading } = useGetSignInMethodsQuery({ const { data, error, loading } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { accountSid, authToken, messagingServiceId } = const { accountSid, authToken, messagingServiceId } =
@@ -72,6 +79,17 @@ export default function SMSSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
accountSid: accountSid || '',
authToken: authToken || '',
messagingServiceId: messagingServiceId || '',
enabled: enabled || false,
});
}
}, [loading, accountSid, authToken, messagingServiceId, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -112,23 +130,29 @@ export default function SMSSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `SMS settings are being updated...`,
success: `SMS settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update SMS settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'SMS settings are being updated...',
successMessage: 'SMS settings have been updated successfully.',
errorMessage: 'An error occurred while trying to update SMS settings.',
},
);
}; };
return ( return (
@@ -145,10 +169,10 @@ export default function SMSSettings() {
}} }}
switchId="enabled" switchId="enabled"
showSwitch showSwitch
docsLink="https://docs.nhost.io/authentication/sign-in-with-phone-number-sms" docsLink="https://docs.nhost.io/guides/auth/sign-in-phone-number"
docsTitle="how to sign in users with a phone number (SMS)" docsTitle="how to sign in users with a phone number (SMS)"
className={twMerge( className={twMerge(
'grid grid-flow-col grid-cols-2 grid-rows-4 gap-y-4 gap-x-3 px-4 py-2', 'grid grid-flow-col grid-cols-2 grid-rows-4 gap-x-3 gap-y-4 px-4 py-2',
!authSmsPasswordlessEnabled && 'hidden', !authSmsPasswordlessEnabled && 'hidden',
)} )}
> >

View File

@@ -0,0 +1,238 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetSmtpSettingsDocument,
useGetSmtpSettingsQuery,
useUpdateConfigMutation,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { type Optional } from 'utility-types';
import * as yup from 'yup';
const smtpValidationSchema = yup
.object({
secure: yup.bool().label('SMTP Secure'),
host: yup
.string()
.label('SMTP Host')
.matches(
/((https?):\/\/)?(www\.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#[a-zA-Z0-9#]+?)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/,
'SMTP Host must be a valid URL',
)
.required(),
port: yup
.number()
.typeError('The SMTP port should contain only numbers.')
.required(),
user: yup.string().label('Username').required(),
password: yup.string().label('Password'),
method: yup.string().required(),
sender: yup.string().label('SMTP Sender').email().required(),
})
.required();
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
export default function SMTPSettings() {
const { maintenanceActive } = useUI();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data } = useGetSmtpSettingsQuery({
variables: { appId: currentProject?.id },
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { secure, host, port, user, method, sender, password } =
data?.config?.provider?.smtp || {};
const form = useForm<Optional<SmtpFormValues, 'password'>>({
reValidateMode: 'onSubmit',
resolver: yupResolver(smtpValidationSchema),
defaultValues: {
secure: false,
host: '',
port: undefined,
user: '',
password: '',
method: '',
sender: '',
},
values: {
secure: secure || false,
host: host || '',
port,
user: user || '',
password: password || '',
method: method || '',
sender: sender || '',
},
mode: 'onSubmit',
criteriaMode: 'all',
});
const {
register: registerSmtp,
formState: { errors, isDirty, isSubmitting },
} = form;
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSmtpSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const handleEditSMTPSettings = async (values: SmtpFormValues) => {
const { password: newPassword, ...valuesWithoutPassword } = values;
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
provider: {
smtp: newPassword ? values : valuesWithoutPassword,
},
},
},
});
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'SMTP settings are being updated...',
successMessage: 'SMTP settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update the SMTP settings.',
},
);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleEditSMTPSettings}>
<SettingsContainer
title="SMTP Settings"
description="Configure your SMTP settings to send emails from your email domain."
submitButtonText="Save"
className="grid grid-cols-9 gap-4"
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: isSubmitting,
},
}}
>
<Input
{...registerSmtp('sender')}
id="sender"
name="sender"
label="From Email"
placeholder="noreply@nhost.app"
className="lg:col-span-4"
hideEmptyHelperText
fullWidth
error={Boolean(errors.sender)}
helperText={errors.sender?.message}
/>
<Input
{...registerSmtp('host')}
id="host"
name="host"
label="SMTP Host"
className="lg:col-span-4"
placeholder="e.g. smtp.sendgrid.net"
hideEmptyHelperText
fullWidth
error={Boolean(errors.host)}
helperText={errors.host?.message}
/>
<Input
{...registerSmtp('port')}
id="port"
name="port"
label="Port"
type="number"
placeholder="587"
className="lg:col-span-1"
hideEmptyHelperText
fullWidth
error={Boolean(errors.port)}
helperText={errors.port?.message}
/>
<Input
{...registerSmtp('user')}
id="user"
label="SMTP Username"
placeholder="SMTP Username"
className="lg:col-span-4"
hideEmptyHelperText
fullWidth
error={Boolean(errors.user)}
helperText={errors.user?.message}
/>
<Input
{...registerSmtp('password')}
id="password"
label="SMTP Password"
type="password"
placeholder="Enter SMTP password"
className="lg:col-span-5"
hideEmptyHelperText
fullWidth
error={Boolean(errors.password)}
helperText={errors.password?.message}
/>
<Input
{...registerSmtp('method')}
id="method"
name="method"
label="SMTP Auth Method"
placeholder="LOGIN"
hideEmptyHelperText
className="lg:col-span-4"
fullWidth
error={Boolean(errors.method)}
helperText={errors.method?.message}
/>
<ControlledCheckbox
name="secure"
id="secure"
label="Use SSL"
className="lg:col-span-9"
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetAuthenticationSettingsDocument, GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery, useGetAuthenticationSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -30,15 +33,19 @@ const validationSchema = Yup.object({
export type SessionFormValues = Yup.InferType<typeof validationSchema>; export type SessionFormValues = Yup.InferType<typeof validationSchema>;
export default function SessionSettings() { export default function SessionSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument], refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetAuthenticationSettingsQuery({ const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { accessToken, refreshToken } = data?.config?.auth?.session || {}; const { accessToken, refreshToken } = data?.config?.auth?.session || {};
@@ -52,6 +59,15 @@ export default function SessionSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && accessToken && refreshToken) {
form.reset({
accessTokenExpiresIn: accessToken?.expiresIn || 900,
refreshTokenExpiresIn: refreshToken?.expiresIn || 43200,
});
}
}, [loading, accessToken, refreshToken, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -84,23 +100,30 @@ export default function SessionSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Session settings are being updated...`,
success: `Session settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's session settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Session settings are being updated...',
successMessage: 'Session settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's session settings.",
},
);
}; };
return ( return (

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,30 +14,35 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function SpotifyProviderSettings() { export default function SpotifyProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -51,6 +58,16 @@ export default function SpotifyProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -87,23 +104,30 @@ export default function SpotifyProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Spotify settings are being updated...`,
success: `Spotify settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Spotify settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Spotify settings are being updated...',
successMessage: 'Spotify settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Spotify settings.",
},
);
} }
return ( return (
@@ -118,13 +142,13 @@ export default function SpotifyProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-spotify" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-spotify"
docsTitle="how to sign in users with Spotify" docsTitle="how to sign in users with Spotify"
icon="/assets/brands/spotify.svg" icon="/assets/brands/spotify.svg"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -160,7 +184,7 @@ export default function SpotifyProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -18,12 +18,10 @@ import {
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function StravaProviderSettings() { export default function StravaProviderSettings() {
@@ -87,23 +85,18 @@ export default function StravaProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `Strava settings are being updated...`, },
success: `Strava settings have been updated successfully.`, {
error: getServerError( loadingMessage: 'Strava settings are being updated...',
`An error occurred while trying to update the project's Strava settings.`, successMessage: 'Strava settings have been updated successfully.',
), errorMessage:
}, "An error occurred while trying to update the project's Strava settings.",
getToastStyleProps(), },
); );
form.reset(values);
} catch {
// Note: The toast will handle the error.
}
} }
return ( return (
@@ -122,7 +115,7 @@ export default function StravaProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,32 +14,37 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function TwitchProviderSettings() { export default function TwitchProviderSettings() {
const theme = useTheme(); const theme = useTheme();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -53,6 +60,16 @@ export default function TwitchProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -89,23 +106,30 @@ export default function TwitchProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Twitch settings are being updated...`,
success: `Twitch settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Twitch settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Twitch settings are being updated...',
successMessage: 'Twitch settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's Twitch settings.",
},
);
} }
return ( return (
@@ -120,7 +144,7 @@ export default function TwitchProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/platform/authentication/sign-in-with-twitch" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-twitch"
docsTitle="how to sign in users with Twitch" docsTitle="how to sign in users with Twitch"
icon={ icon={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
@@ -130,7 +154,7 @@ export default function TwitchProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -166,7 +190,7 @@ export default function TwitchProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -7,18 +9,19 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment'; import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -41,15 +44,19 @@ const validationSchema = Yup.object({
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>; export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function TwitterProviderSettings() { export default function TwitterProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { consumerKey, consumerSecret, enabled } = const { consumerKey, consumerSecret, enabled } =
@@ -65,6 +72,16 @@ export default function TwitterProviderSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
consumerSecret: consumerSecret || '',
consumerKey: consumerKey || '',
enabled: enabled || false,
});
}
}, [loading, consumerKey, consumerSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -98,23 +115,29 @@ export default function TwitterProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Twitter settings are being updated...`,
success: `Twitter settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Twitter settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Twitter settings are being updated...',
successMessage: 'Twitter settings have been updated successfully.',
errorMessage: `An error occurred while trying to update the project's Twitter settings.`,
},
);
} }
return ( return (
@@ -134,7 +157,7 @@ export default function TwitterProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -193,7 +216,7 @@ export default function TwitterProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -22,15 +25,19 @@ const validationSchema = Yup.object({
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>; export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
export default function WebAuthnSettings() { export default function WebAuthnSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enabled } = data?.config?.auth?.method?.webauthn || {}; const { enabled } = data?.config?.auth?.method?.webauthn || {};
@@ -43,6 +50,12 @@ export default function WebAuthnSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({ enabled });
}
}, [loading, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -66,33 +79,41 @@ export default function WebAuthnSettings() {
config: { config: {
auth: { auth: {
method: { method: {
webauthn: {...values, webauthn: {
relyingParty: { ...values,
name: currentProject.name, relyingParty: {
}}, name: currentProject.name,
},
},
}, },
}, },
}, },
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(values);
loading: `WebAuthn settings are being updated...`,
success: `WebAuthn settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's WebAuthn settings.`,
),
},
getToastStyleProps(),
);
form.reset(values); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'WebAuthn settings are being updated...',
successMessage: 'WebAuthn settings have been updated successfully.',
errorMessage: `An error occurred while trying to update the project's WebAuthn settings.`,
},
);
}; };
return ( return (
@@ -107,7 +128,7 @@ export default function WebAuthnSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication/sign-in-with-security-keys" docsLink="https://docs.nhost.io/guides/auth/sign-in-webauthn"
docsTitle="how to sign in users with security keys" docsTitle="how to sign in users with security keys"
switchId="enabled" switchId="enabled"
showSwitch showSwitch

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -12,30 +14,35 @@ import {
baseProviderValidationSchema, baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings'; } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export default function WindowsLiveProviderSettings() { export default function WindowsLiveProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, enabled } = const { clientId, clientSecret, enabled } =
@@ -51,6 +58,16 @@ export default function WindowsLiveProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema), resolver: yupResolver(baseProviderValidationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -87,23 +104,29 @@ export default function WindowsLiveProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Windows Live settings are being updated...`,
success: `Windows Live settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Windows Live settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Windows Live settings are being updated...',
successMessage: 'Windows Live settings have been updated successfully.',
errorMessage: `An error occurred while trying to update the project's Windows Live settings.`,
},
);
} }
return ( return (
@@ -123,7 +146,7 @@ export default function WindowsLiveProviderSettings() {
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2', 'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -159,7 +182,7 @@ export default function WindowsLiveProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -8,18 +10,19 @@ import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment'; import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { BaseProviderSettings } from '@/features/authentication/settings/components/BaseProviderSettings'; import { BaseProviderSettings } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { import {
GetSignInMethodsDocument, GetSignInMethodsDocument,
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -54,15 +57,19 @@ const validationSchema = Yup.object({
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>; export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function WorkOsProviderSettings() { export default function WorkOsProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument], refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetSignInMethodsQuery({ const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { clientId, clientSecret, organization, connection, enabled } = const { clientId, clientSecret, organization, connection, enabled } =
@@ -80,6 +87,26 @@ export default function WorkOsProviderSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
organization: organization || '',
connection: connection || '',
enabled: enabled || false,
});
}
}, [
loading,
clientId,
clientSecret,
organization,
connection,
enabled,
form,
]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -113,23 +140,30 @@ export default function WorkOsProviderSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `WorkOS settings are being updated...`,
success: `WorkOS settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's WorkOS settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'WorkOS settings are being updated...',
successMessage: 'WorkOS settings have been updated successfully.',
errorMessage:
"An error occurred while trying to update the project's WorkOS settings.",
},
);
} }
return ( return (
@@ -144,13 +178,13 @@ export default function WorkOsProviderSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
docsLink="https://docs.nhost.io/authentication/sign-in-with-workos" docsLink="https://docs.nhost.io/guides/auth/social/sign-in-workos"
docsTitle="how to sign in users with WorkOS" docsTitle="how to sign in users with WorkOS"
icon="/assets/brands/workos.svg" icon="/assets/brands/workos.svg"
switchId="enabled" switchId="enabled"
showSwitch showSwitch
className={twMerge( className={twMerge(
'grid grid-flow-row grid-cols-2 gap-y-4 gap-x-3 px-4 py-2', 'grid grid-flow-row grid-cols-2 gap-x-3 gap-y-4 px-4 py-2',
!authEnabled && 'hidden', !authEnabled && 'hidden',
)} )}
> >
@@ -210,7 +244,7 @@ export default function WorkOsProviderSettings() {
); );
}} }}
> >
<CopyIcon className="h-4 w-4" /> <CopyIcon className="w-4 h-4" />
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }

View File

@@ -6,12 +6,10 @@ import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface CreateUserFormProps extends DialogFormProps { export interface CreateUserFormProps extends DialogFormProps {
@@ -77,9 +75,9 @@ export default function CreateUserForm({
async function handleCreateUser({ email, password }: CreateUserFormValues) { async function handleCreateUser({ email, password }: CreateUserFormValues) {
setCreateUserFormError(null); setCreateUserFormError(null);
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
fetch(signUpUrl, { await fetch(signUpUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
@@ -95,21 +93,16 @@ export default function CreateUserForm({
} }
throw new Error(data?.message || 'Something went wrong.'); throw new Error(data?.message || 'Something went wrong.');
}), });
{
loading: 'Creating user...',
success: 'User has been created successfully.',
error: getServerError(
'An error occurred while trying to create the user.',
),
},
getToastStyleProps(),
);
onSubmit?.(); onSubmit?.();
} catch (error) { },
// Note: The error is already handled by the toast promise. {
} loadingMessage: 'Creating user...',
successMessage: 'User has been created successfully.',
errorMessage: 'An error occurred while trying to create the user.',
},
);
} }
return ( return (

View File

@@ -19,9 +19,8 @@ import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/
import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles'; import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { import {
RemoteAppGetUsersDocument, RemoteAppGetUsersDocument,
useGetProjectLocalesQuery, useGetProjectLocalesQuery,
@@ -36,7 +35,6 @@ import Image from 'next/image';
import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users'; import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface EditUserFormProps extends DialogFormProps { export interface EditUserFormProps extends DialogFormProps {
@@ -173,21 +171,15 @@ export default function EditUserForm({
}, },
}); });
await toast.promise( await execPromiseWithErrorToast(() => banUser, {
banUser, loadingMessage: shouldBan ? 'Banning user...' : 'Unbanning user...',
{ successMessage: shouldBan
loading: shouldBan ? 'Banning user...' : 'Unbanning user...', ? 'User has been banned successfully.'
success: shouldBan : 'User has been unbanned successfully.',
? 'User has been banned successfully.' errorMessage: shouldBan
: 'User has been unbanned successfully.', ? 'An error occurred while trying to ban the user.'
error: getServerError( : 'An error occurred while trying to unban the user.',
shouldBan });
? 'An error occurred while trying to ban the user.'
: 'An error occurred while trying to unban the user.',
),
},
getToastStyleProps(),
);
} }
return ( return (
@@ -196,14 +188,14 @@ export default function EditUserForm({
className="flex flex-col overflow-hidden border-t-1 lg:flex-auto lg:content-between" className="flex flex-col overflow-hidden border-t-1 lg:flex-auto lg:content-between"
onSubmit={onSubmit} onSubmit={onSubmit}
> >
<Box className="flex-auto overflow-y-auto divide-y"> <Box className="flex-auto divide-y overflow-y-auto">
<Box <Box
component="section" component="section"
className="grid grid-flow-col p-6 lg:grid-cols-7" className="grid grid-flow-col p-6 lg:grid-cols-7"
> >
<div className="grid items-center grid-flow-col col-span-6 gap-4 place-content-start"> <div className="col-span-6 grid grid-flow-col place-content-start items-center gap-4">
<Avatar className="w-12 h-12" src={user.avatarUrl} /> <Avatar className="h-12 w-12" src={user.avatarUrl} />
<div className="grid items-center grid-flow-row"> <div className="grid grid-flow-row items-center">
<Text className="text-lg font-medium">{user.displayName}</Text> <Text className="text-lg font-medium">{user.displayName}</Text>
<Text className="text-sm+ font-normal" color="secondary"> <Text className="text-sm+ font-normal" color="secondary">
{user.email} {user.email}
@@ -225,7 +217,7 @@ export default function EditUserForm({
Actions Actions
</Button> </Button>
</Dropdown.Trigger> </Dropdown.Trigger>
<Dropdown.Content menu className="w-full h-full"> <Dropdown.Content menu className="h-full w-full">
<Dropdown.Item <Dropdown.Item
className="font-medium" className="font-medium"
sx={{ color: 'error.main' }} sx={{ color: 'error.main' }}
@@ -253,11 +245,11 @@ export default function EditUserForm({
component="section" component="section"
className="grid grid-flow-row grid-cols-4 gap-8 p-6" className="grid grid-flow-row grid-cols-4 gap-8 p-6"
> >
<InputLabel as="h3" className="self-center col-span-1"> <InputLabel as="h3" className="col-span-1 self-center">
User ID User ID
</InputLabel> </InputLabel>
<div className="grid items-center justify-start grid-flow-col col-span-3 gap-2"> <div className="col-span-3 grid grid-flow-col items-center justify-start gap-2">
<Text className="font-medium truncate">{user.id}</Text> <Text className="truncate font-medium">{user.id}</Text>
<IconButton <IconButton
variant="borderless" variant="borderless"
color="secondary" color="secondary"
@@ -267,18 +259,18 @@ export default function EditUserForm({
copy(user.id, 'User ID'); copy(user.id, 'User ID');
}} }}
> >
<CopyIcon className="w-4 h-4" /> <CopyIcon className="h-4 w-4" />
</IconButton> </IconButton>
</div> </div>
<InputLabel as="h3" className="self-center col-span-1 "> <InputLabel as="h3" className="col-span-1 self-center ">
Created At Created At
</InputLabel> </InputLabel>
<Text className="col-span-3 font-medium"> <Text className="col-span-3 font-medium">
{format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm:ss')} {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm:ss')}
</Text> </Text>
<InputLabel as="h3" className="self-center col-span-1 "> <InputLabel as="h3" className="col-span-1 self-center ">
Last Seen Last Seen
</InputLabel> </InputLabel>
<Text className="col-span-3 font-medium"> <Text className="col-span-3 font-medium">
@@ -336,14 +328,14 @@ export default function EditUserForm({
autoComplete="off" autoComplete="off"
/> />
<div className="grid items-center grid-flow-col grid-cols-8 col-span-1 my-1"> <div className="col-span-1 my-1 grid grid-flow-col grid-cols-8 items-center">
<div className="col-span-2 "> <div className="col-span-2 ">
<InputLabel as="h3">Password</InputLabel> <InputLabel as="h3">Password</InputLabel>
</div> </div>
<Button <Button
color="primary" color="primary"
variant="borderless" variant="borderless"
className="col-span-6 px-2 place-self-start" className="col-span-6 place-self-start px-2"
onClick={handleChangeUserPassword} onClick={handleChangeUserPassword}
> >
Change Change
@@ -392,12 +384,12 @@ export default function EditUserForm({
</Box> </Box>
<Box <Box
component="section" component="section"
className="grid gap-4 p-6 place-content-start lg:grid-cols-4" className="grid place-content-start gap-4 p-6 lg:grid-cols-4"
> >
<div className="items-center self-center col-span-1 align-middle"> <div className="col-span-1 items-center self-center align-middle">
<InputLabel as="h3">OAuth Providers</InputLabel> <InputLabel as="h3">OAuth Providers</InputLabel>
</div> </div>
<div className="grid w-full grid-flow-row col-span-3 gap-y-6"> <div className="col-span-3 grid w-full grid-flow-row gap-y-6">
{user.userProviders.length === 0 && ( {user.userProviders.length === 0 && (
<div className="grid grid-flow-col place-content-between gap-x-1"> <div className="grid grid-flow-col place-content-between gap-x-1">
<Text className="font-normal" color="disabled"> <Text className="font-normal" color="disabled">
@@ -408,10 +400,10 @@ export default function EditUserForm({
{user.userProviders.map((provider) => ( {user.userProviders.map((provider) => (
<div <div
className="grid grid-flow-col gap-3 place-content-between" className="grid grid-flow-col place-content-between gap-3"
key={provider.id} key={provider.id}
> >
<div className="grid grid-flow-col gap-2 span-cols-1"> <div className="span-cols-1 grid grid-flow-col gap-2">
<Image <Image
src={ src={
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
@@ -424,7 +416,7 @@ export default function EditUserForm({
} }
width={25} width={25}
height={25} height={25}
alt='Oauth provider logo' alt="Oauth provider logo"
/> />
<Text className="font-medium capitalize"> <Text className="font-medium capitalize">
{getReadableProviderName(provider.providerId)} {getReadableProviderName(provider.providerId)}
@@ -437,7 +429,7 @@ export default function EditUserForm({
{!isAnonymous && ( {!isAnonymous && (
<Box <Box
component="section" component="section"
className="grid grid-flow-row p-6 gap-y-10" className="grid grid-flow-row gap-y-10 p-6"
> >
<ControlledSelect <ControlledSelect
{...register('defaultRole')} {...register('defaultRole')}
@@ -457,11 +449,11 @@ export default function EditUserForm({
</Option> </Option>
))} ))}
</ControlledSelect> </ControlledSelect>
<div className="grid grid-flow-row gap-6 place-content-start lg:grid-flow-col lg:grid-cols-8"> <div className="grid grid-flow-row place-content-start gap-6 lg:grid-flow-col lg:grid-cols-8">
<InputLabel as="h3" className="col-span-2"> <InputLabel as="h3" className="col-span-2">
Allowed Roles Allowed Roles
</InputLabel> </InputLabel>
<div className="grid grid-flow-row col-span-3 gap-6"> <div className="col-span-3 grid grid-flow-row gap-6">
{roles.map((role, i) => ( {roles.map((role, i) => (
<ControlledCheckbox <ControlledCheckbox
id={`roles.${i}`} id={`roles.${i}`}
@@ -477,7 +469,7 @@ export default function EditUserForm({
)} )}
</Box> </Box>
<Box className="grid justify-between flex-shrink-0 w-full grid-flow-col gap-3 p-2 snap-end place-self-end border-t-1"> <Box className="grid w-full flex-shrink-0 snap-end grid-flow-col justify-between gap-3 place-self-end border-t-1 p-2">
<Button <Button
variant="outlined" variant="outlined"
color="secondary" color="secondary"

View File

@@ -6,8 +6,7 @@ import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getServerError } from '@/utils/getServerError';
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql'; import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
import { import {
useGetSignInMethodsQuery, useGetSignInMethodsQuery,
@@ -17,7 +16,6 @@ import { yupResolver } from '@hookform/resolvers/yup';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { useState } from 'react'; import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface EditUserPasswordFormProps extends DialogFormProps { export interface EditUserPasswordFormProps extends DialogFormProps {
@@ -90,23 +88,23 @@ export default function EditUserPasswordForm({
client: remoteProjectGQLClient, client: remoteProjectGQLClient,
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateUserPasswordPromise, await updateUserPasswordPromise;
{ },
loading: 'Updating user password...', {
success: 'User password updated successfully.', loadingMessage: 'Updating user password...',
error: getServerError('Failed to update user password.'), successMessage: 'User password updated successfully.',
errorMessage: 'Failed to update user password.',
onError: (error) => {
setEditUserPasswordFormError(
new Error(error.message || 'Something went wrong.'),
);
}, },
getToastStyleProps(), },
); );
} catch (error) {
setEditUserPasswordFormError( closeDialog();
new Error(error.message || 'Something went wrong.'),
);
} finally {
closeDialog();
}
}; };
const { const {

View File

@@ -17,8 +17,7 @@ import { getReadableProviderName } from '@/features/authentication/users/utils/g
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles'; import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getServerError } from '@/utils/getServerError';
import { import {
useDeleteRemoteAppUserRolesMutation, useDeleteRemoteAppUserRolesMutation,
useGetRolesPermissionsQuery, useGetRolesPermissionsQuery,
@@ -33,7 +32,6 @@ import dynamic from 'next/dynamic';
import Image from 'next/image'; import Image from 'next/image';
import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users'; import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users';
import { Fragment, useMemo } from 'react'; import { Fragment, useMemo } from 'react';
import toast from 'react-hot-toast';
const EditUserForm = dynamic( const EditUserForm = dynamic(
() => () =>
@@ -153,20 +151,18 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
}); });
} }
await toast.promise( await execPromiseWithErrorToast(
updateUserMutationPromise, async () => {
{ await updateUserMutationPromise;
loading: `Updating user's settings...`, },
success: 'User settings have been updated successfully.', {
error: getServerError( loadingMessage: `Updating user's settings...`,
`An error occurred while trying to update this user's settings.`, successMessage: 'User settings have been updated successfully.',
), errorMessage: `An error occurred while trying to update this user's settings.`,
}, },
getToastStyleProps(),
); );
await onSubmit?.(); await onSubmit?.();
closeDrawer(); closeDrawer();
} }
@@ -181,20 +177,20 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
), ),
props: { props: {
onPrimaryAction: async () => { onPrimaryAction: async () => {
await toast.promise( await execPromiseWithErrorToast(
deleteUser({ async () => {
variables: { await deleteUser({
id: user.id, variables: {
}, id: user.id,
}), },
{ });
loading: 'Deleting user...', },
success: 'User deleted successfully.', {
error: getServerError( loadingMessage: 'Deleting user...',
'An error occurred while trying to delete this user.', successMessage: 'User deleted successfully.',
), errorMessage:
'An error occurred while trying to delete this user.',
}, },
getToastStyleProps(),
); );
await onSubmit(); await onSubmit();
@@ -226,12 +222,12 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
if (!users) { if (!users) {
return ( return (
<div className="w-screen h-screen overflow-hidden"> <div className="h-screen w-screen overflow-hidden">
<div className="absolute top-0 left-0 z-50 block w-full h-full"> <div className="absolute left-0 top-0 z-50 block h-full w-full">
<span className="relative block mx-auto my-0 top50percent top-1/2"> <span className="top50percent relative top-1/2 mx-auto my-0 block">
<ActivityIndicator <ActivityIndicator
label="Loading users..." label="Loading users..."
className="flex items-center justify-center my-auto" className="my-auto flex items-center justify-center"
/> />
</span> </span>
</div> </div>
@@ -269,7 +265,7 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
}} }}
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium" className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
> >
<UserIcon className="w-4 h-4" /> <UserIcon className="h-4 w-4" />
<Text className="font-medium">View User</Text> <Text className="font-medium">View User</Text>
</Dropdown.Item> </Dropdown.Item>
@@ -280,7 +276,7 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
sx={{ color: 'error.main' }} sx={{ color: 'error.main' }}
onClick={() => handleDeleteUser(user)} onClick={() => handleDeleteUser(user)}
> >
<TrashIcon className="w-4 h-4" /> <TrashIcon className="h-4 w-4" />
<Text className="font-medium" color="error"> <Text className="font-medium" color="error">
Delete User Delete User
</Text> </Text>
@@ -294,14 +290,14 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
onClick={() => handleViewUser(user)} onClick={() => handleViewUser(user)}
aria-label={`View ${user.displayName}`} aria-label={`View ${user.displayName}`}
> >
<div className="grid grid-flow-col col-span-2 gap-4 place-content-start"> <div className="col-span-2 grid grid-flow-col place-content-start gap-4">
<Avatar <Avatar
src={user.avatarUrl} src={user.avatarUrl}
alt={`Avatar of ${user.displayName}`} alt={`Avatar of ${user.displayName}`}
/> />
<div className="grid items-center grid-flow-row"> <div className="grid grid-flow-row items-center">
<div className="grid items-center grid-flow-col gap-2"> <div className="grid grid-flow-col items-center gap-2">
<Text className="font-medium leading-5 truncate"> <Text className="truncate font-medium leading-5">
{user.displayName} {user.displayName}
</Text> </Text>
{user.disabled && ( {user.disabled && (
@@ -314,7 +310,7 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
)} )}
</div> </div>
<Text className="font-normal truncate" color="secondary"> <Text className="truncate font-normal" color="secondary">
{user.email} {user.email}
</Text> </Text>
</div> </div>
@@ -334,7 +330,7 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
: '-'} : '-'}
</Text> </Text>
<div className="hidden grid-flow-col col-span-2 gap-3 px-4 place-content-start lg:grid"> <div className="col-span-2 hidden grid-flow-col place-content-start gap-3 px-4 lg:grid">
{user.userProviders.length === 0 && ( {user.userProviders.length === 0 && (
<Text className="col-span-3 font-medium">-</Text> <Text className="col-span-3 font-medium">-</Text>
)} )}
@@ -362,7 +358,7 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
} }
width={16} width={16}
height={16} height={16}
alt='Oauth provider logo' alt="Oauth provider logo"
/> />
} }
/> />

View File

@@ -275,7 +275,7 @@ export default function DataBrowserGrid({
() => () =>
columns columns
.map((column) => ({ .map((column) => ({
...createDataGridColumn(column, isSchemaEditable), ...createDataGridColumn(column, true),
onCellEdit: async (variables: UpdateRecordVariables) => { onCellEdit: async (variables: UpdateRecordVariables) => {
const result = await updateRow(variables); const result = await updateRow(variables);
await queryClient.invalidateQueries([currentTablePath]); await queryClient.invalidateQueries([currentTablePath]);
@@ -288,7 +288,6 @@ export default function DataBrowserGrid({
[ [
columns, columns,
currentTablePath, currentTablePath,
isSchemaEditable,
optimisticlyRemovedColumnId, optimisticlyRemovedColumnId,
queryClient, queryClient,
removableColumnId, removableColumnId,
@@ -422,7 +421,7 @@ export default function DataBrowserGrid({
loading={status === 'loading'} loading={status === 'loading'}
sortBy={sortBy} sortBy={sortBy}
className="pb-17 sm:pb-0" className="pb-17 sm:pb-0"
onInsertRow={isSchemaEditable ? handleInsertRowClick : undefined} onInsertRow={handleInsertRowClick}
onInsertColumn={isSchemaEditable ? handleInsertColumnClick : undefined} onInsertColumn={isSchemaEditable ? handleInsertColumnClick : undefined}
onEditColumn={isSchemaEditable ? handleEditColumnClick : undefined} onEditColumn={isSchemaEditable ? handleEditColumnClick : undefined}
onRemoveColumn={isSchemaEditable ? handleColumnRemoveClick : undefined} onRemoveColumn={isSchemaEditable ? handleColumnRemoveClick : undefined}
@@ -445,7 +444,7 @@ export default function DataBrowserGrid({
onInsertColumnClick={ onInsertColumnClick={
isSchemaEditable ? handleInsertColumnClick : undefined isSchemaEditable ? handleInsertColumnClick : undefined
} }
onInsertRowClick={isSchemaEditable ? handleInsertRowClick : undefined} onInsertRowClick={handleInsertRowClick}
paginationProps={{ paginationProps={{
currentPage: Math.max(currentPage, 1), currentPage: Math.max(currentPage, 1),
totalPages: Math.max(numberOfPages, 1), totalPages: Math.max(numberOfPages, 1),

View File

@@ -12,11 +12,9 @@ import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { RowIcon } from '@/components/ui/v2/icons/RowIcon'; import { RowIcon } from '@/components/ui/v2/icons/RowIcon';
import { useDeleteRecordMutation } from '@/features/database/dataGrid/hooks/useDeleteRecordMutation'; import { useDeleteRecordMutation } from '@/features/database/dataGrid/hooks/useDeleteRecordMutation';
import type { DataBrowserGridColumn } from '@/features/database/dataGrid/types/dataBrowser'; import type { DataBrowserGridColumn } from '@/features/database/dataGrid/types/dataBrowser';
import { isSchemaLocked } from '@/features/database/dataGrid/utils/schemaHelpers/isSchemaLocked';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { triggerToast } from '@/utils/toast'; import { triggerToast } from '@/utils/toast';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import type { Row } from 'react-table'; import type { Row } from 'react-table';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
@@ -58,11 +56,6 @@ export default function DataBrowserGridControls({
const { className: paginationClassName, ...restPaginationProps } = const { className: paginationClassName, ...restPaginationProps } =
paginationProps || ({} as DataGridPaginationProps); paginationProps || ({} as DataGridPaginationProps);
const {
query: { schemaSlug },
} = useRouter();
const isSchemaEditable = !isSchemaLocked(schemaSlug as string);
const { const {
selectedFlatRows: selectedRows, selectedFlatRows: selectedRows,
columns, columns,
@@ -126,7 +119,7 @@ export default function DataBrowserGridControls({
numberOfSelectedRows > 0 ? 'justify-between' : 'justify-end', numberOfSelectedRows > 0 ? 'justify-between' : 'justify-end',
)} )}
> >
{isSchemaEditable && numberOfSelectedRows > 0 && ( {numberOfSelectedRows > 0 && (
<div className="grid grid-flow-col place-content-start items-center gap-2"> <div className="grid grid-flow-col place-content-start items-center gap-2">
<Chip <Chip
size="small" size="small"

View File

@@ -331,7 +331,7 @@ export default function EditPermissionsForm({
<NavLink <NavLink
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/roles-and-permissions`} href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/roles-and-permissions`}
passHref passHref
> legacyBehavior>
<Link <Link
href="settings/roles-and-permissions" href="settings/roles-and-permissions"
underline="hover" underline="hover"

View File

@@ -14,13 +14,11 @@ import type {
import { convertToHasuraPermissions } from '@/features/database/dataGrid/utils/convertToHasuraPermissions'; import { convertToHasuraPermissions } from '@/features/database/dataGrid/utils/convertToHasuraPermissions';
import { convertToRuleGroup } from '@/features/database/dataGrid/utils/convertToRuleGroup'; import { convertToRuleGroup } from '@/features/database/dataGrid/utils/convertToRuleGroup';
import type { DialogFormProps } from '@/types/common'; import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import AggregationQuerySection from './sections/AggregationQuerySection'; import AggregationQuerySection from './sections/AggregationQuerySection';
import BackendOnlySection from './sections/BackendOnlySection'; import BackendOnlySection from './sections/BackendOnlySection';
import ColumnPermissionsSection from './sections/ColumnPermissionsSection'; import ColumnPermissionsSection from './sections/ColumnPermissionsSection';
@@ -254,18 +252,18 @@ export default function RolePermissionEditorForm({
}, },
}); });
await toast.promise( await execPromiseWithErrorToast(
managePermissionPromise, async () => {
{ await managePermissionPromise;
loading: 'Saving permission...', onDirtyStateChange(false, location);
success: 'Permission has been saved successfully.', onSubmit?.();
error: getServerError('An error occurred while saving the permission.'), },
{
loadingMessage: 'Saving permission...',
successMessage: 'Permission has been saved successfully.',
errorMessage: 'An error occurred while saving the permission.',
}, },
getToastStyleProps(),
); );
onDirtyStateChange(false, location);
onSubmit?.();
} }
function handleCancelClick() { function handleCancelClick() {
@@ -293,20 +291,18 @@ export default function RolePermissionEditorForm({
mode: 'delete', mode: 'delete',
}); });
await toast.promise( await execPromiseWithErrorToast(
deletePermissionPromise, async () => {
{ await deletePermissionPromise;
loading: 'Deleting permission...', onDirtyStateChange(false, location);
success: 'Permission has been deleted successfully.', onSubmit?.();
error: getServerError( },
'An error occurred while deleting the permission.', {
), loadingMessage: 'Deleting permission...',
successMessage: 'Permission has been deleted successfully.',
errorMessage: 'An error occurred while deleting the permission.',
}, },
getToastStyleProps(),
); );
onDirtyStateChange(false, location);
onSubmit?.();
} }
function handleDeleteClick() { function handleDeleteClick() {

View File

@@ -43,7 +43,13 @@ export default async function deleteRecord({
(row) => (row) =>
`(${primaryOrUniqueColumns `(${primaryOrUniqueColumns
.map((primaryOrUniqueColumn) => .map((primaryOrUniqueColumn) =>
format('%I=%L', primaryOrUniqueColumn, row[primaryOrUniqueColumn]), row[primaryOrUniqueColumn] === null
? format('%I IS NULL', primaryOrUniqueColumn)
: format(
'%I=%L',
primaryOrUniqueColumn,
row[primaryOrUniqueColumn],
),
) )
.join(' AND ')})`, .join(' AND ')})`,
); );

View File

@@ -1,10 +1,13 @@
import { useDatabaseQuery } from '@/features/database/dataGrid/hooks/useDatabaseQuery';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl'; import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import { getToastStyleProps } from '@/utils/constants/settings'; import { getToastStyleProps } from '@/utils/constants/settings';
import { getHasuraAdminSecret } from '@/utils/env'; import { getHasuraAdminSecret } from '@/utils/env';
import { parseIdentifiersFromSQL } from '@/utils/sql'; import { parseIdentifiersFromSQL } from '@/utils/sql';
import toast from 'react-hot-toast'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import toast from 'react-hot-toast';
export default function useRunSQL( export default function useRunSQL(
sqlCode: string, sqlCode: string,
@@ -15,6 +18,7 @@ export default function useRunSQL(
migrationName: string, migrationName: string,
) { ) {
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [commandOk, setCommandOk] = useState(false); const [commandOk, setCommandOk] = useState(false);
@@ -22,6 +26,14 @@ export default function useRunSQL(
const [columns, setColumns] = useState<string[]>([]); const [columns, setColumns] = useState<string[]>([]);
const [rows, setRows] = useState<string[][]>([[]]); const [rows, setRows] = useState<string[][]>([[]]);
const router = useRouter();
const {
query: { dataSourceSlug },
} = router;
const { refetch } = useDatabaseQuery([dataSourceSlug as string]);
const appUrl = generateAppServiceUrl( const appUrl = generateAppServiceUrl(
currentProject?.subdomain, currentProject?.subdomain,
currentProject?.region, currentProject?.region,
@@ -169,8 +181,34 @@ export default function useRunSQL(
} }
}; };
const trackAll = async (objects: any[]): Promise<Response[]> => {
const apiPath = isPlatform ? '/v1/metadata' : '/apis/migrate';
const responses: Response[] = await Promise.all(
objects.map((object) =>
fetch(`${appUrl}${apiPath}`, {
method: 'POST',
headers: { 'x-hasura-admin-secret': adminSecret },
body: JSON.stringify(object),
}).then((response) => {
if (!response.ok) {
console.error('failed to track:', response);
}
return response;
}),
),
).catch((error) => {
console.error('Error in trackAll:', error);
throw error;
});
return responses;
};
const updateMetadata = async (inputSQL: string) => { const updateMetadata = async (inputSQL: string) => {
const entities = parseIdentifiersFromSQL(inputSQL); const entities = parseIdentifiersFromSQL(inputSQL);
if (entities.length === 0) {
return;
}
const tablesOrViewEntities = entities.filter( const tablesOrViewEntities = entities.filter(
(entity) => entity.type !== 'function', (entity) => entity.type !== 'function',
@@ -179,47 +217,75 @@ export default function useRunSQL(
(entity) => entity.type === 'function', (entity) => entity.type === 'function',
); );
const trackTablesOrViews = tablesOrViewEntities.map(({ name, schema }) => ({ let trackTablesOrViews: any[] = [];
type: 'pg_track_table', let trackFunctions: any[] = [];
args: { if (isPlatform) {
source: 'default', // use v2/query
table: { trackTablesOrViews = tablesOrViewEntities.map(({ name, schema }) => ({
name, type: 'pg_track_table',
schema, args: {
source: 'default',
table: {
name,
schema,
},
}, },
}, }));
})); trackFunctions = functionEntities.map(({ name, schema }) => ({
type: 'pg_track_function',
const trackFunctions = functionEntities.map(({ name, schema }) => ({ args: {
type: 'pg_track_function', source: 'default',
args: { function: {
source: 'default', name,
function: { schema,
name, configuration: {},
schema, },
configuration: {},
}, },
}, }));
})); } else {
// use apis/migrate
const metaDataPayload = { trackTablesOrViews = tablesOrViewEntities.map(({ name, schema }) => ({
source: 'default', name: `add_existing_table_or_view_${schema}_${name}`,
type: 'bulk', datasource: 'default',
args: [...trackTablesOrViews, ...trackFunctions], down: [],
}; skip_execution: false,
up: [
{
type: 'pg_track_table',
args: {
table: { name, schema },
source: 'default',
},
},
],
}));
trackFunctions = functionEntities.map(({ name, schema }) => ({
name: `add_existing_function_or_view_${schema}_${name}`,
datasource: 'default',
down: [],
skip_execution: false,
up: [
{
type: 'pg_track_function',
args: {
function: { name, schema },
source: 'default',
},
},
],
}));
}
try { try {
if (entities.length > 0) { await trackAll([...trackTablesOrViews, ...trackFunctions]).then(
const metadataApiResponse = await fetch(`${appUrl}/v1/metadata`, { (responses) => {
method: 'POST', responses.forEach((response) => {
headers: { 'x-hasura-admin-secret': adminSecret }, if (!response.ok) {
body: JSON.stringify(metaDataPayload), console.error('Error tracking table or view:', response);
}); }
});
if (!metadataApiResponse.ok) { },
throw new Error('Metadata API call failed'); );
}
}
} catch (error) { } catch (error) {
toast.error('An error happened when calling the metadata API', { toast.error('An error happened when calling the metadata API', {
style: toastStyle.style, style: toastStyle.style,
@@ -269,6 +335,9 @@ export default function useRunSQL(
} }
} }
// refresh the table list after running the sql
await refetch();
setLoading(false); setLoading(false);
}; };

View File

@@ -114,7 +114,7 @@ export function createDynamicValidationSchema(
['time', 'timetz', 'interval'].includes(column.specificType) ['time', 'timetz', 'interval'].includes(column.specificType)
) { ) {
return { return {
...schema, ...currentSchema,
[column.id]: createTextValidationSchema(details).matches( [column.id]: createTextValidationSchema(details).matches(
/^\d{2}:\d{2}(:\d{2})?$/, /^\d{2}:\d{2}(:\d{2})?$/,
'This is not a valid time (e.g: HH:MM:SS / HH:MM).', 'This is not a valid time (e.g: HH:MM:SS / HH:MM).',
@@ -124,14 +124,14 @@ export function createDynamicValidationSchema(
if (column.type === 'date') { if (column.type === 'date') {
return { return {
...schema, ...currentSchema,
[column.id]: createDateValidationSchema(details), [column.id]: createDateValidationSchema(details),
}; };
} }
if (column.type === 'boolean') { if (column.type === 'boolean') {
return { return {
...schema, ...currentSchema,
[column.id]: createBooleanValidationSchema(details), [column.id]: createBooleanValidationSchema(details),
}; };
} }
@@ -141,13 +141,13 @@ export function createDynamicValidationSchema(
(column.specificType === 'jsonb' || column.specificType === 'json') (column.specificType === 'jsonb' || column.specificType === 'json')
) { ) {
return { return {
...schema, ...currentSchema,
[column.id]: createJSONValidationSchema(details), [column.id]: createJSONValidationSchema(details),
}; };
} }
return { return {
...schema, ...currentSchema,
[column.id]: createTextValidationSchema(details), [column.id]: createTextValidationSchema(details),
}; };
}, {}); }, {});

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete'; import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetPostgresSettingsDocument, GetPostgresSettingsDocument,
Software_Type_Enum, Software_Type_Enum,
@@ -11,11 +14,11 @@ import {
useGetSoftwareVersionsQuery, useGetSoftwareVersionsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -32,24 +35,30 @@ export type DatabaseServiceVersionFormValues = Yup.InferType<
>; >;
export default function DatabaseServiceVersionSettings() { export default function DatabaseServiceVersionSettings() {
const isPlatform = useIsPlatform();
const { openDialog } = useDialog();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetPostgresSettingsDocument], refetchQueries: [GetPostgresSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetPostgresSettingsQuery({ const { data, loading, error } = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data: databaseVersionsData } = useGetSoftwareVersionsQuery({ const { data: databaseVersionsData } = useGetSoftwareVersionsQuery({
variables: { variables: {
software: Software_Type_Enum.PostgreSql, software: Software_Type_Enum.PostgreSql,
}, },
skip: !isPlatform,
}); });
const { version } = data?.config?.postgres || {}; const { version } = data?.config?.postgres || {};
const databaseVersions = databaseVersionsData?.softwareVersions || []; const databaseVersions = databaseVersionsData?.softwareVersions || [];
const availableVersions = Array.from( const availableVersions = Array.from(
new Set(databaseVersions.map((el) => el.version)).add(version), new Set(databaseVersions.map((el) => el.version)).add(version),
@@ -63,10 +72,21 @@ export default function DatabaseServiceVersionSettings() {
const form = useForm<DatabaseServiceVersionFormValues>({ const form = useForm<DatabaseServiceVersionFormValues>({
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } }, defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && version) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -97,23 +117,30 @@ export default function DatabaseServiceVersionSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Postgres version is being updated...`,
success: `Postgres version has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Postgres version.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
} catch { openDialog({
// Note: The toast will handle the error. title: 'Apply your changes',
} component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Postgres version is being updated...',
successMessage: 'Postgres version has been updated successfully.',
errorMessage:
'An error occurred while trying to update Postgres version.',
},
);
}; };
return ( return (
@@ -130,12 +157,20 @@ export default function DatabaseServiceVersionSettings() {
}} }}
docsLink="https://hub.docker.com/r/nhost/postgres/tags" docsLink="https://hub.docker.com/r/nhost/postgres/tags"
docsTitle="the latest releases" docsTitle="the latest releases"
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5" className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
> >
<ControlledAutocomplete <ControlledAutocomplete
id="version" id="version"
name="version" name="version"
autoHighlight autoHighlight
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false} isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase(); const inputValueLower = inputValue.toLowerCase();

View File

@@ -7,26 +7,28 @@ import { Box } from '@/components/ui/v2/Box';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification'; import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
useGetPostgresSettingsQuery, useGetPostgresSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
capacity: Yup.number().required(), capacity: Yup.number().required().min(10),
}); });
export type AuthDomainFormValues = Yup.InferType<typeof validationSchema>; export type AuthDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthDomain() { export default function AuthDomain() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
const { const {
@@ -36,14 +38,17 @@ export default function AuthDomain() {
refetch: refetchPostgresSettings, refetch: refetchPostgresSettings,
} = useGetPostgresSettingsQuery({ } = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const capacity = const capacity =
data?.config?.postgres?.resources?.storage?.capacity ?? (data?.config?.postgres?.resources?.storage?.capacity ??
currentProject.plan.featureMaxDbSize; currentProject?.plan?.featureMaxDbSize) ||
0;
const [updateConfig] = useUpdateConfigMutation(); const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<Yup.InferType<typeof validationSchema>>({ const form = useForm<Yup.InferType<typeof validationSchema>>({
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
@@ -75,9 +80,9 @@ export default function AuthDomain() {
} }
async function handleSubmit(formValues: AuthDomainFormValues) { async function handleSubmit(formValues: AuthDomainFormValues) {
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfig({ await updateConfig({
variables: { variables: {
appId: currentProject.id, appId: currentProject.id,
config: { config: {
@@ -90,22 +95,19 @@ export default function AuthDomain() {
}, },
}, },
}, },
}), });
{
loading: `Database storage capacity is being updated...`,
success: `Database storage capacity has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the database storage capacity.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); form.reset(formValues);
await refetchPostgresSettings(); await refetchPostgresSettings();
} catch { },
// Note: The toast will handle the error. {
} loadingMessage: 'Database storage capacity is being updated...',
successMessage:
'Database storage capacity has been updated successfully.',
errorMessage:
'An error occurred while trying to update the database storage capacity.',
},
);
} }
return ( return (
@@ -122,7 +124,7 @@ export default function AuthDomain() {
}} }}
className="flex flex-col" className="flex flex-col"
> >
{currentProject.plan.isFree && ( {currentProject.plan?.isFree && (
<UpgradeNotification message="Unlock by upgrading your project to the Pro plan." /> <UpgradeNotification message="Unlock by upgrading your project to the Pro plan." />
)} )}
<Box className="grid grid-flow-row lg:grid-cols-5"> <Box className="grid grid-flow-row lg:grid-cols-5">
@@ -132,7 +134,7 @@ export default function AuthDomain() {
name="capacity" name="capacity"
type="number" type="number"
fullWidth fullWidth
disabled={currentProject.plan.isFree} disabled={currentProject.plan?.isFree}
className="lg:col-span-2" className="lg:col-span-2"
error={Boolean(formState.errors.capacity?.message)} error={Boolean(formState.errors.capacity?.message)}
helperText={formState.errors.capacity?.message} helperText={formState.errors.capacity?.message}
@@ -143,7 +145,7 @@ export default function AuthDomain() {
}} }}
/> />
</Box> </Box>
{!currentProject.plan.isFree && ( {!currentProject.plan?.isFree && (
<Alert severity="info" className="col-span-6 text-left"> <Alert severity="info" className="col-span-6 text-left">
Note that volumes can only be increased (not decreased). Also, due Note that volumes can only be increased (not decreased). Also, due
to an AWS limitation, the same volume can only be increased once to an AWS limitation, the same volume can only be increased once

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -22,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraAllowListFormValues = Yup.InferType<typeof validationSchema>; export type HasuraAllowListFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraAllowListSettings() { export default function HasuraAllowListSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enableAllowList } = data?.config?.hasura.settings || {}; const { enableAllowList } = data?.config?.hasura.settings || {};
@@ -44,6 +51,14 @@ export default function HasuraAllowListSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
enabled: enableAllowList,
});
}
}, [loading, enableAllowList, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -72,24 +87,31 @@ export default function HasuraAllowListSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Allow list settings are being updated...`, await refetchWorkspaceAndProject();
success: `Allow list settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update allow list settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Allow list settings are being updated...',
successMessage: 'Allow list settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update allow list settings.',
},
);
} }
return ( return (

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -22,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraConsoleFormValues = Yup.InferType<typeof validationSchema>; export type HasuraConsoleFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraConsoleSettings() { export default function HasuraConsoleSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enableConsole } = data?.config?.hasura.settings || {}; const { enableConsole } = data?.config?.hasura.settings || {};
@@ -44,6 +51,14 @@ export default function HasuraConsoleSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
enabled: enableConsole,
});
}
}, [loading, enableConsole, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -72,24 +87,32 @@ export default function HasuraConsoleSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Hasura Console settings are being updated...`, await refetchWorkspaceAndProject();
success: `Hasura Console settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Hasura Console settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Hasura Console settings are being updated...',
successMessage:
'Hasura Console settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update Hasura Console settings.',
},
);
} }
return ( return (

View File

@@ -1,19 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup'; import * as Yup from 'yup';
@@ -30,16 +32,20 @@ const validationSchema = Yup.object({
export type HasuraCorsDomainFormValues = Yup.InferType<typeof validationSchema>; export type HasuraCorsDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraCorsDomainSettings() { export default function HasuraCorsDomainSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { corsDomain } = data?.config?.hasura.settings || {}; const { corsDomain } = data?.config?.hasura.settings || {};
@@ -95,24 +101,30 @@ export default function HasuraCorsDomainSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `CORS domain settings are being updated...`, await refetchWorkspaceAndProject();
success: `CORS domain settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's CORS domain settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'CORS domain settings are being updated...',
successMessage: 'CORS domain settings have been updated successfully.',
errorMessage: `An error occurred while trying to update the project's CORS domain settings.`,
},
);
} }
return ( return (

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -22,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraDevModeFormValues = Yup.InferType<typeof validationSchema>; export type HasuraDevModeFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraDevModeSettings() { export default function HasuraDevModeSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { devMode } = data?.config?.hasura.settings || {}; const { devMode } = data?.config?.hasura.settings || {};
@@ -44,6 +51,14 @@ export default function HasuraDevModeSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
enabled: devMode,
});
}
}, [loading, devMode, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -65,31 +80,38 @@ export default function HasuraDevModeSettings() {
config: { config: {
hasura: { hasura: {
settings: { settings: {
enableConsole: formValues.enabled, devMode: formValues.enabled,
}, },
}, },
}, },
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Dev Mode settings are being updated...`, await refetchWorkspaceAndProject();
success: `Dev Mode settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Dev Mode settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Dev Mode settings are being updated...',
successMessage: 'Dev Mode settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update Dev Mode settings.',
},
);
} }
return ( return (

View File

@@ -1,19 +1,22 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete'; import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -32,31 +35,40 @@ export type HasuraEnabledAPIFormValues = Yup.InferType<typeof validationSchema>;
const AVAILABLE_HASURA_APIS = ['metadata', 'graphql', 'pgdump', 'config']; const AVAILABLE_HASURA_APIS = ['metadata', 'graphql', 'pgdump', 'config'];
export default function HasuraEnabledAPISettings() { export default function HasuraEnabledAPISettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enabledAPIs } = data?.config?.hasura.settings || {}; const { enabledAPIs = [] } = data?.config?.hasura?.settings || {};
const form = useForm<HasuraEnabledAPIFormValues>({ const form = useForm<HasuraEnabledAPIFormValues>({
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
defaultValues: { defaultValues: {
enabledAPIs: enabledAPIs.map((api) => ({ enabledAPIs: [],
label: api,
value: api,
})),
}, },
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (enabledAPIs && !loading) {
form.reset({
enabledAPIs: enabledAPIs.map((api) => ({ label: api, value: api })),
});
}
}, [form, enabledAPIs, loading]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -93,24 +105,30 @@ export default function HasuraEnabledAPISettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Enabled APIs are being updated...`, await refetchWorkspaceAndProject();
success: `Enabled APIs have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update enabled APIs.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Enabled APIs are being updated...',
successMessage: 'Enabled APIs have been updated successfully.',
errorMessage: 'An error occurred while trying to update enabled APIs.',
},
);
} }
return ( return (
@@ -125,7 +143,7 @@ export default function HasuraEnabledAPISettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-6" className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-6"
> >
<ControlledAutocomplete <ControlledAutocomplete
id="enabledAPIs" id="enabledAPIs"

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete'; import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
@@ -5,16 +7,17 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { HighlightedText } from '@/components/presentational/HighlightedText'; import { HighlightedText } from '@/components/presentational/HighlightedText';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -31,16 +34,20 @@ export type HasuraLogLevelFormValues = Yup.InferType<typeof validationSchema>;
const AVAILABLE_HASURA_LOG_LEVELS = ['debug', 'info', 'warn', 'error']; const AVAILABLE_HASURA_LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
export default function HasuraLogLevelSettings() { export default function HasuraLogLevelSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { level } = data?.config?.hasura.logs || {}; const { level } = data?.config?.hasura.logs || {};
@@ -58,6 +65,17 @@ export default function HasuraLogLevelSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && level) {
form.reset({
logLevel: {
label: level,
value: level,
},
});
}
}, [form, loading, level]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -94,24 +112,30 @@ export default function HasuraLogLevelSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Log level is being updated...`, await refetchWorkspaceAndProject();
success: `Log level has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update log level.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Log level is being updated...',
successMessage: 'Log level has been updated successfully.',
errorMessage: 'An error occurred while trying to update log level.',
},
);
} }
return ( return (
@@ -136,7 +160,7 @@ export default function HasuraLogLevelSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5" className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
> >
<ControlledAutocomplete <ControlledAutocomplete
id="logLevel" id="logLevel"

View File

@@ -1,19 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input'; import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -28,16 +30,20 @@ const validationSchema = Yup.object({
export type HasuraPoolSizeFormValues = Yup.InferType<typeof validationSchema>; export type HasuraPoolSizeFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraPoolSizeSettings() { export default function HasuraPoolSizeSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { httpPoolSize } = data?.config?.hasura.events || {}; const { httpPoolSize } = data?.config?.hasura.events || {};
@@ -79,24 +85,30 @@ export default function HasuraPoolSizeSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Pool size is being updated...`, await refetchWorkspaceAndProject();
success: `Pool size has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the pool size.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Pool size is being updated...',
successMessage: 'Pool size has been updated successfully.',
errorMessage: 'An error occurred while trying to update the pool size.',
},
);
} }
return ( return (
@@ -112,7 +124,7 @@ export default function HasuraPoolSizeSettings() {
loading: formState.isSubmitting, loading: formState.isSubmitting,
}, },
}} }}
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5" className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
> >
<Input <Input
{...register('httpPoolSize')} {...register('httpPoolSize')}

View File

@@ -1,18 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
useGetHasuraSettingsQuery, useGetHasuraSettingsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -24,16 +27,20 @@ export type HasuraRemoteSchemaPermissionsFormValues = Yup.InferType<
>; >;
export default function HasuraRemoteSchemaPermissionsSettings() { export default function HasuraRemoteSchemaPermissionsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { enableRemoteSchemaPermissions } = data?.config?.hasura.settings || {}; const { enableRemoteSchemaPermissions } = data?.config?.hasura.settings || {};
@@ -46,6 +53,14 @@ export default function HasuraRemoteSchemaPermissionsSettings() {
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading) {
form.reset({
enabled: enableRemoteSchemaPermissions,
});
}
}, [loading, enableRemoteSchemaPermissions, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -69,31 +84,40 @@ export default function HasuraRemoteSchemaPermissionsSettings() {
config: { config: {
hasura: { hasura: {
settings: { settings: {
enableConsole: formValues.enabled, enableRemoteSchemaPermissions: formValues.enabled,
}, },
}, },
}, },
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Remote schema permission settings are being updated...`, await refetchWorkspaceAndProject();
success: `Remote schema permission settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update remote schema permission settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage:
'Remote schema permission settings are being updated...',
successMessage:
'Remote schema permission settings have been updated successfully.',
errorMessage:
'An error occurred while trying to update remote schema permission settings.',
},
);
} }
return ( return (

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider'; import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete'; import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form'; import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer'; import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator'; import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject'; import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { import {
GetHasuraSettingsDocument, GetHasuraSettingsDocument,
Software_Type_Enum, Software_Type_Enum,
@@ -11,11 +14,11 @@ import {
useGetSoftwareVersionsQuery, useGetSoftwareVersionsQuery,
useUpdateConfigMutation, useUpdateConfigMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings'; import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -32,22 +35,27 @@ export type HasuraServiceVersionFormValues = Yup.InferType<
>; >;
export default function HasuraServiceVersionSettings() { export default function HasuraServiceVersionSettings() {
const isPlatform = useIsPlatform();
const { openDialog } = useDialog();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } = const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject(); useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({ const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument], refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data, loading, error } = useGetHasuraSettingsQuery({ const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id }, variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only', ...(!isPlatform ? { client: localMimirClient } : {}),
}); });
const { data: hasuraVersionsData } = useGetSoftwareVersionsQuery({ const { data: hasuraVersionsData } = useGetSoftwareVersionsQuery({
variables: { variables: {
software: Software_Type_Enum.Hasura, software: Software_Type_Enum.Hasura,
}, },
skip: !isPlatform,
}); });
const { version } = data?.config?.hasura || {}; const { version } = data?.config?.hasura || {};
@@ -55,6 +63,7 @@ export default function HasuraServiceVersionSettings() {
const availableVersions = Array.from( const availableVersions = Array.from(
new Set(versions.map((el) => el.version)).add(version), new Set(versions.map((el) => el.version)).add(version),
) )
.filter((v) => !!v)
.sort() .sort()
.reverse() .reverse()
.map((availableVersion) => ({ .map((availableVersion) => ({
@@ -62,12 +71,25 @@ export default function HasuraServiceVersionSettings() {
value: availableVersion, value: availableVersion,
})); }));
// TODO make sure the network request is made in the parent component
// also this request should be made against cache only and the data should be available right away
const form = useForm<HasuraServiceVersionFormValues>({ const form = useForm<HasuraServiceVersionFormValues>({
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } }, defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema), resolver: yupResolver(validationSchema),
}); });
useEffect(() => {
if (!loading && version) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) { if (loading) {
return ( return (
<ActivityIndicator <ActivityIndicator
@@ -96,24 +118,31 @@ export default function HasuraServiceVersionSettings() {
}, },
}); });
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
updateConfigPromise, await updateConfigPromise;
{ form.reset(formValues);
loading: `Hasura version is being updated...`, await refetchWorkspaceAndProject();
success: `Hasura version has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Hasura version.`,
),
},
getToastStyleProps(),
);
form.reset(formValues); if (!isPlatform) {
await refetchWorkspaceAndProject(); openDialog({
} catch { title: 'Apply your changes',
// Note: The toast will handle the error. component: <ApplyLocalSettingsDialog />,
} props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Hasura version is being updated...',
successMessage: 'Hasura version has been updated successfully.',
errorMessage:
'An error occurred while trying to update Hasura version.',
},
);
} }
return ( return (
@@ -130,11 +159,19 @@ export default function HasuraServiceVersionSettings() {
}} }}
docsLink="https://hub.docker.com/r/nhost/graphql-engine/tags" docsLink="https://hub.docker.com/r/nhost/graphql-engine/tags"
docsTitle="the latest releases" docsTitle="the latest releases"
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5" className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
> >
<ControlledAutocomplete <ControlledAutocomplete
id="version" id="version"
name="version" name="version"
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
autoHighlight autoHighlight
isOptionEqualToValue={() => false} isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {

View File

@@ -7,13 +7,11 @@ import {
GetAllWorkspacesAndProjectsDocument, GetAllWorkspacesAndProjectsDocument,
useDeleteApplicationMutation, useDeleteApplicationMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { getApplicationStatusString } from '@/utils/helpers'; import { getApplicationStatusString } from '@/utils/helpers';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { toast } from 'react-hot-toast';
export default function ApplicationInfo() { export default function ApplicationInfo() {
const { currentProject } = useCurrentWorkspaceAndProject(); const { currentProject } = useCurrentWorkspaceAndProject();
@@ -23,23 +21,18 @@ export default function ApplicationInfo() {
const router = useRouter(); const router = useRouter();
async function handleClickRemove() { async function handleClickRemove() {
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
deleteApplication({ variables: { appId: currentProject.id } }), await deleteApplication({ variables: { appId: currentProject.id } });
{ await router.push('/');
loading: 'Deleting project...', },
success: 'The project has been deleted successfully.', {
error: getServerError( loadingMessage: 'Deleting project...',
'An error occurred while deleting the project. Please try again.', successMessage: 'The project has been deleted successfully.',
), errorMessage:
}, 'An error occurred while deleting the project. Please try again.',
getToastStyleProps(), },
); );
await router.push('/');
} catch {
// Note: The toast will handle the error.
}
} }
if (!currentProject) { if (!currentProject) {

View File

@@ -18,12 +18,10 @@ import {
useUnpauseApplicationMutation, useUnpauseApplicationMutation,
} from '@/generated/graphql'; } from '@/generated/graphql';
import { MAX_FREE_PROJECTS } from '@/utils/constants/common'; import { MAX_FREE_PROJECTS } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings'; import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import type { ApolloError } from '@apollo/client';
import { useUserData } from '@nhost/nextjs'; import { useUserData } from '@nhost/nextjs';
import Image from 'next/image'; import Image from 'next/image';
import { useState } from 'react'; import { useState } from 'react';
import { toast } from 'react-hot-toast';
export default function ApplicationPaused() { export default function ApplicationPaused() {
const { openDialog } = useDialog(); const { openDialog } = useDialog();
@@ -47,33 +45,18 @@ export default function ApplicationPaused() {
const wakeUpDisabled = numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS; const wakeUpDisabled = numberOfFreeAndLiveProjects >= MAX_FREE_PROJECTS;
async function handleTriggerUnpausing() { async function handleTriggerUnpausing() {
try { await execPromiseWithErrorToast(
await toast.promise( async () => {
unpauseApplication({ variables: { appId: currentProject.id } }), await unpauseApplication({ variables: { appId: currentProject.id } });
{ await refetchWorkspaceAndProject();
loading: 'Starting the project...', },
success: `The project has been started successfully.`, {
error: (arg: ApolloError) => { loadingMessage: 'Starting the project...',
// we need to get the internal error message from the GraphQL error successMessage: 'The project has been started successfully.',
const { internal } = arg.graphQLErrors[0]?.extensions || {}; errorMessage:
const { message } = (internal as Record<string, any>)?.error || {}; 'An error occurred while waking up the project. Please try again.',
},
// we use the default Apollo error message if we can't find the );
// internal error message
return (
message ||
arg.message ||
'An error occurred while waking up the project. Please try again.'
);
},
},
getToastStyleProps(),
);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
} }
if (loading) { if (loading) {

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