Compare commits

..

220 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
b0363a4f4c Merge pull request #2110 from nhost/changeset-release/main
chore: update versions
2023-07-07 17:18:43 +01:00
github-actions[bot]
17045b2018 chore: update versions 2023-07-07 16:07:03 +00:00
Hassan Ben Jobrane
c49cc11862 Merge pull request #2108 from nhost/feat/fix-hasura-storage-file-upload
fix(hasura-storage-js): fix file upload
2023-07-07 17:05:33 +01:00
Hassan Ben Jobrane
c83fe7d776 chore(e2e): change e2e tests timeout 2023-07-07 16:48:49 +01:00
Hassan Ben Jobrane
235b4c7405 chore: wrap secret values in quotes 2023-07-07 16:08:24 +01:00
Hassan Ben Jobrane
c2c0fbd33a chore(e2e): increase timeout 2023-07-07 15:19:02 +01:00
Hassan Ben Jobrane
300e3f49e0 chore: add changeset 2023-07-07 14:21:12 +01:00
Hassan Ben Jobrane
a95a77886b fix(hasura-storage-js): fix file upload 2023-07-07 10:44:42 +01:00
Stephan van Opstal
1f3f683202 Update serverless-functions.mdx (#2105)
Please correct me if I'm wrong but I believe the endpoints in the docs
are wrong.
2023-07-07 08:36:57 +02:00
github-actions[bot]
4c67fd23c4 chore: update versions (#2101)
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@0.19.0

### Minor Changes

- 9c61c69a7: chore(dashboard):add postgres 14.6-20230705-1 to the
version selector

### Patch Changes

-   47bda15ff: feat(settings): add warning to pull config

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-06 20:15:13 +02:00
Hassan Ben Jobrane
93d8d71e34 Merge pull request #2102 from nhost/feat/pull-config-warning
feat(settings): add warning to pull config
2023-07-06 15:06:08 +01:00
Hassan Ben Jobrane
47bda15ff2 chore: add changeset 2023-07-06 14:39:27 +01:00
Hassan Ben Jobrane
4563488b5d feat(settings): show alert when there's a repo 2023-07-06 14:35:31 +01:00
Hassan Ben Jobrane
8fd35f3fea feat(settings): add warning to pull config 2023-07-06 14:26:14 +01:00
David Barroso
9c61c69a7b chore(dashboard):add postgres 14.6-20230705-1 to the version selector (#2100) 2023-07-06 15:24:06 +02:00
github-actions[bot]
030ad4621e chore: update versions (#2098)
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@0.18.0

### Minor Changes

- ee0b9b8ed: chore(dashboard):add hasura v2.28.2 and v2.29.0 to the
version selector

## @nhost/docs@0.4.0

### Minor Changes

-   c6fa8da6d: fix(docs): remove outdated reference/cli

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-06 13:34:58 +02:00
David Barroso
ee0b9b8edc chore(dashboard):add hasura v2.28.2 and v2.29.0 to the version selector (#2097) 2023-07-06 13:21:53 +02:00
David Barroso
c6fa8da6df fix(docs): remove outdated reference/cli (#2093)
Fixes nhost/cli/issues/734
2023-07-06 12:21:14 +02:00
github-actions[bot]
dd9dedc226 chore: update versions (#2086) 2023-06-30 10:12:55 +02:00
Hassan Ben Jobrane
5638a91240 Merge pull request #2080 from nhost/renovate/tsconfig-docusaurus-2.x
chore(deps): update dependency @tsconfig/docusaurus to v2
2023-06-29 18:19:00 +01:00
Hassan Ben Jobrane
cdefbdebee chore: remove unchanged packages from changeset 2023-06-29 17:09:27 +01:00
Hassan Ben Jobrane
923abd3655 chore: add changeset 2023-06-29 17:02:26 +01:00
renovate[bot]
ef28540f9a chore(deps): update dependency @tsconfig/docusaurus to v2 2023-06-29 15:10:11 +00:00
Szilárd Dóró
d54e4cdd4e fix(hasura-storage-js): allow using custom buckets for upload (#2085)
This PR is a fix for the [issue mentioned on our Discord
channel](https://discord.com/channels/552499021260914688/1123893547955933214/1123893547955933214).
It wasn't caused by the latest hasura-storage-js release.
2023-06-29 17:07:29 +02:00
David Barroso
4a00963602 feat(observability): added graph with restarts (#2084) 2023-06-29 13:57:37 +02:00
github-actions[bot]
7ea9b890c8 chore: update versions (#2083)
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@0.17.19

### Patch Changes

-   f866120a6: fix(users): use the password length from the config

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-29 11:19:53 +02:00
Szilárd Dóró
f866120a65 fix(dashboard): use dynamic validation schema for password editing (#2082)
Fixes #2081
2023-06-29 10:52:40 +02:00
github-actions[bot]
472559276c chore: update versions (#2079)
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.2.0

### Minor Changes

- 2cdb13b3e: fix(upload): allow specifying `id` and `name` only when not
using `form-data`

## @nhost/apollo@5.2.13

### Patch Changes

-   @nhost/nhost-js@2.2.11

## @nhost/react-apollo@5.0.30

### Patch Changes

-   @nhost/apollo@5.2.13
-   @nhost/react@2.0.26

## @nhost/react-urql@2.0.27

### Patch Changes

-   @nhost/react@2.0.26

## @nhost/nextjs@1.13.32

### Patch Changes

-   @nhost/react@2.0.26

## @nhost/nhost-js@2.2.11

### Patch Changes

-   Updated dependencies [2cdb13b3e]
    -   @nhost/hasura-storage-js@2.2.0

## @nhost/react@2.0.26

### Patch Changes

-   @nhost/nhost-js@2.2.11

## @nhost/vue@1.13.31

### Patch Changes

-   @nhost/nhost-js@2.2.11

## @nhost/dashboard@0.17.18

### Patch Changes

-   @nhost/react-apollo@5.0.30
-   @nhost/nextjs@1.13.32

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

### Patch Changes

- 2cdb13b3e: fix(upload): allow specifying `id` and `name` only when not
using `form-data`
    -   @nhost/nhost-js@2.2.11

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-27 16:50:56 +02:00
Szilárd Dóró
2cdb13b3ef fix(hasura-storage-js): streamline file upload API (#2072)
Fixes #2071

This PR changes the API of the `hasura-storage-js` SDK slightly.

Before:
```ts
const formData = new FormData();

// first file
formData.append('file[]', '<file>');

// second file
formData.append('file[]', '<file>');

const { fileMetadata, error } = await nhost.storage.upload({
  formData,
  id: '<custom-uuid>', // ID doesn't make sense anymore when uploading multiple files
  name: '<custom-name>', // Name doesn't make sense anymore when uploading multiple files
});
```

Now:
```ts
const formData = new FormData();

// first file
formData.append('file[]', '<file>', '<custom-name>');
formData.append('metadata[]', JSON.stringify({ id: '<custom-uuid>' }))

// second file
formData.append('file[]', '<file>', '<custom-name>');
formData.append('metadata[]', JSON.stringify({ id: '<custom-uuid>' }))

const { fileMetadata, error } = await nhost.storage.upload({ formData });

// Access the metadata of upload files via fileMetadata.processedFiles
```

The `id` and `name` attributes can only be specified if you want to
upload a single file:

```ts
const file = event.target.files[0];

const { fileMetadata, error } = await nhost.storage.upload({
  file,
  id: '<custom-id>',
  name: '<custom-name>',
});

// Access the metadata of the upload file via fileMetadata
```
2023-06-27 16:20:17 +02:00
github-actions[bot]
a41124c5e0 chore: update versions (#2077)
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/google-translation@0.0.6

### Patch Changes

-   a5305e6b5: docs: update old URLs to the new format

## @nhost/dashboard@0.17.17

### Patch Changes

-   ea7b102c0: fix(pat): highlight expired tokens

## @nhost/docs@0.3.4

### Patch Changes

-   a5305e6b5: docs: update old URLs to the new format

## @nhost-examples/seed-data-storage@0.0.4

### Patch Changes

-   a5305e6b5: docs: update old URLs to the new format

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-27 12:59:50 +02:00
Hassan Ben Jobrane
6ecffa81ae Merge pull request #2078 from nhost/fix/highlight-expired-tokens
fix(pat): highlight expired tokens
2023-06-27 11:46:19 +01:00
Hassan Ben Jobrane
ea7b102c07 chroe: add changeset 2023-06-27 11:31:03 +01:00
Hassan Ben Jobrane
e9daf92830 chore: fix code formatting 2023-06-27 10:09:29 +01:00
Hassan Ben Jobrane
9e4ad76e7f style: use darker color 2023-06-27 10:00:34 +01:00
Szilárd Dóró
0fd65db563 chore(dashboard): extend readme (#2076) 2023-06-27 09:19:07 +02:00
Hassan Ben Jobrane
146fbb84b9 fix: highlight expired tokens 2023-06-26 17:57:48 +01:00
Szilárd Dóró
b51c18fedb Merge pull request #2075 from nhost/docs/fix-old-urls
fix(docs): use modern URLs
2023-06-26 16:43:12 +02:00
Szilárd Dóró
a5305e6b56 chore: add changeset 2023-06-26 16:31:36 +02:00
Szilárd Dóró
aa88ef2e5c fix(docs): use correct functions URL 2023-06-26 16:08:58 +02:00
Szilárd Dóró
ee6b3c9ac8 fix(docs): use modern URLs 2023-06-26 16:06:49 +02:00
Szilárd Dóró
79fd86acc5 Merge pull request #2074 from nhost/fix/e2e-timeout
fix(ci): timeout long running e2e tests
2023-06-26 15:43:03 +02:00
Szilárd Dóró
c2cbeddcb8 fix(ci): timeout long running e2e tests 2023-06-26 15:34:36 +02:00
Szilárd Dóró
62b2de59d4 Merge pull request #2073 from nhost/changeset-release/main
chore: update versions
2023-06-25 17:50:18 +02:00
github-actions[bot]
2a760593db chore: update versions 2023-06-25 15:34:14 +00:00
Szilárd Dóró
9288873ce8 Merge pull request #2070 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-06-25 17:32:26 +02:00
Szilárd Dóró
47014be8e3 Merge pull request #2065 from nhost/renovate/turbo-monorepo
chore(deps): update dependency turbo to v1.10.6
2023-06-25 15:37:07 +02:00
Szilárd Dóró
49719f7a84 fix: don't break build 2023-06-25 15:23:22 +02:00
Szilárd Dóró
b3b64a3b74 chore: sync versions and add changeset 2023-06-25 15:21:13 +02:00
Szilárd Dóró
3a56c12df4 chore(dashboard): bump turbo to v1.10.6 2023-06-25 15:15:46 +02:00
renovate[bot]
5b15a4f235 chore(deps): update react monorepo 2023-06-25 13:13:47 +00:00
renovate[bot]
83303017c3 chore(deps): update dependency turbo to v1.10.6 2023-06-25 13:12:39 +00:00
Szilárd Dóró
e0739a5883 Merge pull request #2067 from nhost/renovate/graphiql-react-0.x
fix(deps): update dependency @graphiql/react to ^0.18.0
2023-06-25 15:10:36 +02:00
Szilárd Dóró
0a5a841cc8 fix: don't break builds 2023-06-25 14:57:31 +02:00
Szilárd Dóró
3309835f06 chore: revert PNPM version in flake.nix 2023-06-25 14:05:35 +02:00
Szilárd Dóró
32b221f944 chore: add changeset 2023-06-25 14:02:47 +02:00
renovate[bot]
e8a99badb8 fix(deps): update dependency @graphiql/react to ^0.18.0 2023-06-25 11:54:28 +00:00
Szilárd Dóró
1ea6e01963 Merge pull request #2066 from nhost/renovate/tj-actions-changed-files-37.x
chore(deps): update tj-actions/changed-files action to v37
2023-06-25 13:54:11 +02:00
Szilárd Dóró
958dec5dfe Merge pull request #2060 from nhost/changeset-release/main
chore: update versions
2023-06-25 13:50:55 +02:00
renovate[bot]
09257fbfb2 chore(deps): update tj-actions/changed-files action to v37 2023-06-24 17:15:19 +00:00
github-actions[bot]
61e3497a13 chore: update versions 2023-06-24 17:12:27 +00:00
Szilárd Dóró
a7b4e5606d Merge pull request #2069 from nhost/fix/security-keys
fix(webauthn): don't break webauthn form on save
2023-06-24 19:11:12 +02:00
Szilárd Dóró
34d77c9db1 fix(webauthn): don't break webauthn form on save 2023-06-24 18:54:06 +02:00
Szilárd Dóró
4f1efd28a6 Merge pull request #2058 from nhost/renovate/graphql-16.x
chore(deps): update dependency graphql to v16.7.1
2023-06-23 16:00:02 +02:00
Szilárd Dóró
07a45fde0e chore: add changeset 2023-06-23 14:30:07 +02:00
renovate[bot]
9d0380eef3 chore(deps): update dependency graphql to v16.7.1 2023-06-23 12:12:53 +00:00
Szilárd Dóró
ce3ec36b0a Merge pull request #2059 from nhost/fix/404
fix(dashboard): don't redirect to 404 page
2023-06-23 14:10:21 +02:00
Szilárd Dóró
b62a9d19b5 chore(dashboard): improve verbosity of variables 2023-06-23 13:02:32 +02:00
Szilárd Dóró
c1472079c5 Merge pull request #2057 from nhost/renovate/turbo-monorepo
chore(deps): update dependency turbo to v1.10.5
2023-06-23 12:23:29 +02:00
Szilárd Dóró
dd36971798 chore(pnpm): revert pnpm-lock file 2023-06-23 12:17:08 +02:00
Szilárd Dóró
6199c1c555 fix(dashboard): don't redirect to 404 page 2023-06-23 12:11:22 +02:00
Szilárd Dóró
f41fdc12af chore: bump turbo in the Dockerfile, add changeset 2023-06-23 10:23:43 +02:00
renovate[bot]
fc419ffa4d chore(deps): update dependency turbo to v1.10.5 2023-06-22 19:20:02 +00:00
Szilárd Dóró
b7c102e876 Merge pull request #2056 from nhost/changeset-release/main
chore: update versions
2023-06-21 16:01:32 +02:00
github-actions[bot]
873fc36e61 chore: update versions 2023-06-21 13:29:42 +00:00
Szilárd Dóró
29743f0b71 Merge pull request #2053 from nhost/renovate/react-monorepo
chore(deps): update react monorepo and `@storybook/testing-library`
2023-06-21 15:28:22 +02:00
Szilárd Dóró
d904ca2bbf Merge branch 'renovate/react-monorepo' of https://github.com/nhost/nhost into renovate/react-monorepo 2023-06-21 10:29:52 +02:00
Szilárd Dóró
80b22724de chore(deps): bump @storybook/testing-library 2023-06-21 10:29:38 +02:00
renovate[bot]
80e49f4459 chore(deps): update react monorepo 2023-06-21 07:58:56 +00:00
David Barroso
b3d5ead508 chore(docs): fix stripe reference to env vars (#2054) 2023-06-21 09:55:45 +02:00
renovate[bot]
77dcb8c964 chore(deps): update react monorepo 2023-06-19 16:30:54 +00:00
Szilárd Dóró
3488da9dfd Merge pull request #2052 from nhost/changeset-release/main
chore: update versions
2023-06-19 10:13:57 +02:00
github-actions[bot]
0e68a1fdfd chore: update versions 2023-06-16 12:39:47 +00:00
Szilárd Dóró
8797b2bd17 Merge pull request #2051 from nhost/renovate/commander-11.x
fix(deps): update dependency commander to v11
2023-06-16 14:38:37 +02:00
Szilárd Dóró
5ef0b31573 chore: add changeset 2023-06-16 11:20:58 +02:00
renovate[bot]
86e5e0fb50 fix(deps): update dependency commander to v11 2023-06-16 01:45:29 +00:00
Szilárd Dóró
c2d589dd29 Merge pull request #2049 from nhost/changeset-release/main
chore: update versions
2023-06-15 11:34:48 +02:00
github-actions[bot]
4b807d8134 chore: update versions 2023-06-15 09:16:08 +00:00
Szilárd Dóró
ccdabb707f Merge pull request #2048 from nhost/fix/system-env-var-copy
chore(docs): update environment variable docs
2023-06-15 11:14:40 +02:00
Szilárd Dóró
364bc87fd3 docs: update custom env vars section 2023-06-15 10:48:43 +02:00
Szilárd Dóró
cc02902cbb chore: docs: update env var docs 2023-06-15 10:42:00 +02:00
Szilárd Dóró
0e838b9406 Merge pull request #2043 from nhost/changeset-release/main
chore: update versions
2023-06-15 09:47:18 +02:00
Szilárd Dóró
37ebf7d8e2 Merge pull request #2044 from nhost/chore/update-developers-guide
chore: update DEVELOPERS.md
2023-06-15 09:37:53 +02:00
github-actions[bot]
e23af24bdd chore: update versions 2023-06-15 07:29:50 +00:00
Szilárd Dóró
90eb53cf19 Merge pull request #2045 from nhost/fix/storybook-and-tests
fix(dashboard): don't break storybook and don't show warnings during tests
2023-06-15 09:28:21 +02:00
Szilárd Dóró
7e516d7630 Merge pull request #2046 from nhost/fix/functions-and-graphql-header
fix(nhost-js): pass access token to underlying clients
2023-06-15 08:39:01 +02:00
Szilárd Dóró
0861e41e70 fix: dashboard: correct typo in the readme 2023-06-15 08:38:41 +02:00
Szilárd Dóró
057e7e2572 chore: dashboard: update README 2023-06-14 21:30:43 +02:00
Szilárd Dóró
5a4e237a29 fix(nhost-js): pass access token to underlying clients 2023-06-14 21:17:46 +02:00
Szilárd Dóró
c7501c70ae fix: pin playwright to 1.31.0 2023-06-14 18:57:27 +02:00
Szilárd Dóró
6a45c1abad fix: dashboard: don't break storybook 2023-06-14 15:29:53 +02:00
Szilárd Dóró
660d339e14 fix: dashboard: prevent warnings during tests 2023-06-14 15:29:37 +02:00
Szilárd Dóró
3dca08595d Merge pull request #2038 from nhost/fix/token-reload
fix(hasura-auth-js): prevent infinite token refresh
2023-06-14 14:38:22 +02:00
Szilárd Dóró
7c501c4e4f chore: add section about selecting versions 2023-06-14 14:36:17 +02:00
github-actions[bot]
b9316bb668 chore: update versions (#2041)
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@0.17.11

### Patch Changes

- bd4d0c270: chore(dashboard):add postgres 14.6-20230613-1 to the
version selector

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-14 14:29:07 +02:00
Szilárd Dóró
5e1d5b737c chore: extend DEVELOPERS.md with changeset info 2023-06-14 14:27:08 +02:00
David Barroso
bd4d0c2708 chore(dashboard):add postgres 14.6-20230613-1 to the version selector (#2039) 2023-06-14 14:15:12 +02:00
Szilárd Dóró
1d04ad6306 fix: hasura-auth-js: don't break unit tests 2023-06-14 12:01:13 +02:00
Szilárd Dóró
a4fa5f6f59 fix: don't break unit tests 2023-06-14 10:06:31 +02:00
Szilárd Dóró
7e973d568a fix: hasura-auth-js: prevent infinite token refresh 2023-06-14 09:46:58 +02:00
Szilárd Dóró
d81c52209b Merge pull request #2036 from nhost/chore/bump-nhost-cli
chore(nix): bump Nhost CLI and Node.js versions
2023-06-13 16:05:04 +02:00
Szilárd Dóró
72744b3082 chore: remove duplicate nhost package 2023-06-13 14:43:04 +02:00
Szilárd Dóró
ff4efe2712 chore: bump Node version in workspace 2023-06-13 14:42:28 +02:00
Szilárd Dóró
2982b90469 chore: bump Nhost CLI version 2023-06-13 14:35:21 +02:00
Szilárd Dóró
428a5df038 Merge pull request #2034 from nhost/changeset-release/main
chore: update versions
2023-06-13 13:40:44 +02:00
github-actions[bot]
f79bf784b5 chore: update versions 2023-06-13 11:27:56 +00:00
Szilárd Dóró
3b7449ac08 Merge pull request #2035 from nhost/fix/reset-password
fix(dashboard): don't break the password reset flow
2023-06-13 13:26:46 +02:00
Szilárd Dóró
37bbfdb7ae fix: use system colors when storage is empty 2023-06-13 13:24:30 +02:00
Szilárd Dóró
eb570d2d09 fix: don't break linter 2023-06-13 13:22:09 +02:00
Szilárd Dóró
c8c2a10b2d fix: dashboard: don't break the password reset flow 2023-06-13 13:09:36 +02:00
Szilárd Dóró
92c79eb2fb Merge pull request #2033 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-06-13 10:29:58 +02:00
Szilárd Dóró
e70b45498d chore: add changeset 2023-06-13 09:57:17 +02:00
renovate[bot]
2e1ecfa731 chore(deps): update react monorepo 2023-06-13 06:40:47 +00:00
Szilárd Dóró
8d323a7762 Merge pull request #2031 from nhost/changeset-release/main
chore: update versions
2023-06-13 08:38:01 +02:00
github-actions[bot]
8aa0ff936a chore: update versions 2023-06-13 06:05:58 +00:00
Szilárd Dóró
c6806d60c7 Merge pull request #1995 from nhost/chore/remove-password-input
chore(dashboard): remove password input from project creation
2023-06-13 08:04:39 +02:00
Szilárd Dóró
a13eb25ebc Merge pull request #2021 from nhost/renovate/react-monorepo 2023-06-12 16:59:13 +02:00
Szilárd Dóró
228d8a0686 fix: don't break build 2023-06-12 16:07:08 +02:00
Szilárd Dóró
0de1bc7ce3 Merge branch 'main' into renovate/react-monorepo 2023-06-12 16:01:01 +02:00
Szilárd Dóró
6a94cad04b Merge pull request #2018 from nhost/renovate/vitest-monorepo
chore(deps): update vitest monorepo to ^0.32.0
2023-06-12 15:49:22 +02:00
Szilárd Dóró
8643d25cc8 Merge branch 'renovate/react-monorepo' of https://github.com/nhost/nhost into renovate/react-monorepo 2023-06-12 15:28:54 +02:00
Szilárd Dóró
e820f11dda Merge branch 'renovate/vitest-monorepo' of https://github.com/nhost/nhost into renovate/vitest-monorepo 2023-06-12 15:24:50 +02:00
Szilárd Dóró
3555ab2b71 chore: add changeset and swap coverage dependency 2023-06-12 15:23:40 +02:00
renovate[bot]
6e41d58131 chore(deps): update vitest monorepo to ^0.32.0 2023-06-12 13:15:35 +00:00
renovate[bot]
6cf3beae1c chore(deps): update dependency @types/react to v18.2.11 2023-06-12 13:14:46 +00:00
Szilárd Dóró
022b76e784 chore: add changeset 2023-06-12 15:13:41 +02:00
Szilárd Dóró
2fbe88f806 Merge pull request #2032 from nhost/feat/download-backups
feat(dashboard): add download button to backups
2023-06-12 15:12:19 +02:00
Szilárd Dóró
9457bc32ca chore: dashboard: simplify restoration modal 2023-06-12 14:07:42 +02:00
Szilárd Dóró
3de2639ae9 feat: dashboard: add Hasura v2.27.0-ce to the version selector 2023-06-12 13:22:23 +02:00
Szilárd Dóró
c43e549224 feat: dashboard: add download button to backups 2023-06-12 13:12:42 +02:00
renovate[bot]
fc6fe5007b chore(deps): update vitest monorepo to ^0.32.0 2023-06-12 10:44:57 +00:00
renovate[bot]
829febf33b chore(deps): update dependency @types/react to v18.2.11 2023-06-12 10:44:09 +00:00
David Barroso
ae99ba14b9 docs: added documentation on overlays (#2004) 2023-06-12 12:41:43 +02:00
Szilárd Dóró
a158dc3a17 Merge pull request #2030 from nhost/chore/bump-turbo-pnpm
chore: bump turbo and pnpm
2023-06-12 11:59:06 +02:00
Szilárd Dóró
8420550990 chore: bump turbo and pnpm 2023-06-12 11:41:19 +02:00
Szilárd Dóró
156667cdbd Merge pull request #2027 from nhost/chore/gh-actions-node
fix: revert Node to v16
2023-06-12 11:20:02 +02:00
Szilárd Dóró
7d388a8c91 fix: revert Node to v16 2023-06-12 10:41:26 +02:00
Szilárd Dóró
d32a2fceae Merge pull request #2026 from nhost/changeset-release/main
chore: update versions
2023-06-12 10:07:52 +02:00
github-actions[bot]
d690eb86bb chore: update versions 2023-06-12 07:21:55 +00:00
Szilárd Dóró
d91271cce1 Merge pull request #2020 from luciodale/main
Add missing fields to JWTClaims interface
2023-06-12 09:19:51 +02:00
Szilárd Dóró
1e74a2da85 Merge pull request #2008 from nhost/feat/config-parity
feat(dashboard): bring the dashboard closer to the config
2023-06-12 09:02:54 +02:00
Szilárd Dóró
bc8837b961 chore: remove unnecessary changeset 2023-06-12 08:58:53 +02:00
Lucio D'Alessandro
78fdad8404 Add changeset 2023-06-07 20:10:43 +02:00
Szilárd Dóró
2e8a72d445 fix: remove incomplete sign-in providers 2023-06-07 14:55:08 +02:00
Szilárd Dóró
ce1ae32772 chore: add explanation to Hasura Logs 2023-06-07 14:16:29 +02:00
Szilárd Dóró
b51455d324 fix: use unique input IDs 2023-06-07 13:43:26 +02:00
Szilárd Dóró
28a305d9be chore: add changeset 2023-06-07 11:33:13 +02:00
Lucio D'Alessandro
e23bf4500d Add missing fields to JWTClaims interface 2023-06-07 11:07:11 +02:00
Szilárd Dóró
d0457fe5c3 chore: add changeset 2023-06-07 09:57:30 +02:00
Szilárd Dóró
766d1e1c5a feat: add support for password length config 2023-06-07 09:55:52 +02:00
Szilárd Dóró
44d460cd01 Merge pull request #2017 from nhost/changeset-release/main
chore: update versions
2023-06-06 17:07:25 +02:00
github-actions[bot]
adf934c871 chore: update versions 2023-06-06 14:52:48 +00:00
Szilárd Dóró
0a963486e2 Merge pull request #2016 from nhost/docs/use-correct-sample-urls
docs: use correct sample URLs for custom client
2023-06-06 16:51:37 +02:00
Szilárd Dóró
227d1704f2 docs: use correct sample URLs for custom client 2023-06-06 15:55:41 +02:00
Szilárd Dóró
2baef92988 feat: add support for pool size configuration 2023-06-06 15:41:16 +02:00
Szilárd Dóró
2a10da128d feat: add support for log level settings 2023-06-06 15:31:30 +02:00
Szilárd Dóró
62a51c9fc7 feat: add bitbucket, gitlab and strava support 2023-06-06 15:20:41 +02:00
Szilárd Dóró
58977b173b Merge branch 'main' into feat/config-parity 2023-06-06 14:50:45 +02:00
Szilárd Dóró
b5e5dcf6de chore: bump Node to v18 in CI 2023-06-06 14:39:23 +02:00
Szilárd Dóró
157e1b74b8 Merge pull request #2015 from nhost/changeset-release/main
chore: update versions
2023-06-06 14:10:14 +02:00
github-actions[bot]
b3a475c60f chore: update versions 2023-06-06 12:04:51 +00:00
Szilárd Dóró
3d62871db1 Merge pull request #2014 from nhost/fix/pat-section
fix(dashboard): don't break account settings page
2023-06-06 14:01:53 +02:00
Szilárd Dóró
4f0368b95f fix: don't break account settings page 2023-06-06 13:50:13 +02:00
Szilárd Dóró
0385093111 fix: use correct gravatar value 2023-06-06 13:19:50 +02:00
Szilárd Dóró
463cb50c27 Merge pull request #2011 from nhost/changeset-release/main 2023-06-06 11:40:03 +02:00
Szilárd Dóró
a50174a0a1 feat: add support for changing enabled APIs 2023-06-06 11:19:32 +02:00
Szilárd Dóró
21cbe7487e chore: fix typos 2023-06-06 10:43:24 +02:00
Szilárd Dóró
6e4b34126e feat: add support for enabling/disabling remote schema permissions 2023-06-06 10:40:59 +02:00
Szilárd Dóró
fd3a1a44ef feat: add support for Hasura Dev Mode settings 2023-06-06 10:34:38 +02:00
Szilárd Dóró
66e6021dc0 feat: add support for enabling / disabling the console 2023-06-06 10:27:32 +02:00
Szilárd Dóró
57fdba70e0 feat: add Hasura Allow List configuration 2023-06-06 10:08:23 +02:00
github-actions[bot]
676c11f814 chore: update versions 2023-06-06 07:27:04 +00:00
Szilárd Dóró
d8442a290b Merge pull request #2010 from nhost/chore/lower-max-resources
chore(dashboard): lower the max allowed resources per service
2023-06-06 09:25:53 +02:00
Szilárd Dóró
0db333353b chore: add message to resources 2023-06-05 16:30:53 +02:00
Szilárd Dóró
7ea8120723 Merge pull request #2009 from nhost/docs/custom-urls
docs: add section about using custom URLs
2023-06-05 16:27:33 +02:00
Szilárd Dóró
64a8f41d03 chore: add changeset 2023-06-05 16:18:30 +02:00
Szilárd Dóró
8e12ded94b chore: lower the maximum allowed resources 2023-06-05 16:17:06 +02:00
Szilárd Dóró
564ce1ac2d docs: add section about custom URLs 2023-06-05 16:03:50 +02:00
Szilárd Dóró
b024817eb5 docs: add docs about using custom URLs 2023-06-05 15:55:36 +02:00
Szilárd Dóró
24f98630fd Merge branch 'main' into feat/config-parity 2023-06-05 14:58:57 +02:00
Szilárd Dóró
c1b024cf53 Merge pull request #2006 from nhost/fix/example-visibility
fix: make node-storage example private
2023-06-05 14:51:18 +02:00
Szilárd Dóró
dbacbf140b fix: make node-storage example private 2023-06-05 14:22:17 +02:00
Szilárd Dóró
eda9e57583 feat: add support for Hasura CORS domain settings 2023-06-05 14:20:15 +02:00
Szilárd Dóró
0a9af5075c Merge pull request #2005 from nhost/changeset-release/main
chore: update versions
2023-06-05 13:41:52 +02:00
Szilárd Dóró
f92d9d1fd2 feat: support for access and refresh token config 2023-06-05 13:32:33 +02:00
github-actions[bot]
15168539d8 chore: update versions 2023-06-05 11:13:57 +00:00
Szilárd Dóró
0d74217a4c Merge pull request #2003 from nhost/chore/node-18-upgrade
fix(hasura-storage-js): don't break when using Node 18
2023-06-05 13:12:10 +02:00
Szilárd Dóró
9721527324 chore: improve storage example 2023-06-05 10:48:15 +02:00
Szilárd Dóró
fd4d024bfc feat: add Nhost to the storage example 2023-06-05 10:46:29 +02:00
Szilárd Dóró
c994c8f05b Merge pull request #2000 from nhost/changeset-release/main
chore: update versions
2023-06-05 10:08:26 +02:00
Szilárd Dóró
4c00a796eb fix: don't break builds 2023-06-05 09:50:30 +02:00
github-actions[bot]
2d3a77af76 chore: update versions 2023-06-05 07:06:47 +00:00
Szilárd Dóró
ef05d69889 Merge pull request #2002 from nhost/fix/deployment-timestamp
fix(dashboard): use correct timestamps for deployments
2023-06-05 09:05:02 +02:00
Szilárd Dóró
9b1d0f7a5b fix: use correct timestamps 2023-06-02 16:25:32 +02:00
Szilárd Dóró
07abea4c16 fix: make storage work with Node 18+
feat: add Node.js storage example
2023-06-02 16:19:10 +02:00
David Barroso
8733961026 docs: added entry about seeds (#2001) 2023-06-02 14:51:19 +02:00
Szilárd Dóró
dfa8776b2b chore: show loading state 2023-06-02 11:18:28 +02:00
Szilárd Dóró
1b9f15cb67 chore: improve password reset UX 2023-06-02 11:14:19 +02:00
Szilárd Dóró
b683615269 chore: confirm route change on database form 2023-06-02 10:53:33 +02:00
Szilárd Dóró
3dc97f17ae Merge pull request #1998 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.2.8
2023-06-02 10:48:46 +02:00
Szilárd Dóró
6d2963ffa7 chore: add changeset 2023-06-02 10:26:38 +02:00
renovate[bot]
d1ec8c0781 chore(deps): update dependency @types/react to v18.2.8 2023-06-02 07:29:45 +00:00
Szilárd Dóró
8b205e9c08 Merge pull request #1994 from nhost/fix/react-apollo-e2e
fix(ci): don't break E2E tests and Docker image creation
2023-06-02 09:27:28 +02:00
David Barroso
e2792cd453 docs: cli: added documentation for jsonpatches (#1996) 2023-06-02 09:00:02 +02:00
Szilárd Dóró
a60ca2f6f5 chore: update info message, update reset button 2023-06-01 16:45:10 +02:00
Szilárd Dóró
14a2ead79f chore: unify Alert component's styling 2023-06-01 16:24:29 +02:00
Szilárd Dóró
b625a6b4d4 chore: cleanup unused code 2023-06-01 16:24:12 +02:00
Szilárd Dóró
fd12aa0a8d chore: remove password input 2023-06-01 16:23:57 +02:00
Szilárd Dóró
8871267b91 chore: downgrade pnpm to 8.5.1 2023-06-01 14:50:18 +02:00
Szilárd Dóró
e3001ba4a5 chore: add changeset 2023-06-01 14:21:49 +02:00
Szilárd Dóró
1133b76a7e fix: don't break react-apollo example e2e tests 2023-06-01 14:21:12 +02:00
337 changed files with 9274 additions and 3754 deletions

View File

@@ -14,7 +14,7 @@ runs:
steps:
- uses: pnpm/action-setup@v2.2.4
with:
version: 8.6.0
version: 8.6.2
run_install: false
- name: Get pnpm cache directory
id: pnpm-cache-dir
@@ -26,7 +26,7 @@ runs:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-node-
- name: Use Node.js 16
- name: Use Node.js v16
uses: actions/setup-node@v3
with:
node-version: 16

View File

@@ -43,7 +43,7 @@ jobs:
BUILD: 'all'
- name: Check if the pnpm lockfile changed
id: changed-lockfile
uses: tj-actions/changed-files@v36
uses: tj-actions/changed-files@v37
with:
files: pnpm-lock.yaml
# * Determine a pnpm filter argument for packages that have been modified.
@@ -146,6 +146,7 @@ jobs:
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
- name: Run e2e tests
timeout-minutes: 7
run: pnpm --filter="${{ matrix.package.name }}" run e2e
- id: file-name
if: ${{ failure() }}

4
.gitignore vendored
View File

@@ -62,3 +62,7 @@ todo.md
# Nhost CLI data
.nhost
# Nix
.envrc
.direnv/

View File

@@ -1,16 +1,37 @@
# Developer guide
# Developer Guide
## Requirements
- This repository works with **Node 16**
### Node.js v18
- We use [pnpm](https://pnpm.io/) as a package manager to speed up development and builds, and as a basis for our monorepo. You need to make sure it's installed on your machine. There are [several ways to install it](https://pnpm.io/installation), but the easiest way is with `npm`:
_⚠️ Node.js v16 is also supported for the time being but support will be dropped in the near future_.
### [pnpm](https://pnpm.io/) package manager
The easiest way to install `pnpm` if it's not installed on your machine yet is to use `npm`:
```sh
$ npm install -g pnpm
```
- Our tests and examples use the Nhost CLI, to run the backend services locally. You can follow the installation instructions in [our documentation](https://docs.nhost.io/get-started/cli-workflow/install-cli).
### [Nhost CLI](https://docs.nhost.io/cli)
- The CLI is primarily used for running the E2E tests
- Please refer to the [installation guide](https://docs.nhost.io/get-started/cli-workflow/install-cli) if you have not installed it yet
## File Structure
The repository is organized as a monorepo, with the following structure (only relevant folders are shown):
```
assets/ # Assets used in the README
config/ # Configuration files for the monorepo
dashboard/ # Dashboard
docs/ # Documentation website
examples/ # Example projects
packages/ # Core packages
integrations/ # These are packages that rely on the core packages
```
## Get started
@@ -31,25 +52,25 @@ $ pnpm install
### Development
Although package references are correctly updated on the fly for TypeScript, example projects won't
see the changes because they are depending on the build output. To fix this, you can run packages
in development mode.
Although package references are correctly updated on the fly for TypeScript, example projects and the dashboard won't see the changes because they are depending on the build output. To fix this, you can run packages in development mode.
Running packages in development mode is as simple as:
Running packages in development mode from the root folder is as simple as:
```sh
$ pnpm dev
```
Our packages are linked together using [PNPM's workspace](https://pnpm.io/workspaces) feature. Vite automatically detects changes in the dependencies and rebuilds everything, so that the changes are immediately reflected in the other packages.
Our packages are linked together using [PNPM's workspace](https://pnpm.io/workspaces) feature. Next.js and Vite automatically detect changes in the dependencies and rebuild everything, so the changes will be reflected in the examples and the dashboard.
### Use examples
**Note:** It's possible that Next.js or Vite throw an error when you run `pnpm dev`. Restarting the process should fix it.
### Use Examples
Examples are a great way to test your changes in practice. Make sure you've `pnpm dev` running in your terminal and then run an example.
Let's follow the instructions to run [react-apollo example](https://github.com/nhost/nhost/blob/main/examples/react-apollo/README.md).
## Run the documentation website locally
## Edit Documentation
The easier way to contribute to our documentation is to go to the `docs` folder and follow the [instructions to start local development](https://github.com/nhost/nhost/blob/main/docs/README.md):
@@ -60,9 +81,9 @@ $ pnpm install
$ pnpm start
```
## Run test suites
## Run Test Suites
### Unit tests
### Unit Tests
You can run the unit tests with the following command from the repository root:
@@ -70,7 +91,7 @@ You can run the unit tests with the following command from the repository root:
$ pnpm test
```
### End-to-end tests
### E2E Tests
Each package that defines end-to-end tests embeds their own Nhost configuration, that will be automatically when running the tests. As a result, you must make sure you are not running the Nhost CLI before running the tests.
@@ -83,24 +104,60 @@ $ pnpm e2e
## Changesets
If you've made changes to the packages, you must describe those changes so that they can be reflected in the next release.
We use [changesets](https://github.com/changesets/changesets) to support our versioning and release workflows. When you submit a pull request, a bot checks if some changesets are present, and if not, it directs you to add them.
We use [changesets](https://github.com/changesets/changesets) to support our versioning and release workflows. When you submit a pull request, a bot checks if changesets are present, and if not, it asks you to add them.
The most comprehensive way to add a changeset is to run the following command in the repository root:
To create a changeset, run the following command from the repository root:
```sh
$ pnpm changeset
```
This will create a file in the `.changeset` directory. You can edit it to give more details about the change you just made.
This command will guide you through the process of creating a changeset. It will create a file in the `.changeset` directory.
You can take a look at the changeset documentation: [How to add a changeset](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).
## Committing changes
### Selecting the Version
You'll notice that `git commit` takes a few seconds to run. We set a commit hook that scans the changes in the code, automatically generates documentation from the inline [TSDoc](https://tsdoc.org/) annotations, and adds these generated documentation files to the commit. They automatically update the [reference documentation](https://docs.nhost.io/reference).
When you create a changeset, you will be asked to select the version of the package that you are bumping. The versioning scheme is as follows:
- **major**
- For breaking changes (e.g: changing the function signature, etc.)
- Should be avoided as much as possible as it will require users to update their code. Instead, consider supporting both the old and the new API simultaneously for a while.
- For example: `v1.5.8` -> `v2.0.0`
- **minor**
- For new features (e.g: adding a new page to the dashboard, etc.)
- For example: `v1.5.8` -> `v1.6.0`
- **patch**
- For bug fixes (e.g: fixing a typo, etc.)
- For example: `v1.5.8` -> `v1.5.9`
<!-- ## Good practices
- lint
- prettier
- documentation -->
### Writing Good Changesets
A concise summary that describes the changes should be added to each PR. This summary will be used as the changeset description.
The following structure is used for describing changes:
- **The type of the change**:
- fix
- feat
- chore
- docs
- **The scope of the change** (_broader scopes (e.g: dashboard, hasura-storage-js, etc.) are not recommended as GitHub Releases already contain which project is being bumped_):
- projects
- deployments
- deps
- etc.
- **A short summary of the changes that were made**
**Examples:**
- `fix(deployments): use correct timestamp for deployment details`
- `chore(deps): bump @types/react to v18.2.8`
- `feat(secrets): enable secrets`
- etc.
You can always take a look at examples of changesets in the [GitHub Releases section](https://github.com/nhost/nhost/releases).

View File

@@ -34,7 +34,7 @@ Nhost consists of open source software:
- Authentication: [Hasura Auth](https://github.com/nhost/hasura-auth/)
- Storage: [Hasura Storage](https://github.com/nhost/hasura-storage)
- Serverless Functions: Node.js (JavaScript and TypeScript)
- [Nhost CLI](https://docs.nhost.io/reference/cli) for local development
- [Nhost CLI](https://docs.nhost.io/cli) for local development
## Architecture of Nhost
@@ -97,7 +97,7 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
# Resources
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/reference/cli)
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/cli)
## Nhost Clients

View File

@@ -2,13 +2,14 @@ import '@fontsource/inter';
import '@fontsource/inter/500.css';
import '@fontsource/inter/700.css';
import { CssBaseline, ThemeProvider } from '@mui/material';
import { NhostClient, NhostProvider } from '@nhost/nextjs';
import { NhostApolloProvider } from '@nhost/react-apollo';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Buffer } from 'buffer';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { RouterContext } from 'next/dist/shared/lib/router-context';
import { createTheme } from '../src/components/ui/v2/createTheme';
import '../src/styles/globals.css';
import createTheme from '../src/theme/createTheme';
global.Buffer = Buffer;
@@ -56,5 +57,10 @@ export const decorators = [
<Story />
</NhostApolloProvider>
),
(Story) => (
<NhostProvider nhost={new NhostClient({ subdomain: 'local' })}>
<Story />
</NhostProvider>
),
mswDecorator,
];

View File

@@ -1,5 +1,154 @@
# @nhost/dashboard
## 0.19.1
### Patch Changes
- @nhost/react-apollo@5.0.32
- @nhost/nextjs@1.13.34
## 0.19.0
### Minor Changes
- 9c61c69a7: chore(dashboard):add postgres 14.6-20230705-1 to the version selector
### Patch Changes
- 47bda15ff: feat(settings): add warning to pull config
## 0.18.0
### Minor Changes
- ee0b9b8ed: chore(dashboard):add hasura v2.28.2 and v2.29.0 to the version selector
## 0.17.20
### Patch Changes
- @nhost/react-apollo@5.0.31
- @nhost/nextjs@1.13.33
## 0.17.19
### Patch Changes
- f866120a6: fix(users): use the password length from the config
## 0.17.18
### Patch Changes
- @nhost/react-apollo@5.0.30
- @nhost/nextjs@1.13.32
## 0.17.17
### Patch Changes
- ea7b102c0: fix(pat): highlight expired tokens
## 0.17.16
### Patch Changes
- b3b64a3b7: chore(deps): bump `@types/react` to `v18.2.14` and `@types/react-dom` to `v18.2.6`
- 32b221f94: chore(deps): bump `graphiql` to `v3`
- 3a56c12df: chore(deps): bump `turbo` to `v1.10.6`
- Updated dependencies [b3b64a3b7]
- @nhost/react-apollo@5.0.29
- @nhost/nextjs@1.13.31
## 0.17.15
### Patch Changes
- f41fdc12a: chore(deps): bump `turbo` to `1.10.5`
- 6199c1c55: fix(projects): don't redirect to 404 page
- Updated dependencies [07a45fde0]
- @nhost/react-apollo@5.0.28
- @nhost/nextjs@1.13.30
## 0.17.14
### Patch Changes
- 80b22724d: chore(deps): bump `@types/react` to `v18.2.13`, `@types/react-dom` to `v18.2.6` and `@storybook/testing-library` to `v0.2.0`
## 0.17.13
### Patch Changes
- cc02902cb: chore(docs): update environment variable documentation
## 0.17.12
### Patch Changes
- 660d339e1: fix(storybook): don't break storybook
- 660d339e1: fix(tests): prevent warnings during tests
- @nhost/react-apollo@5.0.27
- @nhost/nextjs@1.13.29
## 0.17.11
### Patch Changes
- bd4d0c270: chore(dashboard):add postgres 14.6-20230613-1 to the version selector
## 0.17.10
### Patch Changes
- c8c2a10b2: fix(database): don't break the password reset flow
- e70b45498: chore(deps): bump `@types/react` to `v18.2.12` and `@types/react-dom` to `v18.2.5`
## 0.17.9
### Patch Changes
- 842055099: chore(deps): bump `turbo` to `v1.10.3` and `pnpm` to `v8.6.2`
- fd12aa0a8: chore(projects): remove the postgres password input from the project creation screen
- 022b76e78: chore(deps): bump `@types/react` to `v18.2.11`
- 3555ab2b7: chore(deps): bump `vitest` monorepo to `v0.32.0`
- c43e54922: feat(backups): add download button to backups
## 0.17.8
### Patch Changes
- d0457fe5c: feat(settings): improve the dashboard and config parity
- @nhost/react-apollo@5.0.26
- @nhost/nextjs@1.13.28
## 0.17.7
### Patch Changes
- 4f0368b95: fix(account): don't break account settings page
## 0.17.6
### Patch Changes
- 64a8f41d0: chore(resources): lower the maximum allowed resources per service
## 0.17.5
### Patch Changes
- @nhost/react-apollo@5.0.25
- @nhost/nextjs@1.13.27
## 0.17.4
### Patch Changes
- 9b1d0f7a5: fix(deployments): use correct timestamp for deployment details
- 6d2963ffa: chore(deps): bump `@types/react` to `v18.2.8`
- 8871267b9: chore(deps): downgrade `pnpm` to `v8.5.1` because of no Turborepo support
## 0.17.3
### Patch Changes

View File

@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
RUN yarn global add turbo@1.10.1
RUN yarn global add turbo@1.10.6
COPY . .
RUN turbo prune --scope="@nhost/dashboard" --docker
@@ -29,7 +29,7 @@ ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL_
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
RUN yarn global add pnpm@8.6.0
RUN yarn global add pnpm@8.6.2
COPY .gitignore .gitignore
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-*.yaml .

View File

@@ -3,10 +3,26 @@
This is the Nhost Dashboard, a web application that allows you to manage your Nhost projects.
To get started, you need to have an Nhost project. If you don't have one, you can [create a project here](https://app.nhost.io).
First, install the dependencies:
```bash
pnpm install
```
Then, build the packages that are used by the Nhost Dashboard:
```bash
pnpm -w build
```
Finally, run the development server:
```bash
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) to see the result in your browser.
## Environment
### Setup Environment Variables
@@ -54,6 +70,12 @@ Components are documented using [Storybook](https://storybook.js.org/). To run S
pnpm storybook
```
By default, Storybook will run on port `6006`. You can change this by passing the `--port` flag:
```bash
pnpm storybook --port 6007
```
### General Environment Variables
| Name | Description |
@@ -110,15 +132,19 @@ pnpm storybook
| `@typescript-eslint/naming-convention` | Enforces a consistent naming convention. |
| `no-restricted-imports` | Enforces absolute imports and consistent import paths for components from `src/components/ui` folder. |
### End-to-End Tests
### Unit Tests
End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:
Unit tests are written using [Vitest](https://vitest.dev/). To run the tests, run the following command:
```bash
pnpm e2e
pnpm test
```
Most of the tests require access to the Nhost test user. To run these tests, you need to set the following environment variables in `.env.test`:
### End-to-End Tests
Most of the end-to-end tests require access to an Nhost test user and a live project. You can register a user and create a test project on the [Nhost Dashboard](https://app.nhost.io/).
Next, you need to create a project. Create a `.env.test` file with the following variables:
```
NHOST_TEST_DASHBOARD_URL=<test_dashboard_url>
@@ -128,3 +154,20 @@ NHOST_TEST_WORKSPACE_NAME=<test_workspace_name>
NHOST_TEST_PROJECT_NAME=<test_project_name>
NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret>
```
**Required Variables**:
- `NHOST_TEST_DASHBOARD_URL`: The URL to run the tests against (e.g: http://localhost:3000 or https://staging.app.nhost.io)
- `NHOST_TEST_USER_EMAIL`: Email address of the test user that owns the test project
- `NHOST_TEST_USER_PASSWORD`: Password of the test user that owns the test project
- `NHOST_TEST_WORKSPACE_NAME`: Name of the workspace that contains the test project
- `NHOST_TEST_PROJECT_NAME`: Name of the test project
- `NHOST_TEST_PROJECT_ADMIN_SECRET`: Admin secret of the test project
Make sure to copy the workspace and project information from the [Nhost Dashboard](https://app.nhost.io/).
End-to-end tests are written using [Playwright](https://playwright.dev/). To run the tests, run the following command:
```bash
pnpm e2e
```

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.17.3",
"version": "0.19.1",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -11,11 +11,11 @@
"lint": "next lint --max-warnings 0",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost up",
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook",
"e2e": "npx playwright@1.34.0 install --with-deps && playwright test"
"install-browsers": "pnpm dlx playwright@1.31.0 install --with-deps",
"e2e": "pnpm install-browsers && pnpm dlx playwright@1.31.0 test"
},
"dependencies": {
"@apollo/client": "^3.7.10",
@@ -26,7 +26,7 @@
"@emotion/styled": "^11.10.5",
"@fontsource/inter": "^5.0.0",
"@fontsource/roboto-mono": "^5.0.0",
"@graphiql/react": "^0.17.0",
"@graphiql/react": "^0.18.0",
"@graphiql/toolkit": "^0.8.2",
"@headlessui/react": "^1.6.5",
"@heroicons/react": "^1.0.6",
@@ -49,7 +49,7 @@
"clsx": "^1.2.1",
"date-fns": "^2.29.3",
"generate-password": "^1.7.0",
"graphiql": "^2.4.0",
"graphiql": "^3.0.0",
"graphql": "^16.6.0",
"graphql-request": "^6.0.0",
"graphql-tag": "^2.12.6",
@@ -87,7 +87,7 @@
"@graphql-codegen/typescript-operations": "^3.0.0",
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
"@next/bundle-analyzer": "^12.3.1",
"@playwright/test": "^1.34.0",
"@playwright/test": "1.31.0",
"@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-interactions": "^6.5.14",
@@ -96,23 +96,24 @@
"@storybook/builder-webpack5": "^6.5.14",
"@storybook/manager-webpack5": "^6.5.14",
"@storybook/react": "^6.5.14",
"@storybook/testing-library": "^0.0.13",
"@storybook/testing-library": "^0.2.0",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/bcryptjs": "^2.4.2",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react-table": "^7.7.12",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-c8": "^0.31.0",
"@vitest/coverage-v8": "^0.32.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -146,7 +147,7 @@
"tsconfig-paths-webpack-plugin": "^4.0.0",
"vite": "^4.0.2",
"vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.31.0"
"vitest": "^0.32.0"
},
"browserslist": {
"production": [

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#2684FF" fill-rule="evenodd" d="M3.41 4.393a.563.563 0 0 1 .434-.195l16.416.003a.562.562 0 0 1 .563.652l-2.388 14.66a.562.562 0 0 1-.563.472H6.417a.765.765 0 0 1-.748-.639L3.281 4.851a.562.562 0 0 1 .13-.458Zm6.832 10.282h3.656l.886-5.173H9.252l.99 5.173Z" clip-rule="evenodd"/><path fill="url(#a)" d="M20.063 9.502h-5.279l-.886 5.173h-3.656l-4.317 5.124a.762.762 0 0 0 .492.186h11.458a.562.562 0 0 0 .563-.473l1.625-10.01Z"/><defs><linearGradient id="a" x1="16.692" x2="10.594" y1="7.717" y2="16.375" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052CC"/><stop offset="1" stop-color="#2684FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#E24329" fill-rule="evenodd" d="m12 19.996 3.223-9.917H8.777L12 19.995Z" clip-rule="evenodd"/><path fill="#FC6D26" fill-rule="evenodd" d="m12 19.996-3.223-9.917H4.261L12 19.996Z" clip-rule="evenodd"/><path fill="#FCA326" fill-rule="evenodd" d="m4.262 10.079-.98 3.013a.667.667 0 0 0 .243.746L12 19.996l-7.738-9.917Z" clip-rule="evenodd"/><path fill="#E24329" fill-rule="evenodd" d="M4.261 10.079h4.517L6.837 4.106a.333.333 0 0 0-.635 0l-1.94 5.973Z" clip-rule="evenodd"/><path fill="#FC6D26" fill-rule="evenodd" d="m12 19.996 3.222-9.917h4.516L12 19.996Z" clip-rule="evenodd"/><path fill="#FCA326" fill-rule="evenodd" d="m19.738 10.079.98 3.013a.667.667 0 0 1-.243.746L12 19.996l7.738-9.917Z" clip-rule="evenodd"/><path fill="#E24329" fill-rule="evenodd" d="M19.739 10.079h-4.517l1.941-5.973a.334.334 0 0 1 .635 0l1.94 5.973Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 941 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#FE7203" d="M4.125 5.25c0-.621.504-1.125 1.125-1.125h13.5c.621 0 1.125.504 1.125 1.125v13.5c0 .621-.504 1.125-1.125 1.125H5.25a1.125 1.125 0 0 1-1.125-1.125V5.25Z"/><path fill="url(#a)" d="M4.125 5.25c0-.621.504-1.125 1.125-1.125h13.5c.621 0 1.125.504 1.125 1.125v13.5c0 .621-.504 1.125-1.125 1.125H5.25a1.125 1.125 0 0 1-1.125-1.125V5.25Z"/><path fill="#fff" fill-rule="evenodd" d="m10.917 12.787 2.461 4.43 2.363-4.43h-1.477l-.886 1.674-.984-1.674h-1.477Z" clip-rule="evenodd" opacity=".6"/><path fill="#fff" fill-rule="evenodd" d="m11.213 6.586 3.051 6.201H8.063l3.15-6.201Zm0 3.74 1.18 2.461h-2.46l1.28-2.46Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="12" x2="12" y1="4.125" y2="19.875" gradientUnits="userSpaceOnUse"><stop stop-color="#FB2F01" stop-opacity="0"/><stop offset="1" stop-color="#FB2F01"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 935 B

View File

@@ -1,25 +1 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0H64C68.4183 0 72 3.58172 72 8V64C72 68.4183 68.4183 72 64 72H8C3.58172 72 0 68.4183 0 64V8Z" fill="#1EB4D4"/>
<path d="M0 8C0 3.58172 3.58172 0 8 0H64C68.4183 0 72 3.58172 72 8V64C72 68.4183 68.4183 72 64 72H8C3.58172 72 0 68.4183 0 64V8Z" fill="url(#paint0_linear_1_85)" fill-opacity="0.2"/>
<g filter="url(#filter0_d_1_85)">
<circle cx="36" cy="39" r="16" fill="#35BCD8"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.3994 17.2212C52.831 21.3475 52.831 26.737 51.7362 29.3896C51.1889 30.6948 51.1467 32.2106 51.5678 33.558C52.1152 35.2001 52.4099 36.9264 52.4099 38.7791C52.4099 48.0001 44.9573 55.3685 35.7783 55.2001C27.0625 55.0738 19.6941 47.6633 19.4836 38.9896C19.4415 37.0948 19.7362 35.2001 20.3257 33.5159C20.7889 32.1685 20.7889 30.6948 20.2415 29.3896C19.1889 26.7791 19.1467 21.3896 20.5783 17.1791C20.9152 16.4212 22.0941 16.6738 22.0941 17.4738V17.7685C22.3467 21.7685 23.8625 24.1685 26.052 24.9685C26.3889 25.137 26.8099 25.0948 27.1467 24.8843C29.7152 23.3264 32.7046 22.358 35.9467 22.358C39.1889 22.358 42.2204 23.2843 44.7467 24.8843C45.1257 25.137 45.6731 25.137 46.0099 24.9685C48.1573 23.9159 49.5889 21.7685 49.8415 17.8106V17.5159C49.8836 16.7159 51.0204 16.4633 51.3994 17.2212ZM36.1994 26.2738C29.1257 26.1054 23.3152 31.9159 23.4836 39.0317C23.5678 45.7264 29.0836 51.158 35.7362 51.3264C42.852 51.4106 48.6204 45.6422 48.4941 38.5685C48.3678 31.8738 42.8941 26.358 36.1994 26.2738ZM34.9362 32.8844L37.8836 37.4318L40.7468 41.9792C40.9152 42.2318 40.9994 42.5265 40.9994 42.8213C40.9994 43.3686 40.7047 43.8739 40.2415 44.1686C39.6941 44.5055 38.9783 44.5055 38.431 44.1265C38.2204 44.0002 38.052 43.8739 37.9678 43.6634L36.3257 41.3055C36.1994 41.0528 35.9047 41.0528 35.6941 41.2634L33.3783 43.9581C33.0836 44.2528 32.7047 44.4634 32.2415 44.4634C31.8626 44.4634 31.4415 44.3371 31.1468 44.0844C30.5152 43.495 30.4731 42.4844 31.0626 41.8528L34.1783 38.4423C34.3047 38.2318 34.3889 37.9371 34.2204 37.6844L32.2415 34.5686C32.0731 34.316 31.9889 34.0213 31.9889 33.7265C31.9889 33.1792 32.2836 32.6739 32.7468 32.3792C33.5047 31.916 34.4731 32.1265 34.9362 32.8844Z" fill="white"/>
<defs>
<filter id="filter0_d_1_85" x="17" y="23" width="38" height="41" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect1_dropShadow_1_85"/>
<feOffset dy="6"/>
<feGaussianBlur stdDeviation="3.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.231373 0 0 0 0 0.278431 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_85"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_85" result="shape"/>
</filter>
<linearGradient id="paint0_linear_1_85" x1="0" y1="0" x2="72" y2="72" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="none"><path fill="#1EB4D4" d="M0 8a8 8 0 0 1 8-8h56a8 8 0 0 1 8 8v56a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8Z"/><path fill="url(#a)" fill-opacity=".2" d="M0 8a8 8 0 0 1 8-8h56a8 8 0 0 1 8 8v56a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8Z"/><g filter="url(#b)"><circle cx="36" cy="39" r="16" fill="#35BCD8"/></g><path fill="#fff" fill-rule="evenodd" d="M51.4 17.221c1.431 4.127 1.431 9.516.336 12.169-.547 1.305-.59 2.82-.168 4.168.547 1.642.842 3.368.842 5.221 0 9.221-7.453 16.59-16.632 16.421-8.715-.126-16.084-7.537-16.294-16.21-.043-1.895.252-3.79.842-5.474.463-1.347.463-2.821-.085-4.126-1.052-2.61-1.094-8 .337-12.21.337-.759 1.516-.506 1.516.294v.294c.253 4 1.768 6.4 3.958 7.2.337.169.758.127 1.095-.084 2.568-1.558 5.558-2.526 8.8-2.526 3.242 0 6.273.926 8.8 2.526.379.253.926.253 1.263.084 2.147-1.052 3.579-3.2 3.832-7.157v-.295c.042-.8 1.178-1.053 1.557-.295Zm-15.2 9.053c-7.074-.169-12.885 5.642-12.716 12.758.084 6.694 5.6 12.126 12.252 12.294 7.116.085 12.884-5.684 12.758-12.758-.126-6.694-5.6-12.21-12.295-12.294Zm-1.264 6.61 2.948 4.548 2.863 4.547c.168.253.252.547.252.842 0 .548-.294 1.053-.758 1.348a1.662 1.662 0 0 1-1.81-.042c-.21-.127-.379-.253-.463-.464l-1.642-2.357c-.127-.253-.421-.253-.632-.043l-2.316 2.695c-.294.295-.673.505-1.136.505-.38 0-.8-.126-1.095-.379a1.59 1.59 0 0 1-.084-2.231l3.115-3.41c.127-.211.21-.506.042-.759l-1.978-3.115a1.518 1.518 0 0 1-.253-.843c0-.547.295-1.052.758-1.347.758-.463 1.726-.252 2.19.505Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="0" x2="72" y1="0" y2="72" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><filter id="b" width="38" height="41" x="17" y="23" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feMorphology in="SourceAlpha" radius="4" result="effect1_dropShadow_1_85"/><feOffset dy="6"/><feGaussianBlur stdDeviation="3.5"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0.231373 0 0 0 0 0.278431 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_85"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_85" result="shape"/></filter></defs></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -3,7 +3,10 @@ import { ProjectLayout } from '@/components/layout/ProjectLayout';
import type { SettingsSidebarProps } from '@/components/layout/SettingsSidebar';
import { SettingsSidebar } from '@/components/layout/SettingsSidebar';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { twMerge } from 'tailwind-merge';
export interface SettingsLayoutProps extends ProjectLayoutProps {
@@ -22,6 +25,9 @@ export default function SettingsLayout({
sidebarProps: { className: sidebarClassName, ...sidebarProps } = {},
...props
}: SettingsLayoutProps) {
const { currentProject } = useCurrentWorkspaceAndProject();
const hasGitRepo = !!currentProject?.githubRepository;
return (
<ProjectLayout
mainContainerProps={{
@@ -37,9 +43,39 @@ export default function SettingsLayout({
<Box
sx={{ backgroundColor: 'background.default' }}
className="flex w-full flex-auto flex-col overflow-x-hidden"
className="flex w-full flex-auto flex-col overflow-scroll overflow-x-hidden"
>
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
<RetryableErrorBoundary>
{hasGitRepo && (
<Alert
severity="warning"
className="grid grid-flow-row place-content-center gap-2"
>
<Text color="warning" className="text-sm ">
As you have a connected repository, make sure to synchronize
your changes with{' '}
<code className="rounded-md bg-slate-200 px-2 py-px text-slate-500">
nhost config pull
</code>{' '}
or they may be reverted with the next push.
<br />
If there are multiple projects linked to the same repository and
you only want these changes to apply to a subset of them, please
check out{' '}
<a
target="_blank"
rel="noopener noreferrer"
className="underline"
href="https://docs.nhost.io/cli/overlays"
>
docs.nhost.io/cli/overlays
</a>{' '}
for guidance.
</Text>
</Alert>
)}
{children}
</RetryableErrorBoundary>
</Box>
</ProjectLayout>
);

View File

@@ -7,7 +7,6 @@ import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { isK8SPostgresEnabledInCurrentEnvironment } from '@/utils/helpers';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -135,15 +134,13 @@ export default function SettingsSidebar({
>
Compute Resources
</SettingsNavLink>
{isK8SPostgresEnabledInCurrentEnvironment && (
<SettingsNavLink
href="/database"
exact={false}
onClick={handleSelect}
>
Database
</SettingsNavLink>
)}
<SettingsNavLink
href="/database"
exact={false}
onClick={handleSelect}
>
Database
</SettingsNavLink>
<SettingsNavLink
href="/hasura"
exact={false}

View File

@@ -1,6 +1,6 @@
import type { BoxProps } from '@/components/ui/v2/Box';
import { Box } from '@/components/ui/v2/Box';
import { twMerge } from 'tailwind-merge';
import { styled } from '@mui/material';
export interface AlertProps extends BoxProps {
/**
@@ -11,19 +11,25 @@ export interface AlertProps extends BoxProps {
severity?: 'info' | 'success' | 'warning' | 'error';
}
const StyledBox = styled(Box)(({ theme }) => ({
borderRadius: 4,
padding: theme.spacing(1.5, 2),
textAlign: 'center',
fontSize: theme.typography.pxToRem(15),
lineHeight: theme.typography.pxToRem(22),
'@media (prefers-reduced-motion: no-preference)': {
transition: theme.transitions.create('background-color'),
},
}));
export default function Alert({
severity = 'info',
children,
className,
sx,
...props
}: AlertProps) {
return (
<Box
className={twMerge(
'rounded-sm+ bg-opacity-20 p-4 text-center text-sm+ motion-safe:transition-colors',
className,
)}
<StyledBox
sx={[
...(Array.isArray(sx) ? sx : [sx]),
severity === 'error' && {
@@ -43,6 +49,6 @@ export default function Alert({
{...props}
>
{children}
</Box>
</StyledBox>
);
}

View File

@@ -1,5 +1,6 @@
import { Chip } from '@/components/ui/v2/Chip';
import type { FormControlProps } from '@/components/ui/v2/FormControl';
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import type { InputProps } from '@/components/ui/v2/Input';
@@ -134,6 +135,13 @@ const StyledAutocomplete = styled(MaterialAutocomplete)(({ theme }) => ({
MaterialAutocompleteProps<AutocompleteOption, boolean, boolean, boolean>
>;
const StyledOptionBase = styled(OptionBase)(({ theme }) => ({
display: 'grid !important',
gridAutoFlow: 'column',
justifyContent: 'space-between !important',
gap: theme.spacing(0.5),
}));
export const AutocompletePopper = styled(PopperUnstyled)(({ theme }) => ({
zIndex: theme.zIndex.modal + 1,
boxShadow: 'none',
@@ -326,6 +334,7 @@ function Autocomplete(
<StyledTag
deleteIcon={<XIcon />}
size="small"
sx={{ fontSize: (theme) => theme.typography.pxToRem(12) }}
label={
typeof option !== 'object' ? option.toString() : option.value
}
@@ -349,17 +358,32 @@ function Autocomplete(
optionProps,
option: string | number | AutocompleteOption<string>,
) => {
const selected = optionProps['aria-selected'];
if (typeof option !== 'object') {
return <OptionBase {...optionProps}>{option.toString()}</OptionBase>;
return (
<StyledOptionBase {...optionProps} key={option.toString()}>
{option.toString()}
{selected && props.multiple && (
<CheckIcon sx={{ width: 16, height: 16 }} />
)}
</StyledOptionBase>
);
}
return (
<OptionBase
<StyledOptionBase
{...optionProps}
key={option.dropdownLabel || option.label}
>
{option.dropdownLabel || option.label}
</OptionBase>
<>
<span>{option.dropdownLabel || option.label}</span>
{selected && props.multiple && (
<CheckIcon key="asd" sx={{ width: 16, height: 16 }} />
)}
</>
</StyledOptionBase>
);
}}
filterOptions={

View File

@@ -1,11 +1,18 @@
import { styled } from '@mui/material';
import { textClasses } from '@/components/ui/v2/Text';
import { getTypographyUtilityClass, styled } from '@mui/material';
import type { ListItemTextProps as MaterialListItemTextProps } from '@mui/material/ListItemText';
import MaterialListItemText, {
listItemTextClasses,
listItemTextClasses as materialListItemTextClasses,
} from '@mui/material/ListItemText';
import clsx from 'clsx';
export interface ListItemTextProps extends MaterialListItemTextProps {}
const listItemTextClasses = {
...materialListItemTextClasses,
warning: getTypographyUtilityClass('colorWarning'),
};
const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
color: theme.palette.text.primary,
display: 'grid',
@@ -16,6 +23,9 @@ const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
[`&.${listItemTextClasses.root}`]: {
margin: 0,
},
[`&.${listItemTextClasses.warning}`]: {
color: theme.palette.warning.dark,
},
[`& > .${listItemTextClasses.primary}`]: {
fontWeight: 500,
textOverflow: 'ellipsis',
@@ -29,8 +39,23 @@ const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
},
}));
function ListItemText({ children, ...props }: ListItemTextProps) {
return <StyledListItemText {...props}>{children}</StyledListItemText>;
function ListItemText({
children,
color = 'primary',
className,
...props
}: ListItemTextProps) {
return (
<StyledListItemText
className={clsx(
color === 'warning' && textClasses.colorWarning,
className,
)}
{...props}
>
{children}
</StyledListItemText>
);
}
ListItemText.displayName = 'NhostListItemText';

View File

@@ -18,7 +18,7 @@ export type TextProps<
*
* @default 'primary'
*/
color?: 'primary' | 'secondary' | 'disabled' | 'error';
color?: 'primary' | 'secondary' | 'disabled' | 'error' | 'warning';
/**
* The component used for the root node.
*/
@@ -31,6 +31,7 @@ const textClasses = {
colorSecondary: getTypographyUtilityClass('colorSecondary'),
colorDisabled: getTypographyUtilityClass('colorDisabled'),
colorError: getTypographyUtilityClass('colorError'),
colorWarning: getTypographyUtilityClass('colorWarning'),
};
const StyledTypography = styled(MaterialTypography)<TextProps>(({ theme }) => ({
@@ -50,6 +51,9 @@ const StyledTypography = styled(MaterialTypography)<TextProps>(({ theme }) => ({
[`&.${textClasses.colorError}`]: {
color: theme.palette.error.main,
},
[`&.${textClasses.colorWarning}`]: {
color: theme.palette.warning.dark,
},
}));
function Text<
@@ -70,6 +74,7 @@ function Text<
color === 'secondary' && textClasses.colorSecondary,
color === 'disabled' && textClasses.colorDisabled,
color === 'error' && textClasses.colorError,
color === 'warning' && textClasses.colorWarning,
className,
)}
{...props}

View File

@@ -0,0 +1,40 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
function WarningIcon(props: IconProps, ref: ForwardedRef<SVGSVGElement>) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="Warning"
ref={ref}
{...props}
>
<path
d="M8 5.5V9.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M7.135 2.49904L1.63648 11.9986C1.5485 12.1506 1.5021 12.3231 1.50195 12.4987C1.50181 12.6743 1.54792 12.8469 1.63565 12.999C1.72338 13.1512 1.84964 13.2776 2.00172 13.3654C2.15379 13.4533 2.32633 13.4995 2.50196 13.4995H13.499C13.6746 13.4995 13.8472 13.4533 13.9992 13.3654C14.1513 13.2776 14.2776 13.1512 14.3653 12.999C14.453 12.8469 14.4991 12.6743 14.499 12.4987C14.4988 12.3231 14.4524 12.1506 14.3645 11.9986L8.86594 2.49904C8.7781 2.34728 8.6519 2.22129 8.49999 2.1337C8.34809 2.04611 8.17582 2 8.00047 2C7.82512 2 7.65285 2.04611 7.50095 2.1337C7.34904 2.22129 7.22284 2.34728 7.135 2.49904V2.49904Z"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinejoin="round"
/>
<path
d="M8 12C8.41421 12 8.75 11.6642 8.75 11.25C8.75 10.8358 8.41421 10.5 8 10.5C7.58579 10.5 7.25 10.8358 7.25 11.25C7.25 11.6642 7.58579 12 8 12Z"
fill="currentColor"
/>
</SvgIcon>
);
}
WarningIcon.displayName = 'NhostWarningIcon';
export default forwardRef(WarningIcon);

View File

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

View File

@@ -1,5 +0,0 @@
{
"k8s-postgres": {
"enabled": ["dev", "staging", "production"]
}
}

View File

@@ -9,9 +9,11 @@ import { Dropdown } from '@/components/ui/v2/Dropdown';
import { IconButton } from '@/components/ui/v2/IconButton';
import { DotsVerticalIcon } from '@/components/ui/v2/icons/DotsVerticalIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { WarningIcon } from '@/components/ui/v2/icons/WarningIcon';
import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { CreatePATForm } from '@/features/account/settings/components/CreatePATForm';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
@@ -133,69 +135,91 @@ export default function PATSettings() {
<Box className="grid grid-flow-row gap-2">
{availablePersonalAccessTokens.length > 0 && (
<List>
{availablePersonalAccessTokens.map((pat, index) => (
<Fragment key={pat.id}>
<ListItem.Root
className="grid grid-cols-3 gap-2 px-4 pr-12"
secondaryAction={
<Dropdown.Root>
<Dropdown.Trigger
asChild
hideChevron
className="absolute right-4 top-1/2 -translate-y-1/2"
>
<IconButton
variant="borderless"
color="secondary"
disabled={maintenanceActive}
aria-label={`More options for ${pat.name}`}
{availablePersonalAccessTokens.map((pat, index) => {
const tokenHasExpired = new Date(pat.expiresAt) < new Date();
return (
<Fragment key={pat.id}>
<ListItem.Root
className="grid grid-cols-3 gap-2 px-4 pr-12"
secondaryAction={
<Dropdown.Root>
<Dropdown.Trigger
asChild
hideChevron
className="absolute right-4 top-1/2 -translate-y-1/2"
>
<DotsVerticalIcon />
</IconButton>
</Dropdown.Trigger>
<IconButton
variant="borderless"
color="secondary"
disabled={maintenanceActive}
aria-label={`More options for ${pat.name}`}
>
<DotsVerticalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content
menu
PaperProps={{ className: 'w-32' }}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<Dropdown.Item onClick={() => handleConfirmDelete(pat)}>
<Text className="font-medium" color="error">
Delete
</Text>
</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
}
>
<ListItem.Text className="truncate">{pat.name}</ListItem.Text>
<Dropdown.Content
menu
PaperProps={{ className: 'w-32' }}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<Dropdown.Item
onClick={() => handleConfirmDelete(pat)}
>
<Text className="font-medium" color="error">
Delete
</Text>
</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
}
>
<ListItem.Text
className="truncate"
color={tokenHasExpired ? 'warning' : 'primary'}
>
<span className="mr-2">{pat.name}</span>
{tokenHasExpired && (
<Tooltip title="This personal access token is expired.">
<WarningIcon className="h-4 w-4" />
</Tooltip>
)}
</ListItem.Text>
<Text className="truncate">
{new Date(pat.expiresAt).toLocaleDateString()}
</Text>
<Text
className="truncate"
color={tokenHasExpired ? 'warning' : 'primary'}
>
{new Date(pat.expiresAt).toLocaleDateString()}
</Text>
<Text className="truncate">
{new Date(pat.createdAt).toLocaleDateString()}
</Text>
</ListItem.Root>
<Text
className="truncate"
color={tokenHasExpired ? 'warning' : 'primary'}
>
{new Date(pat.createdAt).toLocaleDateString()}
</Text>
</ListItem.Root>
<Divider
component="li"
className={twMerge(
index === availablePersonalAccessTokens.length - 1
? '!mt-4'
: '!my-4',
)}
/>
</Fragment>
))}
<Divider
component="li"
className={twMerge(
index === availablePersonalAccessTokens.length - 1
? '!mt-4'
: '!my-4',
)}
/>
</Fragment>
);
})}
</List>
)}

View File

@@ -1,6 +1,6 @@
query GetPersonalAccessTokens {
personalAccessTokens: authRefreshTokens(
where: { type: { _eq: "pat" } }
where: { type: { _eq: pat } }
order_by: { expiresAt: asc }
) {
id

View File

@@ -98,7 +98,7 @@ export default function AppleProviderSettings() {
const { register, formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (values: AppleProviderFormValues) => {
async function handleSubmit(formValues: AppleProviderFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -107,7 +107,7 @@ export default function AppleProviderSettings() {
method: {
oauth: {
apple: {
...values,
...formValues,
scope: [],
},
},
@@ -130,15 +130,15 @@ export default function AppleProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Apple"
description="Allow users to sign in with Apple."
@@ -214,7 +214,7 @@ export default function AppleProviderSettings() {
/>
<Input
name="redirectUrl"
id="redirectUrl"
id="apple-redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,

View File

@@ -90,7 +90,7 @@ export default function AzureADProviderSettings() {
const { register, formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (values: AzureADProviderFormValues) => {
async function handleSubmit(formValues: AzureADProviderFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -98,7 +98,7 @@ export default function AzureADProviderSettings() {
auth: {
method: {
oauth: {
azuread: values,
azuread: formValues,
},
},
},
@@ -119,15 +119,15 @@ export default function AzureADProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Azure AD"
description="Allow users to sign in with Azure AD."
@@ -160,7 +160,7 @@ export default function AzureADProviderSettings() {
/>
<Input
name="redirectUrl"
id="redirectUrl"
id="azuerad-redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,

View File

@@ -0,0 +1,167 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import type { BaseProviderSettingsFormValues } from '@/features/authentication/settings/components/BaseProviderSettings';
import {
BaseProviderSettings,
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export default function BitbucketProviderSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
});
const { clientId, clientSecret, enabled } =
data?.config?.auth?.method?.oauth?.bitbucket || {};
const form = useForm<BaseProviderSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
},
resolver: yupResolver(baseProviderValidationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading settings for Bitbucket..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, watch } = form;
const authEnabled = watch('enabled');
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
method: {
oauth: {
bitbucket: formValues,
},
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Bitbucket settings are being updated...`,
success: `Bitbucket settings have been updated successfully.`,
error: getServerError(
`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 (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Bitbucket"
description="Allow users to sign in with Bitbucket."
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
icon="/assets/brands/bitbucket.svg"
switchId="enabled"
showSwitch
className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
!authEnabled && 'hidden',
)}
>
<BaseProviderSettings providerName="bitbucket" />
<Input
name="redirectUrl"
id="bitbucket-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'auth',
)}/signin/provider/bitbucket/callback`}
disabled
endAdornment={
<InputAdornment position="end" className="absolute right-2">
<IconButton
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'auth',
)}/signin/provider/bitbucket/callback`,
'Redirect URL',
);
}}
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</InputAdornment>
}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -68,9 +68,7 @@ export default function DiscordProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject?.id,
@@ -79,7 +77,7 @@ export default function DiscordProviderSettings() {
method: {
oauth: {
discord: {
...values,
...formValues,
scope: [],
},
},
@@ -102,15 +100,15 @@ export default function DiscordProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Discord"
description="Allow users to sign in with Discord."
@@ -133,7 +131,7 @@ export default function DiscordProviderSettings() {
<BaseProviderSettings providerName="discord" />
<Input
name="redirectUrl"
id="redirectUrl"
id="discord-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -3,6 +3,7 @@ import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
@@ -20,6 +21,11 @@ import * as Yup from 'yup';
const validationSchema = Yup.object({
emailVerificationRequired: Yup.boolean(),
hibpEnabled: Yup.boolean(),
passwordMinLength: Yup.number()
.label('Minimum password length')
.min(3)
.typeError('Minimum password length must be a number')
.required(),
});
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
@@ -36,7 +42,7 @@ export default function EmailAndPasswordSettings() {
fetchPolicy: 'cache-only',
});
const { hibpEnabled, emailVerificationRequired } =
const { hibpEnabled, emailVerificationRequired, passwordMinLength } =
data?.config?.auth?.method?.emailPassword || {};
const form = useForm<EmailAndPasswordFormValues>({
@@ -44,6 +50,7 @@ export default function EmailAndPasswordSettings() {
defaultValues: {
hibpEnabled: hibpEnabled || false,
emailVerificationRequired: emailVerificationRequired || false,
passwordMinLength: passwordMinLength || 9,
},
resolver: yupResolver(validationSchema),
});
@@ -62,18 +69,16 @@ export default function EmailAndPasswordSettings() {
throw error;
}
const { formState } = form;
const { formState, register } = form;
const handleEmailAndPasswordSettingsChange = async (
values: EmailAndPasswordFormValues,
) => {
async function handleSubmit(formValues: EmailAndPasswordFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
method: {
emailPassword: values,
emailPassword: formValues,
},
},
},
@@ -93,15 +98,15 @@ export default function EmailAndPasswordSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleEmailAndPasswordSettingsChange}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Email and Password"
description="Allow users to sign in with email and password."
@@ -118,6 +123,19 @@ export default function EmailAndPasswordSettings() {
},
}}
>
<Input
{...register('passwordMinLength')}
id="passwordMinLength"
name="passwordMinLength"
type="number"
label="Minimum required password length"
fullWidth
className="lg:max-w-[50%]"
error={Boolean(formState.errors.passwordMinLength?.message)}
helperText={formState.errors.passwordMinLength?.message}
slotProps={{ inputRoot: { min: 3 } }}
/>
<ControlledCheckbox
name="emailVerificationRequired"
id="emailVerificationRequired"

View File

@@ -68,9 +68,7 @@ export default function FacebookProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -79,7 +77,7 @@ export default function FacebookProviderSettings() {
method: {
oauth: {
facebook: {
...values,
...formValues,
scope: [],
},
},
@@ -102,15 +100,15 @@ export default function FacebookProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Facebook"
description="Allow users to sign in with Facebook."
@@ -133,7 +131,7 @@ export default function FacebookProviderSettings() {
<BaseProviderSettings providerName="facebook" />
<Input
name="redirectUrl"
id="redirectUrl"
id="facebook-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -70,9 +70,7 @@ export default function GitHubProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -81,7 +79,7 @@ export default function GitHubProviderSettings() {
method: {
oauth: {
github: {
...values,
...formValues,
scope: [],
},
},
@@ -104,15 +102,15 @@ export default function GitHubProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="GitHub"
description="Allow users to sign in with GitHub."
@@ -139,7 +137,7 @@ export default function GitHubProviderSettings() {
<BaseProviderSettings providerName="github" />
<Input
name="redirectUrl"
id="redirectUrl"
id="github-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -0,0 +1,170 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import type { BaseProviderSettingsFormValues } from '@/features/authentication/settings/components/BaseProviderSettings';
import {
BaseProviderSettings,
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export default function GitLabProviderSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
});
const { clientId, clientSecret, enabled } =
data?.config?.auth?.method?.oauth?.gitlab || {};
const form = useForm<BaseProviderSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
},
resolver: yupResolver(baseProviderValidationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading settings for GitLab..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, watch } = form;
const authEnabled = watch('enabled');
async function handleSubmit(values: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
method: {
oauth: {
gitlab: {
...values,
scope: [],
},
},
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `GitLab settings are being updated...`,
success: `GitLab settings have been updated successfully.`,
error: getServerError(
`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 (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="GitLab"
description="Allow users to sign in with GitLab."
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
icon="/assets/brands/gitlab.svg"
switchId="enabled"
showSwitch
className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
!authEnabled && 'hidden',
)}
>
<BaseProviderSettings providerName="gitlab" />
<Input
name="redirectUrl"
id="gitlab-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'auth',
)}/signin/provider/gitlab/callback`}
disabled
endAdornment={
<InputAdornment position="end" className="absolute right-2">
<IconButton
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'auth',
)}/signin/provider/gitlab/callback`,
'Redirect URL',
);
}}
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</InputAdornment>
}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -68,9 +68,7 @@ export default function GoogleProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -79,7 +77,7 @@ export default function GoogleProviderSettings() {
method: {
oauth: {
google: {
...values,
...formValues,
scope: [],
},
},
@@ -102,15 +100,15 @@ export default function GoogleProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Google"
description="Allow users to sign in with Google."
@@ -133,7 +131,7 @@ export default function GoogleProviderSettings() {
<BaseProviderSettings providerName="google" />
<Input
name="redirectUrl"
id="redirectUrl"
id="google-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -68,9 +68,7 @@ export default function LinkedInProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -79,7 +77,7 @@ export default function LinkedInProviderSettings() {
method: {
oauth: {
linkedin: {
...values,
...formValues,
scope: [],
},
},
@@ -102,15 +100,15 @@ export default function LinkedInProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="LinkedIn"
description="Allow users to sign in with LinkedIn."
@@ -133,7 +131,7 @@ export default function LinkedInProviderSettings() {
<BaseProviderSettings providerName="linkedin" />
<Input
name="redirectUrl"
id="redirectUrl"
id="linkedin-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -0,0 +1,145 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
accessTokenExpiresIn: Yup.number()
.label('Access token expiration')
.typeError('Access token expiration must be a number')
.required(),
refreshTokenExpiresIn: Yup.number()
.label('Refresh token expiration')
.typeError('Refresh token expiration must be a number')
.required(),
});
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
export default function SessionSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
});
const { accessToken, refreshToken } = data?.config?.auth?.session || {};
const form = useForm<SessionFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
accessTokenExpiresIn: accessToken?.expiresIn || 900,
refreshTokenExpiresIn: refreshToken?.expiresIn || 43200,
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading session settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const handleSessionSettingsChange = async (formValues: SessionFormValues) => {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
session: {
accessToken: { expiresIn: formValues.accessTokenExpiresIn },
refreshToken: { expiresIn: formValues.refreshTokenExpiresIn },
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
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);
} catch {
// Note: The toast will handle the error.
}
};
return (
<FormProvider {...form}>
<Form onSubmit={handleSessionSettingsChange}>
<SettingsContainer
title="Session"
description="Change the expiration time of the access and refresh tokens."
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="grid grid-cols-5 grid-rows-2 gap-y-6"
>
<Input
{...register('accessTokenExpiresIn')}
id="accessTokenExpiresIn"
type="number"
label="Access Token Expires In (Seconds)"
fullWidth
className="col-span-5 lg:col-span-2"
error={Boolean(formState.errors.accessTokenExpiresIn?.message)}
helperText={formState.errors.accessTokenExpiresIn?.message}
/>
<Input
{...register('refreshTokenExpiresIn')}
id="refreshTokenExpiresIn"
type="number"
label="Refresh Token Expires In (Seconds)"
fullWidth
className="col-span-5 row-start-2 lg:col-span-2"
error={Boolean(formState.errors.refreshTokenExpiresIn?.message)}
helperText={formState.errors.refreshTokenExpiresIn?.message}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -68,9 +68,7 @@ export default function SpotifyProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -79,7 +77,7 @@ export default function SpotifyProviderSettings() {
method: {
oauth: {
spotify: {
...values,
...formValues,
scope: [],
},
},
@@ -102,15 +100,15 @@ export default function SpotifyProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Spotify"
description="Allow users to sign in with Spotify."
@@ -133,7 +131,7 @@ export default function SpotifyProviderSettings() {
<BaseProviderSettings providerName="spotify" />
<Input
name="redirectUrl"
id="redirectUrl"
id="spotify-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -0,0 +1,170 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import type { BaseProviderSettingsFormValues } from '@/features/authentication/settings/components/BaseProviderSettings';
import {
BaseProviderSettings,
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export default function StravaProviderSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
});
const { clientId, clientSecret, enabled } =
data?.config?.auth?.method?.oauth?.strava || {};
const form = useForm<BaseProviderSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
},
resolver: yupResolver(baseProviderValidationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading settings for Strava..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, watch } = form;
const authEnabled = watch('enabled');
async function handleSubmit(values: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
auth: {
method: {
oauth: {
strava: {
...values,
scope: [],
},
},
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Strava settings are being updated...`,
success: `Strava settings have been updated successfully.`,
error: getServerError(
`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 (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Strava"
description="Allow users to sign in with Strava."
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
icon="/assets/brands/strava.svg"
switchId="enabled"
showSwitch
className={twMerge(
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
!authEnabled && 'hidden',
)}
>
<BaseProviderSettings providerName="strava" />
<Input
name="redirectUrl"
id="strava-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'auth',
)}/signin/provider/strava/callback`}
disabled
endAdornment={
<InputAdornment position="end" className="absolute right-2">
<IconButton
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,
'auth',
)}/signin/provider/strava/callback`,
'Redirect URL',
);
}}
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</InputAdornment>
}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -70,9 +70,7 @@ export default function TwitchProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -81,7 +79,7 @@ export default function TwitchProviderSettings() {
method: {
oauth: {
twitch: {
...values,
...formValues,
scope: [],
},
},
@@ -104,15 +102,15 @@ export default function TwitchProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Twitch"
description="Allow users to sign in with Twitch."
@@ -139,7 +137,7 @@ export default function TwitchProviderSettings() {
<BaseProviderSettings providerName="twitch" />
<Input
name="redirectUrl"
id="redirectUrl"
id="twitch-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -82,7 +82,7 @@ export default function TwitterProviderSettings() {
const { register, formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (values: TwitterProviderFormValues) => {
async function handleSubmit(formValues: TwitterProviderFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -90,7 +90,7 @@ export default function TwitterProviderSettings() {
auth: {
method: {
oauth: {
twitter: values,
twitter: formValues,
},
},
},
@@ -111,15 +111,15 @@ export default function TwitterProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Twitter"
description="Allow users to sign in with Twitter."
@@ -164,7 +164,7 @@ export default function TwitterProviderSettings() {
/>
<Input
name="redirectUrl"
id="redirectUrl"
id="twitter-redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,

View File

@@ -66,7 +66,10 @@ export default function WebAuthnSettings() {
config: {
auth: {
method: {
webauthn: values,
webauthn: {...values,
relyingParty: {
name: currentProject.name,
}},
},
},
},

View File

@@ -68,9 +68,7 @@ export default function WindowsLiveProviderSettings() {
const { formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (
values: BaseProviderSettingsFormValues,
) => {
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -79,7 +77,7 @@ export default function WindowsLiveProviderSettings() {
method: {
oauth: {
windowslive: {
...values,
...formValues,
scope: [],
},
},
@@ -102,15 +100,15 @@ export default function WindowsLiveProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Windows Live"
description="Allow users to sign in with Windows Live."
@@ -132,7 +130,7 @@ export default function WindowsLiveProviderSettings() {
<BaseProviderSettings providerName="windowslive" />
<Input
name="redirectUrl"
id="redirectUrl"
id="windowslive-redirectUrl"
className="col-span-2"
fullWidth
hideEmptyHelperText

View File

@@ -97,7 +97,7 @@ export default function WorkOsProviderSettings() {
const { register, formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (values: WorkOsProviderFormValues) => {
async function handleSubmit(formValues: WorkOsProviderFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -105,7 +105,7 @@ export default function WorkOsProviderSettings() {
auth: {
method: {
oauth: {
workos: values,
workos: formValues,
},
},
},
@@ -126,15 +126,15 @@ export default function WorkOsProviderSettings() {
getToastStyleProps(),
);
form.reset(values);
form.reset(formValues);
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="WorkOS"
description="Allow users to sign in with WorkOS."
@@ -181,7 +181,7 @@ export default function WorkOsProviderSettings() {
/>
<Input
name="redirectUrl"
id="redirectUrl"
id="workos-redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region,

View File

@@ -16,6 +16,14 @@ query GetAuthenticationSettings($appId: uuid!) {
signUp {
enabled
}
session {
accessToken {
expiresIn
}
refreshToken {
expiresIn
}
}
user {
email {
allowed

View File

@@ -3,12 +3,16 @@ import { Form } from '@/components/form/Form';
import { Alert } from '@/components/ui/v2/Alert';
import { Button } from '@/components/ui/v2/Button';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
import { useUpdateRemoteAppUserMutation } from '@/utils/__generated__/graphql';
import {
useGetSignInMethodsQuery,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import { yupResolver } from '@hookform/resolvers/yup';
import bcrypt from 'bcryptjs';
import { useState } from 'react';
@@ -27,19 +31,6 @@ export interface EditUserPasswordFormProps extends DialogFormProps {
user: RemoteAppGetUsersQuery['users'][0];
}
export const validationSchema = Yup.object({
password: Yup.string()
.label('Users Password')
.min(8, 'Password must be at least 8 characters long.')
.required('This field is required.'),
cpassword: Yup.string()
.required('Confirm Password is required')
.min(8, 'Password must be at least 8 characters long.')
.oneOf([Yup.ref('password')], 'Passwords do not match'),
});
export type EditUserPasswordFormValues = Yup.InferType<typeof validationSchema>;
export default function EditUserPasswordForm({
onCancel,
user,
@@ -49,26 +40,52 @@ export default function EditUserPasswordForm({
client: remoteProjectGQLClient,
});
const { closeDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
skip: !currentProject?.id,
});
const passwordMinLength =
data?.config?.auth?.method?.emailPassword?.passwordMinLength || 1;
const validationSchema = Yup.object({
password: Yup.string()
.label('Password')
.min(
passwordMinLength,
`Password must be at least ${passwordMinLength} characters long.`,
)
.required('This field is required.'),
cpassword: Yup.string()
.label('Password Confirmation')
.min(
passwordMinLength,
`Password must be at least ${passwordMinLength} characters long.`,
)
.oneOf([Yup.ref('password')], 'Passwords do not match')
.required('This field is required.'),
});
const [editUserPasswordFormError, setEditUserPasswordFormError] =
useState<Error | null>(null);
const form = useForm<EditUserPasswordFormValues>({
const form = useForm<Yup.InferType<typeof validationSchema>>({
defaultValues: {},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
const handleSubmit = async ({ password }: EditUserPasswordFormValues) => {
const handleSubmit = async ({
password,
}: Yup.InferType<typeof validationSchema>) => {
setEditUserPasswordFormError(null);
const passwordHash = await bcrypt.hash(password, 10);
const updateUserPasswordPromise = updateUser({
variables: {
id: user.id,
user: {
passwordHash,
},
user: { passwordHash },
},
client: remoteProjectGQLClient,
});

View File

@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
@@ -34,7 +35,7 @@ const defaultParameters = {
},
},
msw: {
handlers: [tableQuery, hasuraMetadataQuery],
handlers: [tokenQuery, tableQuery, hasuraMetadataQuery],
},
};

View File

@@ -187,11 +187,11 @@ export default async function fetchTable({
const queryError = responseData as QueryError;
const schemaNotFound =
POSTGRESQL_ERROR_CODES.SCHEMA_NOT_FOUND ===
queryError.internal.error.status_code;
queryError.internal?.error?.status_code;
const tableNotFound =
POSTGRESQL_ERROR_CODES.TABLE_NOT_FOUND ===
queryError.internal.error.status_code;
queryError.internal?.error?.status_code;
if (schemaNotFound || tableNotFound) {
return {
@@ -203,7 +203,7 @@ export default async function fetchTable({
}
if (
queryError.internal.error.status_code ===
queryError.internal?.error?.status_code ===
POSTGRESQL_ERROR_CODES.COLUMNS_NOT_FOUND
) {
return {
@@ -214,7 +214,7 @@ export default async function fetchTable({
};
}
throw new Error(queryError.internal.error.message);
throw new Error(queryError.internal?.error?.message);
}
if ('error' in responseData) {

View File

@@ -1,5 +1,6 @@
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Button } from '@/components/ui/v2/Button';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import type { InputProps } from '@/components/ui/v2/Input';
@@ -91,28 +92,33 @@ export default function DatabaseConnectionInfo() {
disabled
value={inputValue}
className={className}
slotProps={{ inputRoot: { className: '!pr-8 truncate' } }}
fullWidth
hideEmptyHelperText
endAdornment={
name !== 'postgresPassword' && (
<InputAdornment position="end" className="absolute right-2">
<Button
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(inputValue as string, `${label}`);
}}
>
<CopyIcon className="h-4 w-4" />
</Button>
</InputAdornment>
)
<InputAdornment position="end" className="absolute right-2">
<Button
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(inputValue as string, `${label}`);
}}
>
<CopyIcon className="h-4 w-4" />
</Button>
</InputAdornment>
}
/>
),
)}
<Alert severity="info" className="col-span-6 text-left">
To connect to the Postgres database directly, generate a new password,
securely save it, and then modify your connection string with the newly
created password.
</Alert>
</SettingsContainer>
);
}

View File

@@ -31,6 +31,8 @@ export type DatabaseServiceVersionFormValues = Yup.InferType<
>;
const AVAILABLE_POSTGRES_VERSIONS = [
'14.6-20230705-1',
'14.6-20230613-1',
'14.6-20230525',
'14.6-20230406-2',
'14.6-20230406-1',

View File

@@ -1,3 +1,4 @@
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -6,30 +7,27 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { generateRandomDatabasePassword } from '@/features/database/common/utils/generateRandomDatabasePassword';
import type { ResetDatabasePasswordFormValues } from '@/features/database/settings/utils/resetDatabasePasswordValidationSchema';
import { resetDatabasePasswordValidationSchema } from '@/features/database/settings/utils/resetDatabasePasswordValidationSchema';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useResetPostgresPasswordMutation,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useResetDatabasePasswordMutation } from '@/generated/graphql';
import { useLeaveConfirm } from '@/hooks/useLeaveConfirm';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup';
import { alpha } from '@mui/system';
import { useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export interface ResetDatabasePasswordFormValues {
/**
* The new password to set for the database.
*/
databasePassword: string;
}
export default function ResetDatabasePasswordSettings() {
const [updateApplication] = useUpdateApplicationMutation();
const [resetPassword, { loading: resetPasswordLoading }] =
useResetDatabasePasswordMutation();
const { maintenanceActive } = useUI();
const user = useUserData();
const { currentProject } = useCurrentWorkspaceAndProject();
const { openAlertDialog } = useDialog();
const form = useForm<ResetDatabasePasswordFormValues>({
reValidateMode: 'onSubmit',
@@ -46,41 +44,36 @@ export default function ResetDatabasePasswordSettings() {
setValue,
getValues,
register,
formState: { errors, isDirty, isSubmitting },
formState: { errors, dirtyFields, isSubmitting },
} = form;
const [resetPostgresPasswordMutation] = useResetPostgresPasswordMutation();
const user = useUserData();
const { currentProject } = useCurrentWorkspaceAndProject();
const isDirty = Object.keys(dirtyFields).length > 0;
const handleGenerateRandomPassword = () => {
useLeaveConfirm({ isDirty });
function handleGenerateRandomPassword() {
const newRandomDatabasePassword = generateRandomDatabasePassword();
triggerToast('New random database password generated.');
triggerToast(
'Random database password was generated and copied to clipboard. Submit the form to save it.',
);
copy(newRandomDatabasePassword);
setValue('databasePassword', newRandomDatabasePassword, {
shouldDirty: true,
});
};
}
const handleChangeDatabasePassword = async (
values: ResetDatabasePasswordFormValues,
) => {
async function handleChangeDatabasePassword(
formValues: ResetDatabasePasswordFormValues,
) {
try {
await resetPostgresPasswordMutation({
variables: {
appID: currentProject.id,
newPassword: values.databasePassword,
},
});
await updateApplication({
await resetPassword({
variables: {
appId: currentProject.id,
app: {
postgresPassword: values.databasePassword,
},
newPassword: formValues.databasePassword,
},
});
form.reset(values);
form.reset(formValues);
triggerToast(
`The database password for ${currentProject.name} has been updated successfully.`,
@@ -93,24 +86,45 @@ export default function ResetDatabasePasswordSettings() {
`An error occurred while trying to update the database password: ${currentProject.name} (${user.email}): ${e.message}`,
);
}
};
}
function handleSubmit(formValues: ResetDatabasePasswordFormValues) {
openAlertDialog({
title: 'Confirm Change',
payload: 'Are you sure you want to change the database password?',
props: {
primaryButtonColor: 'error',
primaryButtonText: 'Confirm',
onPrimaryAction: () => handleChangeDatabasePassword(formValues),
},
});
}
return (
<FormProvider {...form}>
<Form onSubmit={handleChangeDatabasePassword}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Reset Password"
description="This password is used for accessing your database."
submitButtonText="Reset"
description="This password will be used for accessing your database."
submitButtonText="Save"
slotProps={{
root: {
sx: { borderColor: (theme) => theme.palette.error.main },
sx: {
borderColor: (theme) =>
isDirty
? theme.palette.error.main
: alpha(theme.palette.error.main, 0.5),
'@media (prefers-reduced-motion: no-preference)': {
transition: (theme) =>
theme.transitions.create('border-color'),
},
},
},
submitButton: {
variant: 'contained',
color: 'error',
variant: isDirty ? 'contained' : 'outlined',
color: isDirty ? 'error' : 'secondary',
disabled: !isDirty || maintenanceActive,
loading: isSubmitting,
loading: isSubmitting || resetPasswordLoading,
},
}}
className="grid grid-flow-row pb-4"
@@ -126,6 +140,7 @@ export default function ResetDatabasePasswordSettings() {
hideEmptyHelperText
slotProps={{
input: { className: 'lg:w-1/2' },
inputRoot: { className: '!pr-8' },
helperText: { component: 'div' },
}}
helperText={

View File

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

View File

@@ -0,0 +1,3 @@
mutation ResetDatabasePassword($appId: String!, $newPassword: String!) {
resetPostgresPassword(appID: $appId, newPassword: $newPassword)
}

View File

@@ -16,4 +16,8 @@ export const resetDatabasePasswordValidationSchema = yup.object().shape({
.minUppercase(1),
});
export type ResetDatabasePasswordFormValues = yup.InferType<
typeof resetDatabasePasswordValidationSchema
>;
export default resetDatabasePasswordValidationSchema;

View File

@@ -1,118 +0,0 @@
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { IconButton } from '@/components/ui/v2/IconButton';
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
generateAppServiceUrl,
} from '@/features/projects/common/utils/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import { getHasuraConsoleServiceUrl } from '@/utils/env';
import Image from 'next/image';
interface HasuraConnectionInfoProps {
close?: () => void;
}
export default function HasuraConnectionInfo({
close,
}: HasuraConnectionInfoProps) {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const projectAdminSecret = currentProject?.config?.hasura.adminSecret;
if (!currentProject?.subdomain || !projectAdminSecret) {
return <LoadingScreen />;
}
const hasuraUrl =
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
? `${getHasuraConsoleServiceUrl()}`
: generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region,
'hasura',
defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
);
return (
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
<div className="grid grid-flow-row gap-1">
<div className="mx-auto">
<Image
src="/assets/hasuramodal.svg"
width={72}
height={72}
alt="Hasura"
/>
</div>
<Text variant="h3" component="h1" className="text-center">
Open Hasura
</Text>
<Text className="text-center">
Hasura is the dashboard you&apos;ll use to edit your schema and
permissions as well as browse data. Copy the admin secret to your
clipboard and enter it in the next screen.
</Text>
<Box className="mt-6 border-y-1">
<div className="grid w-full grid-cols-1 place-content-between items-center py-2 sm:grid-cols-3">
<Text className="col-span-1 text-center font-medium sm:justify-start sm:text-left">
Admin Secret
</Text>
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
<Text className="font-medium" variant="subtitle2">
{Array(projectAdminSecret.length).fill('•').join('')}
</Text>
<IconButton
onClick={() => copy(projectAdminSecret, 'Hasura admin secret')}
variant="borderless"
color="secondary"
className="min-w-0 p-1"
aria-label="Copy admin secret"
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</div>
</div>
</Box>
<div className="mt-6 grid grid-flow-row gap-2">
<Button
href={hasuraUrl}
// Both `target` and `rel` are available when `href` is set. This is
// a limitation of MUI.
// @ts-ignore
target="_blank"
rel="noreferrer noopener"
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
>
Open Hasura
</Button>
{close && (
<Button
variant="outlined"
color="secondary"
className="text-sm+ font-normal"
onClick={close}
>
Cancel
</Button>
)}
</div>
</div>
</div>
);
}

View File

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

View File

@@ -0,0 +1,116 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabled: Yup.boolean(),
});
export type HasuraAllowListFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraAllowListSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { enableAllowList } = data?.config?.hasura.settings || {};
const form = useForm<HasuraAllowListFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled: enableAllowList,
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading allow list settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
async function handleSubmit(formValues: HasuraAllowListFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
settings: {
enableAllowList: formValues.enabled,
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Allow list settings are being updated...`,
success: `Allow list settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update allow list settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Allow List"
description="Safely allow a limited number of GraphQL queries, mutations and subscriptions for your project."
slotProps={{
submitButton: {
disabled: !form.formState.isDirty || maintenanceActive,
loading: form.formState.isSubmitting,
},
}}
switchId="enabled"
docsTitle="enabling or disabling Allow Lists"
docsLink="https://hasura.io/learn/graphql/hasura-advanced/security/3-allow-list/"
showSwitch
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,116 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabled: Yup.boolean(),
});
export type HasuraConsoleFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraConsoleSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { enableConsole } = data?.config?.hasura.settings || {};
const form = useForm<HasuraConsoleFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled: enableConsole,
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Hasura Console settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
async function handleSubmit(formValues: HasuraConsoleFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
settings: {
enableConsole: formValues.enabled,
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Hasura Console settings are being updated...`,
success: `Hasura Console settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Hasura Console settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Hasura Console"
description="Enable or disable the Hasura Console. This will enable or disable the Hasura Console on the dashboard as well."
slotProps={{
submitButton: {
disabled: !form.formState.isDirty || maintenanceActive,
loading: form.formState.isSubmitting,
},
}}
switchId="enabled"
docsTitle="enabling or disabling the Hasura Console"
docsLink="https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/#enable-console"
showSwitch
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,93 @@
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import { render, screen, waitFor } from '@/tests/testUtils';
import { graphql } from 'msw';
import { setupServer } from 'msw/node';
import { beforeAll, expect, test } from 'vitest';
import HasuraCorsDomainSettings from './HasuraCorsDomainSettings';
const server = setupServer(
tokenQuery,
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
res(
ctx.data({
config: {
id: 'HasuraSettings',
__typename: 'HasuraSettings',
hasura: {
version: 'v2.25.1-ce',
settings: {
corsDomain: ['*'],
enableAllowList: false,
enableRemoteSchemaPermissions: false,
enableConsole: false,
devMode: false,
enabledAPIs: [],
},
logs: [],
events: [],
},
},
}),
),
),
);
beforeAll(() => {
server.listen();
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
test('should not enable switch by default when CORS domain is set to *', async () => {
render(<HasuraCorsDomainSettings />);
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
expect(screen.getByRole('checkbox')).not.toBeChecked();
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
});
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
server.use(
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
res(
ctx.data({
config: {
id: 'HasuraSettings',
__typename: 'HasuraSettings',
hasura: {
version: 'v2.25.1-ce',
settings: {
corsDomain: ['https://example.com', 'https://*.example.com'],
enableAllowList: false,
enableRemoteSchemaPermissions: false,
enableConsole: false,
devMode: false,
enabledAPIs: [],
},
logs: [],
events: [],
},
},
}),
),
),
);
render(<HasuraCorsDomainSettings />);
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
await waitFor(() => expect(screen.getByRole('checkbox')).toBeChecked());
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(
'https://example.com, https://*.example.com',
);
});

View File

@@ -0,0 +1,154 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabled: Yup.boolean().label('Enabled'),
corsDomain: Yup.string()
.label('Allowed CORS domains')
.when('enabled', {
is: true,
then: (schema) => schema.required(),
}),
});
export type HasuraCorsDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraCorsDomainSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { corsDomain } = data?.config?.hasura.settings || {};
const form = useForm<HasuraCorsDomainFormValues>({
reValidateMode: 'onSubmit',
values: {
enabled:
corsDomain && corsDomain.length === 1
? corsDomain[0] !== '*'
: !!corsDomain?.length,
corsDomain:
corsDomain && corsDomain.length === 1 && corsDomain[0] !== '*'
? corsDomain[0]
: corsDomain?.join(', ') || '',
},
resolver: yupResolver(validationSchema),
});
const { register, formState, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading CORS domain settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
async function handleSubmit(formValues: HasuraCorsDomainFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
settings: {
corsDomain: formValues.enabled
? formValues.corsDomain
.split(',')
.map((domain) => domain.trim())
: ['*'],
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `CORS domain settings are being updated...`,
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);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Configure CORS"
description="Allow requests from specific domains to access your GraphQL API. Disable this setting to allow requests from all domains."
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
switchId="enabled"
showSwitch
docsTitle="CORS configuration"
docsLink="https://hasura.io/docs/latest/deployment/graphql-engine-flags/config-examples/#configure-cors"
className={twMerge(
'grid grid-cols-5 gap-4 px-4',
!enabled && 'hidden',
)}
>
<Input
{...register('corsDomain')}
label="Allowed CORS domains"
placeholder="https://example.com, https://*.example.com"
id="corsDomain"
fullWidth
className="col-span-5 lg:col-span-2"
error={Boolean(formState.errors.corsDomain)}
aria-hidden={!enabled}
helperText={formState.errors.corsDomain?.message}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,116 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabled: Yup.boolean(),
});
export type HasuraDevModeFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraDevModeSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { devMode } = data?.config?.hasura.settings || {};
const form = useForm<HasuraDevModeFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled: devMode,
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading Dev Mode settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
async function handleSubmit(formValues: HasuraDevModeFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
settings: {
enableConsole: formValues.enabled,
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Dev Mode settings are being updated...`,
success: `Dev Mode settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update Dev Mode settings.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Dev Mode"
description="Enable or disable Dev Mode."
slotProps={{
submitButton: {
disabled: !form.formState.isDirty || maintenanceActive,
loading: form.formState.isSubmitting,
},
}}
switchId="enabled"
docsTitle="enabling or disabling Dev Mode"
docsLink="https://hasura.io/learn/graphql/hasura-advanced/debugging/1-dev-mode/"
showSwitch
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,145 @@
import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabledAPIs: Yup.array(
Yup.object({
label: Yup.string().required(),
value: Yup.string().required(),
}),
)
.label('Enabled Hasura APIs')
.required(),
});
export type HasuraEnabledAPIFormValues = Yup.InferType<typeof validationSchema>;
const AVAILABLE_HASURA_APIS = ['metadata', 'graphql', 'pgdump', 'config'];
export default function HasuraEnabledAPISettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
});
const { enabledAPIs } = data?.config?.hasura.settings || {};
const form = useForm<HasuraEnabledAPIFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabledAPIs: enabledAPIs.map((api) => ({
label: api,
value: api,
})),
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading enabled APIs..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const availableAPIs = AVAILABLE_HASURA_APIS.map((api) => ({
label: api,
value: api,
}));
async function handleSubmit(formValues: HasuraEnabledAPIFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
settings: {
enabledAPIs: formValues.enabledAPIs.map((api) => api.value),
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Enabled APIs are being updated...`,
success: `Enabled APIs have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update enabled APIs.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Enabled APIs"
description="Enable or disable APIs for your Hasura instance."
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-6"
>
<ControlledAutocomplete
id="enabledAPIs"
name="enabledAPIs"
fullWidth
multiple
className="lg:col-span-3"
aria-label="Enabled APIs"
options={availableAPIs}
error={!!formState.errors?.enabledAPIs?.message}
helperText={formState.errors?.enabledAPIs?.message}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,155 @@
import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { HighlightedText } from '@/components/presentational/HighlightedText';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
logLevel: Yup.object({
label: Yup.string().required(),
value: Yup.string().required(),
})
.label('Log level')
.required(),
});
export type HasuraLogLevelFormValues = Yup.InferType<typeof validationSchema>;
const AVAILABLE_HASURA_LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
export default function HasuraLogLevelSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { level } = data?.config?.hasura.logs || {};
const form = useForm<HasuraLogLevelFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
logLevel: level
? {
label: level,
value: level,
}
: { label: 'warn', value: 'warn' },
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading log level settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const availableLogLevels = AVAILABLE_HASURA_LOG_LEVELS.map((api) => ({
label: api,
value: api,
}));
async function handleSubmit(formValues: HasuraLogLevelFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
logs: {
level: formValues.logLevel?.value || 'warn',
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Log level is being updated...`,
success: `Log level has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update log level.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Log Level"
description={
<>
Setting a log-level will print all logs of priority greater than
the set level. The log-level hierarchy is:{' '}
<HighlightedText>
debug &rarr; info &rarr; warn &rarr; error
</HighlightedText>
</>
}
docsLink="https://hasura.io/docs/latest/deployment/logging/#logging-levels"
docsTitle="Log Levels"
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5"
>
<ControlledAutocomplete
id="logLevel"
name="logLevel"
fullWidth
className="lg:col-span-2"
aria-label="Hasura Log Level"
options={availableLogLevels}
error={!!formState.errors?.logLevel?.message}
helperText={formState.errors?.logLevel?.message}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,133 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
httpPoolSize: Yup.number()
.label('HTTP Pool Size')
.min(1)
.max(100)
.typeError('HTTP Pool Size must be a number')
.required(),
});
export type HasuraPoolSizeFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraPoolSizeSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { httpPoolSize } = data?.config?.hasura.events || {};
const form = useForm<HasuraPoolSizeFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { httpPoolSize: httpPoolSize || 100 },
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading pool size settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { formState, register } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
async function handleSubmit(formValues: HasuraPoolSizeFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
events: {
httpPoolSize: formValues.httpPoolSize,
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Pool size is being updated...`,
success: `Pool size has been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the pool size.`,
),
},
getToastStyleProps(),
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="HTTP Pool Size"
description="Set the maximum number of concurrent HTTP workers for event delivery."
docsLink="https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/#events-http-pool-size"
slotProps={{
submitButton: {
disabled: !isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5"
>
<Input
{...register('httpPoolSize')}
id="httpPoolSize"
name="httpPoolSize"
type="number"
label="HTTP Pool Size"
fullWidth
className="lg:col-span-2"
error={Boolean(formState.errors.httpPoolSize?.message)}
helperText={formState.errors.httpPoolSize?.message}
slotProps={{ inputRoot: { min: 1, max: 100 } }}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -0,0 +1,120 @@
import { useUI } from '@/components/common/UIProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
import { getServerError } from '@/utils/getServerError';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
enabled: Yup.boolean(),
});
export type HasuraRemoteSchemaPermissionsFormValues = Yup.InferType<
typeof validationSchema
>;
export default function HasuraRemoteSchemaPermissionsSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
});
const { enableRemoteSchemaPermissions } = data?.config?.hasura.settings || {};
const form = useForm<HasuraRemoteSchemaPermissionsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled: enableRemoteSchemaPermissions,
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading remote schema permission settings..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
async function handleSubmit(
formValues: HasuraRemoteSchemaPermissionsFormValues,
) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
config: {
hasura: {
settings: {
enableConsole: formValues.enabled,
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Remote schema permission settings are being updated...`,
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);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Remote Schema Permissions"
description="Enable or disable remote schema permissions."
slotProps={{
submitButton: {
disabled: !form.formState.isDirty || maintenanceActive,
loading: form.formState.isSubmitting,
},
}}
switchId="enabled"
docsTitle="enabling or disabling Remote Schema Permissions"
docsLink="https://hasura.io/docs/latest/remote-schemas/auth/remote-schema-permissions/"
showSwitch
className="hidden"
/>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -31,6 +31,9 @@ export type HasuraServiceVersionFormValues = Yup.InferType<
>;
const AVAILABLE_HASURA_VERSIONS = [
'v2.29.0-ce',
'v2.28.2-ce',
'v2.27.0-ce',
'v2.25.1-ce',
'v2.25.0-ce',
'v2.24.1-ce',
@@ -39,7 +42,8 @@ const AVAILABLE_HASURA_VERSIONS = [
export default function HasuraServiceVersionSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
});
@@ -82,9 +86,7 @@ export default function HasuraServiceVersionSettings() {
const { formState } = form;
const handleHasuraServiceVersionsChange = async (
formValues: HasuraServiceVersionFormValues,
) => {
async function handleSubmit(formValues: HasuraServiceVersionFormValues) {
const updateConfigPromise = updateConfig({
variables: {
appId: currentProject.id,
@@ -110,14 +112,15 @@ export default function HasuraServiceVersionSettings() {
);
form.reset(formValues);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
};
}
return (
<FormProvider {...form}>
<Form onSubmit={handleHasuraServiceVersionsChange}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Hasura GraphQL Engine Version"
description="The version of the Hasura GraphQL Engine to use."
@@ -143,6 +146,7 @@ export default function HasuraServiceVersionSettings() {
}}
fullWidth
className="lg:col-span-2"
aria-label="Hasura Service Version"
options={availableVersions}
error={!!formState.errors?.version?.message}
helperText={formState.errors?.version?.message}

View File

@@ -4,6 +4,20 @@ query GetHasuraSettings($appId: uuid!) {
__typename
hasura {
version
settings {
enableAllowList
enableRemoteSchemaPermissions
enableConsole
devMode
corsDomain
enabledAPIs
}
logs {
level
}
events {
httpPoolSize
}
}
}
}

View File

@@ -0,0 +1,72 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Table } from '@/components/ui/v2/Table';
import { TableBody } from '@/components/ui/v2/TableBody';
import { TableCell } from '@/components/ui/v2/TableCell';
import { TableContainer } from '@/components/ui/v2/TableContainer';
import { TableHead } from '@/components/ui/v2/TableHead';
import { TableRow } from '@/components/ui/v2/TableRow';
import { Text } from '@/components/ui/v2/Text';
import { BackupListItem } from '@/features/projects/backups/components/BackupListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useGetApplicationBackupsQuery } from '@/utils/__generated__/graphql';
export default function BackupList() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetApplicationBackupsQuery({
variables: { appId: currentProject.id },
});
if (loading) {
return (
<ActivityIndicator
delay={500}
className="my-5"
label="Loading backups..."
/>
);
}
if (error) {
throw error;
}
const { backups } = data.app;
return (
<TableContainer sx={{ backgroundColor: 'background.paper' }}>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Size</TableCell>
<TableCell>Backed up</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{backups.length === 0 && (
<TableRow>
<TableCell>
<Text className="text-xs" color="secondary">
No backups are available.
</Text>
</TableCell>
<TableCell />
<TableCell />
<TableCell />
</TableRow>
)}
{backups.map((backup) => (
<BackupListItem
key={backup.id}
backup={backup}
projectId={currentProject.id}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
}

View File

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

View File

@@ -0,0 +1,94 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Button } from '@/components/ui/v2/Button';
import { TableCell } from '@/components/ui/v2/TableCell';
import { TableRow } from '@/components/ui/v2/TableRow';
import { RestoreBackupModal } from '@/features/projects/backups/components/RestoreBackupModal';
import type { Backup } from '@/types/application';
import { prettifySize } from '@/utils/prettifySize';
import { triggerToast } from '@/utils/toast';
import { useGetBackupPresignedUrlLazyQuery } from '@/utils/__generated__/graphql';
import { format, formatDistanceStrict, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
export interface BackupListItemProps {
/**
* Project ID.
*/
projectId: string;
/**
* Backup data.
*/
backup: Backup;
}
export default function BackupListItem({
projectId,
backup,
}: BackupListItemProps) {
const { id, createdAt, size } = backup;
const { openDialog, closeDialog } = useDialog();
const [fetchPresignedUrl, { loading: loadingPresignedUrl }] =
useGetBackupPresignedUrlLazyQuery({
variables: {
appId: projectId,
backupId: id,
},
});
async function downloadBackup() {
const { data: presignedUrlData, error } = await fetchPresignedUrl();
if (error) {
triggerToast(
'An error occurred while fetching the presigned URL. Please try again later.',
);
return;
}
if (typeof window === 'undefined') {
return;
}
window.open(presignedUrlData.getBackupPresignedUrl.url, '_blank');
}
function restoreBackup() {
openDialog({
title: 'Restore Backup',
component: <RestoreBackupModal backup={backup} close={closeDialog} />,
});
}
return (
<TableRow>
<TableCell className="text-xs">
{format(parseISO(createdAt), 'yyyy-MM-dd HH:mm:ss')}
</TableCell>
<TableCell className="text-xs">{prettifySize(size)}</TableCell>
<TableCell className="text-xs">
{formatDistanceStrict(new Date(createdAt), new Date(), {
addSuffix: true,
})}
</TableCell>
<TableCell
className={twMerge(
'grid grid-flow-col justify-end gap-2',
!loadingPresignedUrl && 'pl-8',
)}
>
<Button
variant="borderless"
onClick={downloadBackup}
loading={loadingPresignedUrl}
>
Download
</Button>
<Button variant="borderless" onClick={restoreBackup}>
Restore
</Button>
</TableCell>
</TableRow>
);
}

View File

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

View File

@@ -3,38 +3,38 @@ import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { Backup } from '@/types/application';
import { triggerToast } from '@/utils/toast';
import { useRestoreApplicationDatabaseMutation } from '@/utils/__generated__/graphql';
import { formatISO9075 } from 'date-fns';
import { format, parseISO } from 'date-fns';
import { useState } from 'react';
export interface RestoreBackupModalModalProps {
export interface RestoreBackupModalProps {
/**
* Call this function to imperatively close the modal.
*/
close: any;
close: VoidFunction;
/**
* Arbitrary data passed down to the modal.
*
* Backup data.
*/
data: any;
backup: Backup;
}
export default function RestoreBackupModal({
close,
data,
}: RestoreBackupModalModalProps) {
const { id: backupId, createdAt } = data;
backup,
}: RestoreBackupModalProps) {
const { id: backupId, createdAt } = backup;
const [isSure, setIsSure] = useState(false);
const [mutationIsCompleted, setMutationIsCompleted] = useState(false);
const [restoreCompleted, setRestoreCompleted] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [restoreApplicationDatabase, { loading }] =
useRestoreApplicationDatabaseMutation();
const handleSubmit = async () => {
setMutationIsCompleted(false);
async function handleSubmit() {
setRestoreCompleted(false);
try {
await restoreApplicationDatabase({
variables: {
@@ -43,55 +43,49 @@ export default function RestoreBackupModal({
},
});
} catch (error) {
setMutationIsCompleted(false);
setRestoreCompleted(false);
triggerToast('Database backup restoration failed');
return;
}
setMutationIsCompleted(true);
setRestoreCompleted(true);
triggerToast('Database backup successfully scheduled for restoration.');
};
}
if (mutationIsCompleted) {
if (restoreCompleted) {
return (
<Box className="w-modal rounded-lg p-6">
<div className="flex flex-col">
<Text className="text-center text-lg font-medium">
The backup has been restored successfully.
</Text>
<Box className="grid grid-flow-row gap-4 px-6 pb-6">
<Text>The backup has been restored successfully.</Text>
<Button className="mt-5" onClick={close}>
OK
</Button>
</div>
<Button onClick={close}>OK</Button>
</Box>
);
}
return (
<Box className="w-modal rounded-lg px-6 py-6 text-left">
<div className="flex flex-col">
<Text className="text-center text-lg font-medium">
Restore Database Backup
</Text>
<Text className="mt-2 text-center font-normal">
You current database will be deleted, and the backup created{' '}
<span className="font-semibold">
{formatISO9075(new Date(createdAt))}
</span>{' '}
will be restored.
</Text>
<Box className="grid grid-flow-row gap-2 px-6 pb-6">
<Text>
You current database will be deleted, and the backup created at{' '}
<span className="font-semibold">
{format(parseISO(createdAt), 'yyyy-MM-dd HH:mm:ss')}
</span>{' '}
will be restored.
</Text>
<Box className="my-4 border-y py-2 px-2">
<Checkbox
checked={isSure}
onChange={(_event, checked) => setIsSure(checked)}
label="I'm sure I want to restore this backup."
/>
</Box>
<Button onClick={handleSubmit} disabled={!isSure} loading={loading}>
Restore
</Button>
</div>
<Box className="pt-1 pb-2.5">
<Checkbox
checked={isSure}
onChange={(_event, checked) => setIsSure(checked)}
label="I'm sure I want to restore this backup"
/>
</Box>
<Button onClick={handleSubmit} disabled={!isSure} loading={loading}>
Restore
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</Box>
);
}

View File

@@ -0,0 +1,6 @@
fragment Backup on backups {
id
size
createdAt
completedAt
}

View File

@@ -1,10 +1,7 @@
query getApplicationBackups($appId: uuid!) {
app(id: $appId) {
backups(order_by: { createdAt: desc }) {
id
size
createdAt
completedAt
...Backup
}
}
}

View File

@@ -0,0 +1,14 @@
query GetBackupPresignedUrl(
$appId: String!
$backupId: String!
$expireInMinutes: Int
) {
getBackupPresignedUrl: getBackupPresignedURL(
appID: $appId
backupID: $backupId
expireInMinutes: $expireInMinutes
) {
url
expiresAt: expires_at
}
}

View File

@@ -49,7 +49,7 @@ export default function StagingMetadata({
return (
isDevOrStaging() && (
<div className="mx-auto mt-10 max-w-sm">
<Box className="mx-auto flex flex-col rounded-md border p-5 text-center">
<Box className="mx-auto grid grid-flow-row justify-items-center rounded-md border p-5 text-center">
<Status status={StatusEnum.Deploying}>Internal info</Status>
{children}
</Box>

View File

@@ -3,7 +3,7 @@ import type { Project, Workspace } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import { getHasuraAdminSecret } from '@/utils/env';
import { GetWorkspaceAndProjectDocument } from '@/utils/__generated__/graphql';
import { useNhostClient, useUserData } from '@nhost/nextjs';
import { useAuthenticationStatus, useNhostClient } from '@nhost/nextjs';
import type { RefetchOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
@@ -34,14 +34,24 @@ export interface UseCurrentWorkspaceAndProjectReturnType {
export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndProjectReturnType {
const client = useNhostClient();
const user = useUserData();
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading: isAuthLoading } =
useAuthenticationStatus();
const {
query: { workspaceSlug, appSlug },
isReady,
isReady: isRouterReady,
} = useRouter();
const isWorkspaceSlugAvailable = Boolean(workspaceSlug);
const shouldFetchWorkspaceAndProject =
isPlatform &&
isRouterReady &&
isWorkspaceSlugAvailable &&
isAuthenticated &&
!isAuthLoading;
// We can't use the hook exported by the codegen here because there are cases
// where it doesn't target the Nhost backend, but the currently active project
// instead.
@@ -59,7 +69,7 @@ export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndP
}),
{
keepPreviousData: true,
enabled: isPlatform && isReady && !!workspaceSlug && !!user,
enabled: shouldFetchWorkspaceAndProject,
// multiple components are relying on this query, so we don't want to
// refetch it too often - kind of a hack, should be improved later
staleTime: 1000,
@@ -142,7 +152,7 @@ export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndP
return {
currentWorkspace,
currentProject,
loading: response ? false : isFetching,
loading: response ? false : isFetching || isAuthLoading,
error: response?.error
? new Error(error?.message || 'Unknown error occurred.')
: null,

View File

@@ -11,6 +11,7 @@ import { RocketIcon } from '@/components/ui/v2/icons/RocketIcon';
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import type { SvgIconProps } from '@/components/ui/v2/icons/SvgIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type { ReactElement } from 'react';
@@ -55,6 +56,8 @@ export interface ProjectRoute {
export default function useProjectRoutes() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const { currentProject, loading: currentProjectLoading } =
useCurrentWorkspaceAndProject();
const nhostRoutes: ProjectRoute[] = [
{
@@ -119,6 +122,7 @@ export default function useProjectRoutes() {
exact: true,
label: 'Hasura',
icon: <HasuraIcon />,
disabled: !currentProject?.config?.hasura.settings?.enableConsole,
},
{
relativePath: '/users',
@@ -135,5 +139,9 @@ export default function useProjectRoutes() {
...nhostRoutes,
];
return { nhostRoutes, allRoutes };
return {
nhostRoutes,
allRoutes,
loading: currentProjectLoading,
};
}

View File

@@ -1,4 +1,3 @@
import type { ProjectFragment } from '@/utils/__generated__/graphql';
import {
getAuthServiceUrl,
getDatabaseServiceUrl,
@@ -8,6 +7,7 @@ import {
getStorageServiceUrl,
isPlatform,
} from '@/utils/env';
import type { ProjectFragment } from '@/utils/__generated__/graphql';
export type NhostService =
| 'auth'

View File

@@ -123,7 +123,7 @@ export default function SystemEnvironmentVariableSettings() {
return (
<SettingsContainer
title="System Environment Variables"
description="Environment Variables are key-value pairs configured outside your source code. They are used to store environment-specific values such as API keys."
description="System environment variables are automatically generated from the configuration file and your project's subdomain and region."
docsLink="https://docs.nhost.io/platform/environment-variables#system-environment-variables"
rootClassName="gap-0"
className="mt-2 mb-2.5 px-0"

View File

@@ -1,4 +1,5 @@
import { mockApplication, mockWorkspace } from '@/tests/mocks';
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import { queryClient, render, screen } from '@/tests/testUtils';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
@@ -34,6 +35,7 @@ vi.mock('next/router', () => ({
}));
const server = setupServer(
tokenQuery,
rest.get('https://local.graphql.nhost.run/v1', (_req, res, ctx) =>
res(ctx.status(200)),
),
@@ -135,6 +137,7 @@ test('should render an empty state when GitHub is connected, but there are no de
test('should render a list of deployments', async () => {
server.use(
tokenQuery,
rest.post('https://local.graphql.nhost.run/v1', async (_req, res, ctx) => {
const { operationName } = await _req.json();
@@ -194,6 +197,7 @@ test('should render a list of deployments', async () => {
test('should disable redeployments if a deployment is already in progress', async () => {
server.use(
tokenQuery,
rest.post('https://local.graphql.nhost.run/v1', async (req, res, ctx) => {
const { operationName } = await req.json();

View File

@@ -9,6 +9,7 @@ import {
resourcesUpdatedQuery,
} from '@/tests/msw/mocks/graphql/resourceSettingsQuery';
import updateConfigMutation from '@/tests/msw/mocks/graphql/updateConfigMutation';
import tokenQuery from '@/tests/msw/mocks/rest/tokenQuery';
import {
fireEvent,
render,
@@ -35,6 +36,7 @@ vi.mock('next/router', () => ({
}));
const server = setupServer(
tokenQuery,
resourcesAvailableQuery,
getProPlanOnlyQuery,
getWorkspaceAndProjectQuery,

View File

@@ -5,6 +5,7 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Divider } from '@/components/ui/v2/Divider';
import { Link } from '@/components/ui/v2/Link';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
import { ResourcesConfirmationDialog } from '@/features/projects/resources/settings/components/ResourcesConfirmationDialog';
@@ -348,6 +349,13 @@ export default function ResourcesForm() {
</Alert>
</Box>
)}
<Box className="px-4 pb-4">
<Alert severity="info">
In case you need more resources, please reach out to us at{' '}
<Link href="mailto:support@nhost.io">support@nhost.io</Link>.
</Alert>
</Box>
</>
) : (
<Box className={twMerge('px-4', 'pb-4')}>

View File

@@ -21,7 +21,7 @@ export const MIN_TOTAL_MEMORY =
/**
* The maximum total CPU that can be allocated.
*/
export const MAX_TOTAL_VCPU = 60 * RESOURCE_VCPU_MULTIPLIER;
export const MAX_TOTAL_VCPU = 28 * RESOURCE_VCPU_MULTIPLIER;
/**
* The maximum amount of memory that can be allocated in total.
@@ -46,7 +46,7 @@ export const MIN_SERVICE_VCPU = 0.25 * RESOURCE_VCPU_MULTIPLIER;
/**
* The maximum amount of CPU that can be allocated per service.
*/
export const MAX_SERVICE_VCPU = 15 * RESOURCE_VCPU_MULTIPLIER;
export const MAX_SERVICE_VCPU = 7 * RESOURCE_VCPU_MULTIPLIER;
/**
* The minimum amount of memory that has to be allocated per service.

View File

@@ -277,7 +277,16 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
throw new Error(fileError.message);
}
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
if (!fileMetadata) {
throw new Error('File metadata is missing.');
}
const fileId =
'processedFiles' in fileMetadata
? fileMetadata.processedFiles[0]?.id
: fileMetadata.id;
triggerToast(`File has been uploaded successfully (${fileId})`);
await refetchFilesAndAggregate();
} catch (uploadError) {

View File

@@ -19,6 +19,7 @@ query GetSignInMethods($appId: uuid!) {
emailPassword {
emailVerificationRequired
hibpEnabled
passwordMinLength
}
emailPasswordless {
enabled
@@ -40,6 +41,23 @@ query GetSignInMethods($appId: uuid!) {
teamId
privateKey
}
bitbucket {
enabled
clientId
clientSecret
}
gitlab {
enabled
clientId
clientSecret
scope
}
strava {
enabled
clientId
clientSecret
scope
}
discord {
enabled
clientId

View File

@@ -1,3 +0,0 @@
mutation resetPostgresPassword($appID: String!, $newPassword: String!) {
resetPostgresPassword(appID: $appID, newPassword: $newPassword)
}

View File

@@ -17,6 +17,9 @@ fragment Project on apps {
}
hasura {
adminSecret
settings {
enableConsole
}
}
}
featureFlags {

View File

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

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