Compare commits

...

138 Commits

Author SHA1 Message Date
Szilárd Dóró
69caa34c43 Merge pull request #1934 from nhost/changeset-release/main
chore: update versions
2023-05-16 08:34:38 +02:00
github-actions[bot]
1d898e2893 chore: update versions 2023-05-15 18:43:10 +00:00
Szilárd Dóró
e87621cbde Merge pull request #1936 from nhost/fix/security-key-url
fix(hasura-auth-js): make the call to the correct endpoint
2023-05-15 20:41:59 +02:00
Szilárd Dóró
0d6fc42158 fix: make the call to the correct endpoint 2023-05-15 19:46:26 +02:00
Szilárd Dóró
f6fbee6b13 Merge pull request #1933 from nhost/fix/project-rename-prevent-404
fix(dashboard): don't redirect to 404 on project rename
2023-05-15 16:59:00 +02:00
Szilárd Dóró
1230b72222 fix: don't redirect to 404 on project rename 2023-05-15 16:05:45 +02:00
Szilárd Dóró
6cc7704555 Merge pull request #1931 from nhost/changeset-release/main
chore: update versions
2023-05-15 15:47:22 +02:00
github-actions[bot]
c0954dec09 chore: update versions 2023-05-15 13:30:42 +00:00
Szilárd Dóró
6c25480a7a Merge pull request #1929 from nhost/fix/build-targets
chore: change build target to ES2019
2023-05-15 15:29:32 +02:00
Szilárd Dóró
da03bf390c chore: change build target to ES2019 2023-05-15 11:09:00 +02:00
Szilárd Dóró
3b513be9f2 Merge pull request #1926 from nhost/changeset-release/main
chore: update versions
2023-05-12 16:55:46 +02:00
github-actions[bot]
e450e9d636 chore: update versions 2023-05-12 14:27:16 +00:00
Szilárd Dóró
ed1ee10879 Merge pull request #1925 from nhost/fix/postgres-connection-string
fix(dashboard): show correct postgres connection string
2023-05-12 16:25:51 +02:00
Szilárd Dóró
349aac369e chore: add changeset 2023-05-12 14:23:54 +02:00
Szilárd Dóró
5a84362c80 fix: construct postgres connection string 2023-05-12 14:23:05 +02:00
Szilárd Dóró
f59a77b1c8 Merge pull request #1923 from nhost/changeset-release/main
chore: update versions
2023-05-12 10:25:12 +02:00
github-actions[bot]
30686bc4ce chore: update versions 2023-05-12 08:03:41 +00:00
Szilárd Dóró
0c4ac8d368 Merge pull request #1919 from nhost/chore/update-staging-urls
chore(dashboard): change URL construction
2023-05-12 10:02:32 +02:00
Szilárd Dóró
7da0e5e256 Merge pull request #1918 from nhost/changeset-release/main
chore: update versions
2023-05-11 15:30:16 +02:00
github-actions[bot]
8229101efe chore: update versions 2023-05-11 13:07:57 +00:00
Szilárd Dóró
afad1778f8 Merge pull request #1895 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-05-11 15:06:23 +02:00
Szilárd Dóró
28fc7b84c7 chore: update changeset 2023-05-11 13:38:39 +02:00
Szilárd Dóró
3f478a4e3c chore: include vitest in the bump, add changeset 2023-05-11 13:37:34 +02:00
Szilárd Dóró
aa54666941 fix: don't break builds 2023-05-11 12:53:45 +02:00
Szilárd Dóró
20fb69faba chore: change URL construction 2023-05-11 12:46:49 +02:00
renovate[bot]
1caeb2a548 chore(deps): update react monorepo 2023-05-11 10:12:59 +00:00
Szilárd Dóró
6356c5a2c8 Merge pull request #1906 from nhost/chore/bump-pnpm
chore: bump `pnpm` and `turbo` version
2023-05-11 12:10:10 +02:00
Szilárd Dóró
6ec1dd3248 chore: bump lock file again 2023-05-11 11:07:27 +02:00
Szilárd Dóró
8a4b5031dc Merge branch 'main' into chore/bump-pnpm 2023-05-11 11:06:57 +02:00
Szilárd Dóró
4790fee41f Merge pull request #1916 from nhost/changeset-release/main
chore: update versions
2023-05-11 09:26:44 +02:00
github-actions[bot]
0a8033812d chore: update versions 2023-05-11 06:56:16 +00:00
Szilárd Dóró
2b56ffc29e Merge pull request #1915 from nhost/fix/project-redirects
fix(dashboard): redirect an invalid project to the 404 page
2023-05-11 08:54:58 +02:00
Szilárd Dóró
aa9b926cd7 Merge branch 'main' into fix/project-redirects 2023-05-10 17:21:18 +02:00
Szilárd Dóró
575404ad62 Merge pull request #1917 from nhost/fix/imports
fix(dashboard): don't break builds
2023-05-10 17:20:37 +02:00
Szilárd Dóró
3f6dfc7bcd fix: don't break builds 2023-05-10 17:19:32 +02:00
Szilárd Dóró
682e64d7a3 fix: don't break build 2023-05-10 16:51:59 +02:00
Szilárd Dóró
30cee4f86c Merge branch 'main' into fix/project-redirects 2023-05-10 16:51:06 +02:00
Szilárd Dóró
29dcc8c63e Merge pull request #1911 from nhost/fix/non-owner-functionality
fix(dashboard): restrict non-owner functionality
2023-05-10 16:47:06 +02:00
Szilárd Dóró
d926f15676 fix: redirect an invalid project to the 404 page 2023-05-10 16:46:16 +02:00
Szilárd Dóró
d4a0aad2dd Merge pull request #1913 from nhost/changeset-release/main
chore: update versions
2023-05-10 14:24:09 +02:00
github-actions[bot]
1030813279 chore: update versions 2023-05-10 11:50:46 +00:00
Nuno Pato
917a14aa40 Merge pull request #1912 from nhost/docs/add-metrics
Add section on Metrics to the documentation
2023-05-10 11:49:27 +00:00
Szilárd Dóró
6381d1b095 Merge pull request #1893 from nhost/feat/metrics-page 2023-05-10 13:43:34 +02:00
Nuno Pato
8dbdc0bf50 asd 2023-05-10 11:32:34 +00:00
Nuno Pato
8c072a4c6e asd 2023-05-10 10:10:38 +00:00
Nuno Pato
fe341519f7 Add section on Metrics to the documentation 2023-05-10 10:09:12 +00:00
Szilárd Dóró
ea09384064 fix: update import paths 2023-05-10 10:45:20 +02:00
Szilárd Dóró
49b9972885 chore: add changeset 2023-05-10 10:42:35 +02:00
Szilárd Dóró
98c541ee52 feat: introduce useIsCurrentUserOwner hook
- chore: improve file structure
2023-05-10 10:41:35 +02:00
Szilárd Dóró
757c888656 Merge pull request #1910 from nhost/changeset-release/main
chore: update versions
2023-05-09 11:40:16 +02:00
github-actions[bot]
7c13eb5f9b chore: update versions 2023-05-09 09:17:43 +00:00
Szilárd Dóró
a84608e086 Merge pull request #1907 from nhost/fix/upgrade
fix(dashboard): unpause after upgrading a paused project to pro
2023-05-09 11:13:44 +02:00
Szilárd Dóró
e43c079b9c feat: poll project state after unpausing with upgrade 2023-05-09 10:50:34 +02:00
Szilárd Dóró
3f396a9ebb chore: add changesets 2023-05-08 19:28:42 +02:00
Szilárd Dóró
6ed605beb8 fix: update desiredState on plan change 2023-05-08 17:58:06 +02:00
Szilárd Dóró
edd223d29c fix: don't go to 404 page unnecessarily 2023-05-08 17:47:04 +02:00
Szilárd Dóró
15a985e079 fix: don't break dashboard build 2023-05-08 15:29:29 +02:00
Szilárd Dóró
8ff00a4258 chore(ci): bump pnpm version to v8.4.0 2023-05-08 15:19:53 +02:00
Szilárd Dóró
7e27d7c0a1 chore: update changeset, bump turbo version 2023-05-08 15:17:42 +02:00
Szilárd Dóró
49f9b8372a chore: bump pnpm version to v8.4.0 2023-05-08 15:09:29 +02:00
Szilárd Dóró
925bf0f13f Merge pull request #1905 from nhost/changeset-release/main
chore: update versions
2023-05-08 13:51:55 +02:00
github-actions[bot]
30d35f9607 chore: update versions 2023-05-08 10:10:25 +00:00
Szilárd Dóró
755aa56f12 Merge pull request #1904 from nhost/fix/package-json-types
chore: add `types` to `package.json`
2023-05-08 12:09:11 +02:00
Szilárd Dóró
4c7e7c57a9 fix: don't break tests 2023-05-08 10:29:13 +02:00
Szilárd Dóró
36708e2853 Merge pull request #1903 from hrmoller/fix/wrong-linking-in-docs
Fixed linking to wrong destination in docs
2023-05-08 10:26:31 +02:00
Szilárd Dóró
90c6031189 chore: add types to package.json 2023-05-08 09:54:27 +02:00
Martin Møller
f044dbdb10 Fixed linking to wrong destination 2023-05-05 08:03:50 +02:00
Szilárd Dóró
c2f3bce5f9 Merge pull request #1902 from nhost/chore/probot-improvements
chore: refine probot config
2023-05-04 16:20:59 +02:00
Szilárd Dóró
22d9877b97 chore: update probot config 2023-05-04 16:04:09 +02:00
Szilárd Dóró
628e96dcc3 Merge pull request #1901 from nhost/chore/probot-stale
chore: add probot/stale configuration
2023-05-04 15:32:50 +02:00
Szilárd Dóró
3e9d3c42b6 fix: disable exemptLabels 2023-05-04 15:20:23 +02:00
Szilárd Dóró
a1e7b87c38 add probot/stale configuration 2023-05-04 15:08:37 +02:00
David Barroso
1bd800359e Merge pull request #1894 from nhost/dbarroso/obs-dash-improv
fix: observability: filter pod metrics
2023-05-03 12:59:07 +02:00
David Barroso
54a204a34e fix: observability: filter pod metrics 2023-05-03 10:09:27 +02:00
Szilárd Dóró
b17e8d6f3c fix: don't break e2e tests 2023-05-03 10:00:29 +02:00
Szilárd Dóró
12e2855f01 chore: update description and prevent free access
- bump `jsdom` to v22
- increase test timeout
2023-05-03 09:57:11 +02:00
Szilárd Dóró
c1080d9e63 fix: don't break the UI 2023-05-03 09:38:48 +02:00
Szilárd Dóró
e4972b8307 feat: add Grafana page 2023-05-03 09:35:06 +02:00
Szilárd Dóró
2e7ec0697e Merge pull request #1881 from nhost/changeset-release/main
chore: update versions
2023-05-02 21:06:46 +02:00
github-actions[bot]
2d9baec9d4 chore: update versions 2023-05-02 18:56:49 +00:00
Szilárd Dóró
7a7750be0b Merge pull request #1892 from nhost/fix/disable-downgrade
fix: disallow downgrading through the UI
2023-05-02 20:55:24 +02:00
Szilárd Dóró
0f34f0c6b9 fix: disallow downgrading 2023-05-02 15:31:39 +02:00
Nestor Manrique
d05253183a Merge pull request #1883 from nhost/nestor/fix-grafana-dashboard-datasource
fix: use dashboard externally exported version
2023-05-02 13:15:40 +02:00
Nestor Manrique
65df016bbc fix: fix datasource config 2023-04-28 17:28:43 +02:00
David Barroso
3e6ee1ae97 Merge pull request #1882 from nhost/dbarroso/observability-dashboard
feat: added project metrics observability dashboard
2023-04-28 14:32:55 +02:00
David Barroso
6042ed101f feat: added project metrics observability dashboard 2023-04-28 11:49:33 +02:00
Szilárd Dóró
384bce59bf Merge pull request #1859 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-04-28 09:59:24 +02:00
Szilárd Dóró
8da291ad4d chore: add changeset 2023-04-28 09:42:13 +02:00
renovate[bot]
f94eb3c467 chore(deps): update react monorepo 2023-04-27 18:07:47 +00:00
Szilárd Dóró
9baf3f4ac7 Merge pull request #1876 from nhost/changeset-release/main
chore: update versions
2023-04-27 20:00:27 +02:00
github-actions[bot]
9c406548e3 chore: update versions 2023-04-27 17:47:36 +00:00
Szilárd Dóró
1c08cd1949 Merge pull request #1878 from nhost/fix/local-users-page 2023-04-27 19:46:28 +02:00
Szilárd Dóró
adc828a582 fix: don't enter an infinite loop 2023-04-27 17:45:04 +02:00
Szilárd Dóró
f1ec6b9a93 Merge pull request #1871 from nhost/docs/local-development-migration
docs: add migration info
2023-04-27 16:37:40 +02:00
Szilárd Dóró
233b7e383e Merge pull request #1873 from nhost/changeset-release/main
chore: update versions
2023-04-27 16:06:05 +02:00
github-actions[bot]
7ea469a1e3 chore: update versions 2023-04-27 13:46:37 +00:00
Szilárd Dóró
ebd218c180 Merge pull request #1855 from nhost/feat/resource-replicas
feat(dashboard): Service Replicas
2023-04-27 15:45:31 +02:00
Nuno Pato
5ab1626f73 Merge pull request #1869 from nhost/docs/add-service-replicas
docs: add service replicas
2023-04-27 13:35:53 +00:00
Nuno Pato
444c3b86ca asd 2023-04-27 13:34:35 +00:00
Szilárd Dóró
7238412341 Merge pull request #1872 from nhost/chore/remove-backend-url
chore(dashboard): remove deprecated environment variable
2023-04-27 14:36:09 +02:00
Szilárd Dóró
f6639ae05c chore: add changeset 2023-04-27 13:54:27 +02:00
Szilárd Dóró
d8ceccec5d chore: add changeset 2023-04-27 13:41:11 +02:00
Szilárd Dóró
6db257d4c7 chore: remove deprecated backend URL 2023-04-27 13:40:41 +02:00
Szilárd Dóró
93dab2d183 docs: add migration info 2023-04-27 11:56:41 +02:00
Nuno Pato
dfc18368be asd 2023-04-26 18:09:42 +00:00
Nuno Pato
f7c6e80bf2 asd 2023-04-26 17:38:53 +00:00
Nuno Pato
573cac1431 asd 2023-04-26 17:23:11 +00:00
Nuno Pato
d72ae3f362 docs: add service replicas 2023-04-26 00:42:16 +00:00
Szilárd Dóró
49ec7ec385 fix: mobile improvements, improved validation 2023-04-25 15:30:38 +02:00
Szilárd Dóró
7d2b4083c2 chore: reorder components, update labels 2023-04-24 17:24:54 +02:00
Szilárd Dóró
696b493745 chore: increase test timeout 2023-04-24 16:23:51 +02:00
Szilárd Dóró
15a117a861 feat: improve cost calculation 2023-04-24 15:48:20 +02:00
Szilárd Dóró
e7ff1f79f8 feat: add information about replicas 2023-04-24 14:29:20 +02:00
Szilárd Dóró
33c7368a2e chore: update validation error message 2023-04-24 11:40:05 +02:00
Szilárd Dóró
664c182c8e fix: use font-medium for confirmation labels 2023-04-24 11:36:48 +02:00
Szilárd Dóró
c1ab4e0a77 chore: improve validation messages 2023-04-24 11:36:10 +02:00
Szilárd Dóró
4a4bd61757 chore: update tooltip label 2023-04-24 11:16:51 +02:00
Szilárd Dóró
b6d05289be fix: don't fail tests 2023-04-24 11:13:39 +02:00
Szilárd Dóró
5857458ca5 chore: improve resources form validation 2023-04-24 11:09:00 +02:00
Szilárd Dóró
2fb1145fe0 chore: add changeset 2023-04-24 10:22:44 +02:00
Szilárd Dóró
546d710102 Merge branch 'main' into feat/resource-replicas 2023-04-24 10:21:04 +02:00
Szilárd Dóró
7756103476 Merge pull request #1861 from nhost/changeset-release/main 2023-04-24 09:38:50 +02:00
github-actions[bot]
fef9456c12 chore: update versions 2023-04-23 19:36:24 +00:00
Szilárd Dóró
2d6d56f6b0 Merge pull request #1860 from nhost/fix/project-details
fix(dashboard): filter projects by workspace
2023-04-23 21:35:16 +02:00
Szilárd Dóró
f54be0fefd fix: don't break unit tests 2023-04-23 19:27:36 +02:00
Szilárd Dóró
4e76d388ab fix: remove unused query parameter 2023-04-23 16:42:33 +02:00
Szilárd Dóró
84b84ab785 fix: filter projects by workspace 2023-04-23 16:34:39 +02:00
Szilárd Dóró
ed66769688 Merge branch 'main' into feat/resource-replicas 2023-04-21 14:31:51 +02:00
Szilárd Dóró
a0298e0bdb chore: increase test timeout, improve stability 2023-04-20 16:40:42 +02:00
Szilárd Dóró
3fd94b1cdf chore: improve validation, fix tests 2023-04-20 16:08:37 +02:00
Szilárd Dóró
61d5f7d616 feat: make use of replicas from API 2023-04-20 14:56:52 +02:00
Szilárd Dóró
cde9a0a715 chore: extend tests, improve validation 2023-04-20 14:50:12 +02:00
Szilárd Dóró
eae6349b04 feat: add new pricing to confirmation dialog 2023-04-20 14:19:53 +02:00
Szilárd Dóró
211b930b84 chore: fix after effects of the new data structure 2023-04-20 13:53:06 +02:00
Szilárd Dóró
4ae463074b chore: simplify form data structure 2023-04-20 13:19:59 +02:00
Szilárd Dóró
1c5a4746f7 chore: improve validation error 2023-04-20 11:35:25 +02:00
Szilárd Dóró
d6ae1fa44a feat: resource validation when replicas > 1 2023-04-20 10:28:31 +02:00
Szilárd Dóró
a3abb81b37 feat: add replica slider to services 2023-04-19 15:57:57 +02:00
240 changed files with 12725 additions and 9182 deletions

View File

@@ -14,7 +14,7 @@ runs:
steps:
- uses: pnpm/action-setup@v2.2.4
with:
version: 7.17.0
version: 8.4.0
run_install: false
- name: Get pnpm cache directory
id: pnpm-cache-dir

16
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
# Configuration for probot-stale - https://github.com/probot/stale
daysUntilStale: 180
daysUntilClose: 7
limitPerRun: 30
onlyLabels: []
exemptLabels: []
exemptProjects: false
exemptMilestones: false
exemptAssignees: false
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.

View File

@@ -36,6 +36,7 @@ export default defineConfig({
}
},
build: {
target: 'es2019',
sourcemap: true,
lib: {
entry,

View File

@@ -1,5 +1,98 @@
# @nhost/dashboard
## 0.16.11
### Patch Changes
- 1230b722: fix(projects): don't redirect to 404 on when the project is renamed
- @nhost/react-apollo@5.0.22
- @nhost/nextjs@1.13.24
## 0.16.10
### Patch Changes
- Updated dependencies [da03bf39]
- @nhost/react-apollo@5.0.21
- @nhost/nextjs@1.13.23
## 0.16.9
### Patch Changes
- 349aac36: fix(settings): use region domain when constructing the postgres connection string
## 0.16.8
### Patch Changes
- 20fb69fa: chore(projects): change the way how API URLs are constructed
## 0.16.7
### Patch Changes
- 49f9b837: chore(docker): bump `pnpm` to `v8.4.0` and `turbo` to `v1.9.3`
- 3f478a4e: chore(deps): bump `vitest` to `v0.31.0`, `@types/react` to `v18.2.6` and `@types/react-dom` to `v18.2.4`
## 0.16.6
### Patch Changes
- d926f156: fix(projects): redirect to 404 when an invalid project is opened
- 49b99728: fix(projects): disable features for non-owner members of workspaces
## 0.16.5
### Patch Changes
- 12e2855f: chore(deps): bump `jsdom` to v22
- e4972b83: feat(metrics): add Grafana page
## 0.16.4
### Patch Changes
- 3f396a9e: fix(projects): unpause after upgrading a paused project to pro
- 3f396a9e: fix(projects): don't redirect to 404 page after project creation
## 0.16.3
### Patch Changes
- Updated dependencies [90c60311]
- @nhost/react-apollo@5.0.20
- @nhost/nextjs@1.13.22
## 0.16.2
### Patch Changes
- 0f34f0c6: fix(projects): disallow downgrading to free plan
- 8da291ad: chore(deps): bump `@types/react` to v18.2.0 and `@types/react-dom` to v18.2.1
## 0.16.1
### Patch Changes
- adc828a5: fix(gql): don't enter an infinite loop when fetching remote app data
## 0.16.0
### Minor Changes
- 2fb1145f: feat(compute): add support for replicas
### Patch Changes
- d8ceccec: chore(env): remove deprecated `NHOST_BACKEND_URL` environment variable
## 0.15.2
### Patch Changes
- 84b84ab7: fix(projects): filter projects by workspace
## 0.15.1
### Patch Changes

View File

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

View File

@@ -30,7 +30,7 @@ test('should show a sidebar with menu items', async () => {
const navLocator = page.getByRole('navigation', { name: /main navigation/i });
await expect(navLocator).toBeVisible();
await expect(navLocator.getByRole('list').getByRole('listitem')).toHaveCount(
10,
11,
);
await expect(
navLocator.getByRole('link', { name: /overview/i }),
@@ -53,6 +53,9 @@ test('should show a sidebar with menu items', async () => {
navLocator.getByRole('link', { name: /backups/i }),
).toBeVisible();
await expect(navLocator.getByRole('link', { name: /logs/i })).toBeVisible();
await expect(
navLocator.getByRole('link', { name: /metrics/i }),
).toBeVisible();
await expect(
navLocator.getByRole('link', { name: /settings/i }),
).toBeVisible();

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.15.1",
"version": "0.16.11",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -105,15 +105,15 @@
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/react": "18.0.37",
"@types/react-dom": "18.0.11",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"@types/react-table": "^7.7.12",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"@vitejs/plugin-react": "^4.0.0",
"@vitest/coverage-c8": "^0.30.0",
"@vitest/coverage-c8": "^0.31.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^8.3.0",
"babel-plugin-transform-remove-console": "^6.9.4",
@@ -129,7 +129,7 @@
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^21.0.0",
"jsdom": "^22.0.0",
"lint-staged": ">=13",
"msw": "^1.0.1",
"msw-storybook-addon": "^1.6.3",
@@ -147,8 +147,7 @@
"tsconfig-paths-webpack-plugin": "^4.0.0",
"vite": "^4.0.2",
"vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.30.0",
"webpack": "^5.75.0"
"vitest": "^0.31.0"
},
"browserslist": {
"production": [

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,6 +1,6 @@
import FeedbackForm from '@/components/common/FeedbackForm';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useInterval } from '@/hooks/useInterval';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown';

View File

@@ -1,8 +1,9 @@
import FeedbackForm from '@/components/common/FeedbackForm';
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import { useAppCreatedAt } from '@/hooks/useAppCreatedAt';
import { useCurrentDate } from '@/hooks/useCurrentDate';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { ApplicationState } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import { Modal } from '@/ui/Modal';
@@ -56,9 +57,7 @@ export default function ApplicationErrored() {
const client = useApolloClient();
const { currentDate } = useCurrentDate();
const user = useUserData();
const isOwner = currentWorkspace.workspaceMembers.some(
({ id, type }) => id === user?.id && type === 'owner',
);
const isOwner = useIsCurrentUserOwner();
const { appCreatedAt } = useAppCreatedAt();

View File

@@ -1,12 +1,12 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAllWorkspacesAndProjectsDocument,
useDeleteApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Button from '@/ui/v2/Button';
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
import { copy } from '@/utils/copy';
import { getApplicationStatusString } from '@/utils/helpers';
import getServerError from '@/utils/settings/getServerError';
@@ -46,6 +46,10 @@ export default function ApplicationInfo() {
}
}
if (!currentProject) {
return null;
}
return (
<div className="mt-4 grid grid-flow-row gap-4">
<div className="grid grid-flow-row justify-center gap-0.5">

View File

@@ -3,12 +3,13 @@ import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { StagingMetadata } from '@/components/applications/StagingMetadata';
import { useDialog } from '@/components/common/DialogProvider';
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import {
GetAllWorkspacesAndProjectsDocument,
useGetFreeAndActiveProjectsQuery,
useUnpauseApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Modal } from '@/ui';
import { Alert } from '@/ui/Alert';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
@@ -26,15 +27,11 @@ import { RemoveApplicationModal } from './RemoveApplicationModal';
export default function ApplicationPaused() {
const { openDialog } = useDialog();
const {
currentWorkspace,
currentProject,
refetch: refetchWorkspaceAndProject,
} = useCurrentWorkspaceAndProject();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const isOwner = useIsCurrentUserOwner();
const user = useUserData();
const isOwner = currentWorkspace.workspaceMembers.some(
({ id, type }) => id === user?.id && type === 'owner',
);
const [showDeletingModal, setShowDeletingModal] = useState(false);
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
useUnpauseApplicationMutation({
@@ -120,20 +117,22 @@ export default function ApplicationPaused() {
</Box>
<Box className="grid grid-flow-row gap-2">
<Button
className="mx-auto w-full max-w-[280px]"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0' },
maxWidth: 'lg',
},
});
}}
>
Upgrade to Pro
</Button>
{isOwner && (
<Button
className="mx-auto w-full max-w-[280px]"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0' },
maxWidth: 'lg',
},
});
}}
>
Upgrade to Pro
</Button>
)}
<div className="grid grid-flow-row gap-2">
<Button

View File

@@ -1,6 +1,6 @@
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useCheckProvisioning } from '@/hooks/useCheckProvisioning';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { ApplicationStatus } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Text from '@/ui/v2/Text';
@@ -27,7 +27,7 @@ export default function ApplicationProvisioning() {
{currentProjectState.state === ApplicationStatus.Empty ? (
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h1">
Setting Up {currentProject.name}
Setting Up {currentProject?.name}
</Text>
<Text>This normally takes around 2 minutes</Text>
<ActivityIndicator className="mx-auto" />

View File

@@ -1,6 +1,6 @@
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useCheckProvisioning } from '@/hooks/useCheckProvisioning';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { ApplicationStatus } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Text from '@/ui/v2/Text';
@@ -26,7 +26,7 @@ export default function ApplicationRestoring() {
{currentProjectState.state === ApplicationStatus.Empty ? (
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h1">
Setting Up {currentProject.name}
Setting Up {currentProject?.name}
</Text>
<Text>This normally takes around 2 minutes</Text>

View File

@@ -1,11 +1,11 @@
import FeedbackForm from '@/components/common/FeedbackForm';
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import { Modal } from '@/ui/Modal';
import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown';
import Text from '@/ui/v2/Text';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import { useState } from 'react';
import ApplicationInfo from './ApplicationInfo';
@@ -13,12 +13,9 @@ import { RemoveApplicationModal } from './RemoveApplicationModal';
import { StagingMetadata } from './StagingMetadata';
export default function ApplicationUnknown() {
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const { currentProject } = useCurrentWorkspaceAndProject();
const [showDeleteModal, setShowDeleteModal] = useState(false);
const user = useUserData();
const isOwner = currentWorkspace.workspaceMembers.some(
({ id, type }) => id === user?.id && type === 'owner',
);
const isOwner = useIsCurrentUserOwner();
return (
<>

View File

@@ -1,5 +1,5 @@
import Container from '@/components/layout/Container';
import useProjectRedirectWhenReady from '@/hooks/common/useProjectRedirectWhenReady';
import { useProjectRedirectWhenReady } from '@/features/projects/common/hooks/useProjectRedirectWhenReady';
import Image from 'next/image';
import { AppLoader } from './AppLoader';

View File

@@ -1,34 +1,30 @@
import { useDialog } from '@/components/common/DialogProvider';
import { BillingPaymentMethodForm } from '@/components/workspace/BillingPaymentMethodForm';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
refetchGetApplicationPlanQuery,
useGetAppPlanAndGlobalPlansQuery,
useGetPaymentMethodsQuery,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import useApplicationState from '@/hooks/useApplicationState';
import { ApplicationStatus } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Checkbox from '@/ui/v2/Checkbox';
import { BaseDialog } from '@/ui/v2/Dialog';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
import { planDescriptions } from '@/utils/planDescriptions';
import getServerError from '@/utils/settings/getServerError/getServerError';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { toast } from 'react-hot-toast';
function Plan({
planName,
price,
setPlan,
planId,
selectedPlanId,
currentPlan,
}: any) {
function Plan({ planName, price, setPlan, planId, selectedPlanId }: any) {
return (
<button
type="button"
@@ -49,7 +45,7 @@ function Plan({
component="p"
className="self-center text-left font-medium"
>
{currentPlan.price > price ? 'Downgrade' : 'Upgrade'} to {planName}
Upgrade to {planName}
</Text>
</div>
@@ -59,7 +55,7 @@ function Plan({
</div>
<Text variant="h3" component="p">
$ {price}/mo
${price}/mo
</Text>
</button>
);
@@ -68,12 +64,14 @@ function Plan({
export function ChangePlanModalWithData({ app, plans, close }: any) {
const [selectedPlanId, setSelectedPlanId] = useState('');
const { closeAlertDialog } = useDialog();
const [pollingCurrentProject, setPollingCurrentProject] = useState(false);
const {
currentWorkspace,
currentProject,
refetch: refetchWorkspaceAndProject,
} = useCurrentWorkspaceAndProject();
const { state } = useApplicationState();
const { data } = useGetPaymentMethodsQuery({
variables: {
@@ -88,7 +86,28 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
const currentPlan = plans.find((plan) => plan.id === app.plan.id);
const selectedPlan = plans.find((plan) => plan.id === selectedPlanId);
const isDowngrade = currentPlan.price > selectedPlan?.price;
useEffect(() => {
if (!pollingCurrentProject || state === ApplicationStatus.Paused) {
return;
}
close?.();
closeAlertDialog();
setShowPaymentModal(false);
setPollingCurrentProject(false);
}, [state, pollingCurrentProject, close, closeAlertDialog]);
useEffect(() => {
if (!pollingCurrentProject) {
return () => {};
}
const interval = setInterval(() => {
refetchWorkspaceAndProject();
}, 1000);
return () => clearInterval(interval);
}, [pollingCurrentProject, refetchWorkspaceAndProject, currentProject]);
const [updateApp] = useUpdateApplicationMutation({
refetchQueries: [
@@ -107,6 +126,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
appId: app.id,
app: {
planId: selectedPlan.id,
desiredState: 5,
},
},
}),
@@ -120,11 +140,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
getToastStyleProps(),
);
await refetchWorkspaceAndProject();
close?.();
closeAlertDialog();
setShowPaymentModal(false);
setPollingCurrentProject(true);
} catch (error) {
// Note: Error is handled by the toast.
}
@@ -142,12 +158,96 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
}
await handleUpdateAppPlan();
setShowPaymentModal(false);
close?.();
closeAlertDialog();
};
if (pollingCurrentProject) {
return (
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
<div className="flex flex-col">
<div className="mx-auto">
<Image
src="/assets/upgrade.svg"
alt="Nhost Logo"
width={72}
height={72}
/>
</div>
<Text variant="h3" component="h2" className="mt-2 text-center">
Successfully upgraded to {currentPlan.name}
</Text>
<ActivityIndicator
label="We are unpausing your project. This may take some time..."
className="mx-auto mt-2"
/>
<Button
variant="outlined"
color="secondary"
className="mx-auto mt-4 w-full max-w-sm"
onClick={() => {
if (close) {
close();
}
closeAlertDialog();
}}
>
Cancel
</Button>
</div>
</Box>
);
}
if (app.plan.id !== plans.find((plan) => plan.isFree)?.id) {
return (
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
<div className="flex flex-col">
<div className="mx-auto">
<Image
src="/assets/upgrade.svg"
alt="Nhost Logo"
width={72}
height={72}
/>
</div>
<Text variant="h3" component="h2" className="mt-2 text-center">
Downgrade is not available
</Text>
<Text className="mt-1 text-center">
You can&apos;t downgrade from a paid plan to a free plan here.
</Text>
<Text className="text-center">
Please contact us at{' '}
<Link href="mailto:info@nhost.io">info@nhost.io</Link> if you want
to downgrade.
</Text>
<div className="mt-6 grid grid-flow-row gap-2">
<Button
variant="outlined"
color="secondary"
className="mx-auto w-full max-w-sm"
onClick={() => {
if (close) {
close();
}
closeAlertDialog();
}}
>
Cancel
</Button>
</div>
</div>
</Box>
);
}
return (
<Box className="w-full max-w-xl rounded-lg p-6 text-left">
<BaseDialog
@@ -176,7 +276,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
You&apos;re currently on the <strong>{app.plan.name}</strong> plan.
</Text>
<div className="mt-5">
<div className="mt-2">
{plans
.filter((plan) => plan.id !== app.plan.id)
.map((plan) => (
@@ -194,11 +294,13 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
))}
</div>
<div className="mt-6 grid grid-flow-row gap-2">
<Button onClick={handleChangePlanClick} disabled={!selectedPlan}>
{!selectedPlan && 'Change Plan'}
{selectedPlan && isDowngrade && 'Downgrade'}
{selectedPlan && !isDowngrade && 'Upgrade'}
<div className="mt-2 grid grid-flow-row gap-2">
<Button
onClick={handleChangePlanClick}
disabled={!selectedPlan}
loading={pollingCurrentProject}
>
Upgrade
</Button>
<Button

View File

@@ -1,12 +1,12 @@
import { LoadingScreen } from '@/components/common/LoadingScreen';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import IconButton from '@/ui/v2/IconButton';
import Text from '@/ui/v2/Text';
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Text from '@/ui/v2/Text';
import generateAppServiceUrl, {
defaultLocalBackendSlugs,
defaultRemoteBackendSlugs,
@@ -33,7 +33,7 @@ export function HasuraData({ close }: HasuraDataProps) {
? `${getHasuraConsoleServiceUrl()}`
: generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region.awsName,
currentProject?.region,
'hasura',
defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },

View File

@@ -1,4 +1,4 @@
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Checkbox from '@/ui/v2/Checkbox';

View File

@@ -1,4 +1,4 @@
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Checkbox from '@/ui/v2/Checkbox';

View File

@@ -1,5 +1,6 @@
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { useDialog } from '@/components/common/DialogProvider';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
@@ -20,25 +21,36 @@ export function UnlockFeatureByUpgrading({
...props
}: UnlockFeatureByUpgradingProps) {
const { openDialog } = useDialog();
const isOwner = useIsCurrentUserOwner();
return (
<div className={twMerge('flex', className)} {...props}>
<Alert className="grid w-full grid-flow-col place-content-between items-center gap-2">
<Text className="text-left">{message}</Text>
<Text className="grid grid-flow-row justify-items-start gap-0.5">
<Text component="span">{message}</Text>
<Button
variant="borderless"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
},
});
}}
>
Upgrade
</Button>
{!isOwner && (
<Text component="span" color="secondary" className="text-sm">
Ask an owner of this workspace to upgrade the project.
</Text>
)}
</Text>
{isOwner && (
<Button
variant="borderless"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
},
});
}}
>
Upgrade
</Button>
)}
</Alert>
</div>
);

View File

@@ -1,5 +1,5 @@
import type { ConnectGithubModalState } from '@/components/applications/ConnectGithubModal';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { FormProvider, useForm } from 'react-hook-form';
import { EditRepositorySettingsModal } from './EditRepositorySettingsModal';

View File

@@ -2,8 +2,8 @@ import type { EditRepositorySettingsFormData } from '@/components/applications/g
import { useDialog } from '@/components/common/DialogProvider';
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
import GithubIcon from '@/components/icons/GithubIcon';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useUpdateApplicationMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';

View File

@@ -1,5 +1,5 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Option from '@/ui/v2/Option';
import Select from '@/ui/v2/Select';

View File

@@ -1,6 +1,6 @@
import NavLink from '@/components/common/NavLink';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import Text from '@/ui/v2/Text';

View File

@@ -2,8 +2,8 @@ import AudioPreview from '@/components/icons/AudioPreview';
import { FileIcon } from '@/components/icons/FileIcon';
import PDFPreview from '@/components/icons/PDFPreview';
import VideoPreview from '@/components/icons/VideoPreview';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useAppClient } from '@/hooks/useAppClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Modal } from '@/ui/Modal';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';

View File

@@ -9,12 +9,12 @@ import FormActivityIndicator from '@/components/common/FormActivityIndicator';
import InlineCode from '@/components/common/InlineCode';
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useDeleteColumnWithToastMutation from '@/hooks/dataBrowser/useDeleteColumnMutation/useDeleteColumnWithToastMutation';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import type { UpdateRecordVariables } from '@/hooks/dataBrowser/useUpdateRecordMutation';
import useUpdateRecordWithToastMutation from '@/hooks/dataBrowser/useUpdateRecordMutation/useUpdateRecordWithToastMutation';
import useTablePath from '@/hooks/useTablePath';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type {
DataBrowserGridColumn,
NormalizedQueryDataRow,

View File

@@ -1,9 +1,9 @@
import type { DataGridPaginationProps } from '@/components/common/DataGridPagination';
import DataGridPagination from '@/components/common/DataGridPagination';
import { useDialog } from '@/components/common/DialogProvider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useDeleteRecordMutation from '@/hooks/dataBrowser/useDeleteRecordMutation';
import useDataGridConfig from '@/hooks/useDataGridConfig';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';

View File

@@ -3,10 +3,10 @@ import FormActivityIndicator from '@/components/common/FormActivityIndicator';
import InlineCode from '@/components/common/InlineCode';
import NavLink from '@/components/common/NavLink';
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
import useDeleteTableWithToastMutation from '@/hooks/dataBrowser/useDeleteTableMutation/useDeleteTableWithToastMutation';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import FloatingActionButton from '@/ui/FloatingActionButton';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Backdrop from '@/ui/v2/Backdrop';
@@ -17,6 +17,12 @@ import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import Link from '@/ui/v2/Link';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Option from '@/ui/v2/Option';
import Select from '@/ui/v2/Select';
import Text from '@/ui/v2/Text';
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
import DotsHorizontalIcon from '@/ui/v2/icons/DotsHorizontalIcon';
import LockIcon from '@/ui/v2/icons/LockIcon';
@@ -24,12 +30,6 @@ import PencilIcon from '@/ui/v2/icons/PencilIcon';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import TrashIcon from '@/ui/v2/icons/TrashIcon';
import UsersIcon from '@/ui/v2/icons/UsersIcon';
import Link from '@/ui/v2/Link';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Option from '@/ui/v2/Option';
import Select from '@/ui/v2/Select';
import Text from '@/ui/v2/Text';
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
import { useQueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';

View File

@@ -1,8 +1,8 @@
import { useDialog } from '@/components/common/DialogProvider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { DialogFormProps } from '@/types/common';
import type {
DatabaseAccessLevel,

View File

@@ -1,7 +1,7 @@
import ControlledSelect from '@/components/common/ControlledSelect';
import type { RolePermissionEditorFormValues } from '@/components/dataBrowser/EditPermissionsForm/RolePermissionEditorForm';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Autocomplete from '@/ui/v2/Autocomplete';
import Button from '@/ui/v2/Button';

View File

@@ -1,4 +1,4 @@
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { Rule, RuleGroup } from '@/types/dataBrowser';
import { Alert } from '@/ui/Alert';
import type { BoxProps } from '@/ui/v2/Box';
@@ -194,7 +194,7 @@ export default function RuleGroupEditor({
<Link
href={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region?.awsName,
currentProject.region,
'hasura',
)}/console/data/default/schema/${schema}/tables/${table}/permissions`}
underline="hover"

View File

@@ -3,7 +3,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
import ReadOnlyToggle from '@/components/common/ReadOnlyToggle';
import type { ColumnAutocompleteProps } from '@/components/dataBrowser/ColumnAutocomplete';
import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { HasuraOperator } from '@/types/dataBrowser';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';

View File

@@ -1,6 +1,6 @@
import NavLink from '@/components/common/NavLink';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { Avatar } from '@/ui/Avatar';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';

View File

@@ -7,11 +7,11 @@ import DataGridPreviewCell from '@/components/common/DataGridPreviewCell';
import DataGridTextCell from '@/components/common/DataGridTextCell';
import FilesDataGridControls from '@/components/files/FilesDataGridControls';
import { FileIcon } from '@/components/icons/FileIcon';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useAppClient } from '@/hooks/useAppClient';
import useBuckets from '@/hooks/useBuckets';
import useFiles from '@/hooks/useFiles';
import useFilesAggregate from '@/hooks/useFilesAggregate';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { Files } from '@/utils/__generated__/graphql';
import { Order_By as OrderBy } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env';

View File

@@ -1,9 +1,9 @@
import type { DataGridPaginationProps } from '@/components/common/DataGridPagination';
import DataGridPagination from '@/components/common/DataGridPagination';
import { useDialog } from '@/components/common/DialogProvider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useAppClient } from '@/hooks/useAppClient';
import useDataGridConfig from '@/hooks/useDataGridConfig';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { FileUploadButtonProps } from '@/ui/FileUploadButton';
import FileUploadButton from '@/ui/FileUploadButton';
import type { BoxProps } from '@/ui/v2/Box';

View File

@@ -1,5 +1,5 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useInsertFeedbackOneMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Avatar } from '@/ui/Avatar';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';

View File

@@ -2,11 +2,11 @@ import DesktopNav from '@/components/common/DesktopNav';
import { LoadingScreen } from '@/components/common/LoadingScreen';
import type { AuthenticatedLayoutProps } from '@/components/layout/AuthenticatedLayout';
import AuthenticatedLayout from '@/components/layout/AuthenticatedLayout';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import useProjectRoutes from '@/hooks/common/useProjectRoutes';
import { useNavigationVisible } from '@/hooks/useNavigationVisible';
import useNotFoundRedirect from '@/hooks/useNotFoundRedirect';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import { NextSeo } from 'next-seo';
@@ -103,7 +103,7 @@ function ProjectLayoutContent({
>
{children}
<NextSeo title={currentProject.name} />
<NextSeo title={currentProject?.name} />
</Box>
</>
);

View File

@@ -1,5 +1,5 @@
import LogsDatePicker from '@/components/logs/LogsDatePicker';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { AvailableLogsServices, LogsCustomInterval } from '@/types/logs';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';

View File

@@ -1,7 +1,5 @@
import { mockApplication, mockWorkspace } from '@/tests/mocks';
import { queryClient, render, screen } from '@/tests/testUtils';
import type { Project } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import type { Workspace } from '@/types/workspace';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { afterAll, beforeAll, vi } from 'vitest';
@@ -35,43 +33,6 @@ vi.mock('next/router', () => ({
}),
}));
const mockApplication: Project = {
id: '1',
name: 'Test Application',
slug: 'test-application',
appStates: [],
subdomain: '',
isProvisioned: true,
region: {
awsName: 'us-east-1',
city: 'New York',
countryCode: 'US',
id: '1',
},
createdAt: new Date().toISOString(),
deployments: [],
desiredState: ApplicationStatus.Live,
featureFlags: [],
providersUpdated: true,
githubRepository: { fullName: 'test/git-project' },
repositoryProductionBranch: null,
nhostBaseFolder: null,
plan: null,
config: {
hasura: {
adminSecret: 'nhost-admin-secret',
},
},
};
const mockWorkspace: Workspace = {
id: '1',
name: 'Test Workspace',
slug: 'test-workspace',
members: [],
applications: [mockApplication],
};
const server = setupServer(
rest.get('https://local.graphql.nhost.run/v1', (_req, res, ctx) =>
res(ctx.status(200)),

View File

@@ -2,7 +2,7 @@ import useGitHubModal from '@/components/applications/github/useGitHubModal';
import DeploymentListItem from '@/components/deployments/DeploymentListItem';
import GithubIcon from '@/components/icons/GithubIcon';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';

View File

@@ -1,6 +1,6 @@
import type { MetricsCardProps } from '@/components/overview/MetricsCard';
import { MetricsCard } from '@/components/overview/MetricsCard';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import Text from '@/ui/v2/Text';
import { useGetProjectMetricsQuery } from '@/utils/__generated__/graphql';
import { prettifyNumber } from '@/utils/common/prettifyNumber';

View File

@@ -1,5 +1,5 @@
import InfoCard from '@/components/overview/InfoCard';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import Text from '@/ui/v2/Text';
import Image from 'next/image';

View File

@@ -1,6 +1,6 @@
import GithubIcon from '@/components/icons/GithubIcon';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';

View File

@@ -1,12 +1,13 @@
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import CogIcon from '@/ui/v2/icons/CogIcon';
import Text from '@/ui/v2/Text';
import CogIcon from '@/ui/v2/icons/CogIcon';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import Image from 'next/image';
import Link from 'next/link';
@@ -14,6 +15,7 @@ import Link from 'next/link';
export default function OverviewTopBar() {
const isPlatform = useIsPlatform();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const isOwner = useIsCurrentUserOwner();
const isPro = !currentProject?.plan?.isFree;
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
@@ -87,7 +89,7 @@ export default function OverviewTopBar() {
color={isPro ? 'primary' : 'default'}
/>
{!isPro && (
{!isPro && isOwner && (
<Button
variant="borderless"
className="mr-2"

View File

@@ -1,4 +1,5 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useGetAppFunctionsMetadataQuery,
useGetProjectMetricsQuery,
@@ -6,7 +7,6 @@ import {
} from '@/generated/graphql';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import LinearProgress from '@/ui/v2/LinearProgress';
import Text from '@/ui/v2/Text';
import { prettifySize } from '@/utils/common/prettifySize';

View File

@@ -1,15 +1,15 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
useResetPostgresPasswordMutation,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import Button from '@/ui/v2/Button';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputAdornment from '@/ui/v2/InputAdornment';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import { copy } from '@/utils/copy';
import { discordAnnounce } from '@/utils/discordAnnounce';
import generateRandomDatabasePassword from '@/utils/settings/generateRandomDatabasePassword';
@@ -56,7 +56,9 @@ export default function ResetDatabasePasswordSettings() {
const handleGenerateRandomPassword = () => {
const newRandomDatabasePassword = generateRandomDatabasePassword();
triggerToast('New random database password generated.');
setValue('databasePassword', newRandomDatabasePassword);
setValue('databasePassword', newRandomDatabasePassword, {
shouldDirty: true,
});
};
const handleChangeDatabasePassword = async (

View File

@@ -1,5 +1,5 @@
import NavLink from '@/components/common/NavLink';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import FloatingActionButton from '@/ui/FloatingActionButton';
import Backdrop from '@/ui/v2/Backdrop';
import type { BoxProps } from '@/ui/v2/Box';

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -1,7 +1,7 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {
GetAuthenticationSettingsDocument,

View File

@@ -2,12 +2,12 @@ import ControlledSelect from '@/components/common/ControlledSelect';
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Option from '@/ui/v2/Option';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -5,7 +5,7 @@ import type {
import BaseEnvironmentVariableForm, {
baseEnvironmentVariableFormValidationSchema,
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {
GetEnvironmentVariablesDocument,

View File

@@ -5,7 +5,7 @@ import type {
import BaseEnvironmentVariableForm, {
baseEnvironmentVariableFormValidationSchema,
} from '@/components/settings/environmentVariables/BaseEnvironmentVariableForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { EnvironmentVariable } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {

View File

@@ -1,6 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { DialogFormProps } from '@/types/common';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';

View File

@@ -3,7 +3,7 @@ import SettingsContainer from '@/components/settings/SettingsContainer';
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { EnvironmentVariable } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';

View File

@@ -3,9 +3,9 @@ import InlineCode from '@/components/common/InlineCode';
import SettingsContainer from '@/components/settings/SettingsContainer';
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useAppClient } from '@/hooks/useAppClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
@@ -22,7 +22,6 @@ import generateAppServiceUrl, {
defaultRemoteBackendSlugs,
} from '@/utils/common/generateAppServiceUrl';
import { getHasuraConsoleServiceUrl } from '@/utils/env';
import { generateRemoteAppUrl } from '@/utils/helpers';
import getJwtSecretsWithoutFalsyValues from '@/utils/settings/getJwtSecretsWithoutFalsyValues';
import { Fragment, useState } from 'react';
@@ -99,10 +98,6 @@ export default function SystemEnvironmentVariableSettings() {
}
const systemEnvironmentVariables = [
{
key: 'NHOST_BACKEND_URL',
value: generateRemoteAppUrl(currentProject.subdomain),
},
{ key: 'NHOST_SUBDOMAIN', value: currentProject.subdomain },
{ key: 'NHOST_REGION', value: currentProject.region.awsName },
{
@@ -112,7 +107,7 @@ export default function SystemEnvironmentVariableSettings() {
? `${getHasuraConsoleServiceUrl()}/console`
: generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region.awsName,
currentProject?.region,
'hasura',
defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },

View File

@@ -2,11 +2,11 @@ import Form from '@/components/common/Form';
import InlineCode from '@/components/common/InlineCode';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAllWorkspacesAndProjectsDocument,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Alert } from '@/ui/Alert';
import Input from '@/ui/v2/Input';
import { discordAnnounce } from '@/utils/discordAnnounce';

View File

@@ -1,11 +1,11 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAllWorkspacesAndProjectsDocument,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { Alert } from '@/ui/Alert';
import Input from '@/ui/v2/Input';
import { discordAnnounce } from '@/utils/discordAnnounce';

View File

@@ -5,7 +5,7 @@ import type {
import BasePermissionVariableForm, {
basePermissionVariableValidationSchema,
} from '@/components/settings/permissions/BasePermissionVariableForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {
GetRolesPermissionsDocument,

View File

@@ -5,7 +5,7 @@ import type {
import BasePermissionVariableForm, {
basePermissionVariableValidationSchema,
} from '@/components/settings/permissions/BasePermissionVariableForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { PermissionVariable } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {

View File

@@ -3,7 +3,7 @@ import SettingsContainer from '@/components/settings/SettingsContainer';
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { PermissionVariable } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';

View File

@@ -1,24 +1,26 @@
import { prettifyMemory } from '@/features/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/settings/resources/utils/prettifyVCPU';
import useProPlan from '@/hooks/common/useProPlan';
import { calculateBillableResources } from '@/features/projects/settings/resources/utils/calculateBillableResources';
import { prettifyMemory } from '@/features/projects/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/projects/settings/resources/utils/prettifyVCPU';
import type { ResourceSettingsFormValues } from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import { useProPlan } from '@/hooks/common/useProPlan';
import { Alert } from '@/ui/Alert';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Divider from '@/ui/v2/Divider';
import Text from '@/ui/v2/Text';
import Tooltip from '@/ui/v2/Tooltip';
import { InfoIcon } from '@/ui/v2/icons/InfoIcon';
import {
RESOURCE_VCPU_MULTIPLIER,
RESOURCE_VCPU_PRICE,
RESOURCE_VCPU_PRICE_PER_MINUTE,
} from '@/utils/CONSTANTS';
export interface ResourcesConfirmationDialogProps {
/**
* Price of the new plan.
* The updated resources that the user has selected.
*/
updatedResources: {
vcpu: number;
memory: number;
};
formValues: ResourceSettingsFormValues;
/**
* Function to be called when the user clicks the cancel button.
*/
@@ -30,13 +32,47 @@ export interface ResourcesConfirmationDialogProps {
}
export default function ResourcesConfirmationDialog({
updatedResources,
formValues,
onCancel,
onSubmit,
}: ResourcesConfirmationDialogProps) {
const { data: proPlan, loading, error } = useProPlan();
const priceForTotalAvailableVCPU =
(formValues.totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE;
const billableResources = calculateBillableResources(
{
replicas: formValues.database?.replicas,
vcpu: formValues.database?.vcpu,
memory: formValues.database?.memory,
},
{
replicas: formValues.hasura?.replicas,
vcpu: formValues.hasura?.vcpu,
memory: formValues.hasura?.memory,
},
{
replicas: formValues.auth?.replicas,
vcpu: formValues.auth?.vcpu,
memory: formValues.auth?.memory,
},
{
replicas: formValues.storage?.replicas,
vcpu: formValues.storage?.vcpu,
memory: formValues.storage?.memory,
},
);
const totalBillableVCPU = formValues.enabled ? billableResources.vcpu : 0;
const totalBillableMemory = formValues.enabled ? billableResources.memory : 0;
const updatedPrice =
RESOURCE_VCPU_PRICE * (updatedResources.vcpu / RESOURCE_VCPU_MULTIPLIER);
Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
) + proPlan.price;
if (!loading && !proPlan) {
return (
@@ -50,9 +86,22 @@ export default function ResourcesConfirmationDialog({
throw error;
}
const databaseResources = `${prettifyVCPU(
formValues.database.vcpu,
)} vCPU + ${prettifyMemory(formValues.database.memory)}`;
const hasuraResources = `${prettifyVCPU(
formValues.hasura.vcpu,
)} vCPU + ${prettifyMemory(formValues.hasura.memory)}`;
const authResources = `${prettifyVCPU(
formValues.auth.vcpu,
)} vCPU + ${prettifyMemory(formValues.auth.memory)}`;
const storageResources = `${prettifyVCPU(
formValues.storage.vcpu,
)} vCPU + ${prettifyMemory(formValues.storage.memory)}`;
return (
<div className="grid grid-flow-row gap-6 px-6 pb-6">
{updatedResources.vcpu > 0 ? (
{totalBillableVCPU > 0 ? (
<Text className="text-center">
Please allow some time for the selected resources to take effect.
</Text>
@@ -69,28 +118,96 @@ export default function ResourcesConfirmationDialog({
<Text>${proPlan.price.toFixed(2)}/mo</Text>
</Box>
<Box className="grid grid-flow-col items-center justify-between gap-2">
<Box className="grid grid-flow-row gap-0.5">
<Text className="font-medium">Dedicated Resources</Text>
<Text className="text-xs" color="secondary">
{prettifyVCPU(updatedResources.vcpu)} vCPUs +{' '}
{prettifyMemory(updatedResources.memory)} of Memory
<Box className="grid grid-flow-row gap-1.5">
<Box className="grid grid-flow-col items-center justify-between gap-2">
<Box className="grid grid-flow-row gap-0.5">
<Text className="font-medium">Dedicated Resources</Text>
</Box>
<Text>
$
{(
(totalBillableVCPU / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE_PER_MINUTE
).toFixed(4)}
/min
</Text>
</Box>
<Text>${updatedPrice.toFixed(2)}/mo</Text>
<Box className="grid w-full grid-flow-row gap-1.5">
<Box className="grid grid-flow-col justify-between gap-2">
<Text className="text-xs" color="secondary">
PostgreSQL Database
</Text>
<Text className="text-xs" color="secondary">
{formValues.database.replicas > 1
? `${databaseResources} (${formValues.database.replicas} replicas)`
: databaseResources}
</Text>
</Box>
<Box className="grid grid-flow-col justify-between gap-2">
<Text className="text-xs" color="secondary">
Hasura GraphQL
</Text>
<Text className="text-xs" color="secondary">
{formValues.hasura.replicas > 1
? `${hasuraResources} (${formValues.hasura.replicas} replicas)`
: hasuraResources}
</Text>
</Box>
<Box className="grid grid-flow-col justify-between gap-2">
<Text className="text-xs" color="secondary">
Auth
</Text>
<Text className="text-xs" color="secondary">
{formValues.auth.replicas > 1
? `${authResources} (${formValues.auth.replicas} replicas)`
: authResources}
</Text>
</Box>
<Box className="grid grid-flow-col justify-between gap-2">
<Text className="text-xs" color="secondary">
Storage
</Text>
<Text className="text-xs" color="secondary">
{formValues.storage.replicas > 1
? `${storageResources} (${formValues.storage.replicas} replicas)`
: storageResources}
</Text>
</Box>
<Box className="grid grid-flow-col justify-between gap-2">
<Text className="text-xs font-medium" color="secondary">
Total
</Text>
<Text className="text-xs font-medium" color="secondary">
{prettifyVCPU(totalBillableVCPU)} vCPU +{' '}
{prettifyMemory(totalBillableMemory)}
</Text>
</Box>
</Box>
</Box>
<Divider />
<Box className="grid grid-flow-col justify-between gap-2">
<Text className="font-medium">Total</Text>
<Text>${(updatedPrice + proPlan.price).toFixed(2)}/mo</Text>
<Box className="grid grid-flow-col items-center gap-1.5">
<Text className="font-medium">Approximate Cost</Text>
<Tooltip title="$0.0012/minute for every 1 vCPU and 2 GiB of RAM">
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Text>${updatedPrice.toFixed(2)}/mo</Text>
</Box>
</Box>
<Box className="grid grid-flow-row gap-2">
<Button
color={updatedResources.vcpu > 0 ? 'primary' : 'error'}
color={totalBillableVCPU > 0 ? 'primary' : 'error'}
onClick={onSubmit}
autoFocus
>

View File

@@ -1,3 +1,4 @@
import { mockMatchMediaValue, mockRouter } from '@/tests/mocks';
import {
getProPlanOnlyQuery,
getWorkspaceAndProjectQuery,
@@ -12,7 +13,6 @@ import {
fireEvent,
render,
screen,
waitFor,
waitForElementToBeRemoved,
within,
} from '@/tests/testUtils';
@@ -22,49 +22,16 @@ import {
} from '@/utils/CONSTANTS';
import userEvent from '@testing-library/user-event';
import { setupServer } from 'msw/node';
import { test, vi } from 'vitest';
import { expect, test, vi } from 'vitest';
import ResourcesForm from './ResourcesForm';
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
value: vi.fn().mockImplementation(mockMatchMediaValue),
});
vi.mock('next/router', () => ({
useRouter: vi.fn().mockReturnValue({
basePath: '',
pathname: '/test-workspace/test-application',
route: '/[workspaceSlug]/[appSlug]',
asPath: '/test-workspace/test-application',
isLocaleDomain: false,
isReady: true,
isPreview: false,
query: {
workspaceSlug: 'test-workspace',
appSlug: 'test-application',
},
push: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),
back: vi.fn(),
prefetch: vi.fn(),
beforePopState: vi.fn(),
events: {
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
},
isFallback: false,
}),
useRouter: vi.fn().mockReturnValue(mockRouter),
}));
const server = setupServer(
@@ -79,7 +46,10 @@ beforeAll(() => {
server.listen();
});
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
afterAll(() => {
server.close();
vi.restoreAllMocks();
});
// Note: Workaround based on https://github.com/testing-library/user-event/issues/871#issuecomment-1059317998
function changeSliderValue(slider: HTMLElement, value: number) {
@@ -92,9 +62,7 @@ test('should show an empty state message that the feature must be enabled if no
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(screen.getByText(/enable this feature/i)).toBeInTheDocument();
expect(await screen.findByText(/enable this feature/i)).toBeInTheDocument();
});
test('should show the sliders if the switch is enabled', async () => {
@@ -103,28 +71,23 @@ test('should show the sliders if the switch is enabled', async () => {
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(screen.getByText(/enable this feature/i)).toBeInTheDocument();
expect(await screen.findByText(/enable this feature/i)).toBeInTheDocument();
await user.click(screen.getByRole('checkbox'));
expect(screen.queryByText(/enable this feature/i)).not.toBeInTheDocument();
expect(screen.getAllByRole('slider')).toHaveLength(9);
expect(screen.getAllByRole('slider')).toHaveLength(12);
});
test('should not show an empty state message if there is data available', async () => {
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
await waitFor(() =>
expect(
screen.queryByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument(),
);
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
expect(screen.queryByText(/enable this feature/i)).not.toBeInTheDocument();
expect(screen.getAllByRole('slider')).toHaveLength(9);
expect(screen.getAllByRole('slider')).toHaveLength(12);
expect(screen.getByText(/^vcpus:/i)).toHaveTextContent(/vcpus: 8/i);
expect(screen.getByText(/^memory:/i)).toHaveTextContent(/memory: 16384 mib/i);
});
@@ -132,7 +95,9 @@ test('should not show an empty state message if there is data available', async
test('should show a warning message if not all the resources are allocated', async () => {
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
changeSliderValue(
screen.getByRole('slider', {
@@ -145,14 +110,16 @@ test('should show a warning message if not all the resources are allocated', asy
expect(screen.getByText(/^memory:/i)).toHaveTextContent(/memory: 18432 mib/i);
expect(
screen.getByText(/you now have 1 vcpus and 2048 mib of memory unused./i),
screen.getByText(/you have 1 vcpus and 2048 mib of memory unused./i),
).toBeInTheDocument();
});
test('should update the price when the top slider is changed', async () => {
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
expect(screen.queryByText(/\$200\.00\/mo/i)).not.toBeInTheDocument();
@@ -172,7 +139,9 @@ test('should show a validation error when the form is submitted when not everyth
const user = userEvent.setup();
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
@@ -186,8 +155,10 @@ test('should show a validation error when the form is submitted when not everyth
await user.click(screen.getByRole('button', { name: /save/i }));
expect(
screen.getAllByText(/you now have 1 vcpus and 2048 mib of memory unused./i),
).toHaveLength(2);
screen.getByText(/you have 1 vcpus and 2048 mib of memory unused./i),
).toBeInTheDocument();
expect(screen.getByText(/invalid configuration/i)).toBeInTheDocument();
});
test('should show a confirmation dialog when the form is submitted', async () => {
@@ -196,12 +167,9 @@ test('should show a confirmation dialog when the form is submitted', async () =>
const user = userEvent.setup();
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
await waitFor(() =>
expect(
screen.queryByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument(),
);
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
changeSliderValue(
screen.getByRole('slider', {
@@ -212,36 +180,36 @@ test('should show a confirmation dialog when the form is submitted', async () =>
changeSliderValue(
screen.getByRole('slider', { name: /database vcpu/i }),
2.25 * RESOURCE_VCPU_MULTIPLIER,
2 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql vcpu/i }),
2.25 * RESOURCE_VCPU_MULTIPLIER,
2.5 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /auth vcpu/i }),
2.25 * RESOURCE_VCPU_MULTIPLIER,
1.5 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage vcpu/i }),
2.25 * RESOURCE_VCPU_MULTIPLIER,
3 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /database memory/i }),
4.5 * RESOURCE_MEMORY_MULTIPLIER,
4.75 * RESOURCE_MEMORY_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql memory/i }),
4.5 * RESOURCE_MEMORY_MULTIPLIER,
4.25 * RESOURCE_MEMORY_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /auth memory/i }),
4.5 * RESOURCE_MEMORY_MULTIPLIER,
4 * RESOURCE_MEMORY_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage memory/i }),
4.5 * RESOURCE_MEMORY_MULTIPLIER,
5 * RESOURCE_MEMORY_MULTIPLIER,
);
await user.click(screen.getByRole('button', { name: /save/i }));
@@ -253,15 +221,21 @@ test('should show a confirmation dialog when the form is submitted', async () =>
}),
).toBeInTheDocument();
expect(
within(screen.getByRole('dialog')).getByText(
/9 vcpus \+ 18432 mib of memory/i,
{ exact: true },
),
).toBeInTheDocument();
within(screen.getByRole('dialog')).getByText(/postgresql database/i)
.parentElement,
).toHaveTextContent(/2 vcpu \+ 4864 mib/i);
expect(
within(screen.getByRole('dialog')).getByText(/\$475\.00\/mo/i, {
exact: true,
}),
within(screen.getByRole('dialog')).getByText(/hasura graphql/i)
.parentElement,
).toHaveTextContent(/2.5 vcpu \+ 4352 mib/i);
expect(
within(screen.getByRole('dialog')).getByText(/auth/i).parentElement,
).toHaveTextContent(/1.5 vcpu \+ 4096 mib/i);
expect(
within(screen.getByRole('dialog')).getByText(/storage/i).parentElement,
).toHaveTextContent(/3 vcpu \+ 5120 mib/i);
expect(
within(screen.getByRole('dialog')).getByText(/\$475\.00\/mo/i),
).toBeInTheDocument();
// we need to mock the query again because the mutation updated the resources
@@ -270,7 +244,8 @@ test('should show a confirmation dialog when the form is submitted', async () =>
await user.click(screen.getByRole('button', { name: /confirm/i }));
await waitForElementToBeRemoved(() => screen.getByRole('dialog'));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
expect(
await screen.findByText(/resources have been updated successfully./i),
).toBeInTheDocument();
@@ -286,13 +261,15 @@ test('should display a red button when custom resources are disabled', async ()
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
await user.click(screen.getByRole('checkbox'));
expect(screen.getByText(/enable this feature/i)).toBeInTheDocument();
expect(screen.getByText(/total cost:/i)).toHaveTextContent(
/total cost: \$25\.00\/mo/i,
expect(screen.getByText(/approximate cost:/i)).toHaveTextContent(
/approximate cost: \$25\.00\/mo/i,
);
await user.click(screen.getByRole('button', { name: /save/i }));
@@ -307,14 +284,16 @@ test('should display a red button when custom resources are disabled', async ()
});
});
test('should hide the footer when custom resource allocation is disabled', async () => {
test('should hide the pricing information when custom resource allocation is disabled', async () => {
server.use(updateConfigMutation);
const user = userEvent.setup();
render(<ResourcesForm />);
await waitForElementToBeRemoved(() => screen.getByRole('progressbar'));
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
await user.click(screen.getByRole('checkbox'));
await user.click(screen.getByRole('button', { name: /save/i }));
@@ -325,7 +304,203 @@ test('should hide the footer when custom resource allocation is disabled', async
await user.click(screen.getByRole('button', { name: /confirm/i }));
await waitForElementToBeRemoved(() => screen.getByRole('dialog'));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
expect(screen.queryByText(/total cost:/i)).not.toBeInTheDocument();
expect(screen.queryByText(/approximate cost:/i)).not.toBeInTheDocument();
});
test('should show a warning message when resources are overallocated', async () => {
render(<ResourcesForm />);
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
changeSliderValue(
screen.getByRole('slider', {
name: /total available vcpu/i,
}),
7 * RESOURCE_VCPU_MULTIPLIER,
);
expect(
screen.getByText(
/^you have 1 vCPUs and 2048 mib of memory overallocated\. reduce it before saving or increase the total amount\./i,
),
).toBeInTheDocument();
});
test('should change pricing based on selected replicas', async () => {
render(<ResourcesForm />);
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
expect(screen.getByText(/approximate cost:/i)).toHaveTextContent(
/approximate cost: \$425\.00\/mo/i,
);
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql replicas/i }),
2,
);
expect(screen.getByText(/approximate cost:/i)).toHaveTextContent(
/approximate cost: \$525\.00\/mo/i,
);
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql replicas/i }),
1,
);
expect(screen.getByText(/approximate cost:/i)).toHaveTextContent(
/approximate cost: \$425\.00\/mo/i,
);
});
test('should validate if vCPU and Memory match the 1:2 ratio if more than 1 replica is selected', async () => {
const user = userEvent.setup();
render(<ResourcesForm />);
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
changeSliderValue(
screen.getByRole('slider', {
name: /total available vcpu/i,
}),
20 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage replicas/i }),
2,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage vcpu/i }),
1 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage memory/i }),
6 * RESOURCE_MEMORY_MULTIPLIER,
);
await user.click(screen.getByRole('button', { name: /save/i }));
expect(screen.getByText(/invalid configuration/i)).toBeInTheDocument();
expect(
screen.getByText(
/please check the form for errors and the allocation for each service and try again\./i,
),
).toBeInTheDocument();
const validationErrorMessage = screen.getByLabelText(
/vcpu and memory for this service must match the 1:2 ratio if more than one replica is selected\./i,
);
expect(validationErrorMessage).toBeInTheDocument();
expect(validationErrorMessage).toHaveStyle({ color: '#f13154' });
});
test('should take replicas into account when confirming the resources', async () => {
const user = userEvent.setup();
render(<ResourcesForm />);
expect(
await screen.findByRole('slider', { name: /total available vcpu/i }),
).toBeInTheDocument();
changeSliderValue(
screen.getByRole('slider', {
name: /total available vcpu/i,
}),
8.5 * RESOURCE_VCPU_MULTIPLIER,
);
// setting up database
changeSliderValue(
screen.getByRole('slider', { name: /database vcpu/i }),
2 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /database memory/i }),
4 * RESOURCE_MEMORY_MULTIPLIER,
);
// setting up hasura
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql replicas/i }),
3,
);
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql vcpu/i }),
2.5 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /hasura graphql memory/i }),
5 * RESOURCE_MEMORY_MULTIPLIER,
);
// setting up auth
changeSliderValue(screen.getByRole('slider', { name: /auth replicas/i }), 2);
changeSliderValue(
screen.getByRole('slider', { name: /auth vcpu/i }),
1.5 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /auth memory/i }),
3 * RESOURCE_MEMORY_MULTIPLIER,
);
// setting up storage
changeSliderValue(
screen.getByRole('slider', { name: /storage replicas/i }),
4,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage vcpu/i }),
2.5 * RESOURCE_VCPU_MULTIPLIER,
);
changeSliderValue(
screen.getByRole('slider', { name: /storage memory/i }),
5 * RESOURCE_MEMORY_MULTIPLIER,
);
await user.click(screen.getByRole('button', { name: /save/i }));
expect(await screen.findByRole('dialog')).toBeInTheDocument();
const dialog = screen.getByRole('dialog');
expect(
within(dialog).getByText(/postgresql database/i).parentElement,
).toHaveTextContent(/2 vcpu \+ 4096 mib/i);
expect(
within(dialog).getByText(/hasura graphql/i).parentElement,
).toHaveTextContent(/2\.5 vcpu \+ 5120 mib \(3 replicas\)/i);
expect(within(dialog).getByText(/auth/i).parentElement).toHaveTextContent(
/1\.5 vcpu \+ 3072 mib \(2 replicas\)/i,
);
expect(within(dialog).getByText(/storage/i).parentElement).toHaveTextContent(
/2\.5 vcpu \+ 5120 mib \(4 replicas\)/i,
);
// total must contain the sum of all resources when replicas are taken into
// account
expect(within(dialog).getByText(/total/i).parentElement).toHaveTextContent(
/22\.5 vcpu \+ 46080 mib/i,
);
expect(within(dialog).getByText(/\$0.0270\/min/i)).toBeInTheDocument();
expect(within(dialog).getByText(/\$1150\.00\/mo/i)).toBeInTheDocument();
});

View File

@@ -4,18 +4,15 @@ import SettingsContainer from '@/components/settings/SettingsContainer';
import ResourcesConfirmationDialog from '@/components/settings/resources/ResourcesConfirmationDialog';
import ServiceResourcesFormFragment from '@/components/settings/resources/ServiceResourcesFormFragment';
import TotalResourcesFormFragment from '@/components/settings/resources/TotalResourcesFormFragment';
import { prettifyMemory } from '@/features/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/settings/resources/utils/prettifyVCPU';
import type { ResourceSettingsFormValues } from '@/features/settings/resources/utils/resourceSettingsValidationSchema';
import { resourceSettingsValidationSchema } from '@/features/settings/resources/utils/resourceSettingsValidationSchema';
import useProPlan from '@/hooks/common/useProPlan';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { calculateBillableResources } from '@/features/projects/settings/resources/utils/calculateBillableResources';
import type { ResourceSettingsFormValues } from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import { resourceSettingsValidationSchema } from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import { useProPlan } from '@/hooks/common/useProPlan';
import { Alert } from '@/ui/Alert';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Divider from '@/ui/v2/Divider';
import Text from '@/ui/v2/Text';
import {
RESOURCE_VCPU_MULTIPLIER,
RESOURCE_VCPU_PRICE,
@@ -27,29 +24,27 @@ import {
useUpdateConfigMutation,
} from '@/utils/__generated__/graphql';
import getServerError from '@/utils/settings/getServerError';
import getUnallocatedResources from '@/utils/settings/getUnallocatedResources';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
import ResourcesFormFooter from './ResourcesFormFooter';
function getInitialServiceResources(
data: GetResourcesQuery,
service: Exclude<keyof GetResourcesQuery['config'], '__typename'>,
) {
const { cpu, memory } = data?.config?.[service]?.resources?.compute || {};
const { compute, replicas } = data?.config?.[service]?.resources || {};
return {
vcpu: cpu || 0,
memory: memory || 0,
replicas,
vcpu: compute?.cpu || 0,
memory: compute?.memory || 0,
};
}
export default function ResourcesForm() {
const [validationError, setValidationError] = useState<Error | null>(null);
const { openDialog, closeDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -95,14 +90,26 @@ export default function ResourcesForm() {
enabled: totalInitialVCPU > 0 && totalInitialMemory > 0,
totalAvailableVCPU: totalInitialVCPU || 2000,
totalAvailableMemory: totalInitialMemory || 4096,
hasuraVCPU: initialHasuraResources.vcpu || 500,
hasuraMemory: initialHasuraResources.memory || 1536,
databaseVCPU: initialDatabaseResources.vcpu || 1000,
databaseMemory: initialDatabaseResources.memory || 2048,
authVCPU: initialAuthResources.vcpu || 250,
authMemory: initialAuthResources.memory || 256,
storageVCPU: initialStorageResources.vcpu || 250,
storageMemory: initialStorageResources.memory || 256,
database: {
replicas: initialDatabaseResources.replicas || 1,
vcpu: initialDatabaseResources.vcpu || 1000,
memory: initialDatabaseResources.memory || 2048,
},
hasura: {
replicas: initialHasuraResources.replicas || 1,
vcpu: initialHasuraResources.vcpu || 500,
memory: initialHasuraResources.memory || 1536,
},
auth: {
replicas: initialAuthResources.replicas || 1,
vcpu: initialAuthResources.vcpu || 250,
memory: initialAuthResources.memory || 256,
},
storage: {
replicas: initialStorageResources.replicas || 1,
vcpu: initialStorageResources.vcpu || 250,
memory: initialStorageResources.memory || 256,
},
},
resolver: yupResolver(resourceSettingsValidationSchema),
});
@@ -127,16 +134,32 @@ export default function ResourcesForm() {
const { watch, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const hasFormErrors = Object.keys(formState.errors).length > 0;
const enabled = watch('enabled');
const totalAvailableVCPU = enabled ? watch('totalAvailableVCPU') : 0;
const billableResources = calculateBillableResources(
{
replicas: initialDatabaseResources.replicas,
vcpu: initialDatabaseResources.vcpu,
},
{
replicas: initialHasuraResources.replicas,
vcpu: initialHasuraResources.vcpu,
},
{
replicas: initialAuthResources.replicas,
vcpu: initialAuthResources.vcpu,
},
{
replicas: initialStorageResources.replicas,
vcpu: initialStorageResources.vcpu,
},
);
const initialPrice =
RESOURCE_VCPU_PRICE * (totalInitialVCPU / RESOURCE_VCPU_MULTIPLIER) +
proPlan.price;
const updatedPrice =
RESOURCE_VCPU_PRICE * (totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) +
proPlan.price;
proPlan.price +
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE;
async function handleSubmit(formValues: ResourceSettingsFormValues) {
const updateConfigPromise = updateConfig({
@@ -144,46 +167,46 @@ export default function ResourcesForm() {
appId: currentProject?.id,
config: {
postgres: {
resources: enabled
resources: formValues.enabled
? {
compute: {
cpu: formValues.databaseVCPU,
memory: formValues.databaseMemory,
cpu: formValues.database.vcpu,
memory: formValues.database.memory,
},
replicas: 1,
replicas: formValues.database.replicas,
}
: null,
},
hasura: {
resources: enabled
resources: formValues.enabled
? {
compute: {
cpu: formValues.hasuraVCPU,
memory: formValues.hasuraMemory,
cpu: formValues.hasura.vcpu,
memory: formValues.hasura.memory,
},
replicas: 1,
replicas: formValues.hasura.replicas,
}
: null,
},
auth: {
resources: enabled
resources: formValues.enabled
? {
compute: {
cpu: formValues.authVCPU,
memory: formValues.authMemory,
cpu: formValues.auth.vcpu,
memory: formValues.auth.memory,
},
replicas: 1,
replicas: formValues.auth.replicas,
}
: null,
},
storage: {
resources: enabled
resources: formValues.enabled
? {
compute: {
cpu: formValues.storageVCPU,
memory: formValues.storageMemory,
cpu: formValues.storage.vcpu,
memory: formValues.storage.memory,
},
replicas: 1,
replicas: formValues.storage.replicas,
}
: null,
},
@@ -209,14 +232,26 @@ export default function ResourcesForm() {
enabled: false,
totalAvailableVCPU: 2000,
totalAvailableMemory: 4096,
hasuraVCPU: 500,
hasuraMemory: 1536,
databaseVCPU: 1000,
databaseMemory: 2048,
authVCPU: 250,
authMemory: 256,
storageVCPU: 250,
storageMemory: 256,
database: {
replicas: 1,
vcpu: 1000,
memory: 2048,
},
hasura: {
replicas: 1,
vcpu: 500,
memory: 1536,
},
auth: {
replicas: 1,
vcpu: 250,
memory: 256,
},
storage: {
replicas: 1,
vcpu: 250,
memory: 256,
},
});
} else {
form.reset(null, { keepValues: true, keepDirty: false });
@@ -227,41 +262,13 @@ export default function ResourcesForm() {
}
function handleConfirm(formValues: ResourceSettingsFormValues) {
setValidationError(null);
const { vcpu: unallocatedVCPU, memory: unallocatedMemory } =
getUnallocatedResources(formValues);
const hasUnusedResources = unallocatedVCPU > 0 || unallocatedMemory > 0;
if (hasUnusedResources) {
const unusedResourceMessage = [
unallocatedVCPU > 0 ? `${prettifyVCPU(unallocatedVCPU)} vCPUs` : '',
unallocatedMemory > 0
? `${prettifyMemory(unallocatedMemory)} of Memory`
: '',
]
.filter(Boolean)
.join(' and ');
setValidationError(
new Error(
`You now have ${unusedResourceMessage} unused. Allocate it to any of the services before saving.`,
),
);
return;
}
openDialog({
title: enabled
title: formValues.enabled
? 'Confirm Dedicated Resources'
: 'Disable Dedicated Resources',
component: (
<ResourcesConfirmationDialog
updatedResources={{
vcpu: enabled ? formValues.totalAvailableVCPU : 0,
memory: enabled ? formValues.totalAvailableMemory : 0,
}}
formValues={formValues}
onCancel={closeDialog}
onSubmit={async () => {
await handleSubmit(formValues);
@@ -304,47 +311,46 @@ export default function ResourcesForm() {
<ServiceResourcesFormFragment
title="PostgreSQL Database"
description="Manage how much compute you need for the PostgreSQL Database."
cpuKey="databaseVCPU"
memoryKey="databaseMemory"
serviceKey="database"
disableReplicas
/>
<Divider />
<ServiceResourcesFormFragment
title="Hasura GraphQL"
description="Manage how much compute you need for the Hasura GraphQL API."
cpuKey="hasuraVCPU"
memoryKey="hasuraMemory"
serviceKey="hasura"
/>
<Divider />
<ServiceResourcesFormFragment
title="Auth"
description="Manage how much compute you need for Auth."
cpuKey="authVCPU"
memoryKey="authMemory"
serviceKey="auth"
/>
<Divider />
<ServiceResourcesFormFragment
title="Storage"
description="Manage how much compute you need for Storage."
cpuKey="storageVCPU"
memoryKey="storageMemory"
serviceKey="storage"
/>
{validationError && (
{hasFormErrors && (
<Box className="px-4 pb-4">
<Alert
severity="error"
className="flex flex-col gap-2 text-left"
>
<strong>
Please use all the available vCPUs and Memory
</strong>
<strong>Invalid Configuration</strong>
<p>{validationError.message}</p>
<p>
Please check the form for errors and the allocation for
each service and try again.
</p>
</Alert>
</Box>
)}
</>
) : (
<Box className={twMerge('px-4', (enabled || isDirty) && 'pb-4')}>
<Box className={twMerge('px-4', 'pb-4')}>
<Alert className="text-left">
Enable this feature to access custom resource allocation for
your services.
@@ -352,29 +358,7 @@ export default function ResourcesForm() {
</Box>
)}
{(enabled || isDirty) && (
<Box className="flex flex-row items-center justify-between border-t px-4 pt-4">
<span />
<Box className="flex flex-row items-center gap-4">
<Text>
Total cost:{' '}
<span className="font-medium">
${updatedPrice.toFixed(2)}/mo
</span>
</Text>
<Button
type="submit"
variant={isDirty ? 'contained' : 'outlined'}
color={isDirty ? 'primary' : 'secondary'}
disabled={!isDirty}
>
Save
</Button>
</Box>
</Box>
)}
<ResourcesFormFooter />
</SettingsContainer>
</Form>
</FormProvider>

View File

@@ -0,0 +1,118 @@
import { calculateBillableResources } from '@/features/projects/settings/resources/utils/calculateBillableResources';
import type { ResourceSettingsFormValues } from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import { useProPlan } from '@/hooks/common/useProPlan';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
import Tooltip from '@/ui/v2/Tooltip';
import ArrowSquareOutIcon from '@/ui/v2/icons/ArrowSquareOutIcon';
import { InfoIcon } from '@/ui/v2/icons/InfoIcon';
import {
RESOURCE_VCPU_MULTIPLIER,
RESOURCE_VCPU_PRICE,
} from '@/utils/CONSTANTS';
import { useFormState, useWatch } from 'react-hook-form';
export default function ResourcesFormFooter() {
const {
data: proPlan,
loading: proPlanLoading,
error: proPlanError,
} = useProPlan();
const formState = useFormState<ResourceSettingsFormValues>();
const isDirty = Object.keys(formState.dirtyFields).length > 0;
const enabled = useWatch<ResourceSettingsFormValues>({ name: 'enabled' });
const [totalAvailableVCPU, database, hasura, auth, storage] = useWatch<
ResourceSettingsFormValues,
['totalAvailableVCPU', 'database', 'hasura', 'auth', 'storage']
>({
name: ['totalAvailableVCPU', 'database', 'hasura', 'auth', 'storage'],
});
if (proPlanLoading) {
return <ActivityIndicator label="Loading plan details..." delay={1000} />;
}
if (proPlanError) {
throw proPlanError;
}
const priceForTotalAvailableVCPU =
(totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE;
const billableResources = calculateBillableResources(
{
replicas: database?.replicas,
vcpu: database?.vcpu,
},
{
replicas: hasura?.replicas,
vcpu: hasura?.vcpu,
},
{
replicas: auth?.replicas,
vcpu: auth?.vcpu,
},
{
replicas: storage?.replicas,
vcpu: storage?.vcpu,
},
);
const updatedPrice = enabled
? Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE,
) + proPlan.price
: proPlan.price;
return (
<Box
className="grid items-center gap-4 border-t px-4 pt-4 lg:grid-flow-col lg:justify-between lg:gap-2"
component="footer"
>
<Text>
Learn more about{' '}
<Link
href="https://docs.nhost.io/platform/compute"
target="_blank"
rel="noopener noreferrer"
underline="hover"
className="font-medium"
>
Compute Resources
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
</Link>
</Text>
{(enabled || isDirty) && (
<Box className="grid grid-flow-col items-center justify-between gap-4">
<Box className="grid grid-flow-col items-center gap-1.5">
<Text>
Approximate cost:{' '}
<span className="font-medium">${updatedPrice.toFixed(2)}/mo</span>
</Text>
<Tooltip title="$0.0012/minute for every 1 vCPU and 2 GiB of RAM">
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Button
type="submit"
variant={isDirty ? 'contained' : 'outlined'}
color={isDirty ? 'primary' : 'secondary'}
disabled={!isDirty}
>
Save
</Button>
</Box>
)}
</Box>
);
}

View File

@@ -1,15 +1,19 @@
import { prettifyMemory } from '@/features/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/settings/resources/utils/prettifyVCPU';
import type { ResourceSettingsFormValues } from '@/features/settings/resources/utils/resourceSettingsValidationSchema';
import { prettifyMemory } from '@/features/projects/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/projects/settings/resources/utils/prettifyVCPU';
import type { ResourceSettingsFormValues } from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import {
MAX_SERVICE_MEMORY,
MAX_SERVICE_REPLICAS,
MAX_SERVICE_VCPU,
MIN_SERVICE_MEMORY,
MIN_SERVICE_REPLICAS,
MIN_SERVICE_VCPU,
} from '@/features/settings/resources/utils/resourceSettingsValidationSchema';
} from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import Box from '@/ui/v2/Box';
import Slider from '@/ui/v2/Slider';
import Text from '@/ui/v2/Text';
import Tooltip from '@/ui/v2/Tooltip';
import { ExclamationIcon } from '@/ui/v2/icons/ExclamationIcon';
import { RESOURCE_MEMORY_STEP, RESOURCE_VCPU_STEP } from '@/utils/CONSTANTS';
import { useFormContext, useWatch } from 'react-hook-form';
@@ -23,79 +27,98 @@ export interface ServiceResourcesFormFragmentProps {
*/
description: string;
/**
* Form field name for CPU.
* Form field name for service.
*/
cpuKey: Exclude<
serviceKey: Exclude<
keyof ResourceSettingsFormValues,
'enabled' | 'totalAvailableVCPU' | 'totalAvailableMemory'
>;
/**
* Form field name for Memory.
* Whether to disable the replicas field.
*/
memoryKey: Exclude<
keyof ResourceSettingsFormValues,
'enabled' | 'totalAvailableVCPU' | 'totalAvailableMemory'
>;
disableReplicas?: boolean;
}
export default function ServiceResourcesFormFragment({
title,
description,
cpuKey,
memoryKey,
serviceKey,
disableReplicas = false,
}: ServiceResourcesFormFragmentProps) {
const { setValue } = useFormContext<ResourceSettingsFormValues>();
const {
setValue,
trigger: triggerValidation,
formState,
} = useFormContext<ResourceSettingsFormValues>();
const formValues = useWatch<ResourceSettingsFormValues>();
const serviceValues = formValues[serviceKey];
// Total allocated CPU for all resources
const totalAllocatedCPU = Object.keys(formValues)
.filter((key) => key.endsWith('CPU') && key !== 'totalAvailableVCPU')
.reduce((acc, key) => acc + formValues[key], 0);
const totalAllocatedVCPU = Object.keys(formValues)
.filter(
(key) =>
!['enabled', 'totalAvailableVCPU', 'totalAvailableMemory'].includes(
key,
),
)
.reduce((acc, key) => acc + formValues[key].vcpu, 0);
// Total allocated memory for all resources
const totalAllocatedMemory = Object.keys(formValues)
.filter((key) => key.endsWith('Memory') && key !== 'totalAvailableMemory')
.reduce((acc, key) => acc + formValues[key], 0);
.filter(
(key) =>
!['enabled', 'totalAvailableVCPU', 'totalAvailableMemory'].includes(
key,
),
)
.reduce((acc, key) => acc + formValues[key].memory, 0);
const remainingCPU = formValues.totalAvailableVCPU - totalAllocatedCPU;
const allowedCPU = remainingCPU + formValues[cpuKey];
const remainingVCPU = formValues.totalAvailableVCPU - totalAllocatedVCPU;
const allowedVCPU = remainingVCPU + serviceValues.vcpu;
const remainingMemory =
formValues.totalAvailableMemory - totalAllocatedMemory;
const allowedMemory = remainingMemory + formValues[memoryKey];
const allowedMemory = remainingMemory + serviceValues.memory;
function handleCPUChange(value: string) {
const updatedCPU = parseFloat(value);
const exceedsAvailableCPU =
updatedCPU + (totalAllocatedCPU - formValues[cpuKey]) >
formValues.totalAvailableVCPU;
function handleReplicaChange(value: string) {
const updatedReplicas = parseInt(value, 10);
if (
Number.isNaN(updatedCPU) ||
exceedsAvailableCPU ||
updatedCPU < MIN_SERVICE_VCPU
) {
if (updatedReplicas < MIN_SERVICE_REPLICAS) {
return;
}
setValue(cpuKey, updatedCPU, { shouldDirty: true });
setValue(`${serviceKey}.replicas`, updatedReplicas, { shouldDirty: true });
triggerValidation(`${serviceKey}.replicas`);
}
function handleVCPUChange(value: string) {
const updatedVCPU = parseFloat(value);
if (Number.isNaN(updatedVCPU) || updatedVCPU < MIN_SERVICE_VCPU) {
return;
}
setValue(`${serviceKey}.vcpu`, updatedVCPU, { shouldDirty: true });
// trigger validation for "replicas" field
if (!disableReplicas) {
triggerValidation(`${serviceKey}.replicas`);
}
}
function handleMemoryChange(value: string) {
const updatedMemory = parseFloat(value);
const exceedsAvailableMemory =
updatedMemory + (totalAllocatedMemory - formValues[memoryKey]) >
formValues.totalAvailableMemory;
if (
Number.isNaN(updatedMemory) ||
exceedsAvailableMemory ||
updatedMemory < MIN_SERVICE_MEMORY
) {
if (Number.isNaN(updatedMemory) || updatedMemory < MIN_SERVICE_MEMORY) {
return;
}
setValue(memoryKey, updatedMemory, { shouldDirty: true });
setValue(`${serviceKey}.memory`, updatedMemory, { shouldDirty: true });
// trigger validation for "replicas" field
if (!disableReplicas) {
triggerValidation(`${serviceKey}.replicas`);
}
}
return (
@@ -113,14 +136,14 @@ export default function ServiceResourcesFormFragment({
<Text>
Allocated vCPUs:{' '}
<span className="font-medium">
{prettifyVCPU(formValues[cpuKey])}
{prettifyVCPU(serviceValues.vcpu)}
</span>
</Text>
{remainingCPU > 0 && formValues[cpuKey] < MAX_SERVICE_VCPU && (
{remainingVCPU > 0 && serviceValues.vcpu < MAX_SERVICE_VCPU && (
<Text className="text-sm">
<span className="font-medium">
{prettifyVCPU(remainingCPU)} vCPUs
{prettifyVCPU(remainingVCPU)} vCPUs
</span>{' '}
remaining
</Text>
@@ -128,11 +151,11 @@ export default function ServiceResourcesFormFragment({
</Box>
<Slider
value={formValues[cpuKey]}
onChange={(_event, value) => handleCPUChange(value.toString())}
value={serviceValues.vcpu}
onChange={(_event, value) => handleVCPUChange(value.toString())}
max={MAX_SERVICE_VCPU}
step={RESOURCE_VCPU_STEP}
allowed={allowedCPU}
allowed={allowedVCPU}
aria-label={`${title} vCPU`}
marks
/>
@@ -143,11 +166,11 @@ export default function ServiceResourcesFormFragment({
<Text>
Allocated Memory:{' '}
<span className="font-medium">
{prettifyMemory(formValues[memoryKey])}
{prettifyMemory(serviceValues.memory)}
</span>
</Text>
{remainingMemory > 0 && formValues[memoryKey] < MAX_SERVICE_MEMORY && (
{remainingMemory > 0 && serviceValues.memory < MAX_SERVICE_MEMORY && (
<Text className="text-sm">
<span className="font-medium">
{prettifyMemory(remainingMemory)} of Memory
@@ -158,7 +181,7 @@ export default function ServiceResourcesFormFragment({
</Box>
<Slider
value={formValues[memoryKey]}
value={serviceValues.memory}
onChange={(_event, value) => handleMemoryChange(value.toString())}
max={MAX_SERVICE_MEMORY}
step={RESOURCE_MEMORY_STEP}
@@ -167,6 +190,47 @@ export default function ServiceResourcesFormFragment({
marks
/>
</Box>
{!disableReplicas && (
<Box className="grid grid-flow-row gap-2">
<Box className="grid grid-flow-col items-center justify-start gap-2">
<Text
color={
formState.errors?.[serviceKey]?.replicas?.message
? 'error'
: 'primary'
}
aria-errormessage={`${serviceKey}-replicas-error-tooltip`}
>
Replicas:{' '}
<span className="font-medium">{serviceValues.replicas}</span>
</Text>
{formState.errors?.[serviceKey]?.replicas?.message ? (
<Tooltip
title={formState.errors[serviceKey].replicas.message}
id={`${serviceKey}-replicas-error-tooltip`}
>
<ExclamationIcon
color="error"
className="h-4 w-4"
aria-hidden="false"
/>
</Tooltip>
) : null}
</Box>
<Slider
value={serviceValues.replicas}
onChange={(_event, value) => handleReplicaChange(value.toString())}
min={0}
max={MAX_SERVICE_REPLICAS}
step={1}
aria-label={`${title} Replicas`}
marks
/>
</Box>
)}
</Box>
);
}

View File

@@ -1,12 +1,13 @@
import { prettifyMemory } from '@/features/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/settings/resources/utils/prettifyVCPU';
import type { ResourceSettingsFormValues } from '@/features/settings/resources/utils/resourceSettingsValidationSchema';
import { calculateBillableResources } from '@/features/projects/settings/resources/utils/calculateBillableResources';
import { getAllocatedResources } from '@/features/projects/settings/resources/utils/getAllocatedResources';
import { prettifyMemory } from '@/features/projects/settings/resources/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/projects/settings/resources/utils/prettifyVCPU';
import type { ResourceSettingsFormValues } from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import {
MAX_TOTAL_VCPU,
MIN_TOTAL_MEMORY,
MIN_TOTAL_VCPU,
} from '@/features/settings/resources/utils/resourceSettingsValidationSchema';
import useProPlan from '@/hooks/common/useProPlan';
} from '@/features/projects/settings/resources/utils/resourceSettingsValidationSchema';
import { useProPlan } from '@/hooks/common/useProPlan';
import { Alert } from '@/ui/Alert';
import Box from '@/ui/v2/Box';
import Slider, { sliderClasses } from '@/ui/v2/Slider';
@@ -19,7 +20,6 @@ import {
RESOURCE_VCPU_PRICE,
RESOURCE_VCPU_STEP,
} from '@/utils/CONSTANTS';
import getUnallocatedResources from '@/utils/settings/getUnallocatedResources';
import { alpha, styled } from '@mui/material';
import { useFormContext, useWatch } from 'react-hook-form';
@@ -59,51 +59,72 @@ export default function TotalResourcesFormFragment({
throw proPlanError;
}
const allocatedCPU =
formValues.databaseVCPU +
formValues.hasuraVCPU +
formValues.authVCPU +
formValues.storageVCPU;
const allocatedMemory =
formValues.databaseMemory +
formValues.hasuraMemory +
formValues.authMemory +
formValues.storageMemory;
const priceForTotalAvailableVCPU =
(formValues.totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE;
const billableResources = calculateBillableResources(
{
replicas: formValues.database?.replicas,
vcpu: formValues.database?.vcpu,
memory: formValues.database?.memory,
},
{
replicas: formValues.hasura?.replicas,
vcpu: formValues.hasura?.vcpu,
memory: formValues.hasura?.memory,
},
{
replicas: formValues.auth?.replicas,
vcpu: formValues.auth?.vcpu,
memory: formValues.auth?.memory,
},
{
replicas: formValues.storage?.replicas,
vcpu: formValues.storage?.vcpu,
memory: formValues.storage?.memory,
},
);
const updatedPrice =
RESOURCE_VCPU_PRICE *
(formValues.totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) +
proPlan.price;
Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
) + proPlan.price;
const { vcpu: unallocatedVCPU, memory: unallocatedMemory } =
getUnallocatedResources(formValues);
const { vcpu: allocatedVCPU, memory: allocatedMemory } =
getAllocatedResources(formValues);
const remainingVCPU = formValues.totalAvailableVCPU - allocatedVCPU;
const remainingMemory = formValues.totalAvailableMemory - allocatedMemory;
const hasUnusedResources = remainingVCPU > 0 || remainingMemory > 0;
const hasOverallocatedResources = remainingVCPU < 0 || remainingMemory < 0;
const hasUnusedResources = unallocatedVCPU > 0 || unallocatedMemory > 0;
const unusedResourceMessage = [
unallocatedVCPU > 0 ? `${prettifyVCPU(unallocatedVCPU)} vCPUs` : '',
unallocatedMemory > 0
? `${prettifyMemory(unallocatedMemory)} of Memory`
: '',
remainingVCPU > 0 ? `${prettifyVCPU(remainingVCPU)} vCPUs` : '',
remainingMemory > 0 ? `${prettifyMemory(remainingMemory)} of Memory` : '',
]
.filter(Boolean)
.join(' and ');
function handleCPUChange(value: string) {
const updatedCPU = parseFloat(value);
const overallocatedResourceMessage = [
remainingVCPU < 0 ? `${prettifyVCPU(-remainingVCPU)} vCPUs` : '',
remainingMemory < 0 ? `${prettifyMemory(-remainingMemory)} of Memory` : '',
]
.filter(Boolean)
.join(' and ');
function handleVCPUChange(value: string) {
const updatedVCPU = parseFloat(value);
const updatedMemory =
(updatedCPU / RESOURCE_VCPU_MULTIPLIER) *
(updatedVCPU / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_MEMORY_RATIO *
RESOURCE_MEMORY_MULTIPLIER;
if (
Number.isNaN(updatedCPU) ||
updatedCPU < Math.max(MIN_TOTAL_VCPU, allocatedCPU) ||
updatedMemory < Math.max(MIN_TOTAL_MEMORY, allocatedMemory)
) {
if (Number.isNaN(updatedVCPU) || updatedVCPU < MIN_TOTAL_VCPU) {
return;
}
setValue('totalAvailableVCPU', updatedCPU, { shouldDirty: true });
setValue('totalAvailableVCPU', updatedVCPU, { shouldDirty: true });
setValue('totalAvailableMemory', updatedMemory, { shouldDirty: true });
}
@@ -147,7 +168,7 @@ export default function TotalResourcesFormFragment({
<StyledAvailableCpuSlider
value={formValues.totalAvailableVCPU}
onChange={(_event, value) => handleCPUChange(value.toString())}
onChange={(_event, value) => handleVCPUChange(value.toString())}
max={MAX_TOTAL_VCPU}
step={RESOURCE_VCPU_STEP}
aria-label="Total Available vCPU"
@@ -155,19 +176,34 @@ export default function TotalResourcesFormFragment({
</Box>
<Alert
severity={hasUnusedResources ? 'warning' : 'info'}
severity={
hasUnusedResources || hasOverallocatedResources ? 'warning' : 'info'
}
className="grid grid-flow-row gap-2 rounded-t-none rounded-b-[5px] text-left"
>
{hasUnusedResources ? (
{hasUnusedResources && !hasOverallocatedResources && (
<>
<strong>Please use all the available vCPUs and Memory</strong>
<p>
You now have {unusedResourceMessage} unused. Allocate it to any
of the services before saving.
You have {unusedResourceMessage} unused. Allocate it to any of
the services before saving.
</p>
</>
) : (
)}
{hasOverallocatedResources && (
<>
<strong>Overallocated Resources</strong>
<p>
You have {overallocatedResourceMessage} overallocated. Reduce it
before saving or increase the total amount.
</p>
</>
)}
{!hasUnusedResources && !hasOverallocatedResources && (
<>
<strong>You&apos;re All Set</strong>

View File

@@ -5,7 +5,7 @@ import type {
import BaseRoleForm, {
baseRoleFormValidationSchema,
} from '@/components/settings/roles/BaseRoleForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {
GetRolesPermissionsDocument,

View File

@@ -5,7 +5,7 @@ import type {
import BaseRoleForm, {
baseRoleFormValidationSchema,
} from '@/components/settings/roles/BaseRoleForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { Role } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import {

View File

@@ -3,7 +3,7 @@ import SettingsContainer from '@/components/settings/SettingsContainer';
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { Role } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';

View File

@@ -5,7 +5,7 @@ import type {
import BaseSecretForm, {
baseSecretFormValidationSchema,
} from '@/components/settings/secrets/BaseSecretForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSecretsDocument,
useInsertSecretMutation,

View File

@@ -5,7 +5,7 @@ import type {
import BaseSecretForm, {
baseSecretFormValidationSchema,
} from '@/components/settings/secrets/BaseSecretForm';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { Secret } from '@/types/application';
import {
GetSecretsDocument,

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';

View File

@@ -1,17 +1,17 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -217,7 +217,7 @@ export default function AppleProviderSettings() {
id="redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/apple/callback`}
className="col-span-2"
@@ -236,7 +236,7 @@ export default function AppleProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/apple/callback`,
'Redirect URL',

View File

@@ -2,17 +2,17 @@ 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 { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -163,7 +163,7 @@ export default function AzureADProviderSettings() {
id="redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/azuread/callback`}
className="col-span-2"
@@ -182,7 +182,7 @@ export default function AzureADProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/azuread/callback`,
'Redirect URL',

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -139,7 +139,7 @@ export default function DiscordProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/discord/callback`}
disabled
@@ -154,7 +154,7 @@ export default function DiscordProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/discord/callback`,
'Redirect URL',

View File

@@ -2,12 +2,12 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Text from '@/ui/v2/Text';
import getServerError from '@/utils/settings/getServerError';

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -139,7 +139,7 @@ export default function FacebookProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/facebook/callback`}
disabled
@@ -154,7 +154,7 @@ export default function FacebookProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/facebook/callback`,
'Redirect URL',

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -145,7 +145,7 @@ export default function GitHubProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/github/callback`}
disabled
@@ -160,7 +160,7 @@ export default function GitHubProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/github/callback`,
'Redirect URL',

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -139,7 +139,7 @@ export default function GoogleProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/google/callback`}
disabled
@@ -154,7 +154,7 @@ export default function GoogleProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/google/callback`,
'Redirect URL',

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -139,7 +139,7 @@ export default function LinkedInProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/linkedin/callback`}
disabled
@@ -154,7 +154,7 @@ export default function LinkedInProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/linkedin/callback`,
'Redirect URL',

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import Option from '@/ui/v2/Option';

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -139,7 +139,7 @@ export default function SpotifyProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/spotify/callback`}
disabled
@@ -154,7 +154,7 @@ export default function SpotifyProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/spotify/callback`,
'Redirect URL',

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -145,7 +145,7 @@ export default function TwitchProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/twitch/callback`}
disabled
@@ -160,7 +160,7 @@ export default function TwitchProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/twitch/callback`,
'Redirect URL',

View File

@@ -1,17 +1,17 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -167,7 +167,7 @@ export default function TwitterProviderSettings() {
id="redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/twitter/callback`}
className="col-span-2"
@@ -186,7 +186,7 @@ export default function TwitterProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/twitter/callback`,
'Redirect URL',

View File

@@ -1,12 +1,12 @@
import Form from '@/components/common/Form';
import SettingsContainer from '@/components/settings/SettingsContainer';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';

View File

@@ -5,17 +5,17 @@ import BaseProviderSettings, {
baseProviderValidationSchema,
} from '@/components/settings/signInMethods/BaseProviderSettings';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -138,7 +138,7 @@ export default function WindowsLiveProviderSettings() {
label="Redirect URL"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/windowslive/callback`}
disabled
@@ -153,7 +153,7 @@ export default function WindowsLiveProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/windowslive/callback`,
'Redirect URL',

View File

@@ -2,17 +2,17 @@ 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 { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
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 CopyIcon from '@/ui/v2/icons/CopyIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { copy } from '@/utils/copy';
import getServerError from '@/utils/settings/getServerError';
@@ -184,7 +184,7 @@ export default function WorkOsProviderSettings() {
id="redirectUrl"
defaultValue={`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/workos/callback`}
className="col-span-2"
@@ -203,7 +203,7 @@ export default function WorkOsProviderSettings() {
copy(
`${generateAppServiceUrl(
currentProject.subdomain,
currentProject.region.awsName,
currentProject.region,
'auth',
)}/signin/provider/workos/callback`,
'Redirect URL',

View File

@@ -46,7 +46,7 @@ const StyledSlider = styled(MaterialSlider)(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
[`&:focus, &:hover, &.${materialSliderClasses.active}, &.${materialSliderClasses.focusVisible}`]:
{
boxShadow: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.3)}`,
boxShadow: `0 0 0 3px ${alpha(theme.palette.primary.main, 0.35)}`,
},
},
}));

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 ExclamationIcon(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="Exclamation mark"
ref={ref}
{...props}
>
<path
opacity=".2"
d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.75 5.5V4h-1.5v5.5h1.5v-4Zm0 5.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
fill="currentColor"
/>
</SvgIcon>
);
}
ExclamationIcon.displayName = 'NhostExclamationIcon';
export default forwardRef(ExclamationIcon);

View File

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

View File

@@ -0,0 +1,26 @@
import type { IconProps } from '@/ui/v2/icons';
import SvgIcon from '@/ui/v2/icons/SvgIcon';
function GaugeIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="A gauge"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.757 3.295A7.25 7.25 0 0 0 8 2.75h-.026c-3.286.011-6.032 2.231-6.92 5.227a.753.753 0 0 0-.081.293 7.359 7.359 0 0 0-.223 1.8v1.43A1.25 1.25 0 0 0 2 12.75h12a1.25 1.25 0 0 0 1.25-1.25V10a7.25 7.25 0 0 0-.246-1.872l-.001-.004V8.12a7.248 7.248 0 0 0-4.246-4.825Zm-2.77 7.955h5.763V10c0-.252-.017-.503-.05-.751l-1.16.31a.75.75 0 1 1-.387-1.448l1.16-.31-.003-.006a5.751 5.751 0 0 0-4.56-3.496V5.5a.75.75 0 0 1-1.5 0V4.3c-2.053.271-3.764 1.645-4.545 3.505l1.142.306A.75.75 0 1 1 3.46 9.56L2.307 9.25a5.895 5.895 0 0 0-.057.82v1.179h3.845l4.05-5.277a.75.75 0 0 1 1.19.913L7.985 11.25Z"
fill="currentColor"
/>
</SvgIcon>
);
}
GaugeIcon.displayName = 'NhostGaugeIcon';
export default GaugeIcon;

View File

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

View File

@@ -1,6 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { DialogFormProps } from '@/types/common';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
@@ -68,7 +68,7 @@ export default function CreateUserForm({
const baseAuthUrl = generateAppServiceUrl(
currentProject?.subdomain,
currentProject?.region?.awsName,
currentProject?.region,
'auth',
);

View File

@@ -3,8 +3,8 @@ import ControlledSelect from '@/components/common/ControlledSelect';
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form';
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import { useCurrentWorkspaceAndProject } from '@/hooks/v2/useCurrentWorkspaceAndProject';
import type { DialogFormProps } from '@/types/common';
import Avatar from '@/ui/v2/Avatar';
import Box from '@/ui/v2/Box';

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