Compare commits

...

123 Commits

Author SHA1 Message Date
Szilárd Dóró
a8a20cf5e2 Merge pull request #1820 from nhost/changeset-release/main 2023-04-06 14:37:59 +02:00
github-actions[bot]
2f84bf3251 chore: update versions 2023-04-06 12:22:20 +00:00
Szilárd Dóró
368e0371e9 Merge pull request #1818 from nhost/feat/improved-error-state
feat(dashboard): improved error state
2023-04-06 14:21:08 +02:00
Szilárd Dóró
adb5209133 Merge branch 'main' into feat/improved-error-state 2023-04-06 14:00:07 +02:00
Szilárd Dóró
63bf405cdd Merge pull request #1800 from Glenas7/docs/local-development-guide
Update local URLs to new format
2023-04-06 13:48:40 +02:00
Szilárd Dóró
d613d66a0a Merge pull request #1819 from nhost/fix/revert-i18n-lib
fix(dashboard): revert i18n library to improve performance
2023-04-06 13:36:21 +02:00
Szilárd Dóró
e7cb5070cd fix: use updated URLs everywhere 2023-04-06 13:28:55 +02:00
Szilárd Dóró
ee50051802 chore: remove unused next-translate-plugin 2023-04-06 13:08:53 +02:00
Szilárd Dóró
20e7bb4747 fix: remove translation 2023-04-06 13:08:06 +02:00
Szilárd Dóró
ba0d57ee91 fix: revert i18n library 2023-04-06 12:56:53 +02:00
Szilárd Dóró
98093c9023 Update dashboard/locales/en/overview.json
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2023-04-06 12:28:08 +02:00
Szilárd Dóró
2fda299736 fix comment 2023-04-06 12:14:02 +02:00
Szilárd Dóró
3328ed059e chore: add changeset 2023-04-06 11:42:20 +02:00
Szilárd Dóró
cfb7199b79 feat: show error message on project overview 2023-04-06 11:41:09 +02:00
Szilárd Dóró
1ad4bfe815 fix readme 2023-04-06 09:26:44 +02:00
Szilárd Dóró
25859fc421 Merge pull request #1812 from nhost/changeset-release/main
chore: update versions
2023-04-06 09:22:15 +02:00
github-actions[bot]
5a9e7a43c8 chore: update versions 2023-04-06 06:45:13 +00:00
Szilárd Dóró
2739ff90c4 Merge pull request #1811 from nhost/feat/metrics
feat(dashboard): Show Metrics
2023-04-06 08:43:45 +02:00
Szilárd Dóró
2a4623c582 Merge pull request #1815 from nhost/renovate/peter-evans-create-pull-request-5.x
chore(deps): update peter-evans/create-pull-request action to v5
2023-04-05 11:11:26 +02:00
Szilárd Dóró
19b7835d92 fix: remove empty test file 2023-04-05 11:04:04 +02:00
Szilárd Dóró
efbd272298 fix: use separator for sizes 2023-04-05 10:32:52 +02:00
Szilárd Dóró
98546d24e1 chore: improve number formatting 2023-04-05 10:28:59 +02:00
Szilárd Dóró
fe2c0cf81f Merge branch 'main' into feat/metrics 2023-04-05 09:30:37 +02:00
renovate[bot]
b28a04c48e chore(deps): update peter-evans/create-pull-request action to v5 2023-04-05 07:29:01 +00:00
Szilárd Dóró
a014913523 Merge pull request #1810 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.33
2023-04-05 09:28:22 +02:00
Szilárd Dóró
706c9dc3fb chore: add changeset 2023-04-04 18:45:17 +02:00
Szilárd Dóró
fe08faad4a feat: introduce i18n lib 2023-04-04 18:34:26 +02:00
Szilárd Dóró
6719ce92ea feat: add tooltip to metrics 2023-04-04 14:32:35 +02:00
Szilárd Dóró
52c6f09bdd chore: simplify pending UI 2023-04-04 13:57:36 +02:00
Szilárd Dóró
f337a19875 chore: add test 2023-04-04 13:42:38 +02:00
renovate[bot]
d62c909901 chore(deps): update dependency @types/react to v18.0.33 2023-04-04 11:02:39 +00:00
Szilárd Dóró
99f8f6b370 chore: add changeset 2023-04-04 13:01:02 +02:00
Szilárd Dóró
644d94a175 Merge pull request #1803 from nhost/renovate/next-seo-6.x
fix(deps): update dependency next-seo to v6
2023-04-04 13:00:36 +02:00
Szilárd Dóró
05ab111aa4 chore: remove postgres usage 2023-04-04 12:59:31 +02:00
Szilárd Dóró
64cf0acd4a Merge pull request #1792 from nhost/renovate/turbo-1.x
chore(deps): update dependency turbo to v1.8.8
2023-04-04 11:57:19 +02:00
Szilárd Dóró
3d5d530555 chore: improve usage panel 2023-04-04 11:51:06 +02:00
Szilárd Dóró
5e0920ba7c chore: add changeset 2023-04-04 10:12:36 +02:00
Szilárd Dóró
9bf6c3b8c4 feat: add egress volume to metrics 2023-04-03 21:21:36 +02:00
Szilárd Dóró
b25a223d90 feat: add metrics to the project overview 2023-04-03 17:33:58 +02:00
renovate[bot]
748aa443f4 fix(deps): update dependency next-seo to v6 2023-04-03 14:38:59 +00:00
renovate[bot]
684123e5d6 chore(deps): update dependency turbo to v1.8.8 2023-04-03 14:37:37 +00:00
Szilárd Dóró
fa045eed15 Merge pull request #1808 from nhost/changeset-release/main
chore: update versions
2023-04-03 16:35:43 +02:00
github-actions[bot]
61c0583b6d chore: update versions 2023-04-03 13:56:32 +00:00
Szilárd Dóró
1343a6f252 Merge pull request #1806 from nhost/fix/failed-subscriptions
fix: don't open unnecessary connections
2023-04-03 15:55:27 +02:00
Szilárd Dóró
0d73e87a83 fix: don't open unnecessary connections 2023-04-03 15:14:28 +02:00
Szilárd Dóró
1ee0d332bf Merge pull request #1797 from nhost/changeset-release/main
chore: update versions
2023-04-03 13:50:27 +02:00
github-actions[bot]
130ce49c76 chore: update versions 2023-04-03 10:06:30 +00:00
Szilárd Dóró
6be6d6475a Merge pull request #1804 from nhost/fix/date-input
fix(dashboard): don't break logs page
2023-04-03 11:58:37 +02:00
Szilárd Dóró
177b146b93 Merge pull request #1799 from nhost/renovate/urql-4.x
chore(deps): update dependency urql to v4
2023-04-03 11:39:47 +02:00
Szilárd Dóró
3cb673000a fix: don't break logs page 2023-04-03 11:38:34 +02:00
Szilárd Dóró
09cf5d4b39 chore: add changeset 2023-04-03 10:07:16 +02:00
Szilárd Dóró
48c0061a0b Merge pull request #1772 from wollerman/patch-2
Update serverless-functions to link to event trigger docs
2023-04-03 09:47:01 +02:00
Szilárd Dóró
0795d1c6a1 chore: move new text 2023-04-03 09:34:02 +02:00
renovate[bot]
45a23dd1bf chore(deps): update dependency urql to v4 2023-04-03 07:30:32 +00:00
Szilárd Dóró
bb8e3454df Merge pull request #1790 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.32
2023-04-03 09:28:01 +02:00
Szilárd Dóró
6a290bb297 chore: add changeset 2023-04-03 09:10:03 +02:00
renovate[bot]
80baec7356 chore(deps): update dependency @types/react to v18.0.32 2023-04-02 09:44:20 +00:00
Glenas7
feb195fd65 Update local URLs to new format
With the change of local URLs to the subdomain format with "local" as subdiomain and no region, I believe these URLs should be updated here.
2023-04-01 11:19:57 +02:00
Szilárd Dóró
8e43297564 Merge pull request #1798 from nhost/feat/project-creator
feat(dashboard): show project creator
2023-03-31 14:49:16 +02:00
Szilárd Dóró
bb8eb9e387 fix: fix assertion in test 2023-03-31 13:55:05 +02:00
Szilárd Dóró
5b0dc6cb19 fix: use optional chaining in overview header 2023-03-31 13:45:58 +02:00
Szilárd Dóró
826112afd0 fix: don't show upgrade button for pro projects 2023-03-31 13:32:16 +02:00
Szilárd Dóró
97105c390d chore: remove test file 2023-03-31 13:26:59 +02:00
Szilárd Dóró
8e3707ff2c Merge branch 'main' into feat/project-creator 2023-03-31 13:25:32 +02:00
Szilárd Dóró
7453bf3b6a chore: add changeset 2023-03-31 13:25:25 +02:00
Szilárd Dóró
bd739383d2 Merge pull request #1796 from nhost/chore/improved-auth-tests
chore(tests): improve auth page tests
2023-03-31 13:19:44 +02:00
Szilárd Dóró
f75e2e41db chore: prefix email addresses 2023-03-31 10:48:54 +02:00
Szilárd Dóró
7328491be0 feat: add relative time to creator info 2023-03-30 20:37:53 +02:00
Szilárd Dóró
11b4d12f12 Merge pull request #1794 from nhost/changeset-release/main
chore: update versions
2023-03-30 19:56:33 +02:00
Szilárd Dóró
6c24d56b1d fix: remove test.only 2023-03-30 17:41:14 +02:00
Szilárd Dóró
0a523f4b45 feat: add project creator to overviews 2023-03-30 17:37:21 +02:00
Szilárd Dóró
12301e6551 fix: use correct @nhost/apollo version 2023-03-30 15:57:43 +02:00
Szilárd Dóró
8440d0389e chore: restructure auth tests 2023-03-30 15:55:58 +02:00
Szilárd Dóró
c166dad0f8 chore: add changeset 2023-03-30 13:49:14 +02:00
Szilárd Dóró
e31d39b3d2 feat: incorporate global setup in projects 2023-03-30 13:44:34 +02:00
Szilárd Dóró
090f9cef86 chore: extend user management tests 2023-03-30 13:35:06 +02:00
github-actions[bot]
74e52cac2d chore: update versions 2023-03-30 09:07:41 +00:00
Szilárd Dóró
f17823760a Merge pull request #1795 from nhost/fix/presigned-urls
fix: don't alter URLs when no transformation parameters are available
2023-03-30 11:06:32 +02:00
Szilárd Dóró
bb8803a1e3 fix: don't alter URLs 2023-03-30 10:41:57 +02:00
Szilárd Dóró
b846291331 Merge pull request #1793 from nhost/fix/export-issue
fix: don't use conflicting names
2023-03-30 10:07:24 +02:00
Szilárd Dóró
2b2fb94f00 chore: add type checking step 2023-03-30 09:42:23 +02:00
Szilárd Dóró
551760c4f0 fix: don't break builds 2023-03-30 09:37:39 +02:00
Szilárd Dóró
5ae5a8e77d fix: don't break builds 2023-03-30 09:31:54 +02:00
Szilárd Dóró
56aae0c964 fix: don't break builds 2023-03-30 09:28:34 +02:00
Szilárd Dóró
a0e093d77b fix: don't use conflicting names 2023-03-30 09:16:30 +02:00
Szilárd Dóró
5e82e1b3da Merge pull request #1784 from nhost/changeset-release/main
chore: update versions
2023-03-29 09:20:48 +02:00
github-actions[bot]
e618b705e7 chore: update versions 2023-03-28 15:52:47 +00:00
Szilárd Dóró
a232c9f0f6 Merge pull request #1789 from nhost/fix/azuread-description
fix(dashboard): use correct description for Azure AD
2023-03-28 17:50:51 +02:00
Szilárd Dóró
bf4644ea10 fix: use correct description for Azure AD 2023-03-28 16:52:54 +02:00
Szilárd Dóró
0aca907ea4 Merge pull request #1788 from nhost/fix/azuread-provider-name
fix: correct typos in Azure AD
2023-03-28 16:25:59 +02:00
Szilárd Dóró
394f4c4174 fix: correct typos in Azure AD 2023-03-28 16:25:26 +02:00
Szilárd Dóró
8fef08a150 Merge pull request #1786 from nhost/renovate/turbo-1.x
chore(deps): update dependency turbo to v1.8.6
2023-03-28 16:16:57 +02:00
Szilárd Dóró
1bd2c37301 chore: bump turbo in the Dockerfile 2023-03-28 15:54:37 +02:00
renovate[bot]
5cdb70bd81 chore(deps): update dependency turbo to v1.8.6 2023-03-28 12:01:36 +00:00
Szilárd Dóró
1a5f80e1b6 Merge pull request #1785 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.30
2023-03-28 13:59:29 +02:00
Szilárd Dóró
59e0cb00c5 Merge pull request #1787 from nhost/feat/azuread-provider 2023-03-28 12:25:42 +02:00
Szilárd Dóró
406b0f2cb7 Merge pull request #1163 from dipakparmar/feat/dashboard-azuread-settings
feat(dashboard): add azure ad provider settings
2023-03-28 10:52:17 +02:00
Szilárd Dóró
d329b6218f chore: add changeset 2023-03-28 10:46:50 +02:00
Szilárd Dóró
335b58670e Merge branch 'renovate/react-monorepo' of https://github.com/nhost/nhost into renovate/react-monorepo 2023-03-28 10:43:08 +02:00
renovate[bot]
efa2d89067 chore(deps): update dependency @types/react to v18.0.30 2023-03-28 08:35:55 +00:00
Szilárd Dóró
77ce4bd738 Merge pull request #1783 from nhost/fix/random-words
fix(tests): avoid name collision in database tests
2023-03-28 10:33:33 +02:00
Szilárd Dóró
017adea700 chore: update comment 2023-03-28 10:04:38 +02:00
Dipak Parmar
378284faa8 chore(dashboard): remove docs and title for now from azuread component
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 23:44:40 -07:00
renovate[bot]
e5e2d114b1 chore(deps): update dependency @types/react to v18.0.30 2023-03-27 19:03:37 +00:00
Szilárd Dóró
5e3dbdeb7d Merge pull request #1781 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.29
2023-03-27 20:55:47 +02:00
Szilárd Dóró
98b777491a fix: improve flaky tests 2023-03-27 18:13:10 +02:00
Szilárd Dóró
71de870cb0 fix: use admin secret as env var 2023-03-27 17:29:09 +02:00
Szilárd Dóró
74d4deba28 feat: cleanup public schema after tests 2023-03-27 17:00:07 +02:00
Szilárd Dóró
cb248f0d30 chore: add changeset 2023-03-27 15:44:08 +02:00
Szilárd Dóró
09e4f1eb34 fix: avoid duplicate table names in tests 2023-03-27 15:16:40 +02:00
Szilárd Dóró
6e1f03eaee chore: accomodate changes to API 2023-03-27 11:57:24 +02:00
Szilárd Dóró
867c807699 chore: add changeset 2023-03-27 11:21:42 +02:00
renovate[bot]
d0673d7825 chore(deps): update dependency @types/react to v18.0.29 2023-03-27 07:50:19 +00:00
Dipak Parmar
106f23dcfa fixdashboard-settings): remove extra whitespace azuread provider import in settings
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 00:48:56 -07:00
Dipak Parmar
83ef755822 feat(dashboard-settings): update azuread provider settings component
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 00:47:09 -07:00
Dipak Parmar
b7703ffd70 feat(graphql): add azuread to signinmethods query
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 00:46:30 -07:00
Dipak Parmar
340ea5b115 chore: merge branch 'main' into feat/dashboard-azuread-settings
* main: (1322 commits)
  chore(ci): adjust preview fetcher
  chore: add changeset
  fix: fetch valid previews only
  fix: use correct Vercel token
  fix: use staging project ID
  chore: use dynamic test URL
  fix(deps): update docusaurus monorepo to v2.4.0
  chore(hasura-storage-js): improve presignedUrl test
  fix: remove test.only call
  chore: add tests for table deletion
  chore: update versions
  fix: potential subscription fix
  Fix import in docs
  fix: remove `test.only` call
  chore: add remaining table creation tests
  chore: add foreign key constraint test
  chore: add extra database UI tests
  chore: restructure tests, add basic table creation test
  chore: update versions
  chore: add changeset
  ...

Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-26 19:16:40 -07:00
Matt Wollerman
776eca3fb5 Update serverless-functions to link to event trigger docs 2023-03-23 09:06:20 -04:00
Dipak Parmar
ce4b655c55 fix: correct typos 2022-11-22 19:47:21 -08:00
Dipak Parmar
dc57d31ec9 fix: correct extra space in azureadprovidersettings dir 2022-11-22 19:45:38 -08:00
Dipak Parmar
ea29fd6b73 feat(dashboard-settings): add azuread provider to settings 2022-11-21 20:30:53 -08:00
Dipak Parmar
d8e4073957 feat(dashboard-settings): add azuread provider settings component 2022-11-21 20:29:34 -08:00
Dipak Parmar
3f399a54a3 feat(graphql): add azuread to signinmethods query 2022-11-21 20:28:50 -08:00
141 changed files with 2831 additions and 964 deletions

View File

@@ -169,7 +169,7 @@ jobs:
EXPRESSION='s/"'$IMAGE':[0-9]\+\.[0-9]\+\.[0-9]\+"/"'$IMAGE':'$VERSION'"/g' EXPRESSION='s/"'$IMAGE':[0-9]\+\.[0-9]\+\.[0-9]\+"/"'$IMAGE':'$VERSION'"/g'
find ./ -type f -exec sed -i -e $EXPRESSION {} \; find ./ -type f -exec sed -i -e $EXPRESSION {} \;
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
token: ${{ secrets.GH_PAT }} token: ${{ secrets.GH_PAT }}
commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}' commit-message: 'chore: bump nhost/dashboard to ${{ needs.version.outputs.dashboardVersion }}'

View File

@@ -24,6 +24,7 @@ env:
NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }} NHOST_TEST_PROJECT_NAME: ${{ vars.NHOST_TEST_PROJECT_NAME }}
NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }} NHOST_TEST_USER_EMAIL: ${{ secrets.NHOST_TEST_USER_EMAIL }}
NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }} NHOST_TEST_USER_PASSWORD: ${{ secrets.NHOST_TEST_USER_PASSWORD }}
NHOST_TEST_PROJECT_ADMIN_SECRET: ${{ secrets.NHOST_TEST_PROJECT_ADMIN_SECRET }}
jobs: jobs:
build: build:

View File

@@ -8,7 +8,11 @@ module.exports = {
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
project: './tsconfig.json', project: './tsconfig.json',
}, },
ignorePatterns: ['**/.eslintrc.js', '**/prettier.config.js'], ignorePatterns: [
'**/.eslintrc.js',
'**/prettier.config.js',
'**/next.config.js',
],
rules: { rules: {
'react/react-in-jsx-scope': 'off', 'react/react-in-jsx-scope': 'off',
'react/jsx-props-no-spreading': 'off', 'react/jsx-props-no-spreading': 'off',
@@ -21,6 +25,7 @@ module.exports = {
'error', 'error',
{ allowArrowFunctions: true, allowFunctions: true }, { allowArrowFunctions: true, allowFunctions: true },
], ],
'import/prefer-default-export': 'off',
'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
curly: ['error', 'all'], curly: ['error', 'all'],
'no-restricted-exports': 'off', 'no-restricted-exports': 'off',

View File

@@ -53,4 +53,5 @@ tailwind.json
/test-results/ /test-results/
/playwright-report/ /playwright-report/
/playwright/.cache/ /playwright/.cache/
storageState.json storageState.json
e2e/.auth/*

View File

@@ -1,5 +1,55 @@
# @nhost/dashboard # @nhost/dashboard
## 0.14.5
### Patch Changes
- ba0d57ee: fix(i18n): revert i18n library
- 3328ed05: feat(projects): improve overview when there is an error
## 0.14.4
### Patch Changes
- 5e0920ba: chore(deps): bump `next-seo` to v6
- 706c9dc3: chore(deps): bump `@types/react` to 18.0.33
- 99f8f6b3: feat(metrics): show metrics on the overview
## 0.14.3
### Patch Changes
- @nhost/react-apollo@5.0.16
## 0.14.2
### Patch Changes
- 3cb67300: fix(logs): don't break UI when clearing time picker
- 7453bf3b: feat(projects): show project creator info
- c166dad0: chore(tests): improve auth page tests
- 6a290bb2: chore(deps): bump `@types/react` to 18.0.32
## 0.14.1
### Patch Changes
- @nhost/react-apollo@5.0.15
- @nhost/nextjs@1.13.19
## 0.14.0
### Minor Changes
- 6e1f03ea: feat(dashboard): add support for the Azure AD provider
### Patch Changes
- 1bd2c373: chore(deps): bump `turbo` to 1.8.6
- d329b621: chore(deps): bump `@types/react` to 18.0.30
- cb248f0d: fix(tests): avoid name collision in database tests
- 867c8076: chore(deps): bump `@types/react` to 18.0.29
## 0.13.10 ## 0.13.10
### Patch Changes ### Patch Changes

View File

@@ -3,7 +3,7 @@ RUN apk add --no-cache libc6-compat
RUN apk update RUN apk update
WORKDIR /app WORKDIR /app
RUN yarn global add turbo@1.8.3 RUN yarn global add turbo@1.8.6
COPY . . COPY . .
RUN turbo prune --scope="@nhost/dashboard" --docker RUN turbo prune --scope="@nhost/dashboard" --docker

View File

@@ -64,16 +64,15 @@ pnpm storybook
### Environment Variables for Local Development and Self-Hosting ### Environment Variables for Local Development and Self-Hosting
| Name | Description | | Name | Description |
| ---- | ----------- | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `NEXT_PUBLIC_NHOST_AUTH_URL` | The URL of the Auth service. When working locally, point it to the Auth service started by the CLI. When self-hosting, point it to the self-hosted Auth service. |
| `NEXT_PUBLIC_NHOST_AUTH_URL` | The URL of the Auth service. When working locally, point it to the Auth service started by the CLI. When self-hosting, point it to the self-hosted Auth service. | | `NEXT_PUBLIC_NHOST_FUNCTIONS_URL` | The URL of the Functions service. When working locally, point it to the Functions service started by the CLI. When self-hosting, point it to the self-hosted Functions service. |
| `NEXT_PUBLIC_NHOST_FUNCTIONS_URL` | The URL of the Functions service. When working locally, point it to the Functions service started by the CLI. When self-hosting, point it to the self-hosted Functions service. | | `NEXT_PUBLIC_NHOST_GRAPHQL_URL` | The URL of the GraphQL service. When working locally, point it to the GraphQL service started by the CLI. When self-hosting, point it to the self-hosted GraphQL service. |
| `NEXT_PUBLIC_NHOST_GRAPHQL_URL` | The URL of the GraphQL service. When working locally, point it to the GraphQL service started by the CLI. When self-hosting, point it to the self-hosted GraphQL service. | | `NEXT_PUBLIC_NHOST_STORAGE_URL` | The URL of the Storage service. When working locally, point it to the Storage service started by the CLI. When self-hosting, point it to the self-hosted Storage service. |
| `NEXT_PUBLIC_NHOST_STORAGE_URL` | The URL of the Storage service. When working locally, point it to the Storage service started by the CLI. When self-hosting, point it to the self-hosted Storage service. | | `NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL` | The URL of the Hasura Console. When working locally, point it to the Hasura Console started by the CLI. When self-hosting, point it to the self-hosted Hasura Console. |
| `NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL` | The URL of the Hasura Console. When working locally, point it to the Hasura Console started by the CLI. When self-hosting, point it to the self-hosted Hasura Console. | | `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. |
| `NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL` | The URL of Hasura's Migrations service. When working locally, point it to the Migrations service started by the CLI. | | `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. |
| `NEXT_PUBLIC_NHOST_HASURA_API_URL` | The URL of Hasura's Schema and Metadata API. When working locally, point it to the Schema and Metadata API started by the CLI. When self-hosting, point it to the self-hosted Schema and Metadata API. |
### Other Environment Variables ### Other Environment Variables
@@ -128,4 +127,5 @@ NHOST_TEST_USER_EMAIL=<test_user_email>
NHOST_TEST_USER_PASSWORD=<test_user_password> NHOST_TEST_USER_PASSWORD=<test_user_password>
NHOST_TEST_WORKSPACE_NAME=<test_workspace_name> NHOST_TEST_WORKSPACE_NAME=<test_workspace_name>
NHOST_TEST_PROJECT_NAME=<test_project_name> NHOST_TEST_PROJECT_NAME=<test_project_name>
NHOST_TEST_PROJECT_ADMIN_SECRET=<test_project_admin_secret>
``` ```

View File

@@ -0,0 +1,50 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
import { faker } from '@faker-js/faker';
import test, { expect } from '@playwright/test';
test('should be able to ban and unban a user', async ({ page }) => {
await page.goto('/');
await openProject({
page,
projectName: TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
const email = generateTestEmail();
const password = faker.internet.password();
await createUser({ page, email, password });
await page
.getByRole('button', { name: `View ${email}`, exact: true })
.click();
await page.getByRole('button', { name: /actions/i }).click();
await page.getByRole('menuitem', { name: /ban user/i }).click();
await expect(
page.getByText(/user has been banned successfully./i),
).toBeVisible();
await expect(page.locator('form').getByText(/^banned$/i)).toBeVisible();
await page.getByRole('button', { name: /actions/i }).click();
await page.getByRole('menuitem', { name: /unban user/i }).click();
await expect(
page.getByText(/user has been unbanned successfully./i),
).toBeVisible();
await expect(page.locator('form').getByText(/^banned$/i)).not.toBeVisible();
});

View File

@@ -0,0 +1,65 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import test, { expect } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await openProject({
page,
projectName: TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
});
test.afterAll(async () => {
await page.close();
});
test('should create a user', async () => {
const email = generateTestEmail();
const password = faker.internet.password();
await createUser({ page, email, password });
await expect(
page.getByRole('button', { name: `View ${email}`, exact: true }),
).toBeVisible();
});
test('should not be able to create a user with an existing email', async () => {
const email = generateTestEmail();
const password = faker.internet.password();
await createUser({ page, email, password });
await expect(
page.getByRole('button', { name: `View ${email}`, exact: true }),
).toBeVisible();
await createUser({ page, email, password });
await expect(
page.getByRole('dialog').getByText(/email already in use/i),
).toBeVisible();
});

View File

@@ -0,0 +1,96 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import test, { expect } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await openProject({
page,
projectName: TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
});
test.afterAll(async () => {
await page.close();
});
test('should be able to delete a user', async () => {
const email = generateTestEmail();
const password = faker.internet.password();
await createUser({ page, email, password });
await expect(
page.getByRole('button', { name: `View ${email}`, exact: true }),
).toBeVisible();
await page
.getByRole('button', { name: `More options for ${email}`, exact: true })
.click();
await page.getByRole('menuitem', { name: /delete user/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(
page.getByRole('heading', { name: /delete user/i }),
).toBeVisible();
await expect(
page.getByText(`Are you sure you want to delete the "${email}" user?`),
).toBeVisible();
await page.getByRole('button', { name: /delete/i, exact: true }).click();
await expect(page.getByRole('dialog')).not.toBeVisible();
await expect(
page.getByRole('button', { name: `View ${email}`, exact: true }),
).not.toBeVisible();
});
test('should be able to delete a user from the details page', async () => {
const email = generateTestEmail();
const password = faker.internet.password();
await createUser({ page, email, password });
await page
.getByRole('button', { name: `View ${email}`, exact: true })
.click();
await page.getByRole('button', { name: /actions/i }).click();
await page.getByRole('menuitem', { name: /delete user/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(
page.getByRole('heading', { name: /delete user/i }),
).toBeVisible();
await expect(
page.getByText(`Are you sure you want to delete the "${email}" user?`),
).toBeVisible();
await page.getByRole('button', { name: /delete/i, exact: true }).click();
await expect(
page.getByRole('button', { name: `View ${email}`, exact: true }),
).not.toBeVisible();
});

View File

@@ -1,93 +0,0 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { openProject } from '@/e2e/utils';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page;
test.describe.configure({ mode: 'serial' });
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
await page.goto('/');
await openProject({
page,
projectName: TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
});
test.afterAll(async () => {
await page.close();
});
test('should create a user', async () => {
await expect(
page.getByRole('heading', { name: /there are no users yet/i }),
).toBeVisible();
await page
.getByRole('button', { name: /create user/i })
.first()
.click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(
page.getByRole('heading', { name: /create user/i }),
).toBeVisible();
await page
.getByRole('textbox', { name: /email/i })
.fill('testuser@example.com');
await page.getByRole('textbox', { name: /password/i }).fill('test.password');
await page.getByRole('button', { name: /create/i, exact: true }).click();
await expect(page.getByRole('dialog')).not.toBeVisible();
await expect(
page.getByRole('button', { name: /view testuser@example.com/i }),
).toBeVisible();
});
test('should delete a user', async () => {
await expect(
page.getByRole('button', { name: /view testuser@example.com/i }),
).toBeVisible();
await page
.getByRole('button', { name: /more options for testuser@example.com/i })
.click();
await page.getByRole('menuitem', { name: /delete user/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
await expect(
page.getByRole('heading', { name: /delete user/i }),
).toBeVisible();
await expect(
page.getByText(
/are you sure you want to delete the "testuser@example.com" user?/i,
),
).toBeVisible();
await page.getByRole('button', { name: /delete/i, exact: true }).click();
await expect(page.getByRole('dialog')).not.toBeVisible();
await expect(
page.getByRole('heading', { name: /there are no users yet/i }),
).toBeVisible();
});

View File

@@ -0,0 +1,103 @@
import {
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { createUser, generateTestEmail, openProject } from '@/e2e/utils';
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/');
await openProject({
page,
projectName: TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
});
await page
.getByRole('navigation', { name: /main navigation/i })
.getByRole('link', { name: /auth/i })
.click();
await page.waitForURL(`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/users`);
});
test.afterAll(async () => {
await page.close();
});
test('should be able to verify the email of a user', async () => {
const email = generateTestEmail();
const password = faker.internet.password();
await createUser({ page, email, password });
await page
.getByRole('button', { name: `View ${email}`, exact: true })
.click();
await expect(
page.getByRole('checkbox', { name: /email verified/i }),
).not.toBeChecked();
await page.getByRole('checkbox', { name: /email verified/i }).check();
await page.getByRole('button', { name: /save/i }).click();
await expect(
page.getByText(/user settings have been updated successfully./i),
).toBeVisible();
await page
.getByRole('button', { name: `View ${email}`, exact: true })
.click();
await expect(
page.getByRole('checkbox', { name: /email verified/i }),
).toBeChecked();
});
test('should be able to verify the phone number of a user', async () => {
const email = generateTestEmail();
const password = faker.internet.password();
const phoneNumber = faker.phone.number();
await createUser({ page, email, password });
await page
.getByRole('button', { name: `View ${email}`, exact: true })
.click();
await expect(
page.getByRole('checkbox', { name: /phone number verified/i }),
).toBeDisabled();
await page.getByRole('textbox', { name: /phone number/i }).fill(phoneNumber);
await page.getByRole('checkbox', { name: /phone number verified/i }).check();
await page.getByRole('button', { name: /save/i }).click();
await expect(
page.getByText(/user settings have been updated successfully./i),
).toBeVisible();
await page
.getByRole('button', { name: `View ${email}`, exact: true })
.click();
await expect(
page.getByRole('textbox', { name: /phone number/i }),
).toHaveValue(phoneNumber);
await expect(
page.getByRole('checkbox', { name: /phone number verified/i }),
).toBeChecked();
});

View File

@@ -7,11 +7,15 @@ import { openProject, prepareTable } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { snakeCase } from 'snake-case';
let page: Page; let page: Page;
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }) => {
page = await browser.newPage(); page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/'); await page.goto('/');
await openProject({ await openProject({
@@ -35,7 +39,7 @@ test('should create a simple table', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const tableName = faker.random.word().toLowerCase(); const tableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -63,7 +67,7 @@ test('should create a table with unique constraints', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const tableName = faker.random.word().toLowerCase(); const tableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -92,7 +96,7 @@ test('should create a table with nullable columns', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const tableName = faker.random.word().toLowerCase(); const tableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -121,7 +125,7 @@ test('should create a table with an identity column', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const tableName = faker.random.word().toLowerCase(); const tableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -153,7 +157,7 @@ test('should create table with foreign key constraint', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const firstTableName = faker.random.word().toLowerCase(); const firstTableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -175,7 +179,7 @@ test('should create table with foreign key constraint', async () => {
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const secondTableName = faker.random.word().toLowerCase(); const secondTableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -234,7 +238,7 @@ test('should not be able to create a table with a name that already exists', asy
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const tableName = faker.random.word().toLowerCase(); const tableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,

View File

@@ -3,15 +3,19 @@ import {
TEST_PROJECT_SLUG, TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG, TEST_WORKSPACE_SLUG,
} from '@/e2e/env'; } from '@/e2e/env';
import { openProject, prepareTable } from '@/e2e/utils'; import { deleteTable, openProject, prepareTable } from '@/e2e/utils';
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { snakeCase } from 'snake-case';
let page: Page; let page: Page;
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }) => {
page = await browser.newPage(); page = await browser.newPage();
});
test.beforeEach(async () => {
await page.goto('/'); await page.goto('/');
await openProject({ await openProject({
@@ -32,7 +36,7 @@ test.afterAll(async () => {
}); });
test('should delete a table', async () => { test('should delete a table', async () => {
const tableName = faker.random.word().toLowerCase(); const tableName = snakeCase(faker.lorem.words(3));
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
@@ -52,26 +56,11 @@ test('should delete a table', async () => {
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`, `/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/${tableName}`,
); );
const tableLink = page.getByRole('link', { await deleteTable({
page,
name: tableName, name: tableName,
exact: true,
}); });
await tableLink.hover();
await page
.getByRole('listitem')
.filter({ hasText: tableName })
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: /delete table/i }).click();
await expect(
page.getByRole('heading', { name: /delete table/i }),
).toBeVisible();
await page.getByRole('button', { name: /delete/i }).click();
// navigate to next URL // navigate to next URL
await page.waitForURL( await page.waitForURL(
`/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/**`, `/${TEST_WORKSPACE_SLUG}/${TEST_PROJECT_SLUG}/database/browser/default/public/**`,
@@ -86,7 +75,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const firstTableName = faker.random.word().toLowerCase(); const firstTableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -108,7 +97,7 @@ test('should not be able to delete a table if other tables have foreign keys ref
await page.getByRole('button', { name: /new table/i }).click(); await page.getByRole('button', { name: /new table/i }).click();
await expect(page.getByText(/create a new table/i)).toBeVisible(); await expect(page.getByText(/create a new table/i)).toBeVisible();
const secondTableName = faker.random.word().toLowerCase(); const secondTableName = snakeCase(faker.lorem.words(3));
await prepareTable({ await prepareTable({
page, page,
@@ -163,26 +152,11 @@ test('should not be able to delete a table if other tables have foreign keys ref
).toBeVisible(); ).toBeVisible();
// try to delete the first table that is referenced by the second table // try to delete the first table that is referenced by the second table
const tableLink = page.getByRole('link', { await deleteTable({
page,
name: firstTableName, name: firstTableName,
exact: true,
}); });
await tableLink.hover();
await page
.getByRole('listitem')
.filter({ hasText: firstTableName })
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: /delete table/i }).click();
await expect(
page.getByRole('heading', { name: /delete table/i }),
).toBeVisible();
await page.getByRole('button', { name: /delete/i }).click();
await expect( await expect(
page.getByText( page.getByText(
/constraint [a-zA-Z_]+ on table [a-zA-Z_]+ depends on table [a-zA-Z_]+/i, /constraint [a-zA-Z_]+ on table [a-zA-Z_]+ depends on table [a-zA-Z_]+/i,

View File

@@ -31,6 +31,12 @@ export const TEST_PROJECT_SLUG = slugify(TEST_PROJECT_NAME, {
strict: true, strict: true,
}); });
/**
* Hasura admin secret of the test project to use.
*/
export const TEST_PROJECT_ADMIN_SECRET =
process.env.NHOST_TEST_PROJECT_ADMIN_SECRET;
/** /**
* Email of the test account to use. * Email of the test account to use.
*/ */

View File

@@ -72,7 +72,7 @@ test("should show the project's name, the Upgrade button and the Settings button
await expect( await expect(
page.getByRole('heading', { name: TEST_PROJECT_NAME }), page.getByRole('heading', { name: TEST_PROJECT_NAME }),
).toBeVisible(); ).toBeVisible();
await expect(page.getByText(/free plan/i)).toBeVisible(); await expect(page.getByText(/starter/i)).toBeVisible();
await expect(page.getByRole('button', { name: /upgrade/i })).toBeVisible(); await expect(page.getByRole('button', { name: /upgrade/i })).toBeVisible();
await expect( await expect(
page.getByRole('main').getByRole('link', { name: /settings/i }), page.getByRole('main').getByRole('link', { name: /settings/i }),
@@ -94,16 +94,26 @@ test('should not have a GitHub repository connected', async () => {
).toBeVisible(); ).toBeVisible();
}); });
test('should show proper limits for the free project', async () => { test('should show metrics', async () => {
// Limit for Database await expect(page.getByText(/cpu usage seconds\d+/i)).toBeVisible();
await expect(page.getByText(/of 500 MB/i)).toBeVisible(); await expect(page.getByText(/total requests\d+/i)).toBeVisible();
await expect(page.getByText(/function invocations\d+/i)).toBeVisible();
// Limit for Storage await expect(
await expect(page.getByText(/of 1 GB/i)).toBeVisible(); page.getByText(/egress volume\d+(\.\d+)? [a-zA-Z]+/i),
).toBeVisible();
// Limit for Users await expect(page.getByText(/logs\d+(\.\d+)? [a-zA-Z]+/i)).toBeVisible();
await expect(page.getByText(/of 10000/i)).toBeVisible(); });
// Limit for Functions test('should show proper limits for the free project', async () => {
await expect(page.getByText(/of 10$/i, { exact: true })).toBeVisible(); await expect(
page.getByText(/database\d+(\.\d+)? [a-zA-Z]+ of \d+(\.\d+)? [a-zA-Z]+/i),
).toBeVisible();
await expect(
page.getByText(/storage\d+(\.\d+)? [a-zA-Z]+ of \d+(\.\d+)? [a-zA-Z]+/i),
).toBeVisible();
await expect(page.getByText(/users[0-9]+ of [0-9]+/i)).toBeVisible();
await expect(page.getByText(/functions[0-9]+ of [0-9]+/i)).toBeVisible();
}); });

View File

@@ -0,0 +1,20 @@
import {
TEST_DASHBOARD_URL,
TEST_USER_EMAIL,
TEST_USER_PASSWORD,
} from '@/e2e/env';
import { test as setup } from '@playwright/test';
setup('authenticate user', async ({ page }) => {
await page.goto('/');
await page.waitForURL('/signin');
await page.getByRole('link', { name: /continue with email/i }).click();
await page.waitForURL('/signin/email');
await page.getByLabel('Email').fill(TEST_USER_EMAIL);
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL(TEST_DASHBOARD_URL);
await page.context().storageState({ path: 'e2e/.auth/user.json' });
});

View File

@@ -1,3 +1,4 @@
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
/** /**
@@ -66,18 +67,25 @@ export async function prepareTable({
// set type // set type
await page await page
.getByRole('table')
.getByRole('combobox', { name: /type/i }) .getByRole('combobox', { name: /type/i })
.nth(index) .nth(index)
.fill(type); .type(type);
await page.getByRole('option', { name: type }).first().click(); await page
.getByRole('table')
.getByRole('option', { name: type })
.first()
.click();
// optionally set default value // optionally set default value
if (defaultValue) { if (defaultValue) {
await page await page
.getByRole('table')
.getByRole('combobox', { name: /default value/i }) .getByRole('combobox', { name: /default value/i })
.first() .nth(index)
.fill(defaultValue); .type(defaultValue);
await page await page
.getByRole('table')
.getByRole('option', { name: defaultValue }) .getByRole('option', { name: defaultValue })
.first() .first()
.click(); .click();
@@ -111,3 +119,71 @@ export async function prepareTable({
await page.getByRole('button', { name: /primary key/i }).click(); await page.getByRole('button', { name: /primary key/i }).click();
await page.getByRole('option', { name: primaryKey, exact: true }).click(); await page.getByRole('option', { name: primaryKey, exact: true }).click();
} }
/**
* Deletes a table with the given name.
*
* @param page - The Playwright page object.
* @param name - The name of the table to delete.
* @returns A promise that resolves when the table is deleted.
*/
export async function deleteTable({
page,
name,
}: {
page: Page;
name: string;
}) {
const tableLink = page.getByRole('link', {
name,
exact: true,
});
await tableLink.hover();
await page
.getByRole('listitem')
.filter({ hasText: name })
.getByRole('button')
.click();
await page.getByRole('menuitem', { name: /delete table/i }).click();
await page.getByRole('button', { name: /delete/i }).click();
}
/**
* Creates a new user.
*
* @param page - The Playwright page object.
* @param email - The email of the user to create.
* @param password - The password of the user to create.
* @returns A promise that resolves when the user is created.
*/
export async function createUser({
page,
email,
password,
}: {
page: Page;
email: string;
password: string;
}) {
await page
.getByRole('button', { name: /create user/i })
.first()
.click();
await page.getByRole('textbox', { name: /email/i }).fill(email);
await page.getByRole('textbox', { name: /password/i }).fill(password);
await page.getByRole('button', { name: /create/i, exact: true }).click();
}
/**
* Generates a test email address with the given prefix (if provided).
*
* @param prefix - The prefix to use for the email address. (Default: `Nhost_Test_`)
*/
export function generateTestEmail(prefix: string = 'Nhost_Test_') {
const email = faker.internet.email();
return [prefix, email].join('');
}

View File

@@ -1,27 +0,0 @@
import { chromium } from '@playwright/test';
import {
TEST_DASHBOARD_URL,
TEST_USER_EMAIL,
TEST_USER_PASSWORD,
} from './e2e/env';
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(TEST_DASHBOARD_URL);
await page.waitForURL(`${TEST_DASHBOARD_URL}/signin`);
await page.getByRole('link', { name: /continue with email/i }).click();
await page.waitForURL(`${TEST_DASHBOARD_URL}/signin/email`);
await page.getByLabel('Email').fill(TEST_USER_EMAIL);
await page.getByLabel('Password').fill(TEST_USER_PASSWORD);
await page.getByRole('button', { name: /sign in/i }).click();
await page.waitForURL(TEST_DASHBOARD_URL);
await page.context().storageState({ path: 'storageState.json' });
await browser.close();
}
export default globalSetup;

View File

@@ -0,0 +1,66 @@
import {
TEST_DASHBOARD_URL,
TEST_PROJECT_ADMIN_SECRET,
TEST_PROJECT_NAME,
TEST_PROJECT_SLUG,
TEST_WORKSPACE_SLUG,
} from '@/e2e/env';
import { openProject } from '@/e2e/utils';
import { chromium } from '@playwright/test';
async function globalTeardown() {
const browser = await chromium.launch();
const context = await browser.newContext({
baseURL: TEST_DASHBOARD_URL,
storageState: 'e2e/.auth/user.json',
});
const page = await context.newPage();
await page.goto('/');
await openProject({
page,
projectName: TEST_PROJECT_NAME,
workspaceSlug: TEST_WORKSPACE_SLUG,
projectSlug: TEST_PROJECT_SLUG,
});
const pagePromise = context.waitForEvent('page');
await page.getByRole('link', { name: /hasura/i }).click();
await page.getByRole('link', { name: /open hasura/i }).click();
const hasuraPage = await pagePromise;
await hasuraPage.waitForLoadState();
const adminSecretInput = hasuraPage.getByPlaceholder(/enter admin-secret/i);
// note: a more ideal way would be to paste from clipboard, but Playwright
// doesn't support that yet
await adminSecretInput.fill(TEST_PROJECT_ADMIN_SECRET);
await adminSecretInput.press('Enter');
// note: getByRole doesn't work here
await hasuraPage.locator('a', { hasText: /data/i }).click();
await hasuraPage.getByRole('link', { name: /sql/i }).click();
await hasuraPage.getByRole('textbox').fill(`
DO $$ DECLARE
tablename text;
BEGIN
FOR tablename IN
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
LOOP
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(tablename) || ' CASCADE';
END LOOP;
END $$;
`);
await hasuraPage.getByRole('button', { name: /run!/i }).click();
await hasuraPage.getByText(/sql executed!/i).waitFor();
}
export default globalTeardown;

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/dashboard", "name": "@nhost/dashboard",
"version": "0.13.10", "version": "0.14.5",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -57,10 +57,9 @@
"just-kebab-case": "^4.1.1", "just-kebab-case": "^4.1.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"next": "^12.3.1", "next": "^12.3.1",
"next-seo": "^5.14.1", "next-seo": "^6.0.0",
"node-pg-format": "^1.3.5", "node-pg-format": "^1.3.5",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"prettysize": "^2.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-error-boundary": "^4.0.0", "react-error-boundary": "^4.0.0",
@@ -106,7 +105,7 @@
"@types/lodash.debounce": "^4.0.7", "@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7", "@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29", "@types/pluralize": "^0.0.29",
"@types/react": "18.0.28", "@types/react": "18.0.33",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
"@types/react-table": "^7.7.12", "@types/react-table": "^7.7.12",
"@types/testing-library__jest-dom": "^5.14.5", "@types/testing-library__jest-dom": "^5.14.5",
@@ -141,6 +140,7 @@
"prettier-plugin-tailwindcss": "^0.2.0", "prettier-plugin-tailwindcss": "^0.2.0",
"react-date-fns-hooks": "^0.9.4", "react-date-fns-hooks": "^0.9.4",
"require-from-string": "^2.0.2", "require-from-string": "^2.0.2",
"snake-case": "^3.0.4",
"storybook-addon-next-router": "^4.0.1", "storybook-addon-next-router": "^4.0.1",
"tailwindcss": "^3.1.2", "tailwindcss": "^3.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",

View File

@@ -1,5 +1,4 @@
import { defineConfig, devices } from '@playwright/test'; import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
@@ -16,17 +15,24 @@ export default defineConfig({
retries: process.env.CI ? 2 : 0, retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
reporter: 'html', reporter: 'html',
globalSetup: require.resolve('./global-setup'), globalTeardown: require.resolve('./global-teardown'),
use: { use: {
actionTimeout: 0, actionTimeout: 0,
trace: 'on-first-retry', trace: 'on-first-retry',
storageState: 'storageState.json',
baseURL: process.env.NHOST_TEST_DASHBOARD_URL, baseURL: process.env.NHOST_TEST_DASHBOARD_URL,
}, },
projects: [ projects: [
{
name: 'setup',
testMatch: ['**/setup/*.setup.ts'],
},
{ {
name: 'chromium', name: 'chromium',
use: { ...devices['Desktop Chrome'] }, use: {
...devices['Desktop Chrome'],
storageState: 'e2e/.auth/user.json',
},
dependencies: ['setup'],
}, },
], ],
}); });

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -10,24 +10,23 @@ import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown'; import { Dropdown } from '@/ui/v2/Dropdown';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
import { getApplicationStatusString } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache';
import { import {
useDeleteApplicationMutation, useDeleteApplicationMutation,
useGetApplicationStateQuery, useGetApplicationStateQuery,
useInsertApplicationMutation, useInsertApplicationMutation,
useUpdateApplicationMutation, useUpdateApplicationMutation,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
import { getApplicationStatusString } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache';
import { useApolloClient } from '@apollo/client'; import { useApolloClient } from '@apollo/client';
import { useUserData } from '@nhost/nextjs'; import { useUserData } from '@nhost/nextjs';
import Image from 'next/image'; import Image from 'next/image';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import ApplicationInfo from './ApplicationInfo'; import ApplicationInfo from './ApplicationInfo';
import ApplicationLive from './ApplicationLive'; import ApplicationLive from './ApplicationLive';
import ApplicationUnknown from './ApplicationUnknown';
import { RemoveApplicationModal } from './RemoveApplicationModal'; import { RemoveApplicationModal } from './RemoveApplicationModal';
import { StagingMetadata } from './StagingMetadata'; import { StagingMetadata } from './StagingMetadata';
@@ -47,9 +46,9 @@ export default function ApplicationErrored() {
variables: { appId: currentApplication.id }, variables: { appId: currentApplication.id },
}); });
const [previousState, setPreviousState] = useState<ApplicationStatus | null>( const previousState = data?.app?.appStates
null, ? getPreviousApplicationState(data.app.appStates)
); : null;
const [showRecreateModal, setShowRecreateModal] = useState(false); const [showRecreateModal, setShowRecreateModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
@@ -140,20 +139,6 @@ export default function ApplicationErrored() {
await recreateApplication(); await recreateApplication();
} }
useEffect(() => {
if (loading) {
return;
}
if (error) {
return;
}
const previousAcceptedState = getPreviousApplicationState(
data.app.appStates,
);
setPreviousState(previousAcceptedState);
}, [setPreviousState, data, loading, error]);
if (loading || previousState === null) { if (loading || previousState === null) {
return ( return (
<Container className="mx-auto mt-12 max-w-sm text-center"> <Container className="mx-auto mt-12 max-w-sm text-center">
@@ -170,19 +155,13 @@ export default function ApplicationErrored() {
return null; return null;
} }
if (previousState === ApplicationStatus.Live) { if (
return <ApplicationLive />; previousState === ApplicationStatus.Updating ||
} previousState === ApplicationStatus.Empty
) {
// For now, if the application errored and the previous state to this error is an UPDATING state, we want to show the dashboard, return (
// it's likely that most services are up and we shouldn't block all functionality. In the future, we're going to have a way to <ApplicationLive errorMessage="Error deploying the project most likely due to invalid configuration. Please review your project's configuration and logs for more information." />
// redeploy the app again, and get to a healthy state. @GC );
if (previousState === ApplicationStatus.Updating) {
return <ApplicationLive />;
}
if (previousState === ApplicationStatus.Empty) {
return <ApplicationUnknown />;
} }
return ( return (

View File

@@ -1,20 +1,31 @@
import MaintenanceAlert from '@/components/common/MaintenanceAlert'; import MaintenanceAlert from '@/components/common/MaintenanceAlert';
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary'; import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import Container from '@/components/layout/Container'; import Container from '@/components/layout/Container';
import { features } from '@/components/overview/features';
import { frameworks } from '@/components/overview/frameworks';
import OverviewDeployments from '@/components/overview/OverviewDeployments'; import OverviewDeployments from '@/components/overview/OverviewDeployments';
import OverviewDocumentation from '@/components/overview/OverviewDocumentation'; import OverviewDocumentation from '@/components/overview/OverviewDocumentation';
import OverviewMetrics from '@/components/overview/OverviewMetrics/OverviewMetrics';
import OverviewMigration from '@/components/overview/OverviewMigration'; import OverviewMigration from '@/components/overview/OverviewMigration';
import OverviewProjectInfo from '@/components/overview/OverviewProjectInfo'; import OverviewProjectInfo from '@/components/overview/OverviewProjectInfo';
import OverviewRepository from '@/components/overview/OverviewRepository'; import OverviewRepository from '@/components/overview/OverviewRepository';
import OverviewTopBar from '@/components/overview/OverviewTopBar'; import OverviewTopBar from '@/components/overview/OverviewTopBar';
import OverviewUsage from '@/components/overview/OverviewUsage'; import OverviewUsage from '@/components/overview/OverviewUsage';
import { features } from '@/components/overview/features';
import { frameworks } from '@/components/overview/frameworks';
import useIsPlatform from '@/hooks/common/useIsPlatform'; import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Alert } from '@/ui/Alert';
import Divider from '@/ui/v2/Divider'; import Divider from '@/ui/v2/Divider';
export default function ApplicationLive() { export interface ApplicationLiveProps {
/**
* Error message to display in the alert.
*/
errorMessage?: string;
}
export default function ApplicationLive({
errorMessage,
}: ApplicationLiveProps) {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const isProjectUsingRDS = currentApplication?.featureFlags.some( const isProjectUsingRDS = currentApplication?.featureFlags.some(
@@ -24,6 +35,8 @@ export default function ApplicationLive() {
if (!isPlatform) { if (!isPlatform) {
return ( return (
<Container> <Container>
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
<OverviewTopBar /> <OverviewTopBar />
<div className="grid grid-cols-1 gap-12 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-12 lg:grid-cols-3">
@@ -54,10 +67,17 @@ export default function ApplicationLive() {
return ( return (
<Container> <Container>
<MaintenanceAlert /> <MaintenanceAlert />
{errorMessage && <Alert severity="error">{errorMessage}</Alert>}
<OverviewTopBar /> <OverviewTopBar />
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
<div className="order-2 grid grid-flow-row gap-12 lg:order-1 lg:col-span-2"> <div className="grid grid-flow-row gap-12 lg:col-span-2">
<RetryableErrorBoundary>
<OverviewMetrics />
</RetryableErrorBoundary>
<RetryableErrorBoundary> <RetryableErrorBoundary>
<OverviewDeployments /> <OverviewDeployments />
</RetryableErrorBoundary> </RetryableErrorBoundary>
@@ -66,16 +86,18 @@ export default function ApplicationLive() {
title="Pick your favorite framework and start learning" title="Pick your favorite framework and start learning"
description="Nhost integrates smoothly with all of the frameworks you already know." description="Nhost integrates smoothly with all of the frameworks you already know."
cardElements={frameworks} cardElements={frameworks}
className="hidden lg:block"
/> />
<OverviewDocumentation <OverviewDocumentation
title="Platform Documentation" title="Platform Documentation"
description="More in-depth documentation for key features." description="More in-depth documentation for key features."
cardElements={features} cardElements={features}
className="hidden lg:block"
/> />
</div> </div>
<div className="order-1 grid grid-flow-row content-start gap-8 lg:order-2 lg:col-span-1 lg:gap-12"> <div className="grid grid-flow-row content-start gap-8 lg:col-span-1 lg:gap-12">
{isProjectUsingRDS && ( {isProjectUsingRDS && (
<> <>
<OverviewMigration /> <OverviewMigration />
@@ -88,6 +110,20 @@ export default function ApplicationLive() {
<Divider /> <Divider />
<OverviewUsage /> <OverviewUsage />
</div> </div>
<OverviewDocumentation
title="Pick your favorite framework and start learning"
description="Nhost integrates smoothly with all of the frameworks you already know."
cardElements={frameworks}
className="lg:hidden"
/>
<OverviewDocumentation
title="Platform Documentation"
description="More in-depth documentation for key features."
cardElements={features}
className="lg:hidden"
/>
</div> </div>
</Container> </Container>
); );

View File

@@ -9,6 +9,7 @@ import { useGetAllUserWorkspacesAndApplications } from '@/hooks/useGetAllUserWor
import { useNavigationVisible } from '@/hooks/useNavigationVisible'; import { useNavigationVisible } from '@/hooks/useNavigationVisible';
import useNotFoundRedirect from '@/hooks/useNotFoundRedirect'; import useNotFoundRedirect from '@/hooks/useNotFoundRedirect';
import { useSetAppWorkspaceContextFromUserContext } from '@/hooks/useSetAppWorkspaceContextFromUserContext'; import { useSetAppWorkspaceContextFromUserContext } from '@/hooks/useSetAppWorkspaceContextFromUserContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { BoxProps } from '@/ui/v2/Box'; import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import { NextSeo } from 'next-seo'; import { NextSeo } from 'next-seo';
@@ -30,9 +31,15 @@ function ProjectLayoutContent({
...mainContainerProps ...mainContainerProps
} = {}, } = {},
}: ProjectLayoutProps) { }: ProjectLayoutProps) {
const { currentApplication, currentWorkspace } = // TODO: This will be removed once we migrated every occurrence to
// useCurrentWorkspaceAndProject()
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication(); useCurrentWorkspaceAndApplication();
const { currentProject, loading, error } = useCurrentWorkspaceAndProject({
fetchPolicy: 'network-only',
});
const router = useRouter(); const router = useRouter();
const shouldDisplayNav = useNavigationVisible(); const shouldDisplayNav = useNavigationVisible();
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
@@ -63,10 +70,14 @@ function ProjectLayoutContent({
} }
}, [isPlatform, isRestrictedPath, router]); }, [isPlatform, isRestrictedPath, router]);
if (!currentWorkspace || !currentApplication || isRestrictedPath) { if (!currentWorkspace || !currentApplication || isRestrictedPath || loading) {
return <LoadingScreen />; return <LoadingScreen />;
} }
if (error) {
throw error;
}
if (!isPlatform) { if (!isPlatform) {
return ( return (
<> <>
@@ -104,7 +115,7 @@ function ProjectLayoutContent({
> >
{children} {children}
<NextSeo title={currentApplication.name} /> <NextSeo title={currentProject.name} />
</Box> </Box>
</> </>
); );

View File

@@ -4,6 +4,7 @@ import { useDropdown } from '@/ui/v2/Dropdown';
import type { InputProps } from '@/ui/v2/Input'; import type { InputProps } from '@/ui/v2/Input';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import { format, set } from 'date-fns'; import { format, set } from 'date-fns';
import type { ChangeEvent } from 'react';
export interface LogTimePickerProps extends InputProps { export interface LogTimePickerProps extends InputProps {
/** /**
@@ -22,21 +23,21 @@ function LogsTimePicker({
}: any) { }: any) {
const { handleClose } = useDropdown(); const { handleClose } = useDropdown();
const handleCancel = () => { function handleCancel() {
handleClose(); handleClose();
}; }
const handleApply = () => { function handleApply() {
onChange(selectedDate); onChange(selectedDate);
handleClose(); handleClose();
}; }
const handleTimePicking = (event) => { function handleChange(event: ChangeEvent<HTMLInputElement>) {
const [hours, minutes, seconds] = event.target.value.split(':'); const [hours, minutes, seconds] = event.target.value?.split(':') || [];
const hoursNumber = parseInt(hours, 10); const hoursNumber = parseInt(hours || '0', 10);
const minutesNumber = parseInt(minutes, 10); const minutesNumber = parseInt(minutes || '0', 10);
const secondsNumber = parseInt(seconds, 10); const secondsNumber = parseInt(seconds || '0', 10);
const newDate = set(new Date(selectedDate), { const newDate = set(new Date(selectedDate), {
hours: hoursNumber, hours: hoursNumber,
@@ -51,7 +52,7 @@ function LogsTimePicker({
} }
setSelectedDate(newDate); setSelectedDate(newDate);
}; }
return ( return (
<div className="mx-auto grid grid-flow-row items-center self-center"> <div className="mx-auto grid grid-flow-row items-center self-center">
@@ -64,7 +65,7 @@ function LogsTimePicker({
formControl: { className: 'grid grid-flow-col gap-x-3' }, formControl: { className: 'grid grid-flow-col gap-x-3' },
label: { sx: { fontSize: '14px' } }, label: { sx: { fontSize: '14px' } },
}} }}
onChange={handleTimePicking} onChange={handleChange}
type="time" type="time"
label="Select Time" label="Select Time"
sx={{ sx={{

View File

@@ -0,0 +1,58 @@
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import Text from '@/ui/v2/Text';
import Tooltip from '@/ui/v2/Tooltip';
import { InfoIcon } from '@/ui/v2/icons/InfoIcon';
import { twMerge } from 'tailwind-merge';
export interface MetricsCardProps extends BoxProps {
/**
* Label of the card.
*/
label?: string;
/**
* Value of the card.
*/
value?: string;
/**
* Tooltip of the card.
*/
tooltip?: string;
}
export default function MetricsCard({
label,
value,
tooltip,
className,
}: MetricsCardProps) {
return (
<Box
className={twMerge(
'grid grid-flow-row gap-2 rounded-md px-4 py-3',
className,
)}
sx={{ backgroundColor: 'grey.200' }}
>
<div className="grid grid-flow-col items-center justify-between gap-2">
{label && (
<Text className="truncate font-medium" color="secondary">
{label}
</Text>
)}
{tooltip && (
<Tooltip title={tooltip}>
<InfoIcon className="h-4 w-4" />
</Tooltip>
)}
</div>
{value && (
<Text variant="h2" component="p" className="truncate">
{value}
</Text>
)}
</Box>
);
}

View File

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

View File

@@ -35,7 +35,7 @@ export default function OverviewDocumentation({
<Text color="secondary">{description}</Text> <Text color="secondary">{description}</Text>
</div> </div>
<div className="mt-6 grid grid-flow-row items-center gap-6 xs:grid-cols-2 lg:grid-cols-4 lg:gap-4"> <div className="mt-6 grid grid-flow-row items-center gap-6 xs:grid-cols-2 lg:gap-4 xl:grid-cols-4">
{cardElements.map( {cardElements.map(
({ ({
title: cardTitle, title: cardTitle,

View File

@@ -0,0 +1,84 @@
import type { MetricsCardProps } from '@/components/overview/MetricsCard';
import { MetricsCard } from '@/components/overview/MetricsCard';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Text from '@/ui/v2/Text';
import { useGetProjectMetricsQuery } from '@/utils/__generated__/graphql';
import { prettifyNumber } from '@/utils/common/prettifyNumber';
import { prettifySize } from '@/utils/common/prettifySize';
import { twMerge } from 'tailwind-merge';
const now = new Date();
export default function OverviewMetrics() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetProjectMetricsQuery({
variables: {
appId: currentProject?.id,
subdomain: currentProject?.subdomain,
from: new Date(now.getFullYear(), now.getMonth(), 1),
},
skip: !currentProject?.id,
});
const cardElements: MetricsCardProps[] = [
{
label: 'CPU Usage Seconds',
tooltip: 'Total time the service has used the CPUs',
value: prettifyNumber(data?.cpuSecondsUsage?.value || 0),
},
{
label: 'Total Requests',
tooltip:
'Total amount of requests your services have received excluding functions',
value: prettifyNumber(data?.totalRequests?.value || 0, {
numberOfDecimals: data?.totalRequests?.value > 1000 ? 2 : 0,
}),
},
{
label: 'Function Invocations',
tooltip: 'Number of times your functions have been called',
value: prettifyNumber(data?.functionInvocations?.value || 0, {
numberOfDecimals: 0,
}),
},
{
label: 'Egress Volume',
tooltip: 'Amount of data your services have sent to users',
value: prettifySize(data?.egressVolume?.value || 0),
},
{
label: 'Logs',
tooltip: 'Amount of logs stored',
value: prettifySize(data?.logsVolume?.value || 0),
},
];
if (!data && error) {
throw error;
}
return (
<div className="grid grid-flow-row gap-4">
<div className="grid grid-cols-1 justify-start gap-4 xs:grid-cols-2 md:grid-cols-3">
{cardElements.map(({ label, value, tooltip, className, ...props }) => (
<MetricsCard
{...props}
key={label}
label={!loading ? label : null}
value={!loading ? value : null}
tooltip={!loading ? tooltip : null}
className={twMerge(
'min-h-[92px]',
loading && 'animate-pulse',
className,
)}
/>
))}
</div>
<Text color="disabled">
Your resource usage since the beginning of the month.
</Text>
</div>
);
}

View File

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

View File

@@ -1,11 +1,11 @@
import InfoCard from '@/components/overview/InfoCard'; import InfoCard from '@/components/overview/InfoCard';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import Image from 'next/image'; import Image from 'next/image';
export default function OverviewProjectInfo() { export default function OverviewProjectInfo() {
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentProject } = useCurrentWorkspaceAndProject();
const { region, subdomain } = currentApplication || {}; const { region, subdomain } = currentProject || {};
const isRegionAvailable = const isRegionAvailable =
region?.awsName && region?.countryCode && region?.city; region?.awsName && region?.countryCode && region?.city;
@@ -13,14 +13,14 @@ export default function OverviewProjectInfo() {
<div className="grid grid-flow-row content-start gap-6"> <div className="grid grid-flow-row content-start gap-6">
<Text variant="h3">Project Info</Text> <Text variant="h3">Project Info</Text>
{currentApplication && ( {currentProject && (
<div className="grid grid-flow-row gap-3"> <div className="grid grid-flow-row gap-3">
<InfoCard <InfoCard
title="Region" title="Region"
value={region?.awsName} value={region?.awsName}
customValue={ customValue={
region.countryCode && region?.countryCode &&
region.city && ( region?.city && (
<div className="grid grid-flow-col items-center gap-1 self-center"> <div className="grid grid-flow-col items-center gap-1 self-center">
<Image <Image
src={`/assets/flags/${region.countryCode}.svg`} src={`/assets/flags/${region.countryCode}.svg`}
@@ -29,7 +29,7 @@ export default function OverviewProjectInfo() {
height={12} height={12}
/> />
<Text className="text-sm font-medium truncate"> <Text className="truncate text-sm font-medium">
{region.city} ({region.awsName}) {region.city} ({region.awsName})
</Text> </Text>
</div> </div>

View File

@@ -2,20 +2,19 @@ import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/context/UIContext'; import { useUI } from '@/context/UIContext';
import useIsPlatform from '@/hooks/common/useIsPlatform'; import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip'; import Chip from '@/ui/v2/Chip';
import CogIcon from '@/ui/v2/icons/CogIcon'; import CogIcon from '@/ui/v2/icons/CogIcon';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
export default function OverviewTopBar() { export default function OverviewTopBar() {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const { currentWorkspace, currentApplication } = const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
useCurrentWorkspaceAndApplication(); const isPro = !currentProject?.plan?.isFree;
const isPro = !currentApplication?.plan?.isFree;
const { openAlertDialog } = useDialog(); const { openAlertDialog } = useDialog();
const { maintenanceActive } = useUI(); const { maintenanceActive } = useUI();
@@ -44,63 +43,92 @@ export default function OverviewTopBar() {
return ( return (
<div className="grid items-center gap-4 pb-5 md:grid-flow-col md:place-content-between md:py-5"> <div className="grid items-center gap-4 pb-5 md:grid-flow-col md:place-content-between md:py-5">
<div className="grid items-center gap-2 md:grid-flow-col"> <div className="grid items-center gap-4 md:grid-flow-col">
<div className="grid grid-flow-col items-center justify-start gap-2"> <div className="grid grid-flow-col items-center justify-start gap-2">
<div className="h-10 w-10 overflow-hidden rounded-lg"> <div className="h-10 w-10 overflow-hidden rounded-lg">
<Image <Image
src="/logos/new.svg" src="/logos/new.svg"
alt="Nhost Logo" alt="Nhost Logo"
width={40} width={56}
height={40} height={56}
/> />
</div> </div>
<Text variant="h2" component="h1"> <div className="grid grid-flow-row">
{currentApplication.name} <div className="grid grid-flow-row items-center justify-start md:grid-flow-col md:gap-3">
</Text> <Text
</div> variant="h2"
component="h1"
<Box className="grid grid-flow-col items-center justify-start gap-2"> className="grid grid-flow-col items-center gap-3"
{isPro ? (
<Chip
className="self-center font-medium"
size="small"
label="Pro Plan"
color="primary"
/>
) : (
<>
<Chip
className="self-center font-medium"
size="small"
label="Free Plan"
color="default"
variant="filled"
/>
<Button
variant="borderless"
className="mr-2"
onClick={() => {
openAlertDialog({
title: 'Upgrade your plan.',
payload: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
},
});
}}
> >
Upgrade {currentProject.name}
</Button> </Text>
</>
)} {currentProject.creator && (
</Box> <Text
color="secondary"
variant="subtitle2"
className="md:hidden"
>
Created by{' '}
{currentProject.creator?.displayName ||
currentProject.creator?.email}{' '}
{formatDistanceToNowStrict(
parseISO(currentProject.createdAt),
)}{' '}
ago
</Text>
)}
<div className="mt-1 inline-grid grid-flow-col items-center justify-start gap-2 md:mt-0">
<Chip
size="small"
label={isPro ? 'Pro' : 'Starter'}
color={isPro ? 'primary' : 'default'}
/>
{!isPro && (
<Button
variant="borderless"
className="mr-2"
onClick={() => {
openAlertDialog({
title: 'Upgrade your plan.',
payload: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
},
});
}}
>
Upgrade
</Button>
)}
</div>
</div>
{currentProject.creator && (
<Text
color="secondary"
variant="subtitle2"
className="hidden md:block"
>
Created by{' '}
{currentProject.creator?.displayName ||
currentProject.creator?.email}{' '}
{formatDistanceToNowStrict(parseISO(currentProject.createdAt))}{' '}
ago
</Text>
)}
</div>
</div>
</div> </div>
<Link <Link
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/general`} href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/general`}
passHref passHref
> >
<Button <Button

View File

@@ -1,53 +1,55 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary'; import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import { import {
useGetAppFunctionsMetadataQuery, useGetAppFunctionsMetadataQuery,
useGetProjectMetricsQuery,
useGetRemoteAppMetricsQuery, useGetRemoteAppMetricsQuery,
} from '@/generated/graphql'; } from '@/generated/graphql';
import useIsPlatform from '@/hooks/common/useIsPlatform'; import useIsPlatform from '@/hooks/common/useIsPlatform';
import useDatabaseSizeOfApplication from '@/hooks/overview/useDatabaseSizeOfApplication';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import LinearProgress from '@/ui/v2/LinearProgress'; import LinearProgress from '@/ui/v2/LinearProgress';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import prettysize from 'prettysize'; import { prettifySize } from '@/utils/common/prettifySize';
import { useEffect, useState } from 'react';
const now = new Date();
export interface UsageProgressProps { export interface UsageProgressProps {
/** /**
* The title of the current service being rendered. * The title of the current service being rendered.
*/ */
service: string; label: string;
/** /**
* The amount used for a given servince on the current project. * The amount used for a given servince on the current project.
*/ */
used: number; used?: string | number;
/** /**
* The total amount of a given service. * The total amount of a given service.
*/ */
total: number; total?: string | number;
/**
* The percentage of the service used.
*/
percentage?: number;
} }
export function UsageProgress({ service, used, total }: UsageProgressProps) { export function UsageProgress({
const denotesFileSizes = service === 'Database' || service === 'Storage'; label,
const normalizedTotal = denotesFileSizes ? total * 1024 * 1024 : total; used,
const percentage = Math.round((used / normalizedTotal) * 100); total,
const prettyTotal = denotesFileSizes percentage,
? prettysize(total * 1024 * 1024) }: UsageProgressProps) {
: total;
const prettyUsed = denotesFileSizes ? prettysize(used) : used;
return ( return (
<div className="flex flex-col space-y-3"> <div className="flex flex-col space-y-3">
<div className="flex flex-row place-content-between items-center"> <div className="flex flex-row place-content-between items-center">
<Text variant="subtitle2" className="lg:!font-medium"> <Text variant="subtitle2" className="lg:!font-medium">
{service} {label}
</Text> </Text>
<Text className="text-xs !font-medium"> <Text className="text-xs !font-medium">
{prettyUsed}{' '} {used} {total && <span className="opacity-80">of {total}</span>}
{total && <span className="opacity-80">of {prettyTotal}</span>}
</Text> </Text>
</div> </div>
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={percentage === 0 ? -1 : percentage} value={percentage === 0 ? -1 : percentage}
@@ -56,92 +58,120 @@ export function UsageProgress({ service, used, total }: UsageProgressProps) {
); );
} }
const services = [
{
service: 'Database',
total: { Starter: 500, Pro: 10240 },
},
{
service: 'Storage',
total: { Starter: 1024, Pro: 10240 },
},
{
service: 'Users',
total: { Starter: 10000, Pro: 100000 },
},
{
service: 'Functions',
total: { Starter: 10, Pro: 50 },
},
];
export function OverviewUsageMetrics() { export function OverviewUsageMetrics() {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentProject } = useCurrentWorkspaceAndProject();
const remoteAppClient = useRemoteApplicationGQLClient(); const remoteAppClient = useRemoteApplicationGQLClient();
const [metrics, setMetrics] = useState({ const { data: functionsInfoData, loading: functionMetricsLoading } =
functions: 0, useGetAppFunctionsMetadataQuery({
storage: 0, variables: { id: currentProject?.id },
database: 0, skip: !isPlatform || !currentProject,
users: 0, });
});
const { data: functionsInfoData } = useGetAppFunctionsMetadataQuery({ const { data: projectMetrics, loading: projectMetricsLoading } =
variables: { id: currentApplication?.id }, useGetProjectMetricsQuery({
skip: !isPlatform, variables: {
}); appId: currentProject?.id,
subdomain: currentProject?.subdomain,
from: new Date(now.getFullYear(), now.getMonth(), 1),
},
skip: !isPlatform || !currentProject,
});
const { data: databaseSizeData } = useDatabaseSizeOfApplication( const { data: remoteAppMetricsData, loading: remoteAppMetricsLoading } =
[currentApplication?.name, 'databaseSize'], useGetRemoteAppMetricsQuery({
{ enabled: !!currentApplication }, client: remoteAppClient,
); skip: !currentProject,
});
const { data: remoteAppMetricsData } = useGetRemoteAppMetricsQuery({ const metricsLoading =
client: remoteAppClient, functionMetricsLoading || projectMetricsLoading || remoteAppMetricsLoading;
skip: !currentApplication, // metrics for database
}); const usedDatabase = projectMetrics?.postgresVolumeUsage.value || 0;
const totalDatabase = projectMetrics?.postgresVolumeCapacity.value || 0;
useEffect(() => { // metrics for storage
if (databaseSizeData) { const usedStorage =
setMetrics((m) => ({ remoteAppMetricsData?.filesAggregate?.aggregate?.sum?.size || 0;
...m, const totalStorage = currentProject?.plan?.isFree
database: databaseSizeData.databaseSize, ? 1 * 1000 ** 3 // 1 GB
})); : 10 * 1000 ** 3; // 10 GB
}
}, [databaseSizeData]);
useEffect(() => { // metrics for users
if (remoteAppMetricsData) { const usedUsers = remoteAppMetricsData?.usersAggregate?.aggregate?.count || 0;
setMetrics((m) => ({ const totalUsers = currentProject?.plan?.isFree ? 10000 : 100000;
...m,
storage: remoteAppMetricsData.filesAggregate.aggregate.sum.size,
users: remoteAppMetricsData.usersAggregate.aggregate.count,
}));
}
}, [remoteAppMetricsData]);
useEffect(() => { // metrics for functions
if (functionsInfoData) { const usedFunctions = functionsInfoData?.app.metadataFunctions.length || 0;
setMetrics((m) => ({ const totalFunctions = currentProject?.plan?.isFree ? 10 : 50;
...m,
functions: functionsInfoData.app.metadataFunctions.length, if (metricsLoading) {
})); return (
} <div className="grid grid-flow-row content-start gap-6">
}, [functionsInfoData]); <UsageProgress label="Database" percentage={0} />
<UsageProgress label="Storage" percentage={0} />
<UsageProgress label="Users" percentage={0} />
<UsageProgress label="Functions" percentage={0} />
</div>
);
}
if (!isPlatform) {
return (
<div className="grid grid-flow-row content-start gap-6">
<UsageProgress
label="Database"
used={prettifySize(0)}
percentage={100}
/>
<UsageProgress
label="Storage"
used={prettifySize(usedStorage)}
percentage={100}
/>
<UsageProgress label="Users" used={usedUsers} percentage={100} />
<UsageProgress
label="Functions"
used={usedFunctions}
percentage={100}
/>
</div>
);
}
return ( return (
<div className="grid grid-flow-row content-start gap-6"> <div className="grid grid-flow-row content-start gap-6">
{services.map((service) => ( <UsageProgress
<UsageProgress label="Database"
key={service.service} used={prettifySize(usedDatabase)}
service={service.service} total={prettifySize(totalDatabase)}
used={metrics[service.service.toLowerCase()]} percentage={(usedDatabase / totalDatabase) * 100}
total={ />
isPlatform ? service.total[currentApplication.plan?.name] : null
} <UsageProgress
/> label="Storage"
))} used={prettifySize(usedStorage)}
total={prettifySize(totalStorage)}
percentage={(usedStorage / totalStorage) * 100}
/>
<UsageProgress
label="Users"
used={usedUsers}
total={totalUsers}
percentage={(usedUsers / totalUsers) * 100}
/>
<UsageProgress
label="Functions"
used={usedFunctions}
total={totalFunctions}
percentage={(usedFunctions / totalFunctions) * 100}
/>
</div> </div>
); );
} }

View File

@@ -0,0 +1,201 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import BaseProviderSettings from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import IconButton from '@/ui/v2/IconButton';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputAdornment from '@/ui/v2/InputAdornment';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
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({
clientId: Yup.string()
.label('Client ID')
.when('enabled', {
is: true,
then: (schema) => schema.required(),
}),
clientSecret: Yup.string()
.label('Client Secret')
.when('enabled', {
is: true,
then: (schema) => schema.required(),
}),
tenant: Yup.string()
.label('Tenant')
.when('enabled', {
is: true,
then: (schema) => schema.required(),
}),
enabled: Yup.boolean(),
});
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function AzureADProviderSettings() {
const { maintenanceActive } = useUI();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentApplication?.id },
fetchPolicy: 'cache-only',
});
const { clientId, clientSecret, tenant, enabled } =
data?.config?.auth?.method?.oauth?.azuread || {};
const form = useForm<AzureADProviderFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
clientId: clientId || '',
clientSecret: clientSecret || '',
tenant: tenant || '',
enabled: enabled || false,
},
resolver: yupResolver(validationSchema),
});
if (loading) {
return (
<ActivityIndicator
delay={1000}
label="Loading settings for Azure AD..."
className="justify-center"
/>
);
}
if (error) {
throw error;
}
const { register, formState, watch } = form;
const authEnabled = watch('enabled');
const handleProviderUpdate = async (values: AzureADProviderFormValues) => {
const updateConfigPromise = updateConfig({
variables: {
appId: currentApplication.id,
config: {
auth: {
method: {
oauth: {
azuread: values,
},
},
},
},
},
});
try {
await toast.promise(
updateConfigPromise,
{
loading: `Azure AD settings are being updated...`,
success: `Azure AD settings have been updated successfully.`,
error: getServerError(
`An error occurred while trying to update the project's Azure AD settings.`,
),
},
getToastStyleProps(),
);
form.reset(values);
} catch {
// Note: The toast will handle the error.
}
};
return (
<FormProvider {...form}>
<Form onSubmit={handleProviderUpdate}>
<SettingsContainer
title="Azure AD"
description="Allow users to sign in with Azure AD."
slotProps={{
submitButton: {
disabled: !formState.isDirty || maintenanceActive,
loading: formState.isSubmitting,
},
}}
icon="/assets/brands/azuread.svg"
switchId="enabled"
showSwitch
className={twMerge(
'grid grid-flow-row grid-cols-2 gap-y-4 gap-x-3 px-4 py-2',
!authEnabled && 'hidden',
)}
>
<BaseProviderSettings providerName="azuread" />
<Input
{...register('tenant')}
name="tenant"
id="tenant"
label="Tenant ID"
placeholder="Tenant ID"
className="col-span-2"
fullWidth
hideEmptyHelperText
error={!!formState.errors?.tenant}
helperText={formState.errors?.tenant?.message}
/>
<Input
name="redirectUrl"
id="redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'auth',
)}/signin/provider/azuread/callback`}
className="col-span-2"
fullWidth
hideEmptyHelperText
label="Redirect URL"
disabled
endAdornment={
<InputAdornment position="end" className="absolute right-2">
<IconButton
sx={{ minWidth: 0, padding: 0 }}
color="secondary"
variant="borderless"
onClick={(e) => {
e.stopPropagation();
copy(
`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'auth',
)}/signin/provider/azuread/callback`,
'Redirect URL',
);
}}
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</InputAdornment>
}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -11,7 +11,7 @@ const StyledListItemText = styled(MaterialListItemText)(({ theme }) => ({
display: 'grid', display: 'grid',
justifyContent: 'start', justifyContent: 'start',
gridAutoFlow: 'row', gridAutoFlow: 'row',
gap: theme.spacing(0.5), gap: theme.spacing(0.25),
fontSize: theme.typography.pxToRem(15), fontSize: theme.typography.pxToRem(15),
[`&.${listItemTextClasses.root}`]: { [`&.${listItemTextClasses.root}`]: {
margin: 0, margin: 0,

View File

@@ -28,10 +28,10 @@ const StyledTooltip = styled(Box)(({ theme }) => ({
lineHeight: '1.375rem', lineHeight: '1.375rem',
backgroundColor: backgroundColor:
theme.palette.mode === 'dark' theme.palette.mode === 'dark'
? theme.palette.grey[300] ? theme.palette.grey[400]
: theme.palette.grey[700], : theme.palette.grey[700],
color: theme.palette.common.white, color: theme.palette.common.white,
padding: theme.spacing(0.5, 1), padding: theme.spacing(0.75, 1.25),
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
WebkitFontSmoothing: 'antialiased', WebkitFontSmoothing: 'antialiased',
boxShadow: boxShadow:

View File

@@ -0,0 +1,34 @@
import type { IconProps } from '@/ui/v2/icons';
import SvgIcon from '@/ui/v2/icons/SvgIcon';
import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
function InfoIcon(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="Info"
ref={ref}
{...props}
>
<path
opacity="0.2"
d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM6.5 8.25h.75V11c0 .414.336.75.75.75h1.5v-1.5h-.75V7.5A.75.75 0 0 0 8 6.75H6.5v1.5Z"
fill="currentColor"
/>
</SvgIcon>
);
}
InfoIcon.displayName = 'NhostInfoIcon';
export default forwardRef(InfoIcon);

View File

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

View File

@@ -32,7 +32,6 @@ export const validationSchema = Yup.object({
.required('This field is required.'), .required('This field is required.'),
password: Yup.string() password: Yup.string()
.label('Users Password') .label('Users Password')
.min(8, 'Password must be at least 8 characters long.')
.required('This field is required.'), .required('This field is required.'),
}); });
@@ -99,7 +98,7 @@ export default function CreateUserForm({
}), }),
{ {
loading: 'Creating user...', loading: 'Creating user...',
success: 'User created successfully.', success: 'User has been created successfully.',
error: getServerError( error: getServerError(
'An error occurred while trying to create the user.', 'An error occurred while trying to create the user.',
), ),

View File

@@ -19,6 +19,7 @@ import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import getReadableProviderName from '@/utils/common/getReadableProviderName'; import getReadableProviderName from '@/utils/common/getReadableProviderName';
import { copy } from '@/utils/copy'; import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
import getUserRoles from '@/utils/settings/getUserRoles'; import getUserRoles from '@/utils/settings/getUserRoles';
import { getToastStyleProps } from '@/utils/settings/settingsConstants'; import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { import {
@@ -168,11 +169,13 @@ export default function EditUserForm({
{ {
loading: shouldBan ? 'Banning user...' : 'Unbanning user...', loading: shouldBan ? 'Banning user...' : 'Unbanning user...',
success: shouldBan success: shouldBan
? 'User banned successfully' ? 'User has been banned successfully.'
: 'User unbanned successfully.', : 'User has been unbanned successfully.',
error: shouldBan error: getServerError(
? 'An error occurred while trying to ban the user.' shouldBan
: 'An error occurred while trying to unban the user.', ? 'An error occurred while trying to ban the user.'
: 'An error occurred while trying to unban the user.',
),
}, },
getToastStyleProps(), getToastStyleProps(),
); );
@@ -213,7 +216,7 @@ export default function EditUserForm({
Actions Actions
</Button> </Button>
</Dropdown.Trigger> </Dropdown.Trigger>
<Dropdown.Content menu disablePortal className="h-full w-full"> <Dropdown.Content menu className="h-full w-full">
<Dropdown.Item <Dropdown.Item
className="font-medium" className="font-medium"
sx={{ color: 'error.main' }} sx={{ color: 'error.main' }}
@@ -316,6 +319,7 @@ export default function EditUserForm({
id="emailVerified" id="emailVerified"
name="emailVerified" name="emailVerified"
label="Verified" label="Verified"
aria-label="Email Verified"
/> />
) )
} }
@@ -354,6 +358,7 @@ export default function EditUserForm({
id="phoneNumberVerified" id="phoneNumberVerified"
name="phoneNumberVerified" name="phoneNumberVerified"
label="Verified" label="Verified"
aria-label="Phone Number Verified"
disabled={!form.watch('phoneNumber')} disabled={!form.watch('phoneNumber')}
/> />
) )

View File

@@ -151,7 +151,7 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
updateUserMutationPromise, updateUserMutationPromise,
{ {
loading: `Updating user's settings...`, loading: `Updating user's settings...`,
success: 'User settings updated successfully.', success: 'User settings have been updated successfully.',
error: getServerError( error: getServerError(
`An error occurred while trying to update this user's settings.`, `An error occurred while trying to update this user's settings.`,
), ),

View File

@@ -1,21 +1,31 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import Status, { StatusEnum } from '@/ui/Status'; import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider'; import Divider from '@/ui/v2/Divider';
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon'; import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
import List from '@/ui/v2/List'; import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem'; import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import Image from 'next/image'; import Image from 'next/image';
import NavLink from 'next/link'; import NavLink from 'next/link';
import { Fragment } from 'react'; import { Fragment } from 'react';
function AllWorkspaceApps() { function AllWorkspaceApps() {
const { currentWorkspace } = useCurrentWorkspaceAndApplication(); const { currentWorkspace, loading, error } = useCurrentWorkspaceAndProject();
const noApplications = currentWorkspace?.applications.length === 0;
if (noApplications) { if (loading) {
return <ActivityIndicator label="Loading projects..." delay={1000} />;
}
if (error) {
throw error;
}
if (currentWorkspace?.projects?.length === 0) {
return ( return (
<Box className="flex flex-row border-y py-4"> <Box className="flex flex-row border-y py-4">
<Text className="text-xs" color="secondary"> <Text className="text-xs" color="secondary">
@@ -29,25 +39,50 @@ function AllWorkspaceApps() {
<List> <List>
<Divider component="li" /> <Divider component="li" />
{currentWorkspace?.applications.map((app) => ( {currentWorkspace?.projects.map((project) => (
<Fragment key={app.id}> <Fragment key={project.id}>
<ListItem.Root> <ListItem.Root>
<NavLink href={`${currentWorkspace?.slug}/${app.slug}`} passHref> <NavLink
<ListItem.Button className="grid grid-flow-col justify-between gap-2"> href={`${currentWorkspace?.slug}/${project.slug}`}
<div className="grid grid-flow-col items-center gap-2"> passHref
<div className="h-8 w-8 overflow-hidden rounded-lg"> >
<Image <ListItem.Button className="grid grid-flow-col items-center justify-between gap-2">
src="/logos/new.svg" <div className="grid grid-flow-col items-center justify-start gap-2">
alt="Nhost Logo" <ListItem.Avatar>
width={32} <div className="h-8 w-8 overflow-hidden rounded-lg">
height={32} <Image
/> src="/logos/new.svg"
</div> alt="Nhost Logo"
width={32}
height={32}
/>
</div>
</ListItem.Avatar>
<Text className="font-medium">{app.name}</Text> <ListItem.Text
primary={project.name}
secondary={
project.creator ? (
<span>
{`Created by ${
project.creator.displayName || project.creator.email
} ${formatDistanceToNowStrict(
parseISO(project.createdAt),
)} ago`}
</span>
) : undefined
}
secondaryTypographyProps={{
className: 'text-xs',
}}
/>
</div> </div>
<Status status={StatusEnum.Plan}>{app.plan.name}</Status> <Chip
size="small"
label={project.plan.isFree ? 'Starter' : 'Pro'}
color={project.plan.isFree ? 'default' : 'primary'}
/>
</ListItem.Button> </ListItem.Button>
</NavLink> </NavLink>
</ListItem.Root> </ListItem.Root>
@@ -59,30 +94,35 @@ function AllWorkspaceApps() {
); );
} }
export default function WorkspaceApps() { export default function WorkspaceApps() {
const { currentWorkspace } = useCurrentWorkspaceAndApplication(); const { currentWorkspace, loading } = useCurrentWorkspaceAndProject();
return ( return (
<div className="mt-9"> <div className="mt-9">
<div className="mx-auto max-w-3xl font-display"> <div className="mx-auto max-w-3xl font-display">
<div className="mb-4 flex flex-row place-content-between"> <div className="mb-4 grid grid-flow-col items-center justify-between gap-2">
<Text className="text-lg font-medium">Projects</Text> <Text className="text-lg font-medium">Projects</Text>
<NavLink
href={{ {!loading && (
pathname: '/new', <NavLink
query: { workspace: currentWorkspace.slug }, href={{
}} pathname: '/new',
> query: { workspace: currentWorkspace?.slug },
<Button }}
variant="outlined"
color="secondary"
startIcon={<PlusCircleIcon />}
> >
New Project <Button
</Button> variant="outlined"
</NavLink> color="secondary"
startIcon={<PlusCircleIcon />}
>
New Project
</Button>
</NavLink>
)}
</div> </div>
<AllWorkspaceApps /> <RetryableErrorBoundary errorMessageProps={{ className: 'px-0' }}>
<AllWorkspaceApps />
</RetryableErrorBoundary>
</div> </div>
</div> </div>
); );

View File

@@ -0,0 +1,37 @@
query GetProjectMetrics(
$appId: String!
$subdomain: String!
$from: Timestamp
$to: Timestamp
) {
logsVolume: getLogsVolume(appID: $appId, from: $from, to: $to) {
value
}
cpuSecondsUsage: getCPUSecondsUsage(appID: $appId, from: $from, to: $to) {
value
}
functionInvocations: getFunctionsInvocations(
appID: $appId
from: $from
to: $to
) {
value
}
postgresVolumeCapacity: getPostgresVolumeCapacity(appID: $appId) {
value
}
postgresVolumeUsage: getPostgresVolumeUsage(appID: $appId) {
value
}
totalRequests: getTotalRequests(appID: $appId, from: $from, to: $to) {
value
}
egressVolume: getEgressVolume(
appID: $appId
subdomain: $subdomain
from: $from
to: $to
) {
value
}
}

View File

@@ -0,0 +1,26 @@
fragment Workspace on workspaces {
id
name
slug
workspaceMembers {
id
user {
id
email
displayName
}
type
}
projects: apps {
...Project
}
}
query GetWorkspaceAndProject($workspaceSlug: String!, $projectSlug: String) {
workspaces(where: { slug: { _eq: $workspaceSlug } }) {
...Workspace
}
projects: apps(where: { slug: { _eq: $projectSlug } }) {
...Project
}
}

View File

@@ -100,6 +100,12 @@ query GetSignInMethods($appId: uuid!) {
connection connection
organization organization
} }
azuread {
enabled
clientId
clientSecret
tenant
}
} }
} }
} }

View File

@@ -0,0 +1,59 @@
fragment Project on apps {
id
slug
name
repositoryProductionBranch
subdomain
isProvisioned
createdAt
desiredState
nhostBaseFolder
providersUpdated
config(resolve: true) {
hasura {
adminSecret
}
}
featureFlags {
description
id
name
value
}
appStates(order_by: { createdAt: desc }, limit: 1) {
id
appId
message
stateId
createdAt
}
region {
id
countryCode
awsName
city
}
plan {
id
name
isFree
}
githubRepository {
fullName
}
deployments(limit: 4, order_by: { deploymentEndedAt: desc }) {
id
commitSHA
commitMessage
commitUserName
deploymentStartedAt
deploymentEndedAt
commitUserAvatarUrl
deploymentStatus
}
creator {
id
email
displayName
}
}

View File

@@ -1,58 +1,3 @@
fragment Project on apps {
id
slug
name
repositoryProductionBranch
subdomain
isProvisioned
createdAt
desiredState
nhostBaseFolder
providersUpdated
config(resolve: true) {
hasura {
adminSecret
}
}
featureFlags {
description
id
name
value
}
appStates(order_by: { createdAt: desc }, limit: 1) {
id
appId
message
stateId
createdAt
}
region {
id
countryCode
awsName
city
}
plan {
id
name
isFree
}
githubRepository {
fullName
}
deployments(limit: 4, order_by: { deploymentEndedAt: desc }) {
id
commitSHA
commitMessage
commitUserName
deploymentStartedAt
deploymentEndedAt
commitUserAvatarUrl
deploymentStatus
}
}
query getOneUser($userId: uuid!) { query getOneUser($userId: uuid!) {
user(id: $userId) { user(id: $userId) {
id id

View File

@@ -5,23 +5,29 @@ import { useCurrentWorkspaceAndApplication } from './useCurrentWorkspaceAndAppli
* This hook returns the current application state. If the application state * This hook returns the current application state. If the application state
* has not been filled, it returns an Empty application status. * has not been filled, it returns an Empty application status.
*/ */
export default function useApplicationState(): ApplicationStatus { export default function useApplicationState(): {
state: ApplicationStatus;
message?: string;
} {
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const noApplication = !currentApplication; const noApplication = !currentApplication;
if (noApplication) { if (noApplication) {
return ApplicationStatus.Empty; return { state: ApplicationStatus.Empty };
} }
const emptyApplicationStates = !currentApplication.appStates; const emptyApplicationStates = !currentApplication.appStates;
if (noApplication || emptyApplicationStates) { if (noApplication || emptyApplicationStates) {
return ApplicationStatus.Empty; return { state: ApplicationStatus.Empty };
} }
if (currentApplication.appStates?.length === 0) { if (currentApplication.appStates?.length === 0) {
return ApplicationStatus.Empty; return { state: ApplicationStatus.Empty };
} }
return currentApplication.appStates[0].stateId; return {
state: currentApplication.appStates[0].stateId,
message: currentApplication.appStates[0].message,
};
} }

View File

@@ -39,6 +39,8 @@ export function useCheckProvisioning() {
const memoizedUpdateCache = useCallback(updateOwnCache, [client]); const memoizedUpdateCache = useCallback(updateOwnCache, [client]);
const currentApplicationId = currentApplication?.id;
useEffect(() => { useEffect(() => {
startPolling(2000); startPolling(2000);
}, [startPolling]); }, [startPolling]);
@@ -84,7 +86,7 @@ export function useCheckProvisioning() {
createdAt: data.app.appStates[0].createdAt, createdAt: data.app.appStates[0].createdAt,
}); });
discordAnnounce( discordAnnounce(
`Application ${currentApplication.id} errored after provisioning: ${data.app.appStates[0].message}`, `Application ${currentApplicationId} errored after provisioning: ${data.app.appStates[0].message}`,
); );
stopPolling(); stopPolling();
memoizedUpdateCache(); memoizedUpdateCache();
@@ -93,7 +95,7 @@ export function useCheckProvisioning() {
data, data,
stopPolling, stopPolling,
memoizedUpdateCache, memoizedUpdateCache,
currentApplication.id, currentApplicationId,
currentApplicationState.state, currentApplicationState.state,
]); ]);

View File

@@ -9,7 +9,7 @@ import usePreviousApplicationState from './usePreviousApplicationStates';
*/ */
export function useNavigationVisible() { export function useNavigationVisible() {
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const applicationState = useApplicationState(); const { state } = useApplicationState();
const previousApplicationState = usePreviousApplicationState(); const previousApplicationState = usePreviousApplicationState();
const router = useRouter(); const router = useRouter();
@@ -39,14 +39,14 @@ export function useNavigationVisible() {
} }
if ( if (
applicationState === ApplicationStatus.Live || state === ApplicationStatus.Live ||
applicationState === ApplicationStatus.Updating state === ApplicationStatus.Updating
) { ) {
return true; return true;
} }
if ( if (
applicationState === ApplicationStatus.Errored && state === ApplicationStatus.Errored &&
previousApplicationState === ApplicationStatus.Updating previousApplicationState === ApplicationStatus.Updating
) { ) {
return true; return true;

View File

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

View File

@@ -0,0 +1,115 @@
import useIsPlatform from '@/hooks/common/useIsPlatform';
import type { Project, Workspace } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import type { GetWorkspaceAndProjectQueryHookResult } from '@/utils/__generated__/graphql';
import { useGetWorkspaceAndProjectQuery } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env';
import type { QueryHookOptions } from '@apollo/client';
import { useRouter } from 'next/router';
export interface UseCurrentWorkspaceAndProjectOptions {
/**
* The fetch policy to use.
*
* @default 'cache-first'
*/
fetchPolicy?: QueryHookOptions['fetchPolicy'];
}
export interface UseCurrentWorkspaceAndProjectReturnType {
/**
* The current workspace.
*/
currentWorkspace: Workspace;
/**
* The current project.
*/
currentProject: Project;
/**
* Whether the query is loading.
*/
loading?: GetWorkspaceAndProjectQueryHookResult['loading'];
/**
* The error if any.
*/
error?: GetWorkspaceAndProjectQueryHookResult['error'];
}
export default function useCurrentWorkspaceAndProject(
options?: UseCurrentWorkspaceAndProjectOptions,
): UseCurrentWorkspaceAndProjectReturnType {
const isPlatform = useIsPlatform();
const {
query: { workspaceSlug, appSlug },
isReady,
} = useRouter();
const { data, loading, error } = useGetWorkspaceAndProjectQuery({
variables: {
workspaceSlug: (workspaceSlug as string) || '',
projectSlug: (appSlug as string) || '',
},
fetchPolicy: options?.fetchPolicy || 'cache-first',
skip: !isPlatform || !isReady || !workspaceSlug,
});
if (!isPlatform) {
const localProject: Project = {
id: 'local',
slug: 'local',
name: 'local',
appStates: [
{
id: 'local',
appId: 'local',
stateId: ApplicationStatus.Live,
createdAt: new Date().toISOString(),
},
],
deployments: [],
subdomain: 'local',
region: {
id: null,
countryCode: null,
city: null,
awsName: null,
},
isProvisioned: true,
createdAt: new Date().toISOString(),
desiredState: ApplicationStatus.Live,
featureFlags: [],
providersUpdated: true,
repositoryProductionBranch: null,
nhostBaseFolder: null,
plan: null,
config: {
hasura: {
adminSecret: getHasuraAdminSecret(),
},
},
};
return {
currentWorkspace: {
id: 'local',
slug: 'local',
name: 'local',
projects: [localProject],
workspaceMembers: [],
},
currentProject: localProject,
loading: false,
};
}
const [currentWorkspace] = data?.workspaces || [];
const [currentProject] = data?.projects || [];
return {
currentWorkspace,
currentProject,
loading: data ? false : loading,
error,
};
}

View File

@@ -11,8 +11,8 @@ import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip'; import Chip from '@/ui/v2/Chip';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import { prettifySize } from '@/utils/common/prettifySize';
import { formatDistanceStrict, formatISO9075 } from 'date-fns'; import { formatDistanceStrict, formatISO9075 } from 'date-fns';
import prettysize from 'prettysize';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import { useState } from 'react'; import { useState } from 'react';
@@ -58,13 +58,13 @@ function BackupRow({ backup }: any) {
/> />
)} )}
<Box className="flex flex-row place-content-between py-3"> <Box className="flex flex-row place-content-between py-3">
<Text className="w-drop self-center font-medium text-xs"> <Text className="w-drop self-center text-xs font-medium">
{formatISO9075(new Date(createdAt))} {formatISO9075(new Date(createdAt))}
</Text> </Text>
<Text className="w-drop self-center font-medium text-xs"> <Text className="w-drop self-center text-xs font-medium">
{prettysize(size)} {prettifySize(size)}
</Text> </Text>
<Text className="w-drop self-center font-medium text-xs"> <Text className="w-drop self-center text-xs font-medium">
{formatDistanceStrict(new Date(createdAt), new Date(), { {formatDistanceStrict(new Date(createdAt), new Date(), {
addSuffix: true, addSuffix: true,
})} })}
@@ -108,9 +108,9 @@ function BackupsTable() {
return ( return (
<> <>
<Box className="flex flex-row place-content-between border-b-1 py-2"> <Box className="flex flex-row place-content-between border-b-1 py-2">
<Text className="w-drop font-bold text-xs">Backup</Text> <Text className="w-drop text-xs font-bold">Backup</Text>
<Text className="w-drop font-bold text-xs">Size</Text> <Text className="w-drop text-xs font-bold">Size</Text>
<Text className="w-drop font-bold text-xs">Backed Up</Text> <Text className="w-drop text-xs font-bold">Backed Up</Text>
<div className="w-20" /> <div className="w-20" />
</Box> </Box>
<Box className="border-b-1"> <Box className="border-b-1">
@@ -133,7 +133,7 @@ function BackupsTable() {
function SectionContainer({ title }: any) { function SectionContainer({ title }: any) {
return ( return (
<div className="mt-6 w-full"> <div className="mt-6 w-full">
<Text className="font-medium text-lg">{title}</Text> <Text className="text-lg font-medium">{title}</Text>
<Text className="font-normal"> <Text className="font-normal">
The database backup includes database schema, database data and Hasura The database backup includes database schema, database data and Hasura
metadata. It does not include the actual files in Storage. metadata. It does not include the actual files in Storage.

View File

@@ -16,9 +16,7 @@ export default function DataBrowserDatabaseDetailsPage() {
description={ description={
<span> <span>
Database{' '} Database{' '}
<InlineCode className="bg-gray-200 bg-opacity-80 px-1.5 text-sm"> <InlineCode className="px-1.5 text-sm">{dataSourceSlug}</InlineCode>{' '}
{dataSourceSlug}
</InlineCode>{' '}
does not exist. does not exist.
</span> </span>
} }

View File

@@ -15,7 +15,7 @@ import type { ReactElement } from 'react';
export default function AppIndexPage() { export default function AppIndexPage() {
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const applicationState = useApplicationState(); const { state } = useApplicationState();
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
if (!isPlatform) { if (!isPlatform) {
@@ -26,7 +26,7 @@ export default function AppIndexPage() {
return <ApplicationMigrating />; return <ApplicationMigrating />;
} }
switch (applicationState) { switch (state) {
case ApplicationStatus.Empty: case ApplicationStatus.Empty:
return <ApplicationProvisioning />; return <ApplicationProvisioning />;
case ApplicationStatus.Provisioning: case ApplicationStatus.Provisioning:

View File

@@ -134,9 +134,5 @@ export default function LogsPage() {
} }
LogsPage.getLayout = function getLayout(page: ReactElement) { LogsPage.getLayout = function getLayout(page: ReactElement) {
return ( return <ProjectLayout>{page}</ProjectLayout>;
<ProjectLayout mainContainerProps={{ className: 'bg-gray-50' }}>
{page}
</ProjectLayout>
);
}; };

View File

@@ -2,6 +2,7 @@ import Container from '@/components/layout/Container';
import SettingsLayout from '@/components/settings/SettingsLayout'; import SettingsLayout from '@/components/settings/SettingsLayout';
import AnonymousSignInSettings from '@/components/settings/signInMethods/AnonymousSignInSettings'; import AnonymousSignInSettings from '@/components/settings/signInMethods/AnonymousSignInSettings';
import AppleProviderSettings from '@/components/settings/signInMethods/AppleProviderSettings'; import AppleProviderSettings from '@/components/settings/signInMethods/AppleProviderSettings';
import AzureADProviderSettings from '@/components/settings/signInMethods/AzureADProviderSettings';
import DiscordProviderSettings from '@/components/settings/signInMethods/DiscordProviderSettings'; import DiscordProviderSettings from '@/components/settings/signInMethods/DiscordProviderSettings';
import EmailAndPasswordSettings from '@/components/settings/signInMethods/EmailAndPasswordSettings'; import EmailAndPasswordSettings from '@/components/settings/signInMethods/EmailAndPasswordSettings';
import FacebookProviderSettings from '@/components/settings/signInMethods/FacebookProviderSettings'; import FacebookProviderSettings from '@/components/settings/signInMethods/FacebookProviderSettings';
@@ -56,6 +57,7 @@ export default function SettingsSignInMethodsPage() {
<SMSSettings /> <SMSSettings />
{!currentApplication.providersUpdated && <ProvidersUpdatedAlert />} {!currentApplication.providersUpdated && <ProvidersUpdatedAlert />}
<AppleProviderSettings /> <AppleProviderSettings />
<AzureADProviderSettings />
<DiscordProviderSettings /> <DiscordProviderSettings />
<FacebookProviderSettings /> <FacebookProviderSettings />
<GitHubProviderSettings /> <GitHubProviderSettings />

View File

@@ -6,6 +6,7 @@ import type {
PermissionVariableFragment, PermissionVariableFragment,
ProjectFragment, ProjectFragment,
SecretFragment, SecretFragment,
WorkspaceFragment,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
/** /**
@@ -62,6 +63,7 @@ export type FeatureFlag = {
value: string; value: string;
}; };
export type Workspace = WorkspaceFragment;
export type Project = ProjectFragment; export type Project = ProjectFragment;
export interface PermissionVariable extends PermissionVariableFragment { export interface PermissionVariable extends PermissionVariableFragment {

View File

@@ -25,6 +25,7 @@ export type Scalars = {
bpchar: any; bpchar: any;
bytea: any; bytea: any;
citext: any; citext: any;
float64: any;
jsonb: any; jsonb: any;
smallint: any; smallint: any;
timestamp: any; timestamp: any;
@@ -1035,7 +1036,9 @@ export type ConfigGlobalUpdateInput = {
export type ConfigHasura = { export type ConfigHasura = {
__typename?: 'ConfigHasura'; __typename?: 'ConfigHasura';
adminSecret: Scalars['String']; adminSecret: Scalars['String'];
events?: Maybe<ConfigHasuraEvents>;
jwtSecrets?: Maybe<Array<ConfigJwtSecret>>; jwtSecrets?: Maybe<Array<ConfigJwtSecret>>;
logs?: Maybe<ConfigHasuraLogs>;
resources?: Maybe<ConfigResources>; resources?: Maybe<ConfigResources>;
settings?: Maybe<ConfigHasuraSettings>; settings?: Maybe<ConfigHasuraSettings>;
version?: Maybe<Scalars['String']>; version?: Maybe<Scalars['String']>;
@@ -1047,22 +1050,66 @@ export type ConfigHasuraComparisonExp = {
_not?: InputMaybe<ConfigHasuraComparisonExp>; _not?: InputMaybe<ConfigHasuraComparisonExp>;
_or?: InputMaybe<Array<ConfigHasuraComparisonExp>>; _or?: InputMaybe<Array<ConfigHasuraComparisonExp>>;
adminSecret?: InputMaybe<ConfigStringComparisonExp>; adminSecret?: InputMaybe<ConfigStringComparisonExp>;
events?: InputMaybe<ConfigHasuraEventsComparisonExp>;
jwtSecrets?: InputMaybe<ConfigJwtSecretComparisonExp>; jwtSecrets?: InputMaybe<ConfigJwtSecretComparisonExp>;
logs?: InputMaybe<ConfigHasuraLogsComparisonExp>;
resources?: InputMaybe<ConfigResourcesComparisonExp>; resources?: InputMaybe<ConfigResourcesComparisonExp>;
settings?: InputMaybe<ConfigHasuraSettingsComparisonExp>; settings?: InputMaybe<ConfigHasuraSettingsComparisonExp>;
version?: InputMaybe<ConfigStringComparisonExp>; version?: InputMaybe<ConfigStringComparisonExp>;
webhookSecret?: InputMaybe<ConfigStringComparisonExp>; webhookSecret?: InputMaybe<ConfigStringComparisonExp>;
}; };
export type ConfigHasuraEvents = {
__typename?: 'ConfigHasuraEvents';
httpPoolSize?: Maybe<Scalars['ConfigUint32']>;
};
export type ConfigHasuraEventsComparisonExp = {
_and?: InputMaybe<Array<ConfigHasuraEventsComparisonExp>>;
_not?: InputMaybe<ConfigHasuraEventsComparisonExp>;
_or?: InputMaybe<Array<ConfigHasuraEventsComparisonExp>>;
httpPoolSize?: InputMaybe<ConfigUint32ComparisonExp>;
};
export type ConfigHasuraEventsInsertInput = {
httpPoolSize?: InputMaybe<Scalars['ConfigUint32']>;
};
export type ConfigHasuraEventsUpdateInput = {
httpPoolSize?: InputMaybe<Scalars['ConfigUint32']>;
};
export type ConfigHasuraInsertInput = { export type ConfigHasuraInsertInput = {
adminSecret: Scalars['String']; adminSecret: Scalars['String'];
events?: InputMaybe<ConfigHasuraEventsInsertInput>;
jwtSecrets?: InputMaybe<Array<ConfigJwtSecretInsertInput>>; jwtSecrets?: InputMaybe<Array<ConfigJwtSecretInsertInput>>;
logs?: InputMaybe<ConfigHasuraLogsInsertInput>;
resources?: InputMaybe<ConfigResourcesInsertInput>; resources?: InputMaybe<ConfigResourcesInsertInput>;
settings?: InputMaybe<ConfigHasuraSettingsInsertInput>; settings?: InputMaybe<ConfigHasuraSettingsInsertInput>;
version?: InputMaybe<Scalars['String']>; version?: InputMaybe<Scalars['String']>;
webhookSecret: Scalars['String']; webhookSecret: Scalars['String'];
}; };
export type ConfigHasuraLogs = {
__typename?: 'ConfigHasuraLogs';
level?: Maybe<Scalars['String']>;
};
export type ConfigHasuraLogsComparisonExp = {
_and?: InputMaybe<Array<ConfigHasuraLogsComparisonExp>>;
_not?: InputMaybe<ConfigHasuraLogsComparisonExp>;
_or?: InputMaybe<Array<ConfigHasuraLogsComparisonExp>>;
level?: InputMaybe<ConfigStringComparisonExp>;
};
export type ConfigHasuraLogsInsertInput = {
level?: InputMaybe<Scalars['String']>;
};
export type ConfigHasuraLogsUpdateInput = {
level?: InputMaybe<Scalars['String']>;
};
export type ConfigHasuraSettings = { export type ConfigHasuraSettings = {
__typename?: 'ConfigHasuraSettings'; __typename?: 'ConfigHasuraSettings';
enableRemoteSchemaPermissions?: Maybe<Scalars['Boolean']>; enableRemoteSchemaPermissions?: Maybe<Scalars['Boolean']>;
@@ -1085,7 +1132,9 @@ export type ConfigHasuraSettingsUpdateInput = {
export type ConfigHasuraUpdateInput = { export type ConfigHasuraUpdateInput = {
adminSecret?: InputMaybe<Scalars['String']>; adminSecret?: InputMaybe<Scalars['String']>;
events?: InputMaybe<ConfigHasuraEventsUpdateInput>;
jwtSecrets?: InputMaybe<Array<ConfigJwtSecretUpdateInput>>; jwtSecrets?: InputMaybe<Array<ConfigJwtSecretUpdateInput>>;
logs?: InputMaybe<ConfigHasuraLogsUpdateInput>;
resources?: InputMaybe<ConfigResourcesUpdateInput>; resources?: InputMaybe<ConfigResourcesUpdateInput>;
settings?: InputMaybe<ConfigHasuraSettingsUpdateInput>; settings?: InputMaybe<ConfigHasuraSettingsUpdateInput>;
version?: InputMaybe<Scalars['String']>; version?: InputMaybe<Scalars['String']>;
@@ -1639,6 +1688,17 @@ export type Log = {
timestamp: Scalars['Timestamp']; timestamp: Scalars['Timestamp'];
}; };
export type Metrics = {
__typename?: 'Metrics';
value: Scalars['float64'];
};
export type StatsLiveApps = {
__typename?: 'StatsLiveApps';
appID: Array<Scalars['uuid']>;
count: Scalars['Int'];
};
/** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */ /** Boolean expression to compare columns of type "String". All fields are combined with logical 'AND'. */
export type String_Comparison_Exp = { export type String_Comparison_Exp = {
_eq?: InputMaybe<Scalars['String']>; _eq?: InputMaybe<Scalars['String']>;
@@ -4878,6 +4938,7 @@ export type Backups = {
appId: Scalars['uuid']; appId: Scalars['uuid'];
completedAt?: Maybe<Scalars['timestamptz']>; completedAt?: Maybe<Scalars['timestamptz']>;
createdAt: Scalars['timestamptz']; createdAt: Scalars['timestamptz'];
expiresAt?: Maybe<Scalars['timestamptz']>;
id: Scalars['uuid']; id: Scalars['uuid'];
size: Scalars['bigint']; size: Scalars['bigint'];
}; };
@@ -4965,6 +5026,7 @@ export type Backups_Bool_Exp = {
appId?: InputMaybe<Uuid_Comparison_Exp>; appId?: InputMaybe<Uuid_Comparison_Exp>;
completedAt?: InputMaybe<Timestamptz_Comparison_Exp>; completedAt?: InputMaybe<Timestamptz_Comparison_Exp>;
createdAt?: InputMaybe<Timestamptz_Comparison_Exp>; createdAt?: InputMaybe<Timestamptz_Comparison_Exp>;
expiresAt?: InputMaybe<Timestamptz_Comparison_Exp>;
id?: InputMaybe<Uuid_Comparison_Exp>; id?: InputMaybe<Uuid_Comparison_Exp>;
size?: InputMaybe<Bigint_Comparison_Exp>; size?: InputMaybe<Bigint_Comparison_Exp>;
}; };
@@ -4986,6 +5048,7 @@ export type Backups_Insert_Input = {
appId?: InputMaybe<Scalars['uuid']>; appId?: InputMaybe<Scalars['uuid']>;
completedAt?: InputMaybe<Scalars['timestamptz']>; completedAt?: InputMaybe<Scalars['timestamptz']>;
createdAt?: InputMaybe<Scalars['timestamptz']>; createdAt?: InputMaybe<Scalars['timestamptz']>;
expiresAt?: InputMaybe<Scalars['timestamptz']>;
id?: InputMaybe<Scalars['uuid']>; id?: InputMaybe<Scalars['uuid']>;
size?: InputMaybe<Scalars['bigint']>; size?: InputMaybe<Scalars['bigint']>;
}; };
@@ -4996,6 +5059,7 @@ export type Backups_Max_Fields = {
appId?: Maybe<Scalars['uuid']>; appId?: Maybe<Scalars['uuid']>;
completedAt?: Maybe<Scalars['timestamptz']>; completedAt?: Maybe<Scalars['timestamptz']>;
createdAt?: Maybe<Scalars['timestamptz']>; createdAt?: Maybe<Scalars['timestamptz']>;
expiresAt?: Maybe<Scalars['timestamptz']>;
id?: Maybe<Scalars['uuid']>; id?: Maybe<Scalars['uuid']>;
size?: Maybe<Scalars['bigint']>; size?: Maybe<Scalars['bigint']>;
}; };
@@ -5005,6 +5069,7 @@ export type Backups_Max_Order_By = {
appId?: InputMaybe<Order_By>; appId?: InputMaybe<Order_By>;
completedAt?: InputMaybe<Order_By>; completedAt?: InputMaybe<Order_By>;
createdAt?: InputMaybe<Order_By>; createdAt?: InputMaybe<Order_By>;
expiresAt?: InputMaybe<Order_By>;
id?: InputMaybe<Order_By>; id?: InputMaybe<Order_By>;
size?: InputMaybe<Order_By>; size?: InputMaybe<Order_By>;
}; };
@@ -5015,6 +5080,7 @@ export type Backups_Min_Fields = {
appId?: Maybe<Scalars['uuid']>; appId?: Maybe<Scalars['uuid']>;
completedAt?: Maybe<Scalars['timestamptz']>; completedAt?: Maybe<Scalars['timestamptz']>;
createdAt?: Maybe<Scalars['timestamptz']>; createdAt?: Maybe<Scalars['timestamptz']>;
expiresAt?: Maybe<Scalars['timestamptz']>;
id?: Maybe<Scalars['uuid']>; id?: Maybe<Scalars['uuid']>;
size?: Maybe<Scalars['bigint']>; size?: Maybe<Scalars['bigint']>;
}; };
@@ -5024,6 +5090,7 @@ export type Backups_Min_Order_By = {
appId?: InputMaybe<Order_By>; appId?: InputMaybe<Order_By>;
completedAt?: InputMaybe<Order_By>; completedAt?: InputMaybe<Order_By>;
createdAt?: InputMaybe<Order_By>; createdAt?: InputMaybe<Order_By>;
expiresAt?: InputMaybe<Order_By>;
id?: InputMaybe<Order_By>; id?: InputMaybe<Order_By>;
size?: InputMaybe<Order_By>; size?: InputMaybe<Order_By>;
}; };
@@ -5050,6 +5117,7 @@ export type Backups_Order_By = {
appId?: InputMaybe<Order_By>; appId?: InputMaybe<Order_By>;
completedAt?: InputMaybe<Order_By>; completedAt?: InputMaybe<Order_By>;
createdAt?: InputMaybe<Order_By>; createdAt?: InputMaybe<Order_By>;
expiresAt?: InputMaybe<Order_By>;
id?: InputMaybe<Order_By>; id?: InputMaybe<Order_By>;
size?: InputMaybe<Order_By>; size?: InputMaybe<Order_By>;
}; };
@@ -5068,6 +5136,8 @@ export enum Backups_Select_Column {
/** column name */ /** column name */
CreatedAt = 'createdAt', CreatedAt = 'createdAt',
/** column name */ /** column name */
ExpiresAt = 'expiresAt',
/** column name */
Id = 'id', Id = 'id',
/** column name */ /** column name */
Size = 'size' Size = 'size'
@@ -5078,6 +5148,7 @@ export type Backups_Set_Input = {
appId?: InputMaybe<Scalars['uuid']>; appId?: InputMaybe<Scalars['uuid']>;
completedAt?: InputMaybe<Scalars['timestamptz']>; completedAt?: InputMaybe<Scalars['timestamptz']>;
createdAt?: InputMaybe<Scalars['timestamptz']>; createdAt?: InputMaybe<Scalars['timestamptz']>;
expiresAt?: InputMaybe<Scalars['timestamptz']>;
id?: InputMaybe<Scalars['uuid']>; id?: InputMaybe<Scalars['uuid']>;
size?: InputMaybe<Scalars['bigint']>; size?: InputMaybe<Scalars['bigint']>;
}; };
@@ -5128,6 +5199,7 @@ export type Backups_Stream_Cursor_Value_Input = {
appId?: InputMaybe<Scalars['uuid']>; appId?: InputMaybe<Scalars['uuid']>;
completedAt?: InputMaybe<Scalars['timestamptz']>; completedAt?: InputMaybe<Scalars['timestamptz']>;
createdAt?: InputMaybe<Scalars['timestamptz']>; createdAt?: InputMaybe<Scalars['timestamptz']>;
expiresAt?: InputMaybe<Scalars['timestamptz']>;
id?: InputMaybe<Scalars['uuid']>; id?: InputMaybe<Scalars['uuid']>;
size?: InputMaybe<Scalars['bigint']>; size?: InputMaybe<Scalars['bigint']>;
}; };
@@ -5152,6 +5224,8 @@ export enum Backups_Update_Column {
/** column name */ /** column name */
CreatedAt = 'createdAt', CreatedAt = 'createdAt',
/** column name */ /** column name */
ExpiresAt = 'expiresAt',
/** column name */
Id = 'id', Id = 'id',
/** column name */ /** column name */
Size = 'size' Size = 'size'
@@ -9150,6 +9224,8 @@ export type Mutation_Root = {
/** insert a single row into the table: "regions" */ /** insert a single row into the table: "regions" */
insert_regions_one?: Maybe<Regions>; insert_regions_one?: Maybe<Regions>;
migrateRDSToPostgres: Scalars['Boolean']; migrateRDSToPostgres: Scalars['Boolean'];
pauseInactiveApps: Array<Scalars['String']>;
replaceConfig: ConfigConfig;
resetPostgresPassword: Scalars['Boolean']; resetPostgresPassword: Scalars['Boolean'];
restoreApplicationDatabase: Scalars['Boolean']; restoreApplicationDatabase: Scalars['Boolean'];
/** update single row of the table: "apps" */ /** update single row of the table: "apps" */
@@ -9338,9 +9414,16 @@ export type Mutation_Root = {
}; };
/** mutation root */
export type Mutation_RootBackupAllApplicationsDatabaseArgs = {
expireInDays?: InputMaybe<Scalars['Int']>;
};
/** mutation root */ /** mutation root */
export type Mutation_RootBackupApplicationDatabaseArgs = { export type Mutation_RootBackupApplicationDatabaseArgs = {
appID: Scalars['String']; appID: Scalars['String'];
expireInDays?: InputMaybe<Scalars['Int']>;
}; };
@@ -10160,6 +10243,13 @@ export type Mutation_RootMigrateRdsToPostgresArgs = {
}; };
/** mutation root */
export type Mutation_RootReplaceConfigArgs = {
appID: Scalars['uuid'];
config: ConfigConfigInsertInput;
};
/** mutation root */ /** mutation root */
export type Mutation_RootResetPostgresPasswordArgs = { export type Mutation_RootResetPostgresPasswordArgs = {
appID: Scalars['String']; appID: Scalars['String'];
@@ -11338,6 +11428,7 @@ export type Plans = {
featureBackupEnabled: Scalars['Boolean']; featureBackupEnabled: Scalars['Boolean'];
featureCustomDomainsEnabled: Scalars['Boolean']; featureCustomDomainsEnabled: Scalars['Boolean'];
featureCustomEmailTemplatesEnabled: Scalars['Boolean']; featureCustomEmailTemplatesEnabled: Scalars['Boolean'];
featureCustomResources: Scalars['Boolean'];
/** Weather or not to deploy email templates for git deployments */ /** Weather or not to deploy email templates for git deployments */
featureDeployEmailTemplates: Scalars['Boolean']; featureDeployEmailTemplates: Scalars['Boolean'];
/** Function execution timeout in seconds */ /** Function execution timeout in seconds */
@@ -11431,6 +11522,7 @@ export type Plans_Bool_Exp = {
featureBackupEnabled?: InputMaybe<Boolean_Comparison_Exp>; featureBackupEnabled?: InputMaybe<Boolean_Comparison_Exp>;
featureCustomDomainsEnabled?: InputMaybe<Boolean_Comparison_Exp>; featureCustomDomainsEnabled?: InputMaybe<Boolean_Comparison_Exp>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Boolean_Comparison_Exp>; featureCustomEmailTemplatesEnabled?: InputMaybe<Boolean_Comparison_Exp>;
featureCustomResources?: InputMaybe<Boolean_Comparison_Exp>;
featureDeployEmailTemplates?: InputMaybe<Boolean_Comparison_Exp>; featureDeployEmailTemplates?: InputMaybe<Boolean_Comparison_Exp>;
featureFunctionExecutionTimeout?: InputMaybe<Int_Comparison_Exp>; featureFunctionExecutionTimeout?: InputMaybe<Int_Comparison_Exp>;
featureMaxDbSize?: InputMaybe<Int_Comparison_Exp>; featureMaxDbSize?: InputMaybe<Int_Comparison_Exp>;
@@ -11472,6 +11564,7 @@ export type Plans_Insert_Input = {
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>; featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>; featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>; featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
/** Weather or not to deploy email templates for git deployments */ /** Weather or not to deploy email templates for git deployments */
featureDeployEmailTemplates?: InputMaybe<Scalars['Boolean']>; featureDeployEmailTemplates?: InputMaybe<Scalars['Boolean']>;
/** Function execution timeout in seconds */ /** Function execution timeout in seconds */
@@ -11557,6 +11650,7 @@ export type Plans_Order_By = {
featureBackupEnabled?: InputMaybe<Order_By>; featureBackupEnabled?: InputMaybe<Order_By>;
featureCustomDomainsEnabled?: InputMaybe<Order_By>; featureCustomDomainsEnabled?: InputMaybe<Order_By>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Order_By>; featureCustomEmailTemplatesEnabled?: InputMaybe<Order_By>;
featureCustomResources?: InputMaybe<Order_By>;
featureDeployEmailTemplates?: InputMaybe<Order_By>; featureDeployEmailTemplates?: InputMaybe<Order_By>;
featureFunctionExecutionTimeout?: InputMaybe<Order_By>; featureFunctionExecutionTimeout?: InputMaybe<Order_By>;
featureMaxDbSize?: InputMaybe<Order_By>; featureMaxDbSize?: InputMaybe<Order_By>;
@@ -11589,6 +11683,8 @@ export enum Plans_Select_Column {
/** column name */ /** column name */
FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled', FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled',
/** column name */ /** column name */
FeatureCustomResources = 'featureCustomResources',
/** column name */
FeatureDeployEmailTemplates = 'featureDeployEmailTemplates', FeatureDeployEmailTemplates = 'featureDeployEmailTemplates',
/** column name */ /** column name */
FeatureFunctionExecutionTimeout = 'featureFunctionExecutionTimeout', FeatureFunctionExecutionTimeout = 'featureFunctionExecutionTimeout',
@@ -11624,6 +11720,7 @@ export type Plans_Set_Input = {
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>; featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>; featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>; featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
/** Weather or not to deploy email templates for git deployments */ /** Weather or not to deploy email templates for git deployments */
featureDeployEmailTemplates?: InputMaybe<Scalars['Boolean']>; featureDeployEmailTemplates?: InputMaybe<Scalars['Boolean']>;
/** Function execution timeout in seconds */ /** Function execution timeout in seconds */
@@ -11696,6 +11793,7 @@ export type Plans_Stream_Cursor_Value_Input = {
featureBackupEnabled?: InputMaybe<Scalars['Boolean']>; featureBackupEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>; featureCustomDomainsEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>; featureCustomEmailTemplatesEnabled?: InputMaybe<Scalars['Boolean']>;
featureCustomResources?: InputMaybe<Scalars['Boolean']>;
/** Weather or not to deploy email templates for git deployments */ /** Weather or not to deploy email templates for git deployments */
featureDeployEmailTemplates?: InputMaybe<Scalars['Boolean']>; featureDeployEmailTemplates?: InputMaybe<Scalars['Boolean']>;
/** Function execution timeout in seconds */ /** Function execution timeout in seconds */
@@ -11739,6 +11837,8 @@ export enum Plans_Update_Column {
/** column name */ /** column name */
FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled', FeatureCustomEmailTemplatesEnabled = 'featureCustomEmailTemplatesEnabled',
/** column name */ /** column name */
FeatureCustomResources = 'featureCustomResources',
/** column name */
FeatureDeployEmailTemplates = 'featureDeployEmailTemplates', FeatureDeployEmailTemplates = 'featureDeployEmailTemplates',
/** column name */ /** column name */
FeatureFunctionExecutionTimeout = 'featureFunctionExecutionTimeout', FeatureFunctionExecutionTimeout = 'featureFunctionExecutionTimeout',
@@ -11947,6 +12047,13 @@ export type Query_Root = {
files: Array<Files>; files: Array<Files>;
/** fetch aggregated fields from the table: "storage.files" */ /** fetch aggregated fields from the table: "storage.files" */
filesAggregate: Files_Aggregate; filesAggregate: Files_Aggregate;
getCPUSecondsUsage: Metrics;
getEgressVolume: Metrics;
getFunctionsInvocations: Metrics;
getLogsVolume: Metrics;
getPostgresVolumeCapacity: Metrics;
getPostgresVolumeUsage: Metrics;
getTotalRequests: Metrics;
/** fetch data from the table: "github_app_installations" using primary key columns */ /** fetch data from the table: "github_app_installations" using primary key columns */
githubAppInstallation?: Maybe<GithubAppInstallations>; githubAppInstallation?: Maybe<GithubAppInstallations>;
/** fetch data from the table: "github_app_installations" */ /** fetch data from the table: "github_app_installations" */
@@ -11982,6 +12089,13 @@ export type Query_Root = {
regions_aggregate: Regions_Aggregate; regions_aggregate: Regions_Aggregate;
/** fetch data from the table: "regions" using primary key columns */ /** fetch data from the table: "regions" using primary key columns */
regions_by_pk?: Maybe<Regions>; regions_by_pk?: Maybe<Regions>;
/**
* Returns lists of apps that have some live traffic in the give time range.
* From defaults to 24 hours ago and to defaults to now.
*
* Requests that returned a 4xx or 5xx status code are not counted as live traffic.
*/
statsLiveApps: StatsLiveApps;
systemConfig?: Maybe<ConfigSystemConfig>; systemConfig?: Maybe<ConfigSystemConfig>;
systemConfigs: Array<ConfigAppSystemConfig>; systemConfigs: Array<ConfigAppSystemConfig>;
/** fetch data from the table: "auth.users" using primary key columns */ /** fetch data from the table: "auth.users" using primary key columns */
@@ -12511,6 +12625,54 @@ export type Query_RootFilesAggregateArgs = {
}; };
export type Query_RootGetCpuSecondsUsageArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGetEgressVolumeArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
subdomain: Scalars['String'];
to?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGetFunctionsInvocationsArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGetLogsVolumeArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGetPostgresVolumeCapacityArgs = {
appID: Scalars['String'];
t?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGetPostgresVolumeUsageArgs = {
appID: Scalars['String'];
t?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGetTotalRequestsArgs = {
appID: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootGithubAppInstallationArgs = { export type Query_RootGithubAppInstallationArgs = {
id: Scalars['uuid']; id: Scalars['uuid'];
}; };
@@ -12634,6 +12796,12 @@ export type Query_RootRegions_By_PkArgs = {
}; };
export type Query_RootStatsLiveAppsArgs = {
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
};
export type Query_RootSystemConfigArgs = { export type Query_RootSystemConfigArgs = {
appID: Scalars['uuid']; appID: Scalars['uuid'];
}; };
@@ -16385,11 +16553,31 @@ export type GetAppProvisionStatusQueryVariables = Exact<{
export type GetAppProvisionStatusQuery = { __typename?: 'query_root', apps: Array<{ __typename?: 'apps', id: any, isProvisioned: boolean, subdomain: string, createdAt: any }> }; export type GetAppProvisionStatusQuery = { __typename?: 'query_root', apps: Array<{ __typename?: 'apps', id: any, isProvisioned: boolean, subdomain: string, createdAt: any }> };
export type GetProjectMetricsQueryVariables = Exact<{
appId: Scalars['String'];
subdomain: Scalars['String'];
from?: InputMaybe<Scalars['Timestamp']>;
to?: InputMaybe<Scalars['Timestamp']>;
}>;
export type GetProjectMetricsQuery = { __typename?: 'query_root', logsVolume: { __typename?: 'Metrics', value: any }, cpuSecondsUsage: { __typename?: 'Metrics', value: any }, functionInvocations: { __typename?: 'Metrics', value: any }, postgresVolumeCapacity: { __typename?: 'Metrics', value: any }, postgresVolumeUsage: { __typename?: 'Metrics', value: any }, totalRequests: { __typename?: 'Metrics', value: any }, egressVolume: { __typename?: 'Metrics', value: any } };
export type GetRemoteAppRolesQueryVariables = Exact<{ [key: string]: never; }>; export type GetRemoteAppRolesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRemoteAppRolesQuery = { __typename?: 'query_root', authRoles: Array<{ __typename?: 'authRoles', role: string }> }; export type GetRemoteAppRolesQuery = { __typename?: 'query_root', authRoles: Array<{ __typename?: 'authRoles', role: string }> };
export type WorkspaceFragment = { __typename?: 'workspaces', id: any, name: string, slug: string, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
export type GetWorkspaceAndProjectQueryVariables = Exact<{
workspaceSlug: Scalars['String'];
projectSlug?: InputMaybe<Scalars['String']>;
}>;
export type GetWorkspaceAndProjectQuery = { __typename?: 'query_root', workspaces: Array<{ __typename?: 'workspaces', id: any, name: string, slug: string, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, type: string, user: { __typename?: 'users', id: any, email?: any | null, displayName: string } }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> }>, projects: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> };
export type InsertApplicationMutationVariables = Exact<{ export type InsertApplicationMutationVariables = Exact<{
app: Apps_Insert_Input; app: Apps_Insert_Input;
}>; }>;
@@ -16480,7 +16668,7 @@ export type GetSignInMethodsQueryVariables = Exact<{
}>; }>;
export type GetSignInMethodsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', provider?: { __typename: 'ConfigProvider', id: 'ConfigProvider', sms?: { __typename?: 'ConfigSms', accountSid: string, authToken: string, messagingServiceId: string, provider?: string | null } | null } | null, auth?: { __typename: 'ConfigAuth', id: 'ConfigAuth', method?: { __typename?: 'ConfigAuthMethod', emailPassword?: { __typename?: 'ConfigAuthMethodEmailPassword', emailVerificationRequired?: boolean | null, hibpEnabled?: boolean | null } | null, emailPasswordless?: { __typename?: 'ConfigAuthMethodEmailPasswordless', enabled?: boolean | null } | null, smsPasswordless?: { __typename?: 'ConfigAuthMethodSmsPasswordless', enabled?: boolean | null } | null, anonymous?: { __typename?: 'ConfigAuthMethodAnonymous', enabled?: boolean | null } | null, webauthn?: { __typename?: 'ConfigAuthMethodWebauthn', enabled?: boolean | null } | null, oauth?: { __typename?: 'ConfigAuthMethodOauth', apple?: { __typename?: 'ConfigAuthMethodOauthApple', enabled?: boolean | null, clientId?: string | null, keyId?: string | null, teamId?: string | null, privateKey?: string | null } | null, discord?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, facebook?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, github?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, google?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, linkedin?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, spotify?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitch?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitter?: { __typename?: 'ConfigAuthMethodOauthTwitter', enabled?: boolean | null, consumerKey?: string | null, consumerSecret?: string | null } | null, windowslive?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, workos?: { __typename?: 'ConfigAuthMethodOauthWorkos', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, connection?: string | null, organization?: string | null } | null } | null } | null } | null } | null }; export type GetSignInMethodsQuery = { __typename?: 'query_root', config?: { __typename: 'ConfigConfig', id: 'ConfigConfig', provider?: { __typename: 'ConfigProvider', id: 'ConfigProvider', sms?: { __typename?: 'ConfigSms', accountSid: string, authToken: string, messagingServiceId: string, provider?: string | null } | null } | null, auth?: { __typename: 'ConfigAuth', id: 'ConfigAuth', method?: { __typename?: 'ConfigAuthMethod', emailPassword?: { __typename?: 'ConfigAuthMethodEmailPassword', emailVerificationRequired?: boolean | null, hibpEnabled?: boolean | null } | null, emailPasswordless?: { __typename?: 'ConfigAuthMethodEmailPasswordless', enabled?: boolean | null } | null, smsPasswordless?: { __typename?: 'ConfigAuthMethodSmsPasswordless', enabled?: boolean | null } | null, anonymous?: { __typename?: 'ConfigAuthMethodAnonymous', enabled?: boolean | null } | null, webauthn?: { __typename?: 'ConfigAuthMethodWebauthn', enabled?: boolean | null } | null, oauth?: { __typename?: 'ConfigAuthMethodOauth', apple?: { __typename?: 'ConfigAuthMethodOauthApple', enabled?: boolean | null, clientId?: string | null, keyId?: string | null, teamId?: string | null, privateKey?: string | null } | null, discord?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, facebook?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, github?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, google?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, linkedin?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, spotify?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitch?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, twitter?: { __typename?: 'ConfigAuthMethodOauthTwitter', enabled?: boolean | null, consumerKey?: string | null, consumerSecret?: string | null } | null, windowslive?: { __typename?: 'ConfigStandardOauthProviderWithScope', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, scope?: Array<string> | null } | null, workos?: { __typename?: 'ConfigAuthMethodOauthWorkos', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, connection?: string | null, organization?: string | null } | null, azuread?: { __typename?: 'ConfigAuthMethodOauthAzuread', enabled?: boolean | null, clientId?: string | null, clientSecret?: string | null, tenant?: string | null } | null } | null } | null } | null } | null };
export type GetSmtpSettingsQueryVariables = Exact<{ export type GetSmtpSettingsQueryVariables = Exact<{
appId: Scalars['uuid']; appId: Scalars['uuid'];
@@ -16617,6 +16805,8 @@ export type GetFilesAggregateQueryVariables = Exact<{
export type GetFilesAggregateQuery = { __typename?: 'query_root', filesAggregate: { __typename?: 'files_aggregate', aggregate?: { __typename?: 'files_aggregate_fields', count: number } | null } }; export type GetFilesAggregateQuery = { __typename?: 'query_root', filesAggregate: { __typename?: 'files_aggregate', aggregate?: { __typename?: 'files_aggregate_fields', count: number } | null } };
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null };
export type GithubRepositoryFragment = { __typename?: 'githubRepositories', id: any, name: string, fullName: string, private: boolean, githubAppInstallation: { __typename?: 'githubAppInstallations', id: any, accountLogin?: string | null, accountType?: string | null, accountAvatarUrl?: string | null } }; export type GithubRepositoryFragment = { __typename?: 'githubRepositories', id: any, name: string, fullName: string, private: boolean, githubAppInstallation: { __typename?: 'githubAppInstallations', id: any, accountLogin?: string | null, accountType?: string | null, accountAvatarUrl?: string | null } };
export type GetGithubRepositoriesQueryVariables = Exact<{ [key: string]: never; }>; export type GetGithubRepositoriesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -16841,14 +17031,12 @@ export type GetFreeAndActiveProjectsQueryVariables = Exact<{
export type GetFreeAndActiveProjectsQuery = { __typename?: 'query_root', freeAndActiveProjects: Array<{ __typename?: 'apps', id: any }> }; export type GetFreeAndActiveProjectsQuery = { __typename?: 'query_root', freeAndActiveProjects: Array<{ __typename?: 'apps', id: any }> };
export type ProjectFragment = { __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }> };
export type GetOneUserQueryVariables = Exact<{ export type GetOneUserQueryVariables = Exact<{
userId: Scalars['uuid']; userId: Scalars['uuid'];
}>; }>;
export type GetOneUserQuery = { __typename?: 'query_root', user?: { __typename?: 'users', id: any, displayName: string, avatarUrl: string, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, userId: any, workspaceId: any, type: string, workspace: { __typename?: 'workspaces', creatorUserId?: any | null, id: any, slug: string, name: string, apps: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }> }> } }> } | null }; export type GetOneUserQuery = { __typename?: 'query_root', user?: { __typename?: 'users', id: any, displayName: string, avatarUrl: string, workspaceMembers: Array<{ __typename?: 'workspaceMembers', id: any, userId: any, workspaceId: any, type: string, workspace: { __typename?: 'workspaces', creatorUserId?: any | null, id: any, slug: string, name: string, apps: Array<{ __typename?: 'apps', id: any, slug: string, name: string, repositoryProductionBranch: string, subdomain: string, isProvisioned: boolean, createdAt: any, desiredState: number, nhostBaseFolder: string, providersUpdated?: boolean | null, config?: { __typename?: 'ConfigConfig', hasura: { __typename?: 'ConfigHasura', adminSecret: string } } | null, featureFlags: Array<{ __typename?: 'featureFlags', description: string, id: any, name: string, value: string }>, appStates: Array<{ __typename?: 'appStateHistory', id: any, appId: any, message?: string | null, stateId: number, createdAt: any }>, region: { __typename?: 'regions', id: any, countryCode: string, awsName: string, city: string }, plan: { __typename?: 'plans', id: any, name: string, isFree: boolean }, githubRepository?: { __typename?: 'githubRepositories', fullName: string } | null, deployments: Array<{ __typename?: 'deployments', id: any, commitSHA: string, commitMessage?: string | null, commitUserName?: string | null, deploymentStartedAt?: any | null, deploymentEndedAt?: any | null, commitUserAvatarUrl?: string | null, deploymentStatus?: string | null }>, creator?: { __typename?: 'users', id: any, email?: any | null, displayName: string } | null }> } }> } | null };
export type GetUserAllWorkspacesQueryVariables = Exact<{ [key: string]: never; }>; export type GetUserAllWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -17050,6 +17238,86 @@ export const GetAppByWorkspaceAndNameFragmentDoc = gql`
} }
} }
`; `;
export const ProjectFragmentDoc = gql`
fragment Project on apps {
id
slug
name
repositoryProductionBranch
subdomain
isProvisioned
createdAt
desiredState
nhostBaseFolder
providersUpdated
config(resolve: true) {
hasura {
adminSecret
}
}
featureFlags {
description
id
name
value
}
appStates(order_by: {createdAt: desc}, limit: 1) {
id
appId
message
stateId
createdAt
}
region {
id
countryCode
awsName
city
}
plan {
id
name
isFree
}
githubRepository {
fullName
}
deployments(limit: 4, order_by: {deploymentEndedAt: desc}) {
id
commitSHA
commitMessage
commitUserName
deploymentStartedAt
deploymentEndedAt
commitUserAvatarUrl
deploymentStatus
}
creator {
id
email
displayName
}
}
`;
export const WorkspaceFragmentDoc = gql`
fragment Workspace on workspaces {
id
name
slug
workspaceMembers {
id
user {
id
email
displayName
}
type
}
projects: apps {
...Project
}
}
${ProjectFragmentDoc}`;
export const PrefetchNewAppRegionsFragmentDoc = gql` export const PrefetchNewAppRegionsFragmentDoc = gql`
fragment PrefetchNewAppRegions on regions { fragment PrefetchNewAppRegions on regions {
id id
@@ -17218,62 +17486,6 @@ export const RemoteAppGetUsersFragmentDoc = gql`
disabled disabled
} }
`; `;
export const ProjectFragmentDoc = gql`
fragment Project on apps {
id
slug
name
repositoryProductionBranch
subdomain
isProvisioned
createdAt
desiredState
nhostBaseFolder
providersUpdated
config(resolve: true) {
hasura {
adminSecret
}
}
featureFlags {
description
id
name
value
}
appStates(order_by: {createdAt: desc}, limit: 1) {
id
appId
message
stateId
createdAt
}
region {
id
countryCode
awsName
city
}
plan {
id
name
isFree
}
githubRepository {
fullName
}
deployments(limit: 4, order_by: {deploymentEndedAt: desc}) {
id
commitSHA
commitMessage
commitUserName
deploymentStartedAt
deploymentEndedAt
commitUserAvatarUrl
deploymentStatus
}
}
`;
export const GetWorkspaceMembersWorkspaceMemberFragmentDoc = gql` export const GetWorkspaceMembersWorkspaceMemberFragmentDoc = gql`
fragment getWorkspaceMembersWorkspaceMember on workspaceMembers { fragment getWorkspaceMembersWorkspaceMember on workspaceMembers {
id id
@@ -17709,6 +17921,74 @@ export type GetAppProvisionStatusQueryResult = Apollo.QueryResult<GetAppProvisio
export function refetchGetAppProvisionStatusQuery(variables: GetAppProvisionStatusQueryVariables) { export function refetchGetAppProvisionStatusQuery(variables: GetAppProvisionStatusQueryVariables) {
return { query: GetAppProvisionStatusDocument, variables: variables } return { query: GetAppProvisionStatusDocument, variables: variables }
} }
export const GetProjectMetricsDocument = gql`
query GetProjectMetrics($appId: String!, $subdomain: String!, $from: Timestamp, $to: Timestamp) {
logsVolume: getLogsVolume(appID: $appId, from: $from, to: $to) {
value
}
cpuSecondsUsage: getCPUSecondsUsage(appID: $appId, from: $from, to: $to) {
value
}
functionInvocations: getFunctionsInvocations(
appID: $appId
from: $from
to: $to
) {
value
}
postgresVolumeCapacity: getPostgresVolumeCapacity(appID: $appId) {
value
}
postgresVolumeUsage: getPostgresVolumeUsage(appID: $appId) {
value
}
totalRequests: getTotalRequests(appID: $appId, from: $from, to: $to) {
value
}
egressVolume: getEgressVolume(
appID: $appId
subdomain: $subdomain
from: $from
to: $to
) {
value
}
}
`;
/**
* __useGetProjectMetricsQuery__
*
* To run a query within a React component, call `useGetProjectMetricsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetProjectMetricsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetProjectMetricsQuery({
* variables: {
* appId: // value for 'appId'
* subdomain: // value for 'subdomain'
* from: // value for 'from'
* to: // value for 'to'
* },
* });
*/
export function useGetProjectMetricsQuery(baseOptions: Apollo.QueryHookOptions<GetProjectMetricsQuery, GetProjectMetricsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetProjectMetricsQuery, GetProjectMetricsQueryVariables>(GetProjectMetricsDocument, options);
}
export function useGetProjectMetricsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProjectMetricsQuery, GetProjectMetricsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetProjectMetricsQuery, GetProjectMetricsQueryVariables>(GetProjectMetricsDocument, options);
}
export type GetProjectMetricsQueryHookResult = ReturnType<typeof useGetProjectMetricsQuery>;
export type GetProjectMetricsLazyQueryHookResult = ReturnType<typeof useGetProjectMetricsLazyQuery>;
export type GetProjectMetricsQueryResult = Apollo.QueryResult<GetProjectMetricsQuery, GetProjectMetricsQueryVariables>;
export function refetchGetProjectMetricsQuery(variables: GetProjectMetricsQueryVariables) {
return { query: GetProjectMetricsDocument, variables: variables }
}
export const GetRemoteAppRolesDocument = gql` export const GetRemoteAppRolesDocument = gql`
query getRemoteAppRoles { query getRemoteAppRoles {
authRoles { authRoles {
@@ -17746,6 +18026,49 @@ export type GetRemoteAppRolesQueryResult = Apollo.QueryResult<GetRemoteAppRolesQ
export function refetchGetRemoteAppRolesQuery(variables?: GetRemoteAppRolesQueryVariables) { export function refetchGetRemoteAppRolesQuery(variables?: GetRemoteAppRolesQueryVariables) {
return { query: GetRemoteAppRolesDocument, variables: variables } return { query: GetRemoteAppRolesDocument, variables: variables }
} }
export const GetWorkspaceAndProjectDocument = gql`
query GetWorkspaceAndProject($workspaceSlug: String!, $projectSlug: String) {
workspaces(where: {slug: {_eq: $workspaceSlug}}) {
...Workspace
}
projects: apps(where: {slug: {_eq: $projectSlug}}) {
...Project
}
}
${WorkspaceFragmentDoc}
${ProjectFragmentDoc}`;
/**
* __useGetWorkspaceAndProjectQuery__
*
* To run a query within a React component, call `useGetWorkspaceAndProjectQuery` and pass it any options that fit your needs.
* When your component renders, `useGetWorkspaceAndProjectQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetWorkspaceAndProjectQuery({
* variables: {
* workspaceSlug: // value for 'workspaceSlug'
* projectSlug: // value for 'projectSlug'
* },
* });
*/
export function useGetWorkspaceAndProjectQuery(baseOptions: Apollo.QueryHookOptions<GetWorkspaceAndProjectQuery, GetWorkspaceAndProjectQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetWorkspaceAndProjectQuery, GetWorkspaceAndProjectQueryVariables>(GetWorkspaceAndProjectDocument, options);
}
export function useGetWorkspaceAndProjectLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetWorkspaceAndProjectQuery, GetWorkspaceAndProjectQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetWorkspaceAndProjectQuery, GetWorkspaceAndProjectQueryVariables>(GetWorkspaceAndProjectDocument, options);
}
export type GetWorkspaceAndProjectQueryHookResult = ReturnType<typeof useGetWorkspaceAndProjectQuery>;
export type GetWorkspaceAndProjectLazyQueryHookResult = ReturnType<typeof useGetWorkspaceAndProjectLazyQuery>;
export type GetWorkspaceAndProjectQueryResult = Apollo.QueryResult<GetWorkspaceAndProjectQuery, GetWorkspaceAndProjectQueryVariables>;
export function refetchGetWorkspaceAndProjectQuery(variables: GetWorkspaceAndProjectQueryVariables) {
return { query: GetWorkspaceAndProjectDocument, variables: variables }
}
export const InsertApplicationDocument = gql` export const InsertApplicationDocument = gql`
mutation insertApplication($app: apps_insert_input!) { mutation insertApplication($app: apps_insert_input!) {
insertApp(object: $app) { insertApp(object: $app) {
@@ -18282,6 +18605,12 @@ export const GetSignInMethodsDocument = gql`
connection connection
organization organization
} }
azuread {
enabled
clientId
clientSecret
tenant
}
} }
} }
} }

View File

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

View File

@@ -0,0 +1,79 @@
import prettifyNumber from './prettifyNumber';
test('should throw an error if multiplier is lower than 0', () => {
expect(() => prettifyNumber(1000, { multiplier: -1 })).toThrowError(
'Multiplier must be greater than 0',
);
});
test('should return the input value if it is lower than the multiplier', () => {
expect(prettifyNumber(980)).toBe('980');
expect(prettifyNumber(-980)).toBe('-980');
expect(prettifyNumber(1500, { multiplier: 2000 })).toBe('1500');
expect(prettifyNumber(-1500, { multiplier: 2000 })).toBe('-1500');
});
test('should return the converted value', () => {
expect(prettifyNumber(1000)).toBe('1k');
expect(prettifyNumber(-1000)).toBe('-1k');
expect(prettifyNumber(1420)).toBe('1.42k');
expect(prettifyNumber(-1420)).toBe('-1.42k');
expect(prettifyNumber(54091776)).toBe('54.09M');
expect(prettifyNumber(-54091776)).toBe('-54.09M');
expect(prettifyNumber(23475400000)).toBe('23.48B');
expect(prettifyNumber(-23475400000)).toBe('-23.48B');
});
test('should return the converted value with custom multiplier', () => {
expect(prettifyNumber(1024, { multiplier: 1024 })).toBe('1k');
expect(prettifyNumber(1420, { multiplier: 1024 })).toBe('1.39k');
expect(prettifyNumber(54091776, { multiplier: 1024 })).toBe('51.59M');
expect(prettifyNumber(23475400000, { multiplier: 1024 })).toBe('21.86B');
});
test('should be able to change the labels', () => {
const customLabels = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB',
];
expect(prettifyNumber(1024, { labels: customLabels, multiplier: 1024 })).toBe(
'1KiB',
);
expect(prettifyNumber(1420, { labels: customLabels, multiplier: 1024 })).toBe(
'1.39KiB',
);
expect(
prettifyNumber(54091776, { labels: customLabels, multiplier: 1024 }),
).toBe('51.59MiB');
expect(
prettifyNumber(23475400000, { labels: customLabels, multiplier: 1024 }),
).toBe('21.86GiB');
});
test('should be able to change the separator', () => {
expect(prettifyNumber(1024, { separator: ' ' })).toBe('1.02 k');
expect(prettifyNumber(1420, { separator: ' ' })).toBe('1.42 k');
expect(prettifyNumber(54091776, { separator: ' ' })).toBe('54.09 M');
expect(prettifyNumber(23475400000, { separator: ' ' })).toBe('23.48 B');
});
test('should be able to change the number of decimals', () => {
expect(prettifyNumber(1024, { numberOfDecimals: 0 })).toBe('1k');
expect(prettifyNumber(1420, { numberOfDecimals: 0 })).toBe('1k');
expect(prettifyNumber(54091776, { numberOfDecimals: 0 })).toBe('54M');
expect(prettifyNumber(23475400000, { numberOfDecimals: 0 })).toBe('23B');
});
test('should always use the last available label if the value is too large', () => {
expect(prettifyNumber(10000000, { labels: ['Bytes', 'KB'] })).toBe('10000KB');
expect(prettifyNumber(10000, { labels: [] })).toBe('10000');
});

View File

@@ -0,0 +1,90 @@
export interface PrettifyNumberOptions {
/**
* Multiplier for value. Useful if you want to convert to a different unit.
* Must be greater than 0.
*
* @default 1000
*/
multiplier?: number;
/**
* Labels used for prettified numbers.
*
* @default ['','k','M','B','T']
*/
labels?: string[];
/**
* Maximum number of decimals to use.
*
* @default 2
*/
numberOfDecimals?: number;
/**
* Separator between value and label.
*
* @default ''
*/
separator?: string;
}
function formatValue(value: number, numberOfDecimals: number) {
return value.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: numberOfDecimals,
useGrouping: false,
});
}
/**
* Prettify a `value`. For every `multiplier` the value will be divided and the
* next label will be used.
*
* @example
* ```js
* 1000 => '1k' // Given that "k" is the next label
* 1000000 => '1M' // Given that "M" is the next label
* 1234567 => '1.23M' // Given that "M" is the next label
* ```
*
* @param value - Value to prettify
* @param options - Configuration options
* @returns Prettified value
*/
export default function prettifyNumber(
value: number,
options?: PrettifyNumberOptions,
) {
const {
multiplier = 1000,
numberOfDecimals = 2,
labels = ['', 'k', 'M', 'B', 'T'],
separator = '',
} = options || {};
if (multiplier < 0) {
throw new Error('Multiplier must be greater than 0');
}
if (Math.abs(value) < multiplier) {
const label = labels?.[0];
return [formatValue(value, numberOfDecimals), label]
.filter(Boolean)
.join(separator)
.trim();
}
// Power should be between 0 and the length of the labels array
const power = Math.min(
Math.max(labels.length - 1, 0),
Math.floor(Math.log(Math.abs(value)) / Math.log(multiplier)),
);
const formattedValue = formatValue(
value / multiplier ** power,
numberOfDecimals,
);
const label = labels?.[power];
return [formattedValue, label].filter(Boolean).join(separator).trim();
}

View File

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

View File

@@ -0,0 +1,20 @@
import type { PrettifyNumberOptions } from '@/utils/common/prettifyNumber';
import { prettifyNumber } from '@/utils/common/prettifyNumber';
/**
* Prettify a size value in bytes.
*
* @param size - Size in bytes
* @param options - Configuration options
* @returns Prettified size
*/
export default function prettifySize(
size: number,
options?: PrettifyNumberOptions,
) {
return prettifyNumber(size, {
labels: ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
separator: ' ',
...options,
});
}

View File

@@ -8,18 +8,12 @@ type AcceptedState =
| ApplicationStatus.Updating; | ApplicationStatus.Updating;
function checkIfAcceptedState(appState: ApplicationStatus) { function checkIfAcceptedState(appState: ApplicationStatus) {
switch (appState) { return [
case ApplicationStatus.Provisioning: ApplicationStatus.Provisioning,
return true; ApplicationStatus.Unpausing,
case ApplicationStatus.Unpausing: ApplicationStatus.Pausing,
return true; ApplicationStatus.Updating,
case ApplicationStatus.Pausing: ].includes(appState);
return true;
case ApplicationStatus.Updating:
return true;
default:
return false;
}
} }
/** /**

View File

@@ -1,5 +1,17 @@
# @nhost/docs # @nhost/docs
## 0.0.16
### Patch Changes
- e7cb5070: fix(docs): use updated URLs everywhere
## 0.0.15
### Patch Changes
- 0795d1c6: chore: add link to event triggers on the serverless functions page
## 0.0.14 ## 0.0.14
### Patch Changes ### Patch Changes

View File

@@ -85,13 +85,13 @@ Hasura Console starts automatically and your Nhost project is running locally.
## Subdomain and Region ## Subdomain and Region
Use `localhost` as the `subdomain`, and skip `region` when using the CLI and the [JavaScript SDK](/reference/javascript): Use `local` as the `subdomain`, and skip `region` when using the CLI and the [JavaScript SDK](/reference/javascript):
```js ```js
import { NhostClient } from '@nhost/nhost-js' import { NhostClient } from '@nhost/nhost-js'
const nhost = new NhostClient({ const nhost = new NhostClient({
subdomain: 'localhost' subdomain: 'local'
}) })
``` ```
@@ -104,18 +104,19 @@ The Mailhog address is listed after starting [`nhost up`](/reference/cli/up):
``` ```
$ nhost up $ nhost up
URLs: URLs:
- Postgres: postgres://postgres:postgres@localhost:5432/postgres - Postgres: postgres://postgres:postgres@local.db.nhost.run:5432/postgres
- GraphQL: http://localhost:1337/v1/graphql - Hasura: https://local.hasura.nhost.run
- Auth: http://localhost:1337/v1/auth - GraphQL: https://local.graphql.nhost.run/v1
- Storage: http://localhost:1337/v1/storage - Auth: https://local.auth.nhost.run/v1
- Functions: http://localhost:1337/v1/functions - Storage: https://local.storage.nhost.run/v1
- Functions: https://local.functions.nhost.run/v1
- Hasura console: http://localhost:9695 - Dashboard: http://localhost:3030
// highlight-start // highlight-start
- Mailhog: http://localhost:8025 - Mailhog: http://localhost:8025
// highlight-end // highlight-end
- subdomain: localhost - subdomain: local
- region: (empty) - region: (empty)
``` ```

View File

@@ -314,14 +314,14 @@ Every JavaScript and TypeScript file inside the `functions/` folder becomes an
API endpoint. API endpoint.
Locally, the base URL for the serverless functions is Locally, the base URL for the serverless functions is
`http://localhost:1337/v1/functions`. Then, the endpoint for each function is `http://local.functions.nhost.run/v1/`. Then, the endpoint for each function is
determined by its filename or the name of its dedicated parent directory. determined by its filename or the name of its dedicated parent directory.
For example, the endpoint for our function is For example, the endpoint for our function is
`http://localhost:1337/v1/functions/time`. `http://local.functions.nhost.run/v1/time`.
```bash ```bash
curl http://localhost:1337/v1/functions/time\?name\=Greg curl http://local.functions.nhost.run/v1/time\?name\=Greg
Hello Greg! It's now: Wed, 27 Apr 2022 18:52:12 GMT Hello Greg! It's now: Wed, 27 Apr 2022 18:52:12 GMT
``` ```

View File

@@ -24,7 +24,7 @@ Building your GraphQL API is a lot of work, but with Nhost it's easy because eve
## Endpoint ## Endpoint
The GraphQL API is available at `https://[subdomain].graphql.[region].nhost.run/v1` When using the [CLI](/cli) the GraphQL API is available at `http://localhost:1337/v1/graphql`. The GraphQL API is available at `https://[subdomain].graphql.[region].nhost.run/v1` When using the [CLI](/cli) the GraphQL API is available at `https://local.graphql.nhost.run/v1`.
## GraphQL Clients for JavaScript ## GraphQL Clients for JavaScript

View File

@@ -160,7 +160,7 @@ Learn more about the [Nhost CLI](/cli).
Test the Stripe GraphQL API in the browser: Test the Stripe GraphQL API in the browser:
[http://localhost:1337/v1/functions/graphql/stripe](http://localhost:1337/v1/functions/graphql/stripe) [https://local.functions.nhost.run/v1/graphql/stripe](https://local.functions.nhost.run/v1/graphql/stripe)
## Remote Schema ## Remote Schema

View File

@@ -84,6 +84,8 @@ older (`functions/_utils/<utils-files>.js`).
[Environment variables](/platform/environment-variables) are available inside your Serverless Functions. Both in production and when running Nhost locally using the [Nhost CLI](/cli). [Environment variables](/platform/environment-variables) are available inside your Serverless Functions. Both in production and when running Nhost locally using the [Nhost CLI](/cli).
The same [environment variables that are used to configure event triggers](https://docs.nhost.io/database/event-triggers#format) can be used to authenticate regular serverless functions.
## Examples ## Examples
We have multiple examples of Serverless Functions in our [Nhost repository](https://github.com/nhost/nhost/tree/main/examples/serverless-functions/functions). We have multiple examples of Serverless Functions in our [Nhost repository](https://github.com/nhost/nhost/tree/main/examples/serverless-functions/functions).

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/docs", "name": "@nhost/docs",
"version": "0.0.14", "version": "0.0.16",
"private": true, "private": true,
"scripts": { "scripts": {
"docusaurus": "docusaurus", "docusaurus": "docusaurus",

View File

@@ -13,7 +13,7 @@ services:
postgres_password: postgres postgres_password: postgres
postgres_user: postgres postgres_user: postgres
auth: auth:
image: nhost/hasura-auth:0.16.1 image: nhost/hasura-auth:0.16.2
storage: storage:
image: nhost/hasura-storage:0.3.0 image: nhost/hasura-storage:0.3.0
auth: auth:

View File

@@ -13,7 +13,7 @@ services:
postgres_password: postgres postgres_password: postgres
postgres_user: postgres postgres_user: postgres
auth: auth:
image: nhost/hasura-auth:0.16.1 image: nhost/hasura-auth:0.16.2
storage: storage:
image: nhost/hasura-storage:0.3.0 image: nhost/hasura-storage:0.3.0
auth: auth:

View File

@@ -13,7 +13,7 @@ services:
postgres_password: postgres postgres_password: postgres
postgres_user: postgres postgres_user: postgres
auth: auth:
image: nhost/hasura-auth:0.16.1 image: nhost/hasura-auth:0.16.2
storage: storage:
image: nhost/hasura-storage:0.3.0 image: nhost/hasura-storage:0.3.0
auth: auth:

View File

@@ -1,5 +1,19 @@
# @nhost/apollo # @nhost/apollo
## 5.2.1
### Patch Changes
- 0d73e87a: fix(ws): don't open unnecessary connections
- 0d73e87a: fix(ws): increase retry attempts and implement exponential backoff
## 5.2.0
### Patch Changes
- Updated dependencies [a0e093d7]
- @nhost/nhost-js@2.2.0
## 5.1.3 ## 5.1.3
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/apollo", "name": "@nhost/apollo",
"version": "5.1.3", "version": "5.2.1",
"description": "Nhost Apollo Client library", "description": "Nhost Apollo Client library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -72,7 +72,22 @@ export const createApolloClient = ({
? createRestartableClient({ ? createRestartableClient({
url: uri.startsWith('https') ? uri.replace(/^https/, 'wss') : uri.replace(/^http/, 'ws'), url: uri.startsWith('https') ? uri.replace(/^https/, 'wss') : uri.replace(/^http/, 'ws'),
shouldRetry: () => true, shouldRetry: () => true,
retryAttempts: 10, retryAttempts: 100,
retryWait: async (retries) => {
// start with 1 second delay
const baseDelay = 1000
// max 3 seconds of jitter
const maxJitter = 3000
// exponential backoff with jitter
return new Promise((resolve) =>
setTimeout(
resolve,
baseDelay * Math.pow(2, retries) + Math.floor(Math.random() * maxJitter)
)
)
},
connectionParams: () => ({ connectionParams: () => ({
headers: { headers: {
...headers, ...headers,
@@ -141,7 +156,7 @@ export const createApolloClient = ({
// update token // update token
token = state.context.accessToken.value token = state.context.accessToken.value
if (!isBrowser) { if (!isBrowser || !wsClient?.isOpen()) {
return return
} }

View File

@@ -3,6 +3,7 @@ import { Client, ClientOptions, createClient } from 'graphql-ws'
export interface RestartableClient extends Client { export interface RestartableClient extends Client {
restart(): void restart(): void
isOpen(): boolean
} }
export function createRestartableClient(options: ClientOptions): RestartableClient { export function createRestartableClient(options: ClientOptions): RestartableClient {
@@ -10,6 +11,8 @@ export function createRestartableClient(options: ClientOptions): RestartableClie
let restart = () => { let restart = () => {
restartRequested = true restartRequested = true
} }
let connectionOpen = false
let socket: WebSocket let socket: WebSocket
let timedOut: NodeJS.Timeout let timedOut: NodeJS.Timeout
@@ -46,6 +49,7 @@ export function createRestartableClient(options: ClientOptions): RestartableClie
opened: (originalSocket) => { opened: (originalSocket) => {
socket = originalSocket as WebSocket socket = originalSocket as WebSocket
options.on?.opened?.(socket) options.on?.opened?.(socket)
connectionOpen = true
restart = () => { restart = () => {
if (socket.readyState === WebSocket.OPEN) { if (socket.readyState === WebSocket.OPEN) {
@@ -63,12 +67,17 @@ export function createRestartableClient(options: ClientOptions): RestartableClie
restartRequested = false restartRequested = false
restart() restart()
} }
},
closed: (event) => {
options?.on?.closed?.(event)
connectionOpen = false
} }
} }
}) })
return { return {
...client, ...client,
restart: () => restart() restart: () => restart(),
isOpen: () => connectionOpen
} }
} }

View File

@@ -1,5 +1,20 @@
# @nhost/react-apollo # @nhost/react-apollo
## 5.0.16
### Patch Changes
- Updated dependencies [0d73e87a]
- Updated dependencies [0d73e87a]
- @nhost/apollo@5.2.1
## 5.0.15
### Patch Changes
- @nhost/apollo@5.2.0
- @nhost/react@2.0.13
## 5.0.14 ## 5.0.14
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-apollo", "name": "@nhost/react-apollo",
"version": "5.0.14", "version": "5.0.16",
"description": "Nhost React Apollo client", "description": "Nhost React Apollo client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,17 @@
# @nhost/react-urql # @nhost/react-urql
## 2.0.14
### Patch Changes
- 09cf5d4b: chore(deps): bump `urql` to v4
## 2.0.13
### Patch Changes
- @nhost/react@2.0.13
## 2.0.12 ## 2.0.12
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-urql", "name": "@nhost/react-urql",
"version": "2.0.12", "version": "2.0.14",
"description": "Nhost React URQL client", "description": "Nhost React URQL client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
@@ -75,6 +75,6 @@
"graphql": "16.6.0", "graphql": "16.6.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"urql": "^3.0.3" "urql": "^4.0.0"
} }
} }

View File

@@ -76,7 +76,7 @@
"husky": "^8.0.1", "husky": "^8.0.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"turbo": "1.8.5", "turbo": "1.8.8",
"typedoc": "^0.22.18", "typedoc": "^0.22.18",
"typescript": "4.9.5", "typescript": "4.9.5",
"vite": "^4.0.2", "vite": "^4.0.2",

View File

@@ -1,5 +1,11 @@
# @nhost/hasura-auth-js # @nhost/hasura-auth-js
## 2.1.0
### Minor Changes
- a0e093d7: fix(exports): don't use conflicting names in exports
## 2.0.2 ## 2.0.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/hasura-auth-js", "name": "@nhost/hasura-auth-js",
"version": "2.0.2", "version": "2.1.0",
"description": "Hasura-auth client", "description": "Hasura-auth client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
@@ -43,7 +43,8 @@
}, },
"scripts": { "scripts": {
"dev": "vite build --config ./vite.dev.config.js", "dev": "vite build --config ./vite.dev.config.js",
"build": "run-p build:lib build:umd", "build": "run-p typecheck build:lib build:umd",
"typecheck": "tsc --noEmit",
"build:lib": "vite build", "build:lib": "vite build",
"build:umd": "vite build --config ../../config/vite.lib.umd.config.js", "build:umd": "vite build --config ../../config/vite.lib.umd.config.js",
"test": "vitest run", "test": "vitest run",

View File

@@ -1,4 +1,4 @@
import { ErrorPayload } from './types' import { AuthErrorPayload } from './types'
export const NETWORK_ERROR_CODE = 0 export const NETWORK_ERROR_CODE = 0
export const OTHER_ERROR_CODE = 1 export const OTHER_ERROR_CODE = 1
@@ -12,8 +12,8 @@ export const STATE_ERROR_CODE = 20
* See https://github.com/statelyai/xstate/issues/3037 * See https://github.com/statelyai/xstate/issues/3037
*/ */
export class CodifiedError extends Error { export class CodifiedError extends Error {
error: ErrorPayload error: AuthErrorPayload
constructor(original: Error | ErrorPayload) { constructor(original: Error | AuthErrorPayload) {
super(original.message) super(original.message)
Error.captureStackTrace(this, this.constructor) Error.captureStackTrace(this, this.constructor)
if (original instanceof Error) { if (original instanceof Error) {
@@ -30,95 +30,95 @@ export class CodifiedError extends Error {
} }
} }
export type ValidationErrorPayload = ErrorPayload & { status: typeof VALIDATION_ERROR_CODE } export type ValidationAuthErrorPayload = AuthErrorPayload & { status: typeof VALIDATION_ERROR_CODE }
// TODO share with hasura-auth // TODO share with hasura-auth
export const INVALID_EMAIL_ERROR: ValidationErrorPayload = { export const INVALID_EMAIL_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-email', error: 'invalid-email',
message: 'Email is incorrectly formatted' message: 'Email is incorrectly formatted'
} }
export const INVALID_MFA_TYPE_ERROR: ValidationErrorPayload = { export const INVALID_MFA_TYPE_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-mfa-type', error: 'invalid-mfa-type',
message: 'MFA type is invalid' message: 'MFA type is invalid'
} }
export const INVALID_MFA_CODE_ERROR: ValidationErrorPayload = { export const INVALID_MFA_CODE_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-mfa-code', error: 'invalid-mfa-code',
message: 'MFA code is invalid' message: 'MFA code is invalid'
} }
export const INVALID_PASSWORD_ERROR: ValidationErrorPayload = { export const INVALID_PASSWORD_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-password', error: 'invalid-password',
message: 'Password is incorrectly formatted' message: 'Password is incorrectly formatted'
} }
export const INVALID_PHONE_NUMBER_ERROR: ValidationErrorPayload = { export const INVALID_PHONE_NUMBER_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-phone-number', error: 'invalid-phone-number',
message: 'Phone number is incorrectly formatted' message: 'Phone number is incorrectly formatted'
} }
export const INVALID_MFA_TICKET_ERROR: ValidationErrorPayload = { export const INVALID_MFA_TICKET_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-mfa-ticket', error: 'invalid-mfa-ticket',
message: 'MFA ticket is invalid' message: 'MFA ticket is invalid'
} }
export const NO_MFA_TICKET_ERROR: ValidationErrorPayload = { export const NO_MFA_TICKET_ERROR: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'no-mfa-ticket', error: 'no-mfa-ticket',
message: 'No MFA ticket has been provided' message: 'No MFA ticket has been provided'
} }
export const NO_REFRESH_TOKEN: ValidationErrorPayload = { export const NO_REFRESH_TOKEN: ValidationAuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'no-refresh-token', error: 'no-refresh-token',
message: 'No refresh token has been provided' message: 'No refresh token has been provided'
} }
export const TOKEN_REFRESHER_RUNNING_ERROR: ErrorPayload = { export const TOKEN_REFRESHER_RUNNING_ERROR: AuthErrorPayload = {
status: STATE_ERROR_CODE, status: STATE_ERROR_CODE,
error: 'refresher-already-running', error: 'refresher-already-running',
message: message:
'The token refresher is already running. You must wait until is has finished before submitting a new token.' 'The token refresher is already running. You must wait until is has finished before submitting a new token.'
} }
export const USER_ALREADY_SIGNED_IN: ErrorPayload = { export const USER_ALREADY_SIGNED_IN: AuthErrorPayload = {
status: STATE_ERROR_CODE, status: STATE_ERROR_CODE,
error: 'already-signed-in', error: 'already-signed-in',
message: 'User is already signed in' message: 'User is already signed in'
} }
export const USER_UNAUTHENTICATED: ErrorPayload = { export const USER_UNAUTHENTICATED: AuthErrorPayload = {
status: STATE_ERROR_CODE, status: STATE_ERROR_CODE,
error: 'unauthenticated-user', error: 'unauthenticated-user',
message: 'User is not authenticated' message: 'User is not authenticated'
} }
export const USER_NOT_ANONYMOUS: ErrorPayload = { export const USER_NOT_ANONYMOUS: AuthErrorPayload = {
status: STATE_ERROR_CODE, status: STATE_ERROR_CODE,
error: 'user-not-anonymous', error: 'user-not-anonymous',
message: 'User is not anonymous' message: 'User is not anonymous'
} }
export const EMAIL_NEEDS_VERIFICATION: ErrorPayload = { export const EMAIL_NEEDS_VERIFICATION: AuthErrorPayload = {
status: STATE_ERROR_CODE, status: STATE_ERROR_CODE,
error: 'unverified-user', error: 'unverified-user',
message: 'Email needs verification' message: 'Email needs verification'
} }
export const INVALID_REFRESH_TOKEN = { export const INVALID_REFRESH_TOKEN: AuthErrorPayload = {
status: VALIDATION_ERROR_CODE, status: VALIDATION_ERROR_CODE,
error: 'invalid-refresh-token', error: 'invalid-refresh-token',
message: 'Invalid or expired refresh token' message: 'Invalid or expired refresh token'
} }
export const INVALID_SIGN_IN_METHOD = { export const INVALID_SIGN_IN_METHOD: AuthErrorPayload = {
status: OTHER_ERROR_CODE, status: OTHER_ERROR_CODE,
error: 'invalid-sign-in-method', error: 'invalid-sign-in-method',
message: 'Invalid sign-in method' message: 'Invalid sign-in method'

View File

@@ -34,13 +34,13 @@ import {
} from './promises' } from './promises'
import { import {
AuthChangedFunction, AuthChangedFunction,
AuthErrorPayload,
ChangeEmailParams, ChangeEmailParams,
ChangeEmailResponse, ChangeEmailResponse,
ChangePasswordParams, ChangePasswordParams,
ChangePasswordResponse, ChangePasswordResponse,
DeanonymizeParams, DeanonymizeParams,
DeanonymizeResponse, DeanonymizeResponse,
ErrorPayload,
JWTClaims, JWTClaims,
JWTHasuraClaims, JWTHasuraClaims,
NhostAuthConstructorParams, NhostAuthConstructorParams,
@@ -411,7 +411,7 @@ export class HasuraAuthClient {
*/ */
async addSecurityKey( async addSecurityKey(
nickname?: string nickname?: string
): Promise<{ error: ErrorPayload | null; key?: SecurityKey }> { ): Promise<{ error: AuthErrorPayload | null; key?: SecurityKey }> {
const { error, key } = await addSecurityKeyPromise(this._client, nickname) const { error, key } = await addSecurityKeyPromise(this._client, nickname)
return { error, key } return { error, key }
} }

View File

@@ -1,4 +1,4 @@
import { ErrorPayload, User } from '../../types' import { AuthErrorPayload, User } from '../../types'
export type StateErrorTypes = 'registration' | 'authentication' | 'signout' export type StateErrorTypes = 'registration' | 'authentication' | 'signout'
@@ -21,7 +21,7 @@ export type AuthContext = {
} }
/** Number of times the user tried to get an access token from a refresh token but got a network error */ /** Number of times the user tried to get an access token from a refresh token but got a network error */
importTokenAttempts: number importTokenAttempts: number
errors: Partial<Record<StateErrorTypes, ErrorPayload>> errors: Partial<Record<StateErrorTypes, AuthErrorPayload>>
} }
export const INITIAL_MACHINE_CONTEXT: AuthContext = { export const INITIAL_MACHINE_CONTEXT: AuthContext = {

View File

@@ -24,9 +24,9 @@ import {
} from '../../errors' } from '../../errors'
import { localStorageGetter, localStorageSetter } from '../../local-storage' import { localStorageGetter, localStorageSetter } from '../../local-storage'
import { import {
AuthErrorPayload,
AuthOptions, AuthOptions,
DeanonymizeResponse, DeanonymizeResponse,
ErrorPayload,
NhostSession, NhostSession,
NhostSessionResponse, NhostSessionResponse,
PasswordlessEmailResponse, PasswordlessEmailResponse,
@@ -856,7 +856,7 @@ export const createAuthMachine = ({
error: null error: null
} }
} }
let error: ErrorPayload | null = null let error: AuthErrorPayload | null = null
if (autoSignIn) { if (autoSignIn) {
const urlToken = getParameterByName('refreshToken') || null const urlToken = getParameterByName('refreshToken') || null
if (urlToken) { if (urlToken) {
@@ -866,7 +866,7 @@ export const createAuthMachine = ({
}) })
return { session, error: null } return { session, error: null }
} catch (exception) { } catch (exception) {
error = (exception as { error: ErrorPayload }).error error = (exception as { error: AuthErrorPayload }).error
} }
} else { } else {
const error = getParameterByName('error') const error = getParameterByName('error')
@@ -890,7 +890,7 @@ export const createAuthMachine = ({
}) })
return { session, error: null } return { session, error: null }
} catch (exception) { } catch (exception) {
error = (exception as { error: ErrorPayload }).error error = (exception as { error: AuthErrorPayload }).error
} }
} }
if (error) { if (error) {

View File

@@ -1,12 +1,12 @@
import { assign, createMachine, send } from 'xstate' import { assign, createMachine, send } from 'xstate'
import { INVALID_EMAIL_ERROR } from '../errors' import { INVALID_EMAIL_ERROR } from '../errors'
import { AuthClient } from '../internal-client' import { AuthClient } from '../internal-client'
import { ChangeEmailOptions, ChangeEmailResponse, ErrorPayload } from '../types' import { AuthErrorPayload, ChangeEmailOptions, ChangeEmailResponse } from '../types'
import { postFetch, rewriteRedirectTo } from '../utils' import { postFetch, rewriteRedirectTo } from '../utils'
import { isValidEmail } from '../utils/validators' import { isValidEmail } from '../utils/validators'
export type ChangeEmailContext = { export type ChangeEmailContext = {
error: ErrorPayload | null error: AuthErrorPayload | null
} }
export type ChangeEmailEvents = export type ChangeEmailEvents =
@@ -16,7 +16,7 @@ export type ChangeEmailEvents =
options?: ChangeEmailOptions options?: ChangeEmailOptions
} }
| { type: 'SUCCESS' } | { type: 'SUCCESS' }
| { type: 'ERROR'; error: ErrorPayload | null } | { type: 'ERROR'; error: AuthErrorPayload | null }
export type ChangeEmailServices = { export type ChangeEmailServices = {
request: { data: ChangeEmailResponse } request: { data: ChangeEmailResponse }

View File

@@ -16,9 +16,9 @@ export interface Typegen0 {
} }
missingImplementations: { missingImplementations: {
actions: never actions: never
services: never
guards: never
delays: never delays: never
guards: never
services: never
} }
eventsCausingActions: { eventsCausingActions: {
reportError: 'error.platform.requestChange' reportError: 'error.platform.requestChange'
@@ -26,13 +26,13 @@ export interface Typegen0 {
saveInvalidEmailError: 'REQUEST' saveInvalidEmailError: 'REQUEST'
saveRequestError: 'error.platform.requestChange' saveRequestError: 'error.platform.requestChange'
} }
eventsCausingServices: { eventsCausingDelays: {}
requestChange: 'REQUEST'
}
eventsCausingGuards: { eventsCausingGuards: {
invalidEmail: 'REQUEST' invalidEmail: 'REQUEST'
} }
eventsCausingDelays: {} eventsCausingServices: {
requestChange: 'REQUEST'
}
matchesStates: matchesStates:
| 'idle' | 'idle'
| 'idle.error' | 'idle.error'

View File

@@ -1,12 +1,12 @@
import { assign, createMachine, send } from 'xstate' import { assign, createMachine, send } from 'xstate'
import { INVALID_PASSWORD_ERROR } from '../errors' import { INVALID_PASSWORD_ERROR } from '../errors'
import { AuthClient } from '../internal-client' import { AuthClient } from '../internal-client'
import { ChangePasswordResponse, ErrorPayload } from '../types' import { AuthErrorPayload, ChangePasswordResponse } from '../types'
import { postFetch } from '../utils' import { postFetch } from '../utils'
import { isValidPassword } from '../utils/validators' import { isValidPassword } from '../utils/validators'
export type ChangePasswordContext = { export type ChangePasswordContext = {
error: ErrorPayload | null error: AuthErrorPayload | null
} }
export type ChangePasswordEvents = export type ChangePasswordEvents =
| { | {
@@ -15,7 +15,7 @@ export type ChangePasswordEvents =
ticket?: string ticket?: string
} }
| { type: 'SUCCESS' } | { type: 'SUCCESS' }
| { type: 'ERROR'; error: ErrorPayload | null } | { type: 'ERROR'; error: AuthErrorPayload | null }
export type ChangePasswordServices = { export type ChangePasswordServices = {
requestChange: { data: ChangePasswordResponse } requestChange: { data: ChangePasswordResponse }

View File

@@ -16,9 +16,9 @@ export interface Typegen0 {
} }
missingImplementations: { missingImplementations: {
actions: never actions: never
services: never
guards: never
delays: never delays: never
guards: never
services: never
} }
eventsCausingActions: { eventsCausingActions: {
reportError: 'error.platform.requestChange' reportError: 'error.platform.requestChange'
@@ -26,13 +26,13 @@ export interface Typegen0 {
saveInvalidPasswordError: 'REQUEST' saveInvalidPasswordError: 'REQUEST'
saveRequestError: 'error.platform.requestChange' saveRequestError: 'error.platform.requestChange'
} }
eventsCausingServices: { eventsCausingDelays: {}
requestChange: 'REQUEST'
}
eventsCausingGuards: { eventsCausingGuards: {
invalidPassword: 'REQUEST' invalidPassword: 'REQUEST'
} }
eventsCausingDelays: {} eventsCausingServices: {
requestChange: 'REQUEST'
}
matchesStates: matchesStates:
| 'idle' | 'idle'
| 'idle.error' | 'idle.error'

View File

@@ -1,11 +1,11 @@
import { assign, createMachine, send } from 'xstate' import { assign, createMachine, send } from 'xstate'
import { INVALID_MFA_CODE_ERROR, INVALID_MFA_TYPE_ERROR } from '../errors' import { INVALID_MFA_CODE_ERROR, INVALID_MFA_TYPE_ERROR } from '../errors'
import { AuthClient } from '../internal-client' import { AuthClient } from '../internal-client'
import { ErrorPayload } from '../types' import { AuthErrorPayload } from '../types'
import { getFetch, postFetch } from '../utils' import { getFetch, postFetch } from '../utils'
export type EnableMfaContext = { export type EnableMfaContext = {
error: ErrorPayload | null error: AuthErrorPayload | null
imageUrl: string | null imageUrl: string | null
secret: string | null secret: string | null
} }
@@ -20,9 +20,9 @@ export type EnableMfaEvents =
activeMfaType: 'totp' activeMfaType: 'totp'
} }
| { type: 'GENERATED' } | { type: 'GENERATED' }
| { type: 'GENERATED_ERROR'; error: ErrorPayload | null } | { type: 'GENERATED_ERROR'; error: AuthErrorPayload | null }
| { type: 'SUCCESS' } | { type: 'SUCCESS' }
| { type: 'ERROR'; error: ErrorPayload | null } | { type: 'ERROR'; error: AuthErrorPayload | null }
export type EnableMfadMachine = ReturnType<typeof createEnableMfaMachine> export type EnableMfadMachine = ReturnType<typeof createEnableMfaMachine>

View File

@@ -23,9 +23,9 @@ export interface Typegen0 {
} }
missingImplementations: { missingImplementations: {
actions: never actions: never
services: never
guards: never
delays: never delays: never
guards: never
services: never
} }
eventsCausingActions: { eventsCausingActions: {
reportError: 'error.platform.activate' reportError: 'error.platform.activate'
@@ -37,15 +37,15 @@ export interface Typegen0 {
saveInvalidMfaCodeError: 'ACTIVATE' saveInvalidMfaCodeError: 'ACTIVATE'
saveInvalidMfaTypeError: 'ACTIVATE' saveInvalidMfaTypeError: 'ACTIVATE'
} }
eventsCausingServices: { eventsCausingDelays: {}
activate: 'ACTIVATE'
generate: 'GENERATE'
}
eventsCausingGuards: { eventsCausingGuards: {
invalidMfaCode: 'ACTIVATE' invalidMfaCode: 'ACTIVATE'
invalidMfaType: 'ACTIVATE' invalidMfaType: 'ACTIVATE'
} }
eventsCausingDelays: {} eventsCausingServices: {
activate: 'ACTIVATE'
generate: 'GENERATE'
}
matchesStates: matchesStates:
| 'generated' | 'generated'
| 'generated.activated' | 'generated.activated'

View File

@@ -1,12 +1,12 @@
import { assign, createMachine, send } from 'xstate' import { assign, createMachine, send } from 'xstate'
import { INVALID_EMAIL_ERROR } from '../errors' import { INVALID_EMAIL_ERROR } from '../errors'
import { AuthClient } from '../internal-client' import { AuthClient } from '../internal-client'
import { ErrorPayload, ResetPasswordOptions, ResetPasswordResponse } from '../types' import { AuthErrorPayload, ResetPasswordOptions, ResetPasswordResponse } from '../types'
import { postFetch, rewriteRedirectTo } from '../utils' import { postFetch, rewriteRedirectTo } from '../utils'
import { isValidEmail } from '../utils/validators' import { isValidEmail } from '../utils/validators'
export type ResetPasswordContext = { export type ResetPasswordContext = {
error: ErrorPayload | null error: AuthErrorPayload | null
} }
export type ResetPasswordEvents = export type ResetPasswordEvents =
| { | {
@@ -15,7 +15,7 @@ export type ResetPasswordEvents =
options?: ResetPasswordOptions options?: ResetPasswordOptions
} }
| { type: 'SUCCESS' } | { type: 'SUCCESS' }
| { type: 'ERROR'; error: ErrorPayload | null } | { type: 'ERROR'; error: AuthErrorPayload | null }
export type ResetPasswordServices = { export type ResetPasswordServices = {
requestChange: { data: ResetPasswordResponse } requestChange: { data: ResetPasswordResponse }

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