Compare commits
289 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809a2d35f8 | ||
|
|
e17ec7fce7 | ||
|
|
241175b158 | ||
|
|
51ceaf2696 | ||
|
|
490b77cde4 | ||
|
|
7fea29a8b4 | ||
|
|
1a34e011ad | ||
|
|
395839f449 | ||
|
|
399009d66a | ||
|
|
12eb236c4a | ||
|
|
2218e5cd5b | ||
|
|
2dcf1b38c6 | ||
|
|
ad49c92879 | ||
|
|
13dd57eeb4 | ||
|
|
1345741b11 | ||
|
|
511615f176 | ||
|
|
1198c201f1 | ||
|
|
f9b81a2ae9 | ||
|
|
dff0894f37 | ||
|
|
80f3645d57 | ||
|
|
7ddb9a654e | ||
|
|
71f3be15d8 | ||
|
|
96a9070836 | ||
|
|
329e5a91b9 | ||
|
|
6d559d6e23 | ||
|
|
f4f1450d06 | ||
|
|
a1eea9df7d | ||
|
|
26f2b665e6 | ||
|
|
224a5cc805 | ||
|
|
59125b3c77 | ||
|
|
5b69e3efd8 | ||
|
|
c9a444d048 | ||
|
|
2eeac45718 | ||
|
|
fe8ca8aba6 | ||
|
|
086ee46b08 | ||
|
|
203bc97f51 | ||
|
|
b24af44aac | ||
|
|
cdaa6d4e73 | ||
|
|
09e2c8f5c7 | ||
|
|
4581677830 | ||
|
|
7bfa6c9f93 | ||
|
|
80ef430d70 | ||
|
|
bad8af0fd1 | ||
|
|
69caa34c43 | ||
|
|
1d898e2893 | ||
|
|
e87621cbde | ||
|
|
0d6fc42158 | ||
|
|
f6fbee6b13 | ||
|
|
1230b72222 | ||
|
|
6cc7704555 | ||
|
|
c0954dec09 | ||
|
|
6c25480a7a | ||
|
|
da03bf390c | ||
|
|
3b513be9f2 | ||
|
|
e450e9d636 | ||
|
|
ed1ee10879 | ||
|
|
a6120bf366 | ||
|
|
349aac369e | ||
|
|
5a84362c80 | ||
|
|
b23dc058a6 | ||
|
|
ad0dda7493 | ||
|
|
4063507d59 | ||
|
|
f59a77b1c8 | ||
|
|
30686bc4ce | ||
|
|
0c4ac8d368 | ||
|
|
85439307a9 | ||
|
|
0d8baa4065 | ||
|
|
7da0e5e256 | ||
|
|
4bca94425e | ||
|
|
9f948385c0 | ||
|
|
294c504b61 | ||
|
|
1469ec2969 | ||
|
|
8229101efe | ||
|
|
afad1778f8 | ||
|
|
28fc7b84c7 | ||
|
|
3f478a4e3c | ||
|
|
aa54666941 | ||
|
|
20fb69faba | ||
|
|
1caeb2a548 | ||
|
|
6356c5a2c8 | ||
|
|
6ec1dd3248 | ||
|
|
8a4b5031dc | ||
|
|
4790fee41f | ||
|
|
0a8033812d | ||
|
|
2b56ffc29e | ||
|
|
aa9b926cd7 | ||
|
|
575404ad62 | ||
|
|
3f6dfc7bcd | ||
|
|
682e64d7a3 | ||
|
|
30cee4f86c | ||
|
|
29dcc8c63e | ||
|
|
d926f15676 | ||
|
|
726c33d1b2 | ||
|
|
11b9cfbc0d | ||
|
|
d4a0aad2dd | ||
|
|
1030813279 | ||
|
|
917a14aa40 | ||
|
|
6381d1b095 | ||
|
|
8dbdc0bf50 | ||
|
|
8c072a4c6e | ||
|
|
fe341519f7 | ||
|
|
ea09384064 | ||
|
|
49b9972885 | ||
|
|
98c541ee52 | ||
|
|
79aaa91e67 | ||
|
|
df4d24320a | ||
|
|
757c888656 | ||
|
|
7c13eb5f9b | ||
|
|
a84608e086 | ||
|
|
e43c079b9c | ||
|
|
3f396a9ebb | ||
|
|
6ed605beb8 | ||
|
|
edd223d29c | ||
|
|
baa3ef794e | ||
|
|
da7ffbe523 | ||
|
|
15a985e079 | ||
|
|
8ff00a4258 | ||
|
|
7e27d7c0a1 | ||
|
|
49f9b8372a | ||
|
|
3ca70554c8 | ||
|
|
077b200510 | ||
|
|
925bf0f13f | ||
|
|
30d35f9607 | ||
|
|
755aa56f12 | ||
|
|
4c7e7c57a9 | ||
|
|
36708e2853 | ||
|
|
90c6031189 | ||
|
|
f044dbdb10 | ||
|
|
c2f3bce5f9 | ||
|
|
22d9877b97 | ||
|
|
628e96dcc3 | ||
|
|
3e9d3c42b6 | ||
|
|
a1e7b87c38 | ||
|
|
1bd800359e | ||
|
|
54a204a34e | ||
|
|
b17e8d6f3c | ||
|
|
12e2855f01 | ||
|
|
c1080d9e63 | ||
|
|
e4972b8307 | ||
|
|
2e7ec0697e | ||
|
|
2d9baec9d4 | ||
|
|
7a7750be0b | ||
|
|
0f34f0c6b9 | ||
|
|
d05253183a | ||
|
|
65df016bbc | ||
|
|
3e6ee1ae97 | ||
|
|
6042ed101f | ||
|
|
384bce59bf | ||
|
|
8da291ad4d | ||
|
|
f94eb3c467 | ||
|
|
9baf3f4ac7 | ||
|
|
9c406548e3 | ||
|
|
1c08cd1949 | ||
|
|
adc828a582 | ||
|
|
2f220db84a | ||
|
|
f1ec6b9a93 | ||
|
|
233b7e383e | ||
|
|
7ea469a1e3 | ||
|
|
ebd218c180 | ||
|
|
5ab1626f73 | ||
|
|
444c3b86ca | ||
|
|
7238412341 | ||
|
|
f6639ae05c | ||
|
|
d8ceccec5d | ||
|
|
6db257d4c7 | ||
|
|
93dab2d183 | ||
|
|
dfc18368be | ||
|
|
f7c6e80bf2 | ||
|
|
573cac1431 | ||
|
|
d72ae3f362 | ||
|
|
49ec7ec385 | ||
|
|
7d2b4083c2 | ||
|
|
696b493745 | ||
|
|
15a117a861 | ||
|
|
e7ff1f79f8 | ||
|
|
33c7368a2e | ||
|
|
664c182c8e | ||
|
|
c1ab4e0a77 | ||
|
|
4a4bd61757 | ||
|
|
b6d05289be | ||
|
|
5857458ca5 | ||
|
|
2fb1145fe0 | ||
|
|
546d710102 | ||
|
|
7756103476 | ||
|
|
fef9456c12 | ||
|
|
2d6d56f6b0 | ||
|
|
f54be0fefd | ||
|
|
4e76d388ab | ||
|
|
84b84ab785 | ||
|
|
ed66769688 | ||
|
|
899732f280 | ||
|
|
037b566e39 | ||
|
|
829f20c83c | ||
|
|
f1b5a944a3 | ||
|
|
5ccb764ae5 | ||
|
|
ef2b639734 | ||
|
|
a5b895a827 | ||
|
|
b441b4bae2 | ||
|
|
a6c67c1e4c | ||
|
|
7f1785ac0f | ||
|
|
a0298e0bdb | ||
|
|
3fd94b1cdf | ||
|
|
61d5f7d616 | ||
|
|
cde9a0a715 | ||
|
|
eae6349b04 | ||
|
|
211b930b84 | ||
|
|
4ae463074b | ||
|
|
1c5a4746f7 | ||
|
|
d6ae1fa44a | ||
|
|
a3abb81b37 | ||
|
|
ec74e7fe98 | ||
|
|
6713c198c6 | ||
|
|
35a6b9cf47 | ||
|
|
79f97fad76 | ||
|
|
2faf79077d | ||
|
|
4972b6feb6 | ||
|
|
23d5861c4c | ||
|
|
098ac5a71c | ||
|
|
3a15329cfd | ||
|
|
c3e798aa1d | ||
|
|
eec5e6a93d | ||
|
|
d964b689cd | ||
|
|
1e080c1af5 | ||
|
|
177bba7ec0 | ||
|
|
a593b45dc2 | ||
|
|
b384fb8bd8 | ||
|
|
abd8620ded | ||
|
|
e62ccdcaae | ||
|
|
46d01b09d6 | ||
|
|
ff74e712f8 | ||
|
|
770794ccad | ||
|
|
aa80d1795d | ||
|
|
eaa7720c65 | ||
|
|
7f447d1182 | ||
|
|
5d3dd84762 | ||
|
|
117398f5dc | ||
|
|
4e421eb4bd | ||
|
|
771447b089 | ||
|
|
be4831ae62 | ||
|
|
4fb0c18c32 | ||
|
|
99e80cea44 | ||
|
|
f2f1c01e3b | ||
|
|
2c0f98e85c | ||
|
|
20a83362ee | ||
|
|
20b800c3e4 | ||
|
|
baaa510309 | ||
|
|
a84aa5ad68 | ||
|
|
4191b933c9 | ||
|
|
cf2264ce1d | ||
|
|
02dd9dd8c0 | ||
|
|
d4ff25df0f | ||
|
|
3d74374780 | ||
|
|
7063af678c | ||
|
|
2b44a1cf27 | ||
|
|
c4f60b3645 | ||
|
|
f86f658aa5 | ||
|
|
bd02bd3f3e | ||
|
|
a133faa797 | ||
|
|
bb0269691d | ||
|
|
8d6171d22d | ||
|
|
fff178d79f | ||
|
|
5e5e454ae7 | ||
|
|
ce005f6d9e | ||
|
|
85889ee882 | ||
|
|
351873059e | ||
|
|
8ccfc10522 | ||
|
|
82b02ca70b | ||
|
|
14fc132040 | ||
|
|
a35da349ed | ||
|
|
302e1d9d33 | ||
|
|
0db40184e8 | ||
|
|
d38649494e | ||
|
|
5f22f1b5e5 | ||
|
|
494f93a4bf | ||
|
|
84c8af232c | ||
|
|
7f101d54da | ||
|
|
75b497412e | ||
|
|
5bd774afbb | ||
|
|
cdfe203fe4 | ||
|
|
4c7d32e944 | ||
|
|
447c622fc0 | ||
|
|
03f22aed72 | ||
|
|
ede5abf2ac | ||
|
|
0bdd1d0e0c | ||
|
|
61de7b21fd | ||
|
|
4b6ead1b17 | ||
|
|
0b193e6310 | ||
|
|
b21a5403fe | ||
|
|
e8320be941 |
@@ -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
16
.github/stale.yml
vendored
Normal 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.
|
||||
@@ -36,6 +36,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: 'es2019',
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry,
|
||||
|
||||
@@ -25,6 +25,7 @@ module.exports = {
|
||||
'error',
|
||||
{ allowArrowFunctions: true, allowFunctions: true },
|
||||
],
|
||||
'import/no-named-as-default': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
curly: ['error', 'all'],
|
||||
@@ -81,7 +82,7 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
group: ['@testing-library/react*'],
|
||||
message: 'Please use @/utils/testUtils instead.',
|
||||
message: 'Please use @/tests/testUtils instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,5 +1,127 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.16.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 399009d6: fix(gql): don't enter an infinite loop when fetching remote app data
|
||||
- 329e5a91: fix(deployments): use the same sorting of deployments everywhere
|
||||
- 6d559d6e: chore(settings): add under the hood improvements to the settings page
|
||||
- 12eb236c: chore(deps): bump `prettier-plugin-tailwindcss` to `v0.3.0`
|
||||
- f9b81a2a: chore(deps): bump `turbo` to `v1.9.8`
|
||||
- 1345741b: fix(projects): don't redirect to 404 on project creation
|
||||
- Updated dependencies [7fea29a8]
|
||||
- @nhost/react-apollo@5.0.23
|
||||
- @nhost/nextjs@1.13.25
|
||||
|
||||
## 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
|
||||
|
||||
- 2faf7907: chore(deps): bump `graphql-request` to v6
|
||||
- f1b5a944: chore(deps): bump `@vitejs/plugin-react` to v4
|
||||
- 7f1785ac: chore(deps): bump `@types/react` to v18.0.37
|
||||
- @nhost/react-apollo@5.0.19
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 85889ee8: feat(dashboard): add Compute management to the settings
|
||||
|
||||
## 0.14.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -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.8
|
||||
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 .
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.14.8",
|
||||
"version": "0.16.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -15,7 +15,7 @@
|
||||
"format": "prettier --write \"src/**/*.{js,ts,tsx,jsx,json,md}\" --plugin-search-dir=.",
|
||||
"storybook": "start-storybook -p 6006 -s public",
|
||||
"build-storybook": "build-storybook",
|
||||
"e2e": "npx playwright@1.31.2 install --with-deps && playwright test"
|
||||
"e2e": "npx playwright@1.33.0 install --with-deps && playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.10",
|
||||
@@ -51,7 +51,7 @@
|
||||
"generate-password": "^1.7.0",
|
||||
"graphiql": "^2.4.0",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^4.3.0",
|
||||
"graphql-request": "^6.0.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"just-kebab-case": "^4.1.1",
|
||||
@@ -88,7 +88,7 @@
|
||||
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.1",
|
||||
"@next/bundle-analyzer": "^12.3.1",
|
||||
"@playwright/test": "^1.31.2",
|
||||
"@playwright/test": "^1.33.0",
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
"@storybook/addon-essentials": "^6.5.14",
|
||||
"@storybook/addon-interactions": "^6.5.14",
|
||||
@@ -105,15 +105,15 @@
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.0.34",
|
||||
"@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": "^3.0.0",
|
||||
"@vitest/coverage-c8": "^0.30.0",
|
||||
"@vitejs/plugin-react": "^4.0.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",
|
||||
@@ -137,7 +137,7 @@
|
||||
"postcss": "^8.4.19",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-organize-imports": "^3.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"react-date-fns-hooks": "^0.9.4",
|
||||
"require-from-string": "^2.0.2",
|
||||
"snake-case": "^3.0.4",
|
||||
@@ -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": [
|
||||
|
||||
1
dashboard/public/assets/grafana.svg
Normal file
1
dashboard/public/assets/grafana.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.3 KiB |
@@ -114,6 +114,9 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
const { deployments } = deploymentPageData || { deployments: [] };
|
||||
const { deployments: scheduledOrPendingDeployments } =
|
||||
scheduledOrPendingDeploymentsData || { deployments: [] };
|
||||
const isDeploymentInProgress = deployments?.some((deployment) =>
|
||||
['PENDING', 'SCHEDULED'].includes(deployment.deploymentStatus),
|
||||
);
|
||||
|
||||
const latestDeployment = latestDeploymentData?.deployments[0];
|
||||
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
|
||||
@@ -135,7 +138,10 @@ export default function AppDeployments(props: AppDeploymentsProps) {
|
||||
deployment={deployment}
|
||||
isLive={liveDeploymentId === deployment.id}
|
||||
showRedeploy={latestDeployment.id === deployment.id}
|
||||
disableRedeploy={scheduledOrPendingDeployments?.length > 0}
|
||||
disableRedeploy={
|
||||
scheduledOrPendingDeployments?.length > 0 ||
|
||||
isDeploymentInProgress
|
||||
}
|
||||
/>
|
||||
|
||||
{index !== deployments.length - 1 && <Divider component="li" />}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -20,8 +21,6 @@ import { discordAnnounce } from '@/utils/discordAnnounce';
|
||||
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
|
||||
import { getApplicationStatusString } from '@/utils/helpers';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
@@ -31,7 +30,11 @@ import { RemoveApplicationModal } from './RemoveApplicationModal';
|
||||
import { StagingMetadata } from './StagingMetadata';
|
||||
|
||||
export default function ApplicationErrored() {
|
||||
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
|
||||
const {
|
||||
currentWorkspace,
|
||||
currentProject,
|
||||
refetch: refetchProject,
|
||||
} = useCurrentWorkspaceAndProject();
|
||||
const [changingApplicationStateLoading, setChangingApplicationStateLoading] =
|
||||
useState(false);
|
||||
|
||||
@@ -53,12 +56,9 @@ export default function ApplicationErrored() {
|
||||
const [showRecreateModal, setShowRecreateModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [insertApp] = useInsertApplicationMutation();
|
||||
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();
|
||||
|
||||
@@ -95,7 +95,7 @@ export default function ApplicationErrored() {
|
||||
});
|
||||
discordAnnounce(`Recreating: ${currentProject?.name} (${user.email})`);
|
||||
triggerToast(`Recreating ${currentProject?.name} `);
|
||||
await updateOwnCache(client);
|
||||
await refetchProject();
|
||||
} catch (e) {
|
||||
triggerToast(`Error trying to recreate: ${currentProject?.name}`);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -18,18 +18,14 @@ import { toast } from 'react-hot-toast';
|
||||
export default function ApplicationInfo() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [deleteApplication] = useDeleteApplicationMutation({
|
||||
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
async function handleClickRemove() {
|
||||
try {
|
||||
await toast.promise(
|
||||
deleteApplication({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
},
|
||||
}),
|
||||
deleteApplication({ variables: { appId: currentProject.id } }),
|
||||
{
|
||||
loading: 'Deleting project...',
|
||||
success: 'The project has been deleted successfully.',
|
||||
@@ -46,6 +42,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">
|
||||
|
||||
@@ -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,19 +27,15 @@ 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({
|
||||
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
|
||||
const { data, loading } = useGetFreeAndActiveProjectsQuery({
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'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'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
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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';
|
||||
@@ -46,7 +46,7 @@ export function RemoveApplicationModal({
|
||||
className,
|
||||
}: RemoveApplicationModalProps) {
|
||||
const [deleteApplication] = useDeleteApplicationMutation({
|
||||
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const [loadingRemove, setLoadingRemove] = useState(false);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@ 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';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { RepoAndBranch } from './RepoAndBranch';
|
||||
@@ -27,12 +25,11 @@ export function EditRepositorySettingsModal({
|
||||
const isNotCompleted = !watch('productionBranch') || !watch('repoBaseFolder');
|
||||
const { closeAlertDialog } = useDialog();
|
||||
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const { currentProject, refetch: refetchProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
|
||||
const [updateApp, { loading }] = useUpdateApplicationMutation();
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleEditGitHubIntegration = async (
|
||||
data: EditRepositorySettingsFormData,
|
||||
) => {
|
||||
@@ -60,7 +57,8 @@ export function EditRepositorySettingsModal({
|
||||
});
|
||||
}
|
||||
|
||||
await updateOwnCache(client);
|
||||
await refetchProject();
|
||||
|
||||
if (close) {
|
||||
close();
|
||||
} else {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@/utils/testUtils';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import type { Column } from 'react-table';
|
||||
import { expect, test } from 'vitest';
|
||||
import DataGrid from './DataGrid';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import permissionVariablesQuery from '@/utils/msw/mocks/graphql/permissionVariablesQuery';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import { render, screen } from '@/utils/testUtils';
|
||||
import permissionVariablesQuery from '@/tests/msw/mocks/graphql/permissionVariablesQuery';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { test, vi } from 'vitest';
|
||||
import ColumnAutocomplete from './ColumnAutocomplete';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import Form from '@/components/common/Form';
|
||||
import permissionVariablesQuery from '@/tests/msw/mocks/graphql/permissionVariablesQuery';
|
||||
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
|
||||
import type { RuleGroup } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import permissionVariablesQuery from '@/utils/msw/mocks/graphql/permissionVariablesQuery';
|
||||
import hasuraMetadataQuery from '@/utils/msw/mocks/rest/hasuraMetadataQuery';
|
||||
import tableQuery from '@/utils/msw/mocks/rest/tableQuery';
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
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';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Chip from '@/ui/v2/Chip';
|
||||
import { Chip } from '@/ui/v2/Chip';
|
||||
import { ListItem } from '@/ui/v2/ListItem';
|
||||
import Tooltip from '@/ui/v2/Tooltip';
|
||||
import { Tooltip } from '@/ui/v2/Tooltip';
|
||||
import ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
|
||||
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
|
||||
import type { DeploymentRowFragment } from '@/utils/__generated__/graphql';
|
||||
import { useInsertDeploymentMutation } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
useInsertDeploymentMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import getServerError from '@/utils/settings/getServerError';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
@@ -52,9 +56,39 @@ export default function DeploymentListItem({
|
||||
})
|
||||
: '';
|
||||
|
||||
const [insertDeployment, { loading }] = useInsertDeploymentMutation();
|
||||
const [insertDeployment, { loading }] = useInsertDeploymentMutation({
|
||||
refetchQueries: [{ query: GetAllWorkspacesAndProjectsDocument }],
|
||||
});
|
||||
const { commitMessage } = deployment;
|
||||
|
||||
async function redeployDeployment(event: MouseEvent<HTMLButtonElement>) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const insertDeploymentPromise = insertDeployment({
|
||||
variables: {
|
||||
object: {
|
||||
appId: currentProject?.id,
|
||||
commitMessage: deployment.commitMessage,
|
||||
commitSHA: deployment.commitSHA,
|
||||
commitUserAvatarUrl: deployment.commitUserAvatarUrl,
|
||||
commitUserName: deployment.commitUserName,
|
||||
deploymentStatus: 'SCHEDULED',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
insertDeploymentPromise,
|
||||
{
|
||||
loading: 'Scheduling deployment...',
|
||||
success: 'Deployment has been scheduled successfully.',
|
||||
error: getServerError('An error occurred when scheduling deployment.'),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem.Root>
|
||||
<ListItem.Button
|
||||
@@ -88,7 +122,7 @@ export default function DeploymentListItem({
|
||||
{showRedeploy && (
|
||||
<Tooltip
|
||||
title={
|
||||
!disableRedeploy && !loading
|
||||
disableRedeploy || loading
|
||||
? 'Deployments cannot be re-triggered when a deployment is in progress.'
|
||||
: ''
|
||||
}
|
||||
@@ -100,35 +134,7 @@ export default function DeploymentListItem({
|
||||
size="small"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
const insertDeploymentPromise = insertDeployment({
|
||||
variables: {
|
||||
object: {
|
||||
appId: currentProject?.id,
|
||||
commitMessage: deployment.commitMessage,
|
||||
commitSHA: deployment.commitSHA,
|
||||
commitUserAvatarUrl: deployment.commitUserAvatarUrl,
|
||||
commitUserName: deployment.commitUserName,
|
||||
deploymentStatus: 'SCHEDULED',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await toast.promise(
|
||||
insertDeploymentPromise,
|
||||
{
|
||||
loading: 'Scheduling deployment...',
|
||||
success: 'Deployment has been scheduled successfully.',
|
||||
error: getServerError(
|
||||
'An error occurred when scheduling deployment.',
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}}
|
||||
onClick={redeployDeployment}
|
||||
startIcon={
|
||||
<ArrowCounterclockwiseIcon className={twMerge('h-4 w-4')} />
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { render, screen } from '@/tests/testUtils';
|
||||
import type { Deployment } from '@/types/application';
|
||||
import { render, screen } from '@/utils/testUtils';
|
||||
import { test, vi } from 'vitest';
|
||||
import DeploymentStatusMessage from './DeploymentStatusMessage';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -10,7 +10,6 @@ import Button from '@/ui/v2/Button';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { nhost } from '@/utils/nhost';
|
||||
import { triggerToast } from '@/utils/toast';
|
||||
import { updateOwnCache } from '@/utils/updateOwnCache';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { alpha } from '@mui/system';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
@@ -28,13 +27,18 @@ export function InviteAnnounce() {
|
||||
useSubmitState();
|
||||
|
||||
// @FIX: We probably don't want to poll every ten seconds for possible invites. (We can change later depending on how it works in production.) Maybe just on the workspace page?
|
||||
const { data, loading, error, refetch, startPolling } =
|
||||
useGetWorkspaceMemberInvitesToManageQuery({
|
||||
variables: {
|
||||
userId: user?.id,
|
||||
},
|
||||
skip: !isPlatform || !user,
|
||||
});
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch: refetchInvitations,
|
||||
startPolling,
|
||||
} = useGetWorkspaceMemberInvitesToManageQuery({
|
||||
variables: {
|
||||
userId: user?.id,
|
||||
},
|
||||
skip: !isPlatform || !user,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
startPolling(15000);
|
||||
@@ -79,9 +83,14 @@ export function InviteAnnounce() {
|
||||
});
|
||||
}
|
||||
|
||||
await updateOwnCache(client);
|
||||
await client.refetchQueries({
|
||||
include: [
|
||||
GetAllWorkspacesAndProjectsDocument,
|
||||
GetWorkspaceMemberInvitesToManageDocument,
|
||||
],
|
||||
});
|
||||
await router.push(`/${invite.workspace.slug}`);
|
||||
await refetch();
|
||||
await refetchInvitations();
|
||||
triggerToast('Workspace invite accepted');
|
||||
return setSubmitState({
|
||||
error: null,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -207,19 +207,6 @@ export default function WorkspaceAndProjectList({
|
||||
</div>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Text className="font-medium" color="secondary">
|
||||
Looking for your old apps? They're still on{' '}
|
||||
<Link
|
||||
href="https://console.nhost.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
underline="always"
|
||||
>
|
||||
console.nhost.io
|
||||
</Link>{' '}
|
||||
during this beta.
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { Project } from '@/types/application';
|
||||
import { ApplicationStatus } from '@/types/application';
|
||||
import type { Workspace } from '@/types/workspace';
|
||||
import { queryClient, render, screen } from '@/utils/testUtils';
|
||||
import { mockApplication, mockWorkspace } from '@/tests/mocks';
|
||||
import { queryClient, render, screen } from '@/tests/testUtils';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, beforeAll, vi } from 'vitest';
|
||||
import OverviewDeployments from '.';
|
||||
import OverviewDeployments from './OverviewDeployments';
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: vi.fn().mockReturnValue({
|
||||
@@ -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)),
|
||||
|
||||
@@ -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';
|
||||
@@ -119,6 +119,9 @@ function OverviewDeploymentList() {
|
||||
const liveDeploymentId = getLastLiveDeployment(deployments);
|
||||
const { deployments: scheduledOrPendingDeployments } =
|
||||
scheduledOrPendingDeploymentsData || { deployments: [] };
|
||||
const isDeploymentInProgress = deployments?.some((deployment) =>
|
||||
['PENDING', 'SCHEDULED'].includes(deployment.deploymentStatus),
|
||||
);
|
||||
|
||||
return (
|
||||
<List
|
||||
@@ -131,7 +134,10 @@ function OverviewDeploymentList() {
|
||||
deployment={deployment}
|
||||
isLive={deployment.id === liveDeploymentId}
|
||||
showRedeploy={index === 0}
|
||||
disableRedeploy={scheduledOrPendingDeployments?.length > 0}
|
||||
disableRedeploy={
|
||||
scheduledOrPendingDeployments?.length > 0 ||
|
||||
isDeploymentInProgress
|
||||
}
|
||||
/>
|
||||
|
||||
{index !== deployments.length - 1 && <Divider component="li" />}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -39,12 +39,6 @@ export interface SettingsContainerProps
|
||||
* @default 'https://docs.nhost.io/'
|
||||
*/
|
||||
docsLink?: string;
|
||||
/**
|
||||
* Props for the primary action.
|
||||
*
|
||||
* @deprecated Use `slotProps.submitButton` instead.
|
||||
*/
|
||||
primaryActionButtonProps?: ButtonProps;
|
||||
/**
|
||||
* Submit button text.
|
||||
*
|
||||
@@ -106,7 +100,6 @@ export default function SettingsContainer({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
primaryActionButtonProps,
|
||||
submitButtonText = 'Save',
|
||||
className,
|
||||
onEnabledChange,
|
||||
@@ -188,18 +181,10 @@ export default function SettingsContainer({
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant={
|
||||
(submitButton || primaryActionButtonProps)?.disabled
|
||||
? 'outlined'
|
||||
: 'contained'
|
||||
}
|
||||
color={
|
||||
(submitButton || primaryActionButtonProps)?.disabled
|
||||
? 'secondary'
|
||||
: 'primary'
|
||||
}
|
||||
variant={submitButton?.disabled ? 'outlined' : 'contained'}
|
||||
color={submitButton?.disabled ? 'secondary' : 'primary'}
|
||||
type="submit"
|
||||
{...(submitButton || primaryActionButtonProps)}
|
||||
{...submitButton}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
|
||||
@@ -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';
|
||||
@@ -128,6 +128,13 @@ export default function SettingsSidebar({
|
||||
>
|
||||
General
|
||||
</SettingsNavLink>
|
||||
<SettingsNavLink
|
||||
href="/resources"
|
||||
exact={false}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
Compute Resources
|
||||
</SettingsNavLink>
|
||||
{isK8SPostgresEnabledInCurrentEnvironment && (
|
||||
<SettingsNavLink
|
||||
href="/database"
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './BaseDirectorySettings';
|
||||
export { default } from './BaseDirectorySettings';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './DeploymentBranchSettings';
|
||||
export { default } from './DeploymentBranchSettings';
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
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 {
|
||||
/**
|
||||
* The updated resources that the user has selected.
|
||||
*/
|
||||
formValues: ResourceSettingsFormValues;
|
||||
/**
|
||||
* Function to be called when the user clicks the cancel button.
|
||||
*/
|
||||
onCancel: () => void;
|
||||
/**
|
||||
* Function to be called when the user clicks the confirm button.
|
||||
*/
|
||||
onSubmit: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ResourcesConfirmationDialog({
|
||||
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 =
|
||||
Math.max(
|
||||
priceForTotalAvailableVCPU,
|
||||
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
|
||||
) + proPlan.price;
|
||||
|
||||
if (!loading && !proPlan) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Couldn't load the plan for this project. Please try again.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
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">
|
||||
{totalBillableVCPU > 0 ? (
|
||||
<Text className="text-center">
|
||||
Please allow some time for the selected resources to take effect.
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-center">
|
||||
By confirming this you will go back to the original amount of
|
||||
resources of the {proPlan.name} plan.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box className="grid grid-flow-row gap-4">
|
||||
<Box className="grid grid-flow-col justify-between gap-2">
|
||||
<Text className="font-medium">{proPlan.name} Plan</Text>
|
||||
<Text>${proPlan.price.toFixed(2)}/mo</Text>
|
||||
</Box>
|
||||
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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={totalBillableVCPU > 0 ? 'primary' : 'error'}
|
||||
onClick={onSubmit}
|
||||
autoFocus
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
|
||||
<Button variant="borderless" color="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ResourcesConfirmationDialog';
|
||||
export { default } from './ResourcesConfirmationDialog';
|
||||
@@ -0,0 +1,506 @@
|
||||
import { mockMatchMediaValue, mockRouter } from '@/tests/mocks';
|
||||
import {
|
||||
getProPlanOnlyQuery,
|
||||
getWorkspaceAndProjectQuery,
|
||||
} from '@/tests/msw/mocks/graphql/plansQuery';
|
||||
import {
|
||||
resourcesAvailableQuery,
|
||||
resourcesUnavailableQuery,
|
||||
resourcesUpdatedQuery,
|
||||
} from '@/tests/msw/mocks/graphql/resourceSettingsQuery';
|
||||
import updateConfigMutation from '@/tests/msw/mocks/graphql/updateConfigMutation';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from '@/tests/testUtils';
|
||||
import {
|
||||
RESOURCE_MEMORY_MULTIPLIER,
|
||||
RESOURCE_VCPU_MULTIPLIER,
|
||||
} from '@/utils/CONSTANTS';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import ResourcesForm from './ResourcesForm';
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation(mockMatchMediaValue),
|
||||
});
|
||||
|
||||
vi.mock('next/router', () => ({
|
||||
useRouter: vi.fn().mockReturnValue(mockRouter),
|
||||
}));
|
||||
|
||||
const server = setupServer(
|
||||
resourcesAvailableQuery,
|
||||
getProPlanOnlyQuery,
|
||||
getWorkspaceAndProjectQuery,
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
|
||||
process.env.NEXT_PUBLIC_ENV = 'production';
|
||||
server.listen();
|
||||
});
|
||||
afterEach(() => server.resetHandlers());
|
||||
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) {
|
||||
fireEvent.input(slider, { target: { value } });
|
||||
fireEvent.change(slider, { target: { value } });
|
||||
}
|
||||
|
||||
test('should show an empty state message that the feature must be enabled if no data is available', async () => {
|
||||
server.use(resourcesUnavailableQuery);
|
||||
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(await screen.findByText(/enable this feature/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the sliders if the switch is enabled', async () => {
|
||||
server.use(resourcesUnavailableQuery);
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ResourcesForm />);
|
||||
|
||||
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(12);
|
||||
});
|
||||
|
||||
test('should not show an empty state message if there is data available', async () => {
|
||||
render(<ResourcesForm />);
|
||||
|
||||
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(12);
|
||||
expect(screen.getByText(/^vcpus:/i)).toHaveTextContent(/vcpus: 8/i);
|
||||
expect(screen.getByText(/^memory:/i)).toHaveTextContent(/memory: 16384 mib/i);
|
||||
});
|
||||
|
||||
test('should show a warning message if not all the resources are allocated', async () => {
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
}),
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/^vcpus:/i)).toHaveTextContent(/vcpus: 9/i);
|
||||
expect(screen.getByText(/^memory:/i)).toHaveTextContent(/memory: 18432 mib/i);
|
||||
|
||||
expect(
|
||||
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 />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByText(/\$200\.00\/mo/i)).not.toBeInTheDocument();
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
}),
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
|
||||
expect(screen.getByText(/\$425\.00\/mo/i)).toBeInTheDocument();
|
||||
// we display the final price in two places
|
||||
expect(screen.getAllByText(/\$475\.00\/mo/i)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('should show a validation error when the form is submitted when not everything is allocated', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ResourcesForm />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('slider', { name: /total available vcpu/i }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', {
|
||||
name: /total available vcpu/i,
|
||||
}),
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(
|
||||
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 () => {
|
||||
server.use(updateConfigMutation);
|
||||
|
||||
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,
|
||||
}),
|
||||
9 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /database vcpu/i }),
|
||||
2 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /hasura graphql vcpu/i }),
|
||||
2.5 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /auth vcpu/i }),
|
||||
1.5 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /storage vcpu/i }),
|
||||
3 * RESOURCE_VCPU_MULTIPLIER,
|
||||
);
|
||||
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /database memory/i }),
|
||||
4.75 * RESOURCE_MEMORY_MULTIPLIER,
|
||||
);
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /hasura graphql memory/i }),
|
||||
4.25 * RESOURCE_MEMORY_MULTIPLIER,
|
||||
);
|
||||
changeSliderValue(
|
||||
screen.getByRole('slider', { name: /auth memory/i }),
|
||||
4 * RESOURCE_MEMORY_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();
|
||||
expect(
|
||||
within(screen.getByRole('dialog')).getByRole('heading', {
|
||||
name: /confirm dedicated resources/i,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(screen.getByRole('dialog')).getByText(/postgresql database/i)
|
||||
.parentElement,
|
||||
).toHaveTextContent(/2 vcpu \+ 4864 mib/i);
|
||||
expect(
|
||||
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
|
||||
// and we need to return the updated values
|
||||
server.use(resourcesUpdatedQuery);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /confirm/i }));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
|
||||
expect(
|
||||
await screen.findByText(/resources have been updated successfully./i),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('slider', { name: /total available vcpu/i }),
|
||||
).toHaveValue((9 * RESOURCE_VCPU_MULTIPLIER).toString());
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should display a red button when custom resources are disabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ResourcesForm />);
|
||||
|
||||
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(/approximate cost:/i)).toHaveTextContent(
|
||||
/approximate cost: \$25\.00\/mo/i,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /save/i }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', { name: /disable dedicated resources/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /confirm/i })).toHaveStyle({
|
||||
'background-color': '#f13154',
|
||||
});
|
||||
});
|
||||
|
||||
test('should hide the pricing information when custom resource allocation is disabled', async () => {
|
||||
server.use(updateConfigMutation);
|
||||
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ResourcesForm />);
|
||||
|
||||
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 }));
|
||||
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
server.use(resourcesUnavailableQuery);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /confirm/i }));
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,366 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
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 { 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 Divider from '@/ui/v2/Divider';
|
||||
import {
|
||||
RESOURCE_VCPU_MULTIPLIER,
|
||||
RESOURCE_VCPU_PRICE,
|
||||
} from '@/utils/CONSTANTS';
|
||||
import type { GetResourcesQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetResourcesDocument,
|
||||
useGetResourcesQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import getServerError from '@/utils/settings/getServerError';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import ResourcesFormFooter from './ResourcesFormFooter';
|
||||
|
||||
function getInitialServiceResources(
|
||||
data: GetResourcesQuery,
|
||||
service: Exclude<keyof GetResourcesQuery['config'], '__typename'>,
|
||||
) {
|
||||
const { compute, replicas } = data?.config?.[service]?.resources || {};
|
||||
|
||||
return {
|
||||
replicas,
|
||||
vcpu: compute?.cpu || 0,
|
||||
memory: compute?.memory || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ResourcesForm() {
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const {
|
||||
data,
|
||||
loading,
|
||||
error: resourcesError,
|
||||
} = useGetResourcesQuery({
|
||||
variables: {
|
||||
appId: currentProject?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: proPlan,
|
||||
loading: proPlanLoading,
|
||||
error: proPlanError,
|
||||
} = useProPlan();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetResourcesDocument],
|
||||
});
|
||||
|
||||
const initialDatabaseResources = getInitialServiceResources(data, 'postgres');
|
||||
const initialHasuraResources = getInitialServiceResources(data, 'hasura');
|
||||
const initialAuthResources = getInitialServiceResources(data, 'auth');
|
||||
const initialStorageResources = getInitialServiceResources(data, 'storage');
|
||||
|
||||
const totalInitialVCPU =
|
||||
initialDatabaseResources.vcpu +
|
||||
initialHasuraResources.vcpu +
|
||||
initialAuthResources.vcpu +
|
||||
initialStorageResources.vcpu;
|
||||
|
||||
const totalInitialMemory =
|
||||
initialDatabaseResources.memory +
|
||||
initialHasuraResources.memory +
|
||||
initialAuthResources.memory +
|
||||
initialStorageResources.memory;
|
||||
|
||||
const form = useForm<ResourceSettingsFormValues>({
|
||||
values: {
|
||||
enabled: totalInitialVCPU > 0 && totalInitialMemory > 0,
|
||||
totalAvailableVCPU: totalInitialVCPU || 2000,
|
||||
totalAvailableMemory: totalInitialMemory || 4096,
|
||||
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),
|
||||
});
|
||||
|
||||
if (!proPlan && !proPlanLoading) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Couldn't load the plan for this project. Please try again.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || proPlanLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
label="Loading resource settings..."
|
||||
delay={1000}
|
||||
className="mx-auto"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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 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 =
|
||||
proPlan.price +
|
||||
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE;
|
||||
|
||||
async function handleSubmit(formValues: ResourceSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject?.id,
|
||||
config: {
|
||||
postgres: {
|
||||
resources: formValues.enabled
|
||||
? {
|
||||
compute: {
|
||||
cpu: formValues.database.vcpu,
|
||||
memory: formValues.database.memory,
|
||||
},
|
||||
replicas: formValues.database.replicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
hasura: {
|
||||
resources: formValues.enabled
|
||||
? {
|
||||
compute: {
|
||||
cpu: formValues.hasura.vcpu,
|
||||
memory: formValues.hasura.memory,
|
||||
},
|
||||
replicas: formValues.hasura.replicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
auth: {
|
||||
resources: formValues.enabled
|
||||
? {
|
||||
compute: {
|
||||
cpu: formValues.auth.vcpu,
|
||||
memory: formValues.auth.memory,
|
||||
},
|
||||
replicas: formValues.auth.replicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
storage: {
|
||||
resources: formValues.enabled
|
||||
? {
|
||||
compute: {
|
||||
cpu: formValues.storage.vcpu,
|
||||
memory: formValues.storage.memory,
|
||||
},
|
||||
replicas: formValues.storage.replicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: 'Updating resources...',
|
||||
success: 'Resources have been updated successfully.',
|
||||
error: getServerError(
|
||||
'An error occurred while updating resources. Please try again.',
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
if (!formValues.enabled) {
|
||||
form.reset({
|
||||
enabled: false,
|
||||
totalAvailableVCPU: 2000,
|
||||
totalAvailableMemory: 4096,
|
||||
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 });
|
||||
}
|
||||
} catch {
|
||||
// Note: The error has already been handled by the toast.
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfirm(formValues: ResourceSettingsFormValues) {
|
||||
openDialog({
|
||||
title: formValues.enabled
|
||||
? 'Confirm Dedicated Resources'
|
||||
: 'Disable Dedicated Resources',
|
||||
component: (
|
||||
<ResourcesConfirmationDialog
|
||||
formValues={formValues}
|
||||
onCancel={closeDialog}
|
||||
onSubmit={async () => {
|
||||
await handleSubmit(formValues);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
props: {
|
||||
titleProps: { className: 'justify-center pb-1' },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (resourcesError || proPlanError) {
|
||||
throw resourcesError || proPlanError;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleConfirm}>
|
||||
<SettingsContainer
|
||||
title="Compute Resources"
|
||||
description="See how much compute you have available and customise allocation on this page."
|
||||
className="gap-0 px-0"
|
||||
showSwitch
|
||||
switchId="enabled"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !enabled || !isDirty,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
// Note: We need a custom footer because of the pricing
|
||||
// information
|
||||
footer: { className: 'hidden', 'aria-hidden': true },
|
||||
}}
|
||||
>
|
||||
{enabled ? (
|
||||
<>
|
||||
<TotalResourcesFormFragment initialPrice={initialPrice} />
|
||||
<Divider />
|
||||
<ServiceResourcesFormFragment
|
||||
title="PostgreSQL Database"
|
||||
description="Manage how much compute you need for the PostgreSQL Database."
|
||||
serviceKey="database"
|
||||
disableReplicas
|
||||
/>
|
||||
<Divider />
|
||||
<ServiceResourcesFormFragment
|
||||
title="Hasura GraphQL"
|
||||
description="Manage how much compute you need for the Hasura GraphQL API."
|
||||
serviceKey="hasura"
|
||||
/>
|
||||
<Divider />
|
||||
<ServiceResourcesFormFragment
|
||||
title="Auth"
|
||||
description="Manage how much compute you need for Auth."
|
||||
serviceKey="auth"
|
||||
/>
|
||||
<Divider />
|
||||
<ServiceResourcesFormFragment
|
||||
title="Storage"
|
||||
description="Manage how much compute you need for Storage."
|
||||
serviceKey="storage"
|
||||
/>
|
||||
|
||||
{hasFormErrors && (
|
||||
<Box className="px-4 pb-4">
|
||||
<Alert
|
||||
severity="error"
|
||||
className="flex flex-col gap-2 text-left"
|
||||
>
|
||||
<strong>Invalid Configuration</strong>
|
||||
|
||||
<p>
|
||||
Please check the form for errors and the allocation for
|
||||
each service and try again.
|
||||
</p>
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Box className={twMerge('px-4', 'pb-4')}>
|
||||
<Alert className="text-left">
|
||||
Enable this feature to access custom resource allocation for
|
||||
your services.
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<ResourcesFormFooter />
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ResourcesForm';
|
||||
@@ -0,0 +1,236 @@
|
||||
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/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';
|
||||
|
||||
export interface ServiceResourcesFormFragmentProps {
|
||||
/**
|
||||
* The title of the form fragment.
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* The description of the form fragment.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Form field name for service.
|
||||
*/
|
||||
serviceKey: Exclude<
|
||||
keyof ResourceSettingsFormValues,
|
||||
'enabled' | 'totalAvailableVCPU' | 'totalAvailableMemory'
|
||||
>;
|
||||
/**
|
||||
* Whether to disable the replicas field.
|
||||
*/
|
||||
disableReplicas?: boolean;
|
||||
}
|
||||
|
||||
export default function ServiceResourcesFormFragment({
|
||||
title,
|
||||
description,
|
||||
serviceKey,
|
||||
disableReplicas = false,
|
||||
}: ServiceResourcesFormFragmentProps) {
|
||||
const {
|
||||
setValue,
|
||||
trigger: triggerValidation,
|
||||
formState,
|
||||
} = useFormContext<ResourceSettingsFormValues>();
|
||||
const formValues = useWatch<ResourceSettingsFormValues>();
|
||||
const serviceValues = formValues[serviceKey];
|
||||
|
||||
// Total allocated CPU for all resources
|
||||
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) =>
|
||||
!['enabled', 'totalAvailableVCPU', 'totalAvailableMemory'].includes(
|
||||
key,
|
||||
),
|
||||
)
|
||||
.reduce((acc, key) => acc + formValues[key].memory, 0);
|
||||
|
||||
const remainingVCPU = formValues.totalAvailableVCPU - totalAllocatedVCPU;
|
||||
const allowedVCPU = remainingVCPU + serviceValues.vcpu;
|
||||
|
||||
const remainingMemory =
|
||||
formValues.totalAvailableMemory - totalAllocatedMemory;
|
||||
const allowedMemory = remainingMemory + serviceValues.memory;
|
||||
|
||||
function handleReplicaChange(value: string) {
|
||||
const updatedReplicas = parseInt(value, 10);
|
||||
|
||||
if (updatedReplicas < MIN_SERVICE_REPLICAS) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (Number.isNaN(updatedMemory) || updatedMemory < MIN_SERVICE_MEMORY) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(`${serviceKey}.memory`, updatedMemory, { shouldDirty: true });
|
||||
|
||||
// trigger validation for "replicas" field
|
||||
if (!disableReplicas) {
|
||||
triggerValidation(`${serviceKey}.replicas`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="grid grid-flow-row gap-4 p-4">
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
<Text variant="h3" className="font-semibold">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text color="secondary">{description}</Text>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
<Box className="grid grid-flow-col items-center justify-between gap-2">
|
||||
<Text>
|
||||
Allocated vCPUs:{' '}
|
||||
<span className="font-medium">
|
||||
{prettifyVCPU(serviceValues.vcpu)}
|
||||
</span>
|
||||
</Text>
|
||||
|
||||
{remainingVCPU > 0 && serviceValues.vcpu < MAX_SERVICE_VCPU && (
|
||||
<Text className="text-sm">
|
||||
<span className="font-medium">
|
||||
{prettifyVCPU(remainingVCPU)} vCPUs
|
||||
</span>{' '}
|
||||
remaining
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Slider
|
||||
value={serviceValues.vcpu}
|
||||
onChange={(_event, value) => handleVCPUChange(value.toString())}
|
||||
max={MAX_SERVICE_VCPU}
|
||||
step={RESOURCE_VCPU_STEP}
|
||||
allowed={allowedVCPU}
|
||||
aria-label={`${title} vCPU`}
|
||||
marks
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className="grid grid-flow-row gap-2">
|
||||
<Box className="grid grid-flow-col items-center justify-between gap-2">
|
||||
<Text>
|
||||
Allocated Memory:{' '}
|
||||
<span className="font-medium">
|
||||
{prettifyMemory(serviceValues.memory)}
|
||||
</span>
|
||||
</Text>
|
||||
|
||||
{remainingMemory > 0 && serviceValues.memory < MAX_SERVICE_MEMORY && (
|
||||
<Text className="text-sm">
|
||||
<span className="font-medium">
|
||||
{prettifyMemory(remainingMemory)} of Memory
|
||||
</span>{' '}
|
||||
remaining
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Slider
|
||||
value={serviceValues.memory}
|
||||
onChange={(_event, value) => handleMemoryChange(value.toString())}
|
||||
max={MAX_SERVICE_MEMORY}
|
||||
step={RESOURCE_MEMORY_STEP}
|
||||
allowed={allowedMemory}
|
||||
aria-label={`${title} Memory`}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ServiceResourcesFormFragment';
|
||||
export { default } from './ServiceResourcesFormFragment';
|
||||
@@ -0,0 +1,220 @@
|
||||
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_VCPU,
|
||||
} 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';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import ArrowRightIcon from '@/ui/v2/icons/ArrowRightIcon';
|
||||
import {
|
||||
RESOURCE_MEMORY_MULTIPLIER,
|
||||
RESOURCE_VCPU_MEMORY_RATIO,
|
||||
RESOURCE_VCPU_MULTIPLIER,
|
||||
RESOURCE_VCPU_PRICE,
|
||||
RESOURCE_VCPU_STEP,
|
||||
} from '@/utils/CONSTANTS';
|
||||
import { alpha, styled } from '@mui/material';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
export interface TotalResourcesFormFragmentProps {
|
||||
/**
|
||||
* The initial price of the resources.
|
||||
*/
|
||||
initialPrice: number;
|
||||
}
|
||||
|
||||
const StyledAvailableCpuSlider = styled(Slider)(({ theme }) => ({
|
||||
[`& .${sliderClasses.rail}`]: {
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.15),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function TotalResourcesFormFragment({
|
||||
initialPrice,
|
||||
}: TotalResourcesFormFragmentProps) {
|
||||
const {
|
||||
data: proPlan,
|
||||
error: proPlanError,
|
||||
loading: proPlanLoading,
|
||||
} = useProPlan();
|
||||
const { setValue } = useFormContext<ResourceSettingsFormValues>();
|
||||
const formValues = useWatch<ResourceSettingsFormValues>();
|
||||
|
||||
if (!proPlan && !proPlanLoading) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Couldn't load the plan for this projectee. Please try again.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (proPlanError) {
|
||||
throw proPlanError;
|
||||
}
|
||||
|
||||
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 =
|
||||
Math.max(
|
||||
priceForTotalAvailableVCPU,
|
||||
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
|
||||
) + proPlan.price;
|
||||
|
||||
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 unusedResourceMessage = [
|
||||
remainingVCPU > 0 ? `${prettifyVCPU(remainingVCPU)} vCPUs` : '',
|
||||
remainingMemory > 0 ? `${prettifyMemory(remainingMemory)} of Memory` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' and ');
|
||||
|
||||
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 =
|
||||
(updatedVCPU / RESOURCE_VCPU_MULTIPLIER) *
|
||||
RESOURCE_VCPU_MEMORY_RATIO *
|
||||
RESOURCE_MEMORY_MULTIPLIER;
|
||||
|
||||
if (Number.isNaN(updatedVCPU) || updatedVCPU < MIN_TOTAL_VCPU) {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('totalAvailableVCPU', updatedVCPU, { shouldDirty: true });
|
||||
setValue('totalAvailableMemory', updatedMemory, { shouldDirty: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="px-4 pb-4">
|
||||
<Box className="rounded-md border">
|
||||
<Box className="flex flex-col gap-4 bg-transparent p-4">
|
||||
<Box className="flex flex-row items-center justify-between gap-4">
|
||||
<Text color="secondary">
|
||||
Total available compute for your project:
|
||||
</Text>
|
||||
|
||||
{initialPrice !== updatedPrice && (
|
||||
<Text className="flex flex-row items-center justify-end gap-2">
|
||||
<Text component="span" color="secondary">
|
||||
${initialPrice.toFixed(2)}/mo
|
||||
</Text>
|
||||
<ArrowRightIcon />
|
||||
<Text component="span" className="font-medium">
|
||||
${updatedPrice.toFixed(2)}/mo
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Box className="flex flex-row items-center justify-start gap-4">
|
||||
<Text color="secondary">
|
||||
vCPUs:{' '}
|
||||
<Text component="span" color="primary" className="font-medium">
|
||||
{prettifyVCPU(formValues.totalAvailableVCPU)}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text color="secondary">
|
||||
Memory:{' '}
|
||||
<Text component="span" color="primary" className="font-medium">
|
||||
{prettifyMemory(formValues.totalAvailableMemory)}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<StyledAvailableCpuSlider
|
||||
value={formValues.totalAvailableVCPU}
|
||||
onChange={(_event, value) => handleVCPUChange(value.toString())}
|
||||
max={MAX_TOTAL_VCPU}
|
||||
step={RESOURCE_VCPU_STEP}
|
||||
aria-label="Total Available vCPU"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Alert
|
||||
severity={
|
||||
hasUnusedResources || hasOverallocatedResources ? 'warning' : 'info'
|
||||
}
|
||||
className="grid grid-flow-row gap-2 rounded-t-none rounded-b-[5px] text-left"
|
||||
>
|
||||
{hasUnusedResources && !hasOverallocatedResources && (
|
||||
<>
|
||||
<strong>Please use all the available vCPUs and Memory</strong>
|
||||
|
||||
<p>
|
||||
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're All Set</strong>
|
||||
|
||||
<p>
|
||||
You have successfully allocated all the available vCPUs and
|
||||
Memory.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Alert>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './TotalResourcesFormFragment';
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user