Compare commits

..

417 Commits

Author SHA1 Message Date
Szilárd Dóró
69caa34c43 Merge pull request #1934 from nhost/changeset-release/main
chore: update versions
2023-05-16 08:34:38 +02:00
github-actions[bot]
1d898e2893 chore: update versions 2023-05-15 18:43:10 +00:00
Szilárd Dóró
e87621cbde Merge pull request #1936 from nhost/fix/security-key-url
fix(hasura-auth-js): make the call to the correct endpoint
2023-05-15 20:41:59 +02:00
Szilárd Dóró
0d6fc42158 fix: make the call to the correct endpoint 2023-05-15 19:46:26 +02:00
Szilárd Dóró
f6fbee6b13 Merge pull request #1933 from nhost/fix/project-rename-prevent-404
fix(dashboard): don't redirect to 404 on project rename
2023-05-15 16:59:00 +02:00
Szilárd Dóró
1230b72222 fix: don't redirect to 404 on project rename 2023-05-15 16:05:45 +02:00
Szilárd Dóró
6cc7704555 Merge pull request #1931 from nhost/changeset-release/main
chore: update versions
2023-05-15 15:47:22 +02:00
github-actions[bot]
c0954dec09 chore: update versions 2023-05-15 13:30:42 +00:00
Szilárd Dóró
6c25480a7a Merge pull request #1929 from nhost/fix/build-targets
chore: change build target to ES2019
2023-05-15 15:29:32 +02:00
Szilárd Dóró
da03bf390c chore: change build target to ES2019 2023-05-15 11:09:00 +02:00
Szilárd Dóró
3b513be9f2 Merge pull request #1926 from nhost/changeset-release/main
chore: update versions
2023-05-12 16:55:46 +02:00
github-actions[bot]
e450e9d636 chore: update versions 2023-05-12 14:27:16 +00:00
Szilárd Dóró
ed1ee10879 Merge pull request #1925 from nhost/fix/postgres-connection-string
fix(dashboard): show correct postgres connection string
2023-05-12 16:25:51 +02:00
Szilárd Dóró
349aac369e chore: add changeset 2023-05-12 14:23:54 +02:00
Szilárd Dóró
5a84362c80 fix: construct postgres connection string 2023-05-12 14:23:05 +02:00
Szilárd Dóró
f59a77b1c8 Merge pull request #1923 from nhost/changeset-release/main
chore: update versions
2023-05-12 10:25:12 +02:00
github-actions[bot]
30686bc4ce chore: update versions 2023-05-12 08:03:41 +00:00
Szilárd Dóró
0c4ac8d368 Merge pull request #1919 from nhost/chore/update-staging-urls
chore(dashboard): change URL construction
2023-05-12 10:02:32 +02:00
Szilárd Dóró
7da0e5e256 Merge pull request #1918 from nhost/changeset-release/main
chore: update versions
2023-05-11 15:30:16 +02:00
github-actions[bot]
8229101efe chore: update versions 2023-05-11 13:07:57 +00:00
Szilárd Dóró
afad1778f8 Merge pull request #1895 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-05-11 15:06:23 +02:00
Szilárd Dóró
28fc7b84c7 chore: update changeset 2023-05-11 13:38:39 +02:00
Szilárd Dóró
3f478a4e3c chore: include vitest in the bump, add changeset 2023-05-11 13:37:34 +02:00
Szilárd Dóró
aa54666941 fix: don't break builds 2023-05-11 12:53:45 +02:00
Szilárd Dóró
20fb69faba chore: change URL construction 2023-05-11 12:46:49 +02:00
renovate[bot]
1caeb2a548 chore(deps): update react monorepo 2023-05-11 10:12:59 +00:00
Szilárd Dóró
6356c5a2c8 Merge pull request #1906 from nhost/chore/bump-pnpm
chore: bump `pnpm` and `turbo` version
2023-05-11 12:10:10 +02:00
Szilárd Dóró
6ec1dd3248 chore: bump lock file again 2023-05-11 11:07:27 +02:00
Szilárd Dóró
8a4b5031dc Merge branch 'main' into chore/bump-pnpm 2023-05-11 11:06:57 +02:00
Szilárd Dóró
4790fee41f Merge pull request #1916 from nhost/changeset-release/main
chore: update versions
2023-05-11 09:26:44 +02:00
github-actions[bot]
0a8033812d chore: update versions 2023-05-11 06:56:16 +00:00
Szilárd Dóró
2b56ffc29e Merge pull request #1915 from nhost/fix/project-redirects
fix(dashboard): redirect an invalid project to the 404 page
2023-05-11 08:54:58 +02:00
Szilárd Dóró
aa9b926cd7 Merge branch 'main' into fix/project-redirects 2023-05-10 17:21:18 +02:00
Szilárd Dóró
575404ad62 Merge pull request #1917 from nhost/fix/imports
fix(dashboard): don't break builds
2023-05-10 17:20:37 +02:00
Szilárd Dóró
3f6dfc7bcd fix: don't break builds 2023-05-10 17:19:32 +02:00
Szilárd Dóró
682e64d7a3 fix: don't break build 2023-05-10 16:51:59 +02:00
Szilárd Dóró
30cee4f86c Merge branch 'main' into fix/project-redirects 2023-05-10 16:51:06 +02:00
Szilárd Dóró
29dcc8c63e Merge pull request #1911 from nhost/fix/non-owner-functionality
fix(dashboard): restrict non-owner functionality
2023-05-10 16:47:06 +02:00
Szilárd Dóró
d926f15676 fix: redirect an invalid project to the 404 page 2023-05-10 16:46:16 +02:00
Szilárd Dóró
d4a0aad2dd Merge pull request #1913 from nhost/changeset-release/main
chore: update versions
2023-05-10 14:24:09 +02:00
github-actions[bot]
1030813279 chore: update versions 2023-05-10 11:50:46 +00:00
Nuno Pato
917a14aa40 Merge pull request #1912 from nhost/docs/add-metrics
Add section on Metrics to the documentation
2023-05-10 11:49:27 +00:00
Szilárd Dóró
6381d1b095 Merge pull request #1893 from nhost/feat/metrics-page 2023-05-10 13:43:34 +02:00
Nuno Pato
8dbdc0bf50 asd 2023-05-10 11:32:34 +00:00
Nuno Pato
8c072a4c6e asd 2023-05-10 10:10:38 +00:00
Nuno Pato
fe341519f7 Add section on Metrics to the documentation 2023-05-10 10:09:12 +00:00
Szilárd Dóró
ea09384064 fix: update import paths 2023-05-10 10:45:20 +02:00
Szilárd Dóró
49b9972885 chore: add changeset 2023-05-10 10:42:35 +02:00
Szilárd Dóró
98c541ee52 feat: introduce useIsCurrentUserOwner hook
- chore: improve file structure
2023-05-10 10:41:35 +02:00
Szilárd Dóró
757c888656 Merge pull request #1910 from nhost/changeset-release/main
chore: update versions
2023-05-09 11:40:16 +02:00
github-actions[bot]
7c13eb5f9b chore: update versions 2023-05-09 09:17:43 +00:00
Szilárd Dóró
a84608e086 Merge pull request #1907 from nhost/fix/upgrade
fix(dashboard): unpause after upgrading a paused project to pro
2023-05-09 11:13:44 +02:00
Szilárd Dóró
e43c079b9c feat: poll project state after unpausing with upgrade 2023-05-09 10:50:34 +02:00
Szilárd Dóró
3f396a9ebb chore: add changesets 2023-05-08 19:28:42 +02:00
Szilárd Dóró
6ed605beb8 fix: update desiredState on plan change 2023-05-08 17:58:06 +02:00
Szilárd Dóró
edd223d29c fix: don't go to 404 page unnecessarily 2023-05-08 17:47:04 +02:00
Szilárd Dóró
15a985e079 fix: don't break dashboard build 2023-05-08 15:29:29 +02:00
Szilárd Dóró
8ff00a4258 chore(ci): bump pnpm version to v8.4.0 2023-05-08 15:19:53 +02:00
Szilárd Dóró
7e27d7c0a1 chore: update changeset, bump turbo version 2023-05-08 15:17:42 +02:00
Szilárd Dóró
49f9b8372a chore: bump pnpm version to v8.4.0 2023-05-08 15:09:29 +02:00
Szilárd Dóró
925bf0f13f Merge pull request #1905 from nhost/changeset-release/main
chore: update versions
2023-05-08 13:51:55 +02:00
github-actions[bot]
30d35f9607 chore: update versions 2023-05-08 10:10:25 +00:00
Szilárd Dóró
755aa56f12 Merge pull request #1904 from nhost/fix/package-json-types
chore: add `types` to `package.json`
2023-05-08 12:09:11 +02:00
Szilárd Dóró
4c7e7c57a9 fix: don't break tests 2023-05-08 10:29:13 +02:00
Szilárd Dóró
36708e2853 Merge pull request #1903 from hrmoller/fix/wrong-linking-in-docs
Fixed linking to wrong destination in docs
2023-05-08 10:26:31 +02:00
Szilárd Dóró
90c6031189 chore: add types to package.json 2023-05-08 09:54:27 +02:00
Martin Møller
f044dbdb10 Fixed linking to wrong destination 2023-05-05 08:03:50 +02:00
Szilárd Dóró
c2f3bce5f9 Merge pull request #1902 from nhost/chore/probot-improvements
chore: refine probot config
2023-05-04 16:20:59 +02:00
Szilárd Dóró
22d9877b97 chore: update probot config 2023-05-04 16:04:09 +02:00
Szilárd Dóró
628e96dcc3 Merge pull request #1901 from nhost/chore/probot-stale
chore: add probot/stale configuration
2023-05-04 15:32:50 +02:00
Szilárd Dóró
3e9d3c42b6 fix: disable exemptLabels 2023-05-04 15:20:23 +02:00
Szilárd Dóró
a1e7b87c38 add probot/stale configuration 2023-05-04 15:08:37 +02:00
David Barroso
1bd800359e Merge pull request #1894 from nhost/dbarroso/obs-dash-improv
fix: observability: filter pod metrics
2023-05-03 12:59:07 +02:00
David Barroso
54a204a34e fix: observability: filter pod metrics 2023-05-03 10:09:27 +02:00
Szilárd Dóró
b17e8d6f3c fix: don't break e2e tests 2023-05-03 10:00:29 +02:00
Szilárd Dóró
12e2855f01 chore: update description and prevent free access
- bump `jsdom` to v22
- increase test timeout
2023-05-03 09:57:11 +02:00
Szilárd Dóró
c1080d9e63 fix: don't break the UI 2023-05-03 09:38:48 +02:00
Szilárd Dóró
e4972b8307 feat: add Grafana page 2023-05-03 09:35:06 +02:00
Szilárd Dóró
2e7ec0697e Merge pull request #1881 from nhost/changeset-release/main
chore: update versions
2023-05-02 21:06:46 +02:00
github-actions[bot]
2d9baec9d4 chore: update versions 2023-05-02 18:56:49 +00:00
Szilárd Dóró
7a7750be0b Merge pull request #1892 from nhost/fix/disable-downgrade
fix: disallow downgrading through the UI
2023-05-02 20:55:24 +02:00
Szilárd Dóró
0f34f0c6b9 fix: disallow downgrading 2023-05-02 15:31:39 +02:00
Nestor Manrique
d05253183a Merge pull request #1883 from nhost/nestor/fix-grafana-dashboard-datasource
fix: use dashboard externally exported version
2023-05-02 13:15:40 +02:00
Nestor Manrique
65df016bbc fix: fix datasource config 2023-04-28 17:28:43 +02:00
David Barroso
3e6ee1ae97 Merge pull request #1882 from nhost/dbarroso/observability-dashboard
feat: added project metrics observability dashboard
2023-04-28 14:32:55 +02:00
David Barroso
6042ed101f feat: added project metrics observability dashboard 2023-04-28 11:49:33 +02:00
Szilárd Dóró
384bce59bf Merge pull request #1859 from nhost/renovate/react-monorepo
chore(deps): update react monorepo
2023-04-28 09:59:24 +02:00
Szilárd Dóró
8da291ad4d chore: add changeset 2023-04-28 09:42:13 +02:00
renovate[bot]
f94eb3c467 chore(deps): update react monorepo 2023-04-27 18:07:47 +00:00
Szilárd Dóró
9baf3f4ac7 Merge pull request #1876 from nhost/changeset-release/main
chore: update versions
2023-04-27 20:00:27 +02:00
github-actions[bot]
9c406548e3 chore: update versions 2023-04-27 17:47:36 +00:00
Szilárd Dóró
1c08cd1949 Merge pull request #1878 from nhost/fix/local-users-page 2023-04-27 19:46:28 +02:00
Szilárd Dóró
adc828a582 fix: don't enter an infinite loop 2023-04-27 17:45:04 +02:00
Szilárd Dóró
f1ec6b9a93 Merge pull request #1871 from nhost/docs/local-development-migration
docs: add migration info
2023-04-27 16:37:40 +02:00
Szilárd Dóró
233b7e383e Merge pull request #1873 from nhost/changeset-release/main
chore: update versions
2023-04-27 16:06:05 +02:00
github-actions[bot]
7ea469a1e3 chore: update versions 2023-04-27 13:46:37 +00:00
Szilárd Dóró
ebd218c180 Merge pull request #1855 from nhost/feat/resource-replicas
feat(dashboard): Service Replicas
2023-04-27 15:45:31 +02:00
Nuno Pato
5ab1626f73 Merge pull request #1869 from nhost/docs/add-service-replicas
docs: add service replicas
2023-04-27 13:35:53 +00:00
Nuno Pato
444c3b86ca asd 2023-04-27 13:34:35 +00:00
Szilárd Dóró
7238412341 Merge pull request #1872 from nhost/chore/remove-backend-url
chore(dashboard): remove deprecated environment variable
2023-04-27 14:36:09 +02:00
Szilárd Dóró
f6639ae05c chore: add changeset 2023-04-27 13:54:27 +02:00
Szilárd Dóró
d8ceccec5d chore: add changeset 2023-04-27 13:41:11 +02:00
Szilárd Dóró
6db257d4c7 chore: remove deprecated backend URL 2023-04-27 13:40:41 +02:00
Szilárd Dóró
93dab2d183 docs: add migration info 2023-04-27 11:56:41 +02:00
Nuno Pato
dfc18368be asd 2023-04-26 18:09:42 +00:00
Nuno Pato
f7c6e80bf2 asd 2023-04-26 17:38:53 +00:00
Nuno Pato
573cac1431 asd 2023-04-26 17:23:11 +00:00
Nuno Pato
d72ae3f362 docs: add service replicas 2023-04-26 00:42:16 +00:00
Szilárd Dóró
49ec7ec385 fix: mobile improvements, improved validation 2023-04-25 15:30:38 +02:00
Szilárd Dóró
7d2b4083c2 chore: reorder components, update labels 2023-04-24 17:24:54 +02:00
Szilárd Dóró
696b493745 chore: increase test timeout 2023-04-24 16:23:51 +02:00
Szilárd Dóró
15a117a861 feat: improve cost calculation 2023-04-24 15:48:20 +02:00
Szilárd Dóró
e7ff1f79f8 feat: add information about replicas 2023-04-24 14:29:20 +02:00
Szilárd Dóró
33c7368a2e chore: update validation error message 2023-04-24 11:40:05 +02:00
Szilárd Dóró
664c182c8e fix: use font-medium for confirmation labels 2023-04-24 11:36:48 +02:00
Szilárd Dóró
c1ab4e0a77 chore: improve validation messages 2023-04-24 11:36:10 +02:00
Szilárd Dóró
4a4bd61757 chore: update tooltip label 2023-04-24 11:16:51 +02:00
Szilárd Dóró
b6d05289be fix: don't fail tests 2023-04-24 11:13:39 +02:00
Szilárd Dóró
5857458ca5 chore: improve resources form validation 2023-04-24 11:09:00 +02:00
Szilárd Dóró
2fb1145fe0 chore: add changeset 2023-04-24 10:22:44 +02:00
Szilárd Dóró
546d710102 Merge branch 'main' into feat/resource-replicas 2023-04-24 10:21:04 +02:00
Szilárd Dóró
7756103476 Merge pull request #1861 from nhost/changeset-release/main 2023-04-24 09:38:50 +02:00
github-actions[bot]
fef9456c12 chore: update versions 2023-04-23 19:36:24 +00:00
Szilárd Dóró
2d6d56f6b0 Merge pull request #1860 from nhost/fix/project-details
fix(dashboard): filter projects by workspace
2023-04-23 21:35:16 +02:00
Szilárd Dóró
f54be0fefd fix: don't break unit tests 2023-04-23 19:27:36 +02:00
Szilárd Dóró
4e76d388ab fix: remove unused query parameter 2023-04-23 16:42:33 +02:00
Szilárd Dóró
84b84ab785 fix: filter projects by workspace 2023-04-23 16:34:39 +02:00
Szilárd Dóró
ed66769688 Merge branch 'main' into feat/resource-replicas 2023-04-21 14:31:51 +02:00
Szilárd Dóró
899732f280 Merge pull request #1852 from nhost/changeset-release/main
chore: update versions
2023-04-21 13:40:09 +02:00
github-actions[bot]
037b566e39 chore: update versions 2023-04-21 11:23:53 +00:00
Szilárd Dóró
829f20c83c Merge pull request #1856 from nhost/renovate/vitejs-plugin-react-4.x
chore(deps): update dependency @vitejs/plugin-react to v4
2023-04-21 13:22:44 +02:00
Szilárd Dóró
f1b5a944a3 chore: add changeset 2023-04-21 11:42:44 +02:00
renovate[bot]
5ccb764ae5 chore(deps): update dependency @vitejs/plugin-react to v4 2023-04-21 09:38:43 +00:00
Szilárd Dóró
ef2b639734 Merge pull request #1839 from nhost/renovate/vueuse-core-10.x
fix(deps): update dependency @vueuse/core to v10
2023-04-21 11:36:00 +02:00
Szilárd Dóró
a5b895a827 chore: add changeset 2023-04-21 11:14:13 +02:00
renovate[bot]
b441b4bae2 fix(deps): update dependency @vueuse/core to v10 2023-04-21 09:02:30 +00:00
Szilárd Dóró
a6c67c1e4c Merge pull request #1836 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.37
2023-04-21 10:54:19 +02:00
Szilárd Dóró
7f1785ac0f chore: add changeset 2023-04-21 10:21:02 +02:00
Szilárd Dóró
a0298e0bdb chore: increase test timeout, improve stability 2023-04-20 16:40:42 +02:00
Szilárd Dóró
3fd94b1cdf chore: improve validation, fix tests 2023-04-20 16:08:37 +02:00
Szilárd Dóró
61d5f7d616 feat: make use of replicas from API 2023-04-20 14:56:52 +02:00
Szilárd Dóró
cde9a0a715 chore: extend tests, improve validation 2023-04-20 14:50:12 +02:00
Szilárd Dóró
eae6349b04 feat: add new pricing to confirmation dialog 2023-04-20 14:19:53 +02:00
Szilárd Dóró
211b930b84 chore: fix after effects of the new data structure 2023-04-20 13:53:06 +02:00
Szilárd Dóró
4ae463074b chore: simplify form data structure 2023-04-20 13:19:59 +02:00
Szilárd Dóró
1c5a4746f7 chore: improve validation error 2023-04-20 11:35:25 +02:00
Szilárd Dóró
d6ae1fa44a feat: resource validation when replicas > 1 2023-04-20 10:28:31 +02:00
Szilárd Dóró
a3abb81b37 feat: add replica slider to services 2023-04-19 15:57:57 +02:00
renovate[bot]
ec74e7fe98 chore(deps): update dependency @types/react to v18.0.37 2023-04-19 12:53:55 +00:00
Szilárd Dóró
6713c198c6 Merge pull request #1833 from nhost/renovate/turbo-1.x
chore(deps): update dependency turbo to v1.9.3
2023-04-19 14:52:03 +02:00
renovate[bot]
35a6b9cf47 chore(deps): update dependency turbo to v1.9.3 2023-04-19 09:32:19 +00:00
Szilárd Dóró
79f97fad76 Merge pull request #1838 from nhost/renovate/graphql-request-6.x
fix(deps): update dependency graphql-request to v6
2023-04-19 11:29:16 +02:00
Szilárd Dóró
2faf79077d chore: add changeset 2023-04-19 11:07:46 +02:00
Szilárd Dóró
4972b6feb6 chore: sync versions, update codegen 2023-04-19 11:07:15 +02:00
Szilárd Dóró
23d5861c4c Merge pull request #1837 from rikardwissing/fix/wait-for-valid-token
Wait for valid token or sign out before establishing connection.
2023-04-19 09:31:56 +02:00
renovate[bot]
098ac5a71c fix(deps): update dependency graphql-request to v6 2023-04-18 18:54:27 +00:00
Nuno Pato
3a15329cfd Merge pull request #1849 from nhost/docs/add-compute-section
add compute section to the docs
2023-04-18 13:20:39 +00:00
Nuno Pato
c3e798aa1d asd 2023-04-18 13:15:40 +00:00
Szilárd Dóró
eec5e6a93d Merge pull request #1843 from nhost/changeset-release/main
chore: update versions
2023-04-18 15:10:41 +02:00
Nuno Pato
d964b689cd move compute resources to position 1 2023-04-18 09:37:28 +00:00
Nuno Pato
1e080c1af5 add compute section to the docs 2023-04-18 09:35:01 +00:00
github-actions[bot]
177bba7ec0 chore: update versions 2023-04-18 07:27:56 +00:00
Szilárd Dóró
a593b45dc2 Merge pull request #1845 from nhost/chore/dashboard-update-nomenclature-compute
chore: dashboard: update nomenclature for compute
2023-04-18 09:24:46 +02:00
Szilárd Dóró
b384fb8bd8 chore: merge changesets 2023-04-17 20:58:38 +02:00
Nuno Pato
abd8620ded "Resources" -> "Compute" 2023-04-17 16:58:45 +00:00
Szilárd Dóró
e62ccdcaae Merge pull request #1844 from nhost/fix/resource-memory-limit
fix(dashboard): use correct vCPUs and memory after reset
2023-04-17 14:17:49 +02:00
Szilárd Dóró
46d01b09d6 chore: use constants 2023-04-17 13:45:59 +02:00
Szilárd Dóró
ff74e712f8 fix: use correct vCPUs and memory after reset 2023-04-17 13:38:47 +02:00
Szilárd Dóró
770794ccad Merge pull request #1709 from nhost/feat/resource-sliders
feat(dashboard): Resource Sliders
2023-04-17 13:03:04 +02:00
Szilárd Dóró
aa80d1795d chore: update initial resource ratio 2023-04-17 09:28:17 +02:00
Szilárd Dóró
eaa7720c65 fix: fix tests 2023-04-17 09:17:34 +02:00
Szilárd Dóró
7f447d1182 fix: don't break the initial UI 2023-04-17 08:43:23 +02:00
Szilárd Dóró
5d3dd84762 Merge branch 'main' into feat/resource-sliders 2023-04-17 08:36:55 +02:00
Szilárd Dóró
c625317342 Merge pull request #1841 from nhost/changeset-release/main
chore: update versions
2023-04-17 08:36:12 +02:00
Rikard Wissing
117398f5dc Add changeset 2023-04-16 15:00:50 +01:00
Rikard Wissing
4e421eb4bd Refactor a bit 2023-04-16 14:55:51 +01:00
Rikard Wissing
771447b089 Remove log 2023-04-16 14:52:35 +01:00
github-actions[bot]
8ab75a4146 chore: update versions 2023-04-14 09:54:44 +00:00
Szilárd Dóró
607f465616 Merge pull request #1840 from nhost/chore/use-dialog-hook
chore(dashboard): unify payment dialog management
2023-04-14 11:50:15 +02:00
Szilárd Dóró
668c877130 chore: add changeset 2023-04-14 11:17:32 +02:00
Szilárd Dóró
4bd870eb96 chore: relocate BillingPaymentMethodForm 2023-04-14 11:15:58 +02:00
Szilárd Dóró
39b3161d91 fix: use up-to-date card information 2023-04-14 10:31:27 +02:00
Szilárd Dóró
ae090a6585 chore: unify modal management for payments 2023-04-13 16:53:30 +02:00
Rikard Wissing
be4831ae62 Update integrations/apollo/src/index.ts
Co-authored-by: Szilárd Dóró <doroszilard@gmail.com>
2023-04-13 15:10:14 +02:00
Szilárd Dóró
4fb0c18c32 Merge branch 'main' into feat/resource-sliders 2023-04-13 14:56:03 +02:00
Szilárd Dóró
22cdd7f8d7 Merge pull request #1834 from nhost/changeset-release/main
chore: update versions
2023-04-13 14:38:10 +02:00
github-actions[bot]
f3a91a1f76 chore: update versions 2023-04-13 11:09:10 +00:00
Szilárd Dóró
1e9b92fcf8 Merge pull request #1835 from nhost/chore/remove-user-context
chore(dashboard): cleanup unused code
2023-04-13 13:07:30 +02:00
Szilárd Dóró
6cc56066c2 chore: update changeset 2023-04-13 11:44:38 +02:00
Rikard Wissing
99e80cea44 Wait for valid token 2023-04-12 23:45:38 +02:00
Szilárd Dóró
f2f1c01e3b chore: refactor memory steps 2023-04-12 17:43:44 +02:00
Szilárd Dóró
2c0f98e85c Merge branch 'main' into feat/resource-sliders 2023-04-12 14:43:12 +02:00
Szilárd Dóró
a3ad84925c chore: cleanup additional GraphQL operations 2023-04-12 14:36:11 +02:00
Szilárd Dóró
b8611b6a1c chore: cleanup unused GraphQL operations 2023-04-12 14:25:41 +02:00
Szilárd Dóró
a0e3030005 chore: cleanup UIContext 2023-04-12 14:01:41 +02:00
Szilárd Dóró
0cf1f1d938 Merge branch 'main' into chore/remove-user-context 2023-04-12 13:32:25 +02:00
Szilárd Dóró
88f026066f Merge pull request #1830 from rikardwissing/patch-1
Add generateLinks instead of link and onError args
2023-04-12 13:26:13 +02:00
Szilárd Dóró
185bef878d fix: accommodate dashboard test 2023-04-12 11:56:03 +02:00
Szilárd Dóró
a1c7b00e74 chore: add changeset 2023-04-12 11:55:29 +02:00
Szilárd Dóró
6da4562e79 chore: format code 2023-04-12 11:55:22 +02:00
Szilárd Dóró
e44cfcb2f2 Merge branch 'patch-1' of https://github.com/rikardwissing/nhost into pr/1830 2023-04-12 11:50:32 +02:00
Rikard Wissing
23fabaf8a6 Check for undefined 2023-04-12 11:48:48 +02:00
Szilárd Dóró
f4dca9836f fix: block UI when user is not available 2023-04-12 11:34:47 +02:00
Szilárd Dóró
f2704ea149 chore: improve query refetch 2023-04-12 11:30:34 +02:00
Szilárd Dóró
dd1b053212 fix: don't break provisioning page 2023-04-12 10:46:37 +02:00
Szilárd Dóró
d4ccc65655 chore: add changeset 2023-04-12 10:41:23 +02:00
Szilárd Dóró
2c2570fc82 chore: cleanup unused hooks 2023-04-12 10:40:49 +02:00
Rikard Wissing
a60f26966b Update integrations/apollo/src/index.ts
Co-authored-by: Szilárd Dóró <doroszilard@gmail.com>
2023-04-12 09:52:19 +02:00
Rikard Wissing
a988de2d61 Update integrations/apollo/src/index.ts
Co-authored-by: Szilárd Dóró <doroszilard@gmail.com>
2023-04-12 09:51:58 +02:00
Rikard Wissing
de54ca460e Update integrations/apollo/src/index.ts
Co-authored-by: Szilárd Dóró <doroszilard@gmail.com>
2023-04-12 09:51:48 +02:00
Szilárd Dóró
afdffab743 Merge pull request #1831 from nhost/fix/functions-response
fix(nhost-js): don't suppress error messages
2023-04-12 09:46:02 +02:00
Szilárd Dóró
4c61520397 chore: add changeset 2023-04-12 09:09:09 +02:00
Szilárd Dóró
f02cd444d5 fix: don't break builds 2023-04-11 17:38:10 +02:00
Szilárd Dóró
7f45a51aca fix: don't break build 2023-04-11 17:30:21 +02:00
Szilárd Dóró
08e70b9df9 fix: don't suppress error messages 2023-04-11 17:21:40 +02:00
Szilárd Dóró
32f92489a4 Merge pull request #1829 from nhost/changeset-release/main
chore: update versions
2023-04-11 15:58:09 +02:00
Szilárd Dóró
20a83362ee fix: don't break builds 2023-04-11 15:28:23 +02:00
Szilárd Dóró
20b800c3e4 Merge branch 'main' into feat/resource-sliders 2023-04-11 15:24:28 +02:00
github-actions[bot]
94c9cd151a chore: update versions 2023-04-11 13:13:16 +00:00
Szilárd Dóró
0e9eb18052 Merge pull request #1827 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.34
2023-04-11 15:11:46 +02:00
Szilárd Dóró
bfaa5b4c4a Merge branch 'main' into pr/1830 2023-04-11 14:57:39 +02:00
Szilárd Dóró
52ec6fe70c Merge pull request #1826 from nhost/renovate/glob-10.x
fix(deps): update dependency glob to v10
2023-04-11 14:45:48 +02:00
Szilárd Dóró
43b1b1442c chore: sync @types/react and @types/react-dom 2023-04-11 14:45:26 +02:00
Szilárd Dóró
b06239cc14 chore: add changeset 2023-04-11 14:30:35 +02:00
renovate[bot]
73dde87a65 fix(deps): update dependency glob to v10 2023-04-11 10:58:36 +00:00
renovate[bot]
7e7d810b74 chore(deps): update dependency @types/react to v18.0.34 2023-04-11 10:58:08 +00:00
Szilárd Dóró
b6b2403562 Merge pull request #1825 from nhost/renovate/rimraf-5.x
chore(deps): update dependency rimraf to v5
2023-04-11 12:54:11 +02:00
Szilárd Dóró
9a1f095a45 chore: add changeset 2023-04-11 12:13:55 +02:00
Rikard Wissing
a1a00b33ad Make backwards compatible
There is a behavioral change because of how customLink was implemented before. But I think this is the intended behavior.
2023-04-11 11:49:24 +02:00
renovate[bot]
a3b1ffe77c chore(deps): update dependency rimraf to v5 2023-04-11 09:42:50 +00:00
Szilárd Dóró
4f22ab3a99 Merge pull request #1816 from nhost/chore/current-project-hook
chore(dashboard): cleanup home page
2023-04-11 11:39:55 +02:00
Rikard Wissing
a269f4ca3f Add generateLinks instead of link and onError args
Breaking change, but makes it more versatile.
Not tested
2023-04-11 11:16:15 +02:00
Szilárd Dóró
411cb65ba4 chore: add changeset 2023-04-11 11:14:35 +02:00
Szilárd Dóró
f691c1f753 Merge pull request #1824 from nhost/renovate/vitest-monorepo
chore(deps): update vitest monorepo to ^0.30.0
2023-04-11 10:33:09 +02:00
Szilárd Dóró
b299cfc943 chore: add changeset 2023-04-11 08:38:55 +02:00
renovate[bot]
6157680963 chore(deps): update vitest monorepo to ^0.30.0 2023-04-09 15:40:35 +00:00
Szilárd Dóró
1d4bdfa88b fix integration tests 2023-04-06 18:43:10 +02:00
Szilárd Dóró
2755fc43b9 fix linter error 2023-04-06 17:18:01 +02:00
Szilárd Dóró
0c80d141aa fix: don't break integration tests 2023-04-06 16:55:31 +02:00
Szilárd Dóró
f285883c88 chore: improve integration tests 2023-04-06 16:48:29 +02:00
Szilárd Dóró
39f9a325d3 chore: delete old useCurrentWorkspaceAndApplication hook 2023-04-06 16:26:52 +02:00
Szilárd Dóró
e8f66e346f chore: migrate more components to new hook 2023-04-06 16:16:39 +02:00
Szilárd Dóró
98c0535fc9 chore: migrate more components to new hook 2023-04-06 15:55:49 +02:00
Szilárd Dóró
7a61c2e976 chore: migrate more components to new hook 2023-04-06 15:49:14 +02:00
Szilárd Dóró
a15a4db210 fix: revert locale related changes 2023-04-06 15:25:38 +02:00
Szilárd Dóró
11fcb8c72f Merge branch 'main' into chore/current-project-hook 2023-04-06 15:18:08 +02:00
Szilárd Dóró
a8a20cf5e2 Merge pull request #1820 from nhost/changeset-release/main 2023-04-06 14:37:59 +02:00
github-actions[bot]
2f84bf3251 chore: update versions 2023-04-06 12:22:20 +00:00
Szilárd Dóró
368e0371e9 Merge pull request #1818 from nhost/feat/improved-error-state
feat(dashboard): improved error state
2023-04-06 14:21:08 +02:00
Szilárd Dóró
adb5209133 Merge branch 'main' into feat/improved-error-state 2023-04-06 14:00:07 +02:00
Szilárd Dóró
63bf405cdd Merge pull request #1800 from Glenas7/docs/local-development-guide
Update local URLs to new format
2023-04-06 13:48:40 +02:00
Szilárd Dóró
d613d66a0a Merge pull request #1819 from nhost/fix/revert-i18n-lib
fix(dashboard): revert i18n library to improve performance
2023-04-06 13:36:21 +02:00
Szilárd Dóró
e7cb5070cd fix: use updated URLs everywhere 2023-04-06 13:28:55 +02:00
Szilárd Dóró
ee50051802 chore: remove unused next-translate-plugin 2023-04-06 13:08:53 +02:00
Szilárd Dóró
20e7bb4747 fix: remove translation 2023-04-06 13:08:06 +02:00
Szilárd Dóró
ba0d57ee91 fix: revert i18n library 2023-04-06 12:56:53 +02:00
Szilárd Dóró
98093c9023 Update dashboard/locales/en/overview.json
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2023-04-06 12:28:08 +02:00
Szilárd Dóró
2fda299736 fix comment 2023-04-06 12:14:02 +02:00
Szilárd Dóró
3328ed059e chore: add changeset 2023-04-06 11:42:20 +02:00
Szilárd Dóró
cfb7199b79 feat: show error message on project overview 2023-04-06 11:41:09 +02:00
Szilárd Dóró
1ad4bfe815 fix readme 2023-04-06 09:26:44 +02:00
Szilárd Dóró
25859fc421 Merge pull request #1812 from nhost/changeset-release/main
chore: update versions
2023-04-06 09:22:15 +02:00
github-actions[bot]
5a9e7a43c8 chore: update versions 2023-04-06 06:45:13 +00:00
Szilárd Dóró
2739ff90c4 Merge pull request #1811 from nhost/feat/metrics
feat(dashboard): Show Metrics
2023-04-06 08:43:45 +02:00
Szilárd Dóró
93910f27e1 chore: partially migrate components to useCurrentWorkspaceAndProject 2023-04-05 16:00:09 +02:00
Szilárd Dóró
04e2d19dda fix: fix issue plaguing sign outs for a while now 2023-04-05 14:21:11 +02:00
Szilárd Dóró
a2175f6df7 chore: cleanup unused files 2023-04-05 13:54:50 +02:00
Szilárd Dóró
18d415a8fd feat: rework project list 2023-04-05 12:54:20 +02:00
Szilárd Dóró
2a4623c582 Merge pull request #1815 from nhost/renovate/peter-evans-create-pull-request-5.x
chore(deps): update peter-evans/create-pull-request action to v5
2023-04-05 11:11:26 +02:00
Szilárd Dóró
19b7835d92 fix: remove empty test file 2023-04-05 11:04:04 +02:00
Szilárd Dóró
efbd272298 fix: use separator for sizes 2023-04-05 10:32:52 +02:00
Szilárd Dóró
98546d24e1 chore: improve number formatting 2023-04-05 10:28:59 +02:00
Szilárd Dóró
fe2c0cf81f Merge branch 'main' into feat/metrics 2023-04-05 09:30:37 +02:00
renovate[bot]
b28a04c48e chore(deps): update peter-evans/create-pull-request action to v5 2023-04-05 07:29:01 +00:00
Szilárd Dóró
a014913523 Merge pull request #1810 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.33
2023-04-05 09:28:22 +02:00
Szilárd Dóró
706c9dc3fb chore: add changeset 2023-04-04 18:45:17 +02:00
Szilárd Dóró
fe08faad4a feat: introduce i18n lib 2023-04-04 18:34:26 +02:00
Szilárd Dóró
6719ce92ea feat: add tooltip to metrics 2023-04-04 14:32:35 +02:00
Szilárd Dóró
52c6f09bdd chore: simplify pending UI 2023-04-04 13:57:36 +02:00
Szilárd Dóró
f337a19875 chore: add test 2023-04-04 13:42:38 +02:00
renovate[bot]
d62c909901 chore(deps): update dependency @types/react to v18.0.33 2023-04-04 11:02:39 +00:00
Szilárd Dóró
99f8f6b370 chore: add changeset 2023-04-04 13:01:02 +02:00
Szilárd Dóró
644d94a175 Merge pull request #1803 from nhost/renovate/next-seo-6.x
fix(deps): update dependency next-seo to v6
2023-04-04 13:00:36 +02:00
Szilárd Dóró
05ab111aa4 chore: remove postgres usage 2023-04-04 12:59:31 +02:00
Szilárd Dóró
64cf0acd4a Merge pull request #1792 from nhost/renovate/turbo-1.x
chore(deps): update dependency turbo to v1.8.8
2023-04-04 11:57:19 +02:00
Szilárd Dóró
3d5d530555 chore: improve usage panel 2023-04-04 11:51:06 +02:00
Szilárd Dóró
5e0920ba7c chore: add changeset 2023-04-04 10:12:36 +02:00
Szilárd Dóró
9bf6c3b8c4 feat: add egress volume to metrics 2023-04-03 21:21:36 +02:00
Szilárd Dóró
b25a223d90 feat: add metrics to the project overview 2023-04-03 17:33:58 +02:00
renovate[bot]
748aa443f4 fix(deps): update dependency next-seo to v6 2023-04-03 14:38:59 +00:00
renovate[bot]
684123e5d6 chore(deps): update dependency turbo to v1.8.8 2023-04-03 14:37:37 +00:00
Szilárd Dóró
fa045eed15 Merge pull request #1808 from nhost/changeset-release/main
chore: update versions
2023-04-03 16:35:43 +02:00
github-actions[bot]
61c0583b6d chore: update versions 2023-04-03 13:56:32 +00:00
Szilárd Dóró
1343a6f252 Merge pull request #1806 from nhost/fix/failed-subscriptions
fix: don't open unnecessary connections
2023-04-03 15:55:27 +02:00
Szilárd Dóró
0d73e87a83 fix: don't open unnecessary connections 2023-04-03 15:14:28 +02:00
Szilárd Dóró
1ee0d332bf Merge pull request #1797 from nhost/changeset-release/main
chore: update versions
2023-04-03 13:50:27 +02:00
github-actions[bot]
130ce49c76 chore: update versions 2023-04-03 10:06:30 +00:00
Szilárd Dóró
6be6d6475a Merge pull request #1804 from nhost/fix/date-input
fix(dashboard): don't break logs page
2023-04-03 11:58:37 +02:00
Szilárd Dóró
177b146b93 Merge pull request #1799 from nhost/renovate/urql-4.x
chore(deps): update dependency urql to v4
2023-04-03 11:39:47 +02:00
Szilárd Dóró
3cb673000a fix: don't break logs page 2023-04-03 11:38:34 +02:00
Szilárd Dóró
09cf5d4b39 chore: add changeset 2023-04-03 10:07:16 +02:00
Szilárd Dóró
48c0061a0b Merge pull request #1772 from wollerman/patch-2
Update serverless-functions to link to event trigger docs
2023-04-03 09:47:01 +02:00
Szilárd Dóró
0795d1c6a1 chore: move new text 2023-04-03 09:34:02 +02:00
renovate[bot]
45a23dd1bf chore(deps): update dependency urql to v4 2023-04-03 07:30:32 +00:00
Szilárd Dóró
bb8e3454df Merge pull request #1790 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.32
2023-04-03 09:28:01 +02:00
Szilárd Dóró
6a290bb297 chore: add changeset 2023-04-03 09:10:03 +02:00
renovate[bot]
80baec7356 chore(deps): update dependency @types/react to v18.0.32 2023-04-02 09:44:20 +00:00
Glenas7
feb195fd65 Update local URLs to new format
With the change of local URLs to the subdomain format with "local" as subdiomain and no region, I believe these URLs should be updated here.
2023-04-01 11:19:57 +02:00
Szilárd Dóró
baaa510309 fix: add price to GQL 2023-03-31 15:28:59 +02:00
Szilárd Dóró
a84aa5ad68 Merge branch 'main' into feat/resource-sliders 2023-03-31 15:19:09 +02:00
Szilárd Dóró
8e43297564 Merge pull request #1798 from nhost/feat/project-creator
feat(dashboard): show project creator
2023-03-31 14:49:16 +02:00
Szilárd Dóró
bb8eb9e387 fix: fix assertion in test 2023-03-31 13:55:05 +02:00
Szilárd Dóró
5b0dc6cb19 fix: use optional chaining in overview header 2023-03-31 13:45:58 +02:00
Szilárd Dóró
826112afd0 fix: don't show upgrade button for pro projects 2023-03-31 13:32:16 +02:00
Szilárd Dóró
97105c390d chore: remove test file 2023-03-31 13:26:59 +02:00
Szilárd Dóró
8e3707ff2c Merge branch 'main' into feat/project-creator 2023-03-31 13:25:32 +02:00
Szilárd Dóró
7453bf3b6a chore: add changeset 2023-03-31 13:25:25 +02:00
Szilárd Dóró
bd739383d2 Merge pull request #1796 from nhost/chore/improved-auth-tests
chore(tests): improve auth page tests
2023-03-31 13:19:44 +02:00
Szilárd Dóró
f75e2e41db chore: prefix email addresses 2023-03-31 10:48:54 +02:00
Szilárd Dóró
7328491be0 feat: add relative time to creator info 2023-03-30 20:37:53 +02:00
Szilárd Dóró
11b4d12f12 Merge pull request #1794 from nhost/changeset-release/main
chore: update versions
2023-03-30 19:56:33 +02:00
Szilárd Dóró
6c24d56b1d fix: remove test.only 2023-03-30 17:41:14 +02:00
Szilárd Dóró
0a523f4b45 feat: add project creator to overviews 2023-03-30 17:37:21 +02:00
Szilárd Dóró
12301e6551 fix: use correct @nhost/apollo version 2023-03-30 15:57:43 +02:00
Szilárd Dóró
8440d0389e chore: restructure auth tests 2023-03-30 15:55:58 +02:00
Szilárd Dóró
c166dad0f8 chore: add changeset 2023-03-30 13:49:14 +02:00
Szilárd Dóró
e31d39b3d2 feat: incorporate global setup in projects 2023-03-30 13:44:34 +02:00
Szilárd Dóró
090f9cef86 chore: extend user management tests 2023-03-30 13:35:06 +02:00
github-actions[bot]
74e52cac2d chore: update versions 2023-03-30 09:07:41 +00:00
Szilárd Dóró
f17823760a Merge pull request #1795 from nhost/fix/presigned-urls
fix: don't alter URLs when no transformation parameters are available
2023-03-30 11:06:32 +02:00
Szilárd Dóró
bb8803a1e3 fix: don't alter URLs 2023-03-30 10:41:57 +02:00
Szilárd Dóró
b846291331 Merge pull request #1793 from nhost/fix/export-issue
fix: don't use conflicting names
2023-03-30 10:07:24 +02:00
Szilárd Dóró
2b2fb94f00 chore: add type checking step 2023-03-30 09:42:23 +02:00
Szilárd Dóró
551760c4f0 fix: don't break builds 2023-03-30 09:37:39 +02:00
Szilárd Dóró
5ae5a8e77d fix: don't break builds 2023-03-30 09:31:54 +02:00
Szilárd Dóró
56aae0c964 fix: don't break builds 2023-03-30 09:28:34 +02:00
Szilárd Dóró
a0e093d77b fix: don't use conflicting names 2023-03-30 09:16:30 +02:00
Szilárd Dóró
5e82e1b3da Merge pull request #1784 from nhost/changeset-release/main
chore: update versions
2023-03-29 09:20:48 +02:00
github-actions[bot]
e618b705e7 chore: update versions 2023-03-28 15:52:47 +00:00
Szilárd Dóró
a232c9f0f6 Merge pull request #1789 from nhost/fix/azuread-description
fix(dashboard): use correct description for Azure AD
2023-03-28 17:50:51 +02:00
Szilárd Dóró
bf4644ea10 fix: use correct description for Azure AD 2023-03-28 16:52:54 +02:00
Szilárd Dóró
0aca907ea4 Merge pull request #1788 from nhost/fix/azuread-provider-name
fix: correct typos in Azure AD
2023-03-28 16:25:59 +02:00
Szilárd Dóró
394f4c4174 fix: correct typos in Azure AD 2023-03-28 16:25:26 +02:00
Szilárd Dóró
8fef08a150 Merge pull request #1786 from nhost/renovate/turbo-1.x
chore(deps): update dependency turbo to v1.8.6
2023-03-28 16:16:57 +02:00
Szilárd Dóró
1bd2c37301 chore: bump turbo in the Dockerfile 2023-03-28 15:54:37 +02:00
renovate[bot]
5cdb70bd81 chore(deps): update dependency turbo to v1.8.6 2023-03-28 12:01:36 +00:00
Szilárd Dóró
1a5f80e1b6 Merge pull request #1785 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.30
2023-03-28 13:59:29 +02:00
Szilárd Dóró
59e0cb00c5 Merge pull request #1787 from nhost/feat/azuread-provider 2023-03-28 12:25:42 +02:00
Szilárd Dóró
406b0f2cb7 Merge pull request #1163 from dipakparmar/feat/dashboard-azuread-settings
feat(dashboard): add azure ad provider settings
2023-03-28 10:52:17 +02:00
Szilárd Dóró
d329b6218f chore: add changeset 2023-03-28 10:46:50 +02:00
Szilárd Dóró
335b58670e Merge branch 'renovate/react-monorepo' of https://github.com/nhost/nhost into renovate/react-monorepo 2023-03-28 10:43:08 +02:00
renovate[bot]
efa2d89067 chore(deps): update dependency @types/react to v18.0.30 2023-03-28 08:35:55 +00:00
Szilárd Dóró
77ce4bd738 Merge pull request #1783 from nhost/fix/random-words
fix(tests): avoid name collision in database tests
2023-03-28 10:33:33 +02:00
Szilárd Dóró
017adea700 chore: update comment 2023-03-28 10:04:38 +02:00
Dipak Parmar
378284faa8 chore(dashboard): remove docs and title for now from azuread component
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 23:44:40 -07:00
renovate[bot]
e5e2d114b1 chore(deps): update dependency @types/react to v18.0.30 2023-03-27 19:03:37 +00:00
Szilárd Dóró
5e3dbdeb7d Merge pull request #1781 from nhost/renovate/react-monorepo
chore(deps): update dependency @types/react to v18.0.29
2023-03-27 20:55:47 +02:00
Szilárd Dóró
98b777491a fix: improve flaky tests 2023-03-27 18:13:10 +02:00
Szilárd Dóró
71de870cb0 fix: use admin secret as env var 2023-03-27 17:29:09 +02:00
Szilárd Dóró
74d4deba28 feat: cleanup public schema after tests 2023-03-27 17:00:07 +02:00
Szilárd Dóró
cb248f0d30 chore: add changeset 2023-03-27 15:44:08 +02:00
Szilárd Dóró
09e4f1eb34 fix: avoid duplicate table names in tests 2023-03-27 15:16:40 +02:00
Szilárd Dóró
6e1f03eaee chore: accomodate changes to API 2023-03-27 11:57:24 +02:00
Szilárd Dóró
867c807699 chore: add changeset 2023-03-27 11:21:42 +02:00
renovate[bot]
d0673d7825 chore(deps): update dependency @types/react to v18.0.29 2023-03-27 07:50:19 +00:00
Dipak Parmar
106f23dcfa fixdashboard-settings): remove extra whitespace azuread provider import in settings
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 00:48:56 -07:00
Dipak Parmar
83ef755822 feat(dashboard-settings): update azuread provider settings component
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 00:47:09 -07:00
Dipak Parmar
b7703ffd70 feat(graphql): add azuread to signinmethods query
Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-27 00:46:30 -07:00
Dipak Parmar
340ea5b115 chore: merge branch 'main' into feat/dashboard-azuread-settings
* main: (1322 commits)
  chore(ci): adjust preview fetcher
  chore: add changeset
  fix: fetch valid previews only
  fix: use correct Vercel token
  fix: use staging project ID
  chore: use dynamic test URL
  fix(deps): update docusaurus monorepo to v2.4.0
  chore(hasura-storage-js): improve presignedUrl test
  fix: remove test.only call
  chore: add tests for table deletion
  chore: update versions
  fix: potential subscription fix
  Fix import in docs
  fix: remove `test.only` call
  chore: add remaining table creation tests
  chore: add foreign key constraint test
  chore: add extra database UI tests
  chore: restructure tests, add basic table creation test
  chore: update versions
  chore: add changeset
  ...

Signed-off-by: Dipak Parmar <hi@dipak.tech>
2023-03-26 19:16:40 -07:00
Szilárd Dóró
4191b933c9 Merge branch 'main' into feat/resource-sliders 2023-03-24 10:57:01 +01:00
Matt Wollerman
776eca3fb5 Update serverless-functions to link to event trigger docs 2023-03-23 09:06:20 -04:00
Szilárd Dóró
cf2264ce1d Merge branch 'main' into feat/resource-sliders 2023-03-20 16:25:34 +01:00
Szilárd Dóró
02dd9dd8c0 Merge branch 'main' into feat/resource-sliders 2023-03-20 12:41:38 +01:00
Szilárd Dóró
d4ff25df0f fix: adjust warning color 2023-03-10 14:19:10 +01:00
Szilárd Dóró
3d74374780 fix: update codegen 2023-03-09 10:22:43 +01:00
Szilárd Dóró
7063af678c Merge remote-tracking branch 'origin/main' into feat/resource-sliders 2023-03-09 10:14:33 +01:00
Szilárd Dóró
2b44a1cf27 chore: add tests, fix flaky tests
- fix a UI glitch where values jumped after reset
2023-03-08 16:33:02 +01:00
Szilárd Dóró
c4f60b3645 feat: fetch plan independently 2023-03-08 15:44:30 +01:00
Szilárd Dóró
f86f658aa5 feat: improve upgrade / downgrade experience 2023-03-08 13:54:35 +01:00
Szilárd Dóró
bd02bd3f3e fix: cpu -> vcpu and selected -> available 2023-03-08 13:41:50 +01:00
Szilárd Dóró
a133faa797 chore: added submission tests 2023-03-08 11:24:17 +01:00
Szilárd Dóró
bb0269691d Merge remote-tracking branch 'origin/main' into feat/resource-sliders 2023-03-08 10:23:48 +01:00
Szilárd Dóró
8d6171d22d chore: move test-related stuff 2023-03-07 17:23:09 +01:00
Szilárd Dóró
fff178d79f fix: fix unit tests 2023-03-07 16:59:15 +01:00
Szilárd Dóró
5e5e454ae7 chore: remove inputs from total resources 2023-03-07 16:34:40 +01:00
Szilárd Dóró
ce005f6d9e Merge remote-tracking branch 'origin/main' into feat/resource-sliders 2023-03-07 15:37:42 +01:00
Szilárd Dóró
85889ee882 chore: add changeset 2023-03-07 10:23:33 +01:00
Szilárd Dóró
351873059e fix: use correct colors in light mode 2023-03-07 10:09:00 +01:00
Szilárd Dóró
8ccfc10522 fix: fix inner slider behaviour 2023-03-07 10:05:25 +01:00
Szilárd Dóró
82b02ca70b feat: improve slider appearance 2023-03-07 09:12:28 +01:00
Szilárd Dóró
14fc132040 fix: show footer when downgrading 2023-03-06 17:02:54 +01:00
Szilárd Dóró
a35da349ed fix: fix tests and prevent error when plan is missing 2023-03-06 16:48:43 +01:00
Szilárd Dóró
302e1d9d33 feat: retrieve plan info from the project 2023-03-06 16:18:42 +01:00
Szilárd Dóró
0db40184e8 fix: show values correctly after saving resources 2023-03-06 15:38:07 +01:00
Szilárd Dóró
d38649494e chore: added tests 2023-03-06 15:14:30 +01:00
Szilárd Dóró
5f22f1b5e5 chore: restructure to support tests
- added network requests to retrieve computation info
2023-03-06 14:13:48 +01:00
Szilárd Dóró
494f93a4bf chore: change terminology RAM -> Memory 2023-03-06 10:28:40 +01:00
Szilárd Dóró
84c8af232c fix: don't nest p in p 2023-03-03 16:54:21 +01:00
Szilárd Dóró
7f101d54da feat: add pricing placeholders 2023-03-03 16:53:28 +01:00
Szilárd Dóró
75b497412e fix: reset error, fix RAM label 2023-03-03 16:16:09 +01:00
Szilárd Dóró
5bd774afbb chore: improve readability 2023-03-03 16:11:54 +01:00
Szilárd Dóró
cdfe203fe4 Merge remote-tracking branch 'origin/main' into feat/resource-sliders 2023-03-03 15:37:45 +01:00
Szilárd Dóró
4c7d32e944 fix: resource labels 2023-03-03 15:35:17 +01:00
Szilárd Dóró
447c622fc0 fix: remove unnecessary inputs from services 2023-03-03 15:33:01 +01:00
Szilárd Dóró
03f22aed72 chore: improved resource constants 2023-03-03 15:21:15 +01:00
Szilárd Dóró
ede5abf2ac fix: use correct width for inner slider rail 2023-03-03 15:09:33 +01:00
Szilárd Dóró
0bdd1d0e0c fix: fixed top slider vlaidation 2023-03-03 14:19:08 +01:00
Szilárd Dóró
61de7b21fd feat: improve slider rails 2023-03-03 14:00:57 +01:00
Szilárd Dóró
4b6ead1b17 feat: add validation to top slider 2023-03-03 13:18:57 +01:00
Szilárd Dóró
0b193e6310 feat: create form fragments 2023-03-03 12:15:29 +01:00
Szilárd Dóró
b21a5403fe feat: add sections for services, improve slider 2023-03-03 11:15:01 +01:00
Szilárd Dóró
e8320be941 feat: initial Resource page code 2023-03-02 16:23:23 +01:00
Dipak Parmar
ce4b655c55 fix: correct typos 2022-11-22 19:47:21 -08:00
Dipak Parmar
dc57d31ec9 fix: correct extra space in azureadprovidersettings dir 2022-11-22 19:45:38 -08:00
Dipak Parmar
ea29fd6b73 feat(dashboard-settings): add azuread provider to settings 2022-11-21 20:30:53 -08:00
Dipak Parmar
d8e4073957 feat(dashboard-settings): add azuread provider settings component 2022-11-21 20:29:34 -08:00
Dipak Parmar
3f399a54a3 feat(graphql): add azuread to signinmethods query 2022-11-21 20:28:50 -08:00
448 changed files with 21526 additions and 18834 deletions

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,188 @@
# @nhost/dashboard
## 0.16.11
### Patch Changes
- 1230b722: fix(projects): don't redirect to 404 on when the project is renamed
- @nhost/react-apollo@5.0.22
- @nhost/nextjs@1.13.24
## 0.16.10
### Patch Changes
- Updated dependencies [da03bf39]
- @nhost/react-apollo@5.0.21
- @nhost/nextjs@1.13.23
## 0.16.9
### Patch Changes
- 349aac36: fix(settings): use region domain when constructing the postgres connection string
## 0.16.8
### Patch Changes
- 20fb69fa: chore(projects): change the way how API URLs are constructed
## 0.16.7
### Patch Changes
- 49f9b837: chore(docker): bump `pnpm` to `v8.4.0` and `turbo` to `v1.9.3`
- 3f478a4e: chore(deps): bump `vitest` to `v0.31.0`, `@types/react` to `v18.2.6` and `@types/react-dom` to `v18.2.4`
## 0.16.6
### Patch Changes
- d926f156: fix(projects): redirect to 404 when an invalid project is opened
- 49b99728: fix(projects): disable features for non-owner members of workspaces
## 0.16.5
### Patch Changes
- 12e2855f: chore(deps): bump `jsdom` to v22
- e4972b83: feat(metrics): add Grafana page
## 0.16.4
### Patch Changes
- 3f396a9e: fix(projects): unpause after upgrading a paused project to pro
- 3f396a9e: fix(projects): don't redirect to 404 page after project creation
## 0.16.3
### Patch Changes
- Updated dependencies [90c60311]
- @nhost/react-apollo@5.0.20
- @nhost/nextjs@1.13.22
## 0.16.2
### Patch Changes
- 0f34f0c6: fix(projects): disallow downgrading to free plan
- 8da291ad: chore(deps): bump `@types/react` to v18.2.0 and `@types/react-dom` to v18.2.1
## 0.16.1
### Patch Changes
- adc828a5: fix(gql): don't enter an infinite loop when fetching remote app data
## 0.16.0
### Minor Changes
- 2fb1145f: feat(compute): add support for replicas
### Patch Changes
- d8ceccec: chore(env): remove deprecated `NHOST_BACKEND_URL` environment variable
## 0.15.2
### Patch Changes
- 84b84ab7: fix(projects): filter projects by workspace
## 0.15.1
### Patch Changes
- 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
- 668c8771: chore(dialogs): unify dialog management of payment dialogs
## 0.14.7
### Patch Changes
- d4ccc656: chore: cleanup unused code
- @nhost/react-apollo@5.0.18
- @nhost/nextjs@1.13.21
## 0.14.6
### Patch Changes
- b299cfc9: chore(deps): bump `vitest` to v0.30.0
- 411cb65b: chore(projects): refactor workspace and project hooks
- 43b1b144: chore(deps): bump `@types/react` to v18.0.34 and `@types/react-dom` to v18.0.11
- Updated dependencies [43b1b144]
- @nhost/react-apollo@5.0.17
- @nhost/nextjs@1.13.20
## 0.14.5
### Patch Changes
- ba0d57ee: fix(i18n): revert i18n library
- 3328ed05: feat(projects): improve overview when there is an error
## 0.14.4
### Patch Changes
- 5e0920ba: chore(deps): bump `next-seo` to v6
- 706c9dc3: chore(deps): bump `@types/react` to 18.0.33
- 99f8f6b3: feat(metrics): show metrics on the overview
## 0.14.3
### Patch Changes
- @nhost/react-apollo@5.0.16
## 0.14.2
### Patch Changes
- 3cb67300: fix(logs): don't break UI when clearing time picker
- 7453bf3b: feat(projects): show project creator info
- c166dad0: chore(tests): improve auth page tests
- 6a290bb2: chore(deps): bump `@types/react` to 18.0.32
## 0.14.1
### Patch Changes
- @nhost/react-apollo@5.0.15
- @nhost/nextjs@1.13.19
## 0.14.0
### Minor Changes
- 6e1f03ea: feat(dashboard): add support for the Azure AD provider
### Patch Changes
- 1bd2c373: chore(deps): bump `turbo` to 1.8.6
- d329b621: chore(deps): bump `@types/react` to 18.0.30
- cb248f0d: fix(tests): avoid name collision in database tests
- 867c8076: chore(deps): bump `@types/react` to 18.0.29
## 0.13.10
### Patch Changes

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.13.10",
"version": "0.16.11",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -8,7 +8,7 @@
"build": "next build --no-lint",
"analyze": "ANALYZE=true pnpm build --no-lint",
"start": "next start",
"lint": "next lint --max-warnings 2",
"lint": "next lint --max-warnings 0",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost dev -d",
@@ -51,16 +51,15 @@
"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",
"lodash.debounce": "^4.0.8",
"next": "^12.3.1",
"next-seo": "^5.14.1",
"next-seo": "^6.0.0",
"node-pg-format": "^1.3.5",
"pluralize": "^8.0.0",
"prettysize": "^2.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.0",
@@ -106,15 +105,15 @@
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/react": "18.0.28",
"@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.29.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",
@@ -130,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",
@@ -141,14 +140,14 @@
"prettier-plugin-tailwindcss": "^0.2.0",
"react-date-fns-hooks": "^0.9.4",
"require-from-string": "^2.0.2",
"snake-case": "^3.0.4",
"storybook-addon-next-router": "^4.0.1",
"tailwindcss": "^3.1.2",
"ts-node": "^10.9.1",
"tsconfig-paths-webpack-plugin": "^4.0.0",
"vite": "^4.0.2",
"vite-tsconfig-paths": "^4.0.3",
"vitest": "^0.29.0",
"webpack": "^5.75.0"
"vitest": "^0.31.0"
},
"browserslist": {
"production": [

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,5 +1,5 @@
import FeedbackForm from '@/components/common/FeedbackForm';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useInterval } from '@/hooks/useInterval';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
@@ -33,7 +33,7 @@ export function AppLoader({
date,
restoring,
}: AppLoaderProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
let timeElapsedSinceEventCreation: number;
@@ -41,11 +41,11 @@ export function AppLoader({
timeElapsedSinceEventCreation = getRelativeDateByApplicationState(date);
} else if (unpause) {
timeElapsedSinceEventCreation = getRelativeDateByApplicationState(
currentApplication.appStates[0].createdAt,
currentProject.appStates[0].createdAt,
);
} else {
timeElapsedSinceEventCreation = getRelativeDateByApplicationState(
currentApplication.createdAt,
currentProject.createdAt,
);
}
@@ -63,9 +63,9 @@ export function AppLoader({
<div className="grid grid-flow-row gap-2">
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h1">
{restoring && `Restoring ${currentApplication.name} from backup`}
{!restoring && unpause && `Unpausing ${currentApplication.name}`}
{!restoring && !unpause && `Provisioning ${currentApplication.name}`}
{restoring && `Restoring ${currentProject.name} from backup`}
{!restoring && unpause && `Unpausing ${currentProject.name}`}
{!restoring && !unpause && `Provisioning ${currentProject.name}`}
</Text>
<Text>This normally takes around 2 minutes</Text>
</div>

View File

@@ -1,8 +1,9 @@
import FeedbackForm from '@/components/common/FeedbackForm';
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import { useAppCreatedAt } from '@/hooks/useAppCreatedAt';
import { useCurrentDate } from '@/hooks/useCurrentDate';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { ApplicationState } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import { Modal } from '@/ui/Modal';
@@ -10,30 +11,28 @@ import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Button from '@/ui/v2/Button';
import { Dropdown } from '@/ui/v2/Dropdown';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
import { getApplicationStatusString } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache';
import {
useDeleteApplicationMutation,
useGetApplicationStateQuery,
useInsertApplicationMutation,
useUpdateApplicationMutation,
} from '@/utils/__generated__/graphql';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { getPreviousApplicationState } from '@/utils/getPreviousApplicationState';
import { getApplicationStatusString } from '@/utils/helpers';
import { triggerToast } from '@/utils/toast';
import { updateOwnCache } from '@/utils/updateOwnCache';
import { useApolloClient } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import ApplicationInfo from './ApplicationInfo';
import ApplicationLive from './ApplicationLive';
import ApplicationUnknown from './ApplicationUnknown';
import { RemoveApplicationModal } from './RemoveApplicationModal';
import { StagingMetadata } from './StagingMetadata';
export default function ApplicationErrored() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const [changingApplicationStateLoading, setChangingApplicationStateLoading] =
useState(false);
@@ -44,12 +43,13 @@ export default function ApplicationErrored() {
// state, but we want to query again to double-check that we have the latest state
// of the application. @GC.
const { data, loading, error } = useGetApplicationStateQuery({
variables: { appId: currentApplication.id },
variables: { appId: currentProject?.id },
skip: !currentProject,
});
const [previousState, setPreviousState] = useState<ApplicationStatus | null>(
null,
);
const previousState = data?.app?.appStates
? getPreviousApplicationState(data.app.appStates)
: null;
const [showRecreateModal, setShowRecreateModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
@@ -57,9 +57,7 @@ export default function ApplicationErrored() {
const client = useApolloClient();
const { currentDate } = useCurrentDate();
const user = useUserData();
const isOwner = currentWorkspace.members.some(
({ userId, type }) => userId === user?.id && type === 'owner',
);
const isOwner = useIsCurrentUserOwner();
const { appCreatedAt } = useAppCreatedAt();
@@ -70,15 +68,15 @@ export default function ApplicationErrored() {
try {
await deleteApplication({
variables: {
appId: currentApplication.id,
appId: currentProject.id,
},
});
triggerToast(`${currentApplication.name} deleted`);
triggerToast(`${currentProject?.name} deleted`);
} catch (e) {
triggerToast(`Error deleting ${currentApplication.name}`);
triggerToast(`Error deleting ${currentProject?.name}`);
discordAnnounce(
`Error deleting app: ${currentApplication.name} (${user.email})`,
`Error deleting app: ${currentProject?.name} (${user.email})`,
);
return;
}
@@ -86,19 +84,19 @@ export default function ApplicationErrored() {
await insertApp({
variables: {
app: {
name: currentApplication.name,
slug: currentApplication.slug,
planId: currentApplication.plan.id,
name: currentProject.name,
slug: currentProject.slug,
planId: currentProject.plan.id,
workspaceId: currentWorkspace.id,
regionId: currentApplication.region.id,
regionId: currentProject.region.id,
},
},
});
discordAnnounce(`Recreating: ${currentApplication.name} (${user.email})`);
triggerToast(`Recreating ${currentApplication.name} `);
discordAnnounce(`Recreating: ${currentProject?.name} (${user.email})`);
triggerToast(`Recreating ${currentProject?.name} `);
await updateOwnCache(client);
} catch (e) {
triggerToast(`Error trying to recreate: ${currentApplication.name}`);
triggerToast(`Error trying to recreate: ${currentProject?.name}`);
}
}
@@ -107,18 +105,18 @@ export default function ApplicationErrored() {
try {
await updateApplication({
variables: {
appId: currentApplication.id,
appId: currentProject?.id,
app: {
desiredState: ApplicationStatus.Live,
},
},
});
triggerToast(`${currentApplication.name} set to awake.`);
triggerToast(`${currentProject?.name} set to awake.`);
} catch (e) {
triggerToast(`Error trying to awake ${currentApplication.name}`);
triggerToast(`Error trying to awake ${currentProject?.name}`);
discordAnnounce(
`Error trying to awake app: ${currentApplication.name} (${user.email})`,
`Error trying to awake app: ${currentProject?.name} (${user.email})`,
);
}
}
@@ -140,20 +138,6 @@ export default function ApplicationErrored() {
await recreateApplication();
}
useEffect(() => {
if (loading) {
return;
}
if (error) {
return;
}
const previousAcceptedState = getPreviousApplicationState(
data.app.appStates,
);
setPreviousState(previousAcceptedState);
}, [setPreviousState, data, loading, error]);
if (loading || previousState === null) {
return (
<Container className="mx-auto mt-12 max-w-sm text-center">
@@ -170,19 +154,13 @@ export default function ApplicationErrored() {
return null;
}
if (previousState === ApplicationStatus.Live) {
return <ApplicationLive />;
}
// For now, if the application errored and the previous state to this error is an UPDATING state, we want to show the dashboard,
// it's likely that most services are up and we shouldn't block all functionality. In the future, we're going to have a way to
// redeploy the app again, and get to a healthy state. @GC
if (previousState === ApplicationStatus.Updating) {
return <ApplicationLive />;
}
if (previousState === ApplicationStatus.Empty) {
return <ApplicationUnknown />;
if (
previousState === ApplicationStatus.Updating ||
previousState === ApplicationStatus.Empty
) {
return (
<ApplicationLive errorMessage="Error deploying the project most likely due to invalid configuration. Please review your project's configuration and logs for more information." />
);
}
return (
@@ -196,8 +174,8 @@ export default function ApplicationErrored() {
// which instead of deleting just an application, it deletes and recreates.
handler={recreateApplication}
close={() => setShowRecreateModal(false)}
title={`Recreate project ${currentApplication.name}?`}
description={`The project ${currentApplication.name} will be removed and then re-created. All data will be lost and there will be no way to
title={`Recreate project ${currentProject.name}?`}
description={`The project ${currentProject?.name} will be removed and then re-created. All data will be lost and there will be no way to
recover the app once it has been deleted.`}
/>
</Modal>
@@ -208,8 +186,8 @@ export default function ApplicationErrored() {
>
<RemoveApplicationModal
close={() => setShowDeleteModal(false)}
title={`Remove project ${currentApplication.name}?`}
description={`The project ${currentApplication.name} will be removed. All data will be lost and there will be no way to
title={`Remove project ${currentProject.name}?`}
description={`The project ${currentProject?.name} will be removed. All data will be lost and there will be no way to
recover the app once it has been deleted.`}
/>
</Modal>

View File

@@ -1,23 +1,24 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetOneUserDocument,
GetAllWorkspacesAndProjectsDocument,
useDeleteApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { formatDistance } from 'date-fns';
import { useRouter } from 'next/router';
import { toast } from 'react-hot-toast';
export default function ApplicationInfo() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [deleteApplication] = useDeleteApplicationMutation({
refetchQueries: [GetOneUserDocument],
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
});
const router = useRouter();
@@ -26,7 +27,7 @@ export default function ApplicationInfo() {
await toast.promise(
deleteApplication({
variables: {
appId: currentApplication.id,
appId: currentProject.id,
},
}),
{
@@ -36,6 +37,7 @@ export default function ApplicationInfo() {
'An error occurred while deleting the project. Please try again.',
),
},
getToastStyleProps(),
);
await router.push('/');
@@ -44,6 +46,10 @@ export default function ApplicationInfo() {
}
}
if (!currentProject) {
return null;
}
return (
<div className="mt-4 grid grid-flow-row gap-4">
<div className="grid grid-flow-row justify-center gap-0.5">
@@ -51,10 +57,10 @@ export default function ApplicationInfo() {
<Button
variant="borderless"
onClick={() => copy(currentApplication.id, 'Application ID')}
onClick={() => copy(currentProject.id, 'Application ID')}
className="py-1 text-xs"
>
{currentApplication.id}
{currentProject.id}
</Button>
</div>
@@ -65,27 +71,27 @@ export default function ApplicationInfo() {
variant="borderless"
onClick={() =>
copy(
currentApplication.desiredState.toString(),
currentProject.desiredState.toString(),
'Application Desired State',
)
}
className="py-1 text-xs"
>
{getApplicationStatusString(currentApplication.desiredState)}
{getApplicationStatusString(currentProject.desiredState)}
</Button>
</div>
<div className="grid grid-flow-row gap-0.5">
<Text variant="subtitle2">Region:</Text>
<Text variant="subtitle1">{currentApplication.region.city}</Text>
<Text variant="subtitle1">{currentProject.region.city}</Text>
</div>
<div className="grid grid-flow-row gap-0.5">
<Text variant="subtitle2">Created:</Text>
<Text variant="subtitle1">
{formatDistance(new Date(currentApplication.createdAt), new Date(), {
{formatDistance(new Date(currentProject.createdAt), new Date(), {
addSuffix: true,
})}
</Text>
@@ -93,7 +99,7 @@ export default function ApplicationInfo() {
<div className="grid grid-flow-row gap-2">
<Link
href={`https://staging.nhost.run/console/data/default/schema/public/tables/app_state_history/browse?filter=app_id%3B%24eq%3B${currentApplication.id}`}
href={`https://staging.nhost.run/console/data/default/schema/public/tables/app_state_history/browse?filter=app_id%3B%24eq%3B${currentProject.id}`}
target="_blank"
rel="noreferrer noopener"
className="grid grid-flow-col items-center justify-center gap-1 p-2"

View File

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

View File

@@ -1,206 +0,0 @@
import { useDialog } from '@/components/common/DialogProvider';
import Container from '@/components/layout/Container';
import ProjectStatusInfo from '@/components/project/ProjectStatusInfo';
import useProjectRedirectWhenReady from '@/hooks/common/useProjectRedirectWhenReady';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useInterval } from '@/hooks/useInterval';
import { ApplicationStatus } from '@/types/application';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import {
useInsertFeatureFlagMutation,
useUpdateApplicationMutation,
} from '@/utils/__generated__/graphql';
import { useUserEmail } from '@nhost/nextjs';
import { useEffect, useState } from 'react';
/**
* Number of minutes to wait before enabling the "Cancel Migration" button.
*/
const MIGRATION_CANCEL_TIMEOUT_MINUTES = 15;
function MigrationDialog() {
const { closeAlertDialog } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [countdownTimer, setCountdownTimer] = useState(-1);
const minutes = Math.floor(countdownTimer / 60);
const seconds = Math.floor(countdownTimer % 60);
const countdownActive = countdownTimer > 0;
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const rawTimestamp = localStorage.getItem(
`migration-${currentApplication?.id}`,
);
if (!rawTimestamp) {
return;
}
const timestamp = new Date(rawTimestamp);
const timeDifference =
timestamp.getTime() +
1000 * 60 * MIGRATION_CANCEL_TIMEOUT_MINUTES -
Date.now();
if (timeDifference < 0) {
setCountdownTimer(0);
return;
}
setCountdownTimer(timeDifference / 1000);
}, [currentApplication?.id]);
useInterval(
() =>
setCountdownTimer((prev) => {
if (prev === 0) {
return 0;
}
return prev - 1;
}),
1000,
);
useEffect(() => {
if (countdownTimer !== 0 || typeof window === 'undefined') {
return;
}
localStorage.removeItem(`migration-${currentApplication.id}`);
}, [countdownTimer, currentApplication.id]);
const [updateApplication] = useUpdateApplicationMutation({
refetchQueries: ['getOneUser'],
});
const [insertFeatureFlag] = useInsertFeatureFlagMutation();
const userEmail = useUserEmail();
async function handleCancelMigration() {
try {
await updateApplication({
variables: {
appId: currentApplication.id,
app: {
desiredState: ApplicationStatus.Live,
},
},
});
await insertFeatureFlag({
variables: {
flag: {
appId: currentApplication.id,
name: 'fleetcontrol_use_rds',
value: 'console',
description: 'Use RDS',
},
},
});
triggerToast(`${currentApplication.name} migration cancelled.`);
} catch (e) {
triggerToast(`Error trying to migrate ${currentApplication.name}`);
await discordAnnounce(
`Error trying to migrate app: ${currentApplication.subdomain} (${userEmail})`,
);
} finally {
closeAlertDialog();
}
}
return (
<div className="grid grid-flow-row gap-2 px-6">
<Text>
Cancelling this migration will revert your project to use the shared
Postgres instance.
</Text>
{!countdownActive && (
<Alert severity="warning" className="px-3 text-left">
Reach out to us at{' '}
<Link
underline="none"
target="_blank"
className="hover:underline focus:underline focus:outline-none"
href="https://discord.com/channels/552499021260914688/1029043079946182676"
>
#migratedb
</Link>{' '}
if you think the migration should have finished by now.
</Alert>
)}
<div className="grid grid-flow-row gap-2 pb-1">
<Button onClick={closeAlertDialog}>Continue Migration</Button>
<Button
onClick={handleCancelMigration}
variant="outlined"
color="secondary"
disabled={countdownActive}
>
{countdownActive
? `Cancel in ${String(minutes).padStart(2, '0')}:${String(
seconds,
).padStart(2, '0')}`
: 'Cancel Migration'}
</Button>
</div>
</div>
);
}
export default function ApplicationMigrating() {
const { openAlertDialog } = useDialog();
useProjectRedirectWhenReady({ pollInterval: 10000 });
return (
<Container className="flex flex-col gap-6">
<ProjectStatusInfo
className="mx-auto max-w-sm"
title="Migration in progress"
description="Your project is being migrated to use a dedicated and more performant Postgres instance."
imageProps={{
src: '/assets/migrating.svg',
alt: 'Application Migrating',
}}
/>
<Button
variant="borderless"
color="error"
className="mx-auto"
onClick={() =>
openAlertDialog({
title: 'Cancel Migration',
payload: <MigrationDialog />,
props: {
titleProps: {
className: 'px-6',
},
PaperProps: {
className: 'py-6 px-0 max-w-sm w-full',
},
hidePrimaryAction: true,
hideSecondaryAction: true,
},
})
}
>
Cancel Migration
</Button>
</Container>
);
}

View File

@@ -3,12 +3,13 @@ import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { StagingMetadata } from '@/components/applications/StagingMetadata';
import { useDialog } from '@/components/common/DialogProvider';
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import {
GetOneUserDocument,
GetAllWorkspacesAndProjectsDocument,
useGetFreeAndActiveProjectsQuery,
useUnpauseApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Modal } from '@/ui';
import { Alert } from '@/ui/Alert';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
@@ -25,22 +26,22 @@ import { toast } from 'react-hot-toast';
import { RemoveApplicationModal } from './RemoveApplicationModal';
export default function ApplicationPaused() {
const { openAlertDialog } = useDialog();
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { id } = useUserData();
const isOwner = currentWorkspace.members.some(
({ userId, type }) => userId === id && type === 'owner',
);
const { openDialog } = useDialog();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const isOwner = useIsCurrentUserOwner();
const user = useUserData();
const [showDeletingModal, setShowDeletingModal] = useState(false);
const [unpauseApplication, { loading: changingApplicationStateLoading }] =
useUnpauseApplicationMutation({
refetchQueries: [GetOneUserDocument],
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
});
const { data, loading } = useGetFreeAndActiveProjectsQuery({
variables: { userId: id },
variables: { userId: user?.id },
fetchPolicy: 'cache-and-network',
skip: !user,
});
const numberOfFreeAndLiveProjects = data?.freeAndActiveProjects.length || 0;
@@ -49,7 +50,7 @@ export default function ApplicationPaused() {
async function handleTriggerUnpausing() {
try {
await toast.promise(
unpauseApplication({ variables: { appId: currentApplication.id } }),
unpauseApplication({ variables: { appId: currentProject.id } }),
{
loading: 'Starting the project...',
success: `The project has been started successfully.`,
@@ -69,6 +70,8 @@ export default function ApplicationPaused() {
},
getToastStyleProps(),
);
await refetchWorkspaceAndProject();
} catch {
// Note: The toast will handle the error.
}
@@ -86,8 +89,8 @@ export default function ApplicationPaused() {
>
<RemoveApplicationModal
close={() => setShowDeletingModal(false)}
title={`Remove project ${currentApplication.name}?`}
description={`The project ${currentApplication.name} will be removed. All data will be lost and there will be no way to
title={`Remove project ${currentProject.name}?`}
description={`The project ${currentProject.name} will be removed. All data will be lost and there will be no way to
recover the app once it has been deleted.`}
/>
</Modal>
@@ -104,7 +107,7 @@ export default function ApplicationPaused() {
<Box className="grid grid-flow-row gap-1">
<Text variant="h3" component="h1">
{currentApplication.name} is sleeping
{currentProject.name} is sleeping
</Text>
<Text>
@@ -114,24 +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={() => {
openAlertDialog({
title: 'Upgrade your plan.',
payload: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
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
@@ -148,7 +149,7 @@ export default function ApplicationPaused() {
<Alert severity="warning" className="mx-auto max-w-xs text-left">
Note: Only one free project can be active at any given time.
Please pause your active free project before unpausing{' '}
{currentApplication.name}.
{currentProject.name}.
</Alert>
)}

View File

@@ -1,17 +1,17 @@
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useCheckProvisioning } from '@/hooks/useCheckProvisioning';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { ApplicationStatus } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Text from '@/ui/v2/Text';
import Image from 'next/image';
import ApplicationInfo from './ApplicationInfo';
import { AppLoader } from './AppLoader';
import ApplicationInfo from './ApplicationInfo';
import { StagingMetadata } from './StagingMetadata';
export default function ApplicationProvisioning() {
const currentApplicationState = useCheckProvisioning();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const currentProjectState = useCheckProvisioning();
const { currentProject } = useCurrentWorkspaceAndProject();
return (
<Container className="mx-auto mt-8 grid max-w-sm grid-flow-row gap-4 text-center">
@@ -24,16 +24,16 @@ export default function ApplicationProvisioning() {
/>
</div>
{currentApplicationState.state === ApplicationStatus.Empty ? (
{currentProjectState.state === ApplicationStatus.Empty ? (
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h1">
Setting Up {currentApplication.name}
Setting Up {currentProject?.name}
</Text>
<Text>This normally takes around 2 minutes</Text>
<ActivityIndicator className="mx-auto" />
</div>
) : (
<AppLoader startLoader date={currentApplicationState.createdAt} />
<AppLoader startLoader date={currentProjectState.createdAt} />
)}
<StagingMetadata>

View File

@@ -1,17 +1,17 @@
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useCheckProvisioning } from '@/hooks/useCheckProvisioning';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { ApplicationStatus } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Text from '@/ui/v2/Text';
import Image from 'next/image';
import ApplicationInfo from './ApplicationInfo';
import { AppLoader } from './AppLoader';
import ApplicationInfo from './ApplicationInfo';
import { StagingMetadata } from './StagingMetadata';
export default function ApplicationRestoring() {
const currentApplicationState = useCheckProvisioning();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const currentProjectState = useCheckProvisioning();
const { currentProject } = useCurrentWorkspaceAndProject();
return (
<Container className="mx-auto mt-8 grid max-w-sm grid-flow-row gap-4 text-center">
@@ -23,10 +23,10 @@ export default function ApplicationRestoring() {
height={72}
/>
</div>
{currentApplicationState.state === ApplicationStatus.Empty ? (
{currentProjectState.state === ApplicationStatus.Empty ? (
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h1">
Setting Up {currentApplication.name}
Setting Up {currentProject?.name}
</Text>
<Text>This normally takes around 2 minutes</Text>
@@ -34,11 +34,7 @@ export default function ApplicationRestoring() {
<ActivityIndicator className="mx-auto" />
</div>
) : (
<AppLoader
startLoader
restoring
date={currentApplicationState.createdAt}
/>
<AppLoader startLoader restoring date={currentProjectState.createdAt} />
)}
<StagingMetadata>
<ApplicationInfo />

View File

@@ -1,11 +1,11 @@
import FeedbackForm from '@/components/common/FeedbackForm';
import Container from '@/components/layout/Container';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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,13 +13,9 @@ import { RemoveApplicationModal } from './RemoveApplicationModal';
import { StagingMetadata } from './StagingMetadata';
export default function ApplicationUnknown() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [showDeleteModal, setShowDeleteModal] = useState(false);
const user = useUserData();
const isOwner = currentWorkspace.members.some(
({ userId, type }) => userId === user?.id && type === 'owner',
);
const isOwner = useIsCurrentUserOwner();
return (
<>
@@ -29,8 +25,8 @@ export default function ApplicationUnknown() {
>
<RemoveApplicationModal
close={() => setShowDeleteModal(false)}
title={`Remove project ${currentApplication.name}?`}
description={`The project ${currentApplication.name} will be removed. All data will be lost and there will be no way to
title={`Remove project ${currentProject.name}?`}
description={`The project ${currentProject.name} will be removed. All data will be lost and there will be no way to
recover the app once it has been deleted.`}
/>
</Modal>

View File

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

View File

@@ -1,34 +1,30 @@
import { BillingPaymentMethodForm } from '@/components/billing-payment-method/BillingPaymentMethodForm';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/context/UIContext';
import { BillingPaymentMethodForm } from '@/components/workspace/BillingPaymentMethodForm';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
refetchGetApplicationPlanQuery,
useGetAppPlanAndGlobalPlansQuery,
useGetPaymentMethodsQuery,
useUpdateAppMutation,
useUpdateApplicationMutation,
} from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Modal } from '@/ui/Modal';
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 { triggerToast } from '@/utils/toast';
import { useTheme } from '@mui/material';
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,67 +55,95 @@ function Plan({
</div>
<Text variant="h3" component="p">
$ {price}/mo
${price}/mo
</Text>
</button>
);
}
export function ChangePlanModalWithData({ app, plans, close }: any) {
const theme = useTheme();
const [selectedPlanId, setSelectedPlanId] = useState('');
const { closeAlertDialog } = useDialog();
const [pollingCurrentProject, setPollingCurrentProject] = useState(false);
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const {
currentWorkspace,
currentProject,
refetch: refetchWorkspaceAndProject,
} = useCurrentWorkspaceAndProject();
const { state } = useApplicationState();
// get workspace payment methods
const { data } = useGetPaymentMethodsQuery({
variables: {
workspaceId: currentWorkspace.id,
workspaceId: currentWorkspace?.id,
},
skip: !currentWorkspace,
});
const { openPaymentModal, closePaymentModal, paymentModal } = useUI();
const [showPaymentModal, setShowPaymentModal] = useState(false);
const paymentMethodAvailable = data?.paymentMethods.length > 0;
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;
}
// graphql mutations
const [updateApp] = useUpdateAppMutation({
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: [
refetchGetApplicationPlanQuery({
workspace: currentWorkspace.slug,
slug: currentApplication.slug,
slug: currentProject.slug,
}),
],
});
// function handlers
const handleUpdateAppPlan = async () => {
await updateApp({
variables: {
id: app.id,
app: {
planId: selectedPlan.id,
try {
await toast.promise(
updateApp({
variables: {
appId: app.id,
app: {
planId: selectedPlan.id,
desiredState: 5,
},
},
}),
{
loading: 'Updating plan...',
success: `Plan has been updated successfully to ${selectedPlan.name}.`,
error: getServerError(
'An error occurred while updating the plan. Please try again.',
),
},
},
});
getToastStyleProps(),
);
if (isDowngrade) {
if (close) {
close();
}
closeAlertDialog();
setPollingCurrentProject(true);
} catch (error) {
// Note: Error is handled by the toast.
}
triggerToast(
`${currentApplication.name} plan changed to ${selectedPlan.name}.`,
);
};
const handleChangePlanClick = async () => {
@@ -128,33 +152,114 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
}
if (!paymentMethodAvailable) {
openPaymentModal();
setShowPaymentModal(true);
return;
}
await handleUpdateAppPlan();
if (close) {
close();
}
closeAlertDialog();
};
if (pollingCurrentProject) {
return (
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
<div className="flex flex-col">
<div className="mx-auto">
<Image
src="/assets/upgrade.svg"
alt="Nhost Logo"
width={72}
height={72}
/>
</div>
<Text variant="h3" component="h2" className="mt-2 text-center">
Successfully upgraded to {currentPlan.name}
</Text>
<ActivityIndicator
label="We are unpausing your project. This may take some time..."
className="mx-auto mt-2"
/>
<Button
variant="outlined"
color="secondary"
className="mx-auto mt-4 w-full max-w-sm"
onClick={() => {
if (close) {
close();
}
closeAlertDialog();
}}
>
Cancel
</Button>
</div>
</Box>
);
}
if (app.plan.id !== plans.find((plan) => plan.isFree)?.id) {
return (
<Box className="mx-auto w-full max-w-xl rounded-lg p-6 text-left">
<div className="flex flex-col">
<div className="mx-auto">
<Image
src="/assets/upgrade.svg"
alt="Nhost Logo"
width={72}
height={72}
/>
</div>
<Text variant="h3" component="h2" className="mt-2 text-center">
Downgrade is not available
</Text>
<Text className="mt-1 text-center">
You can&apos;t downgrade from a paid plan to a free plan here.
</Text>
<Text className="text-center">
Please contact us at{' '}
<Link href="mailto:info@nhost.io">info@nhost.io</Link> if you want
to downgrade.
</Text>
<div className="mt-6 grid grid-flow-row gap-2">
<Button
variant="outlined"
color="secondary"
className="mx-auto w-full max-w-sm"
onClick={() => {
if (close) {
close();
}
closeAlertDialog();
}}
>
Cancel
</Button>
</div>
</div>
</Box>
);
}
return (
<Box className="w-full max-w-xl rounded-lg p-6 text-left">
<Modal
showModal={paymentModal}
close={closePaymentModal}
dialogStyle={{ zIndex: theme.zIndex.modal + 1 }}
<BaseDialog
open={showPaymentModal}
onClose={() => setShowPaymentModal(false)}
>
<BillingPaymentMethodForm
close={closePaymentModal}
onPaymentMethodAdded={handleUpdateAppPlan}
workspaceId={currentWorkspace.id}
/>
</Modal>
</BaseDialog>
<div className="flex flex-col">
<div className="mx-auto">
<Image
@@ -171,7 +276,7 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
You&apos;re currently on the <strong>{app.plan.name}</strong> plan.
</Text>
<div className="mt-5">
<div className="mt-2">
{plans
.filter((plan) => plan.id !== app.plan.id)
.map((plan) => (
@@ -189,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
@@ -217,14 +324,12 @@ export function ChangePlanModalWithData({ app, plans, close }: any) {
export interface ChangePlanModalProps {
/**
* Function to close the modal if mounted on parent component.
*
* @deprecated Implement modal by using `openAlertDialog` hook instead.
* Function to close the modal.
*/
close?: () => void;
onCancel?: () => void;
}
export function ChangePlanModal({ close }: ChangePlanModalProps) {
export function ChangePlanModal({ onCancel }: ChangePlanModalProps) {
const {
query: { workspaceSlug, appSlug },
} = useRouter();
@@ -250,5 +355,5 @@ export function ChangePlanModal({ close }: ChangePlanModalProps) {
const { apps, plans } = data;
const app = apps[0];
return <ChangePlanModalWithData app={app} plans={plans} close={close} />;
return <ChangePlanModalWithData app={app} plans={plans} close={onCancel} />;
}

View File

@@ -1,12 +1,12 @@
import { LoadingScreen } from '@/components/common/LoadingScreen';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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,
@@ -20,11 +20,11 @@ interface HasuraDataProps {
}
export function HasuraData({ close }: HasuraDataProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const projectAdminSecret = currentApplication?.config?.hasura.adminSecret;
const projectAdminSecret = currentProject?.config?.hasura.adminSecret;
if (!currentApplication?.subdomain || !projectAdminSecret) {
if (!currentProject?.subdomain || !projectAdminSecret) {
return <LoadingScreen />;
}
@@ -32,8 +32,8 @@ export function HasuraData({ close }: HasuraDataProps) {
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
? `${getHasuraConsoleServiceUrl()}`
: generateAppServiceUrl(
currentApplication?.subdomain,
currentApplication?.region.awsName,
currentProject?.subdomain,
currentProject?.region,
'hasura',
defaultLocalBackendSlugs,
{ ...defaultRemoteBackendSlugs, hasura: '/console' },

View File

@@ -1,15 +1,15 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import Divider from '@/ui/v2/Divider';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import {
GetOneUserDocument,
GetAllWorkspacesAndProjectsDocument,
useDeleteApplicationMutation,
} from '@/utils/__generated__/graphql';
import { discordAnnounce } from '@/utils/discordAnnounce';
import { triggerToast } from '@/utils/toast';
import router from 'next/router';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
@@ -46,14 +46,14 @@ export function RemoveApplicationModal({
className,
}: RemoveApplicationModalProps) {
const [deleteApplication] = useDeleteApplicationMutation({
refetchQueries: [GetOneUserDocument],
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
});
const [loadingRemove, setLoadingRemove] = useState(false);
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [remove, setRemove] = useState(false);
const [remove2, setRemove2] = useState(false);
const appName = currentApplication?.name;
const appName = currentProject?.name;
async function handleClick() {
setLoadingRemove(true);
@@ -70,7 +70,7 @@ export function RemoveApplicationModal({
try {
await deleteApplication({
variables: {
appId: currentApplication.id,
appId: currentProject.id,
},
});
} catch (error) {
@@ -78,7 +78,7 @@ export function RemoveApplicationModal({
}
close();
await router.push('/');
triggerToast(`${currentApplication.name} deleted`);
triggerToast(`${currentProject.name} deleted`);
}
return (

View File

@@ -1,165 +0,0 @@
import DeploymentStatusMessage from '@/components/deployments/DeploymentStatusMessage';
import { FindOldApps } from '@/components/home';
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
import type { ApplicationState } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import StateBadge from '@/ui/StateBadge';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import Divider from '@/ui/v2/Divider';
import Link from '@/ui/v2/Link';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import { getApplicationStatusString } from '@/utils/helpers';
import Image from 'next/image';
import NavLink from 'next/link';
import { Fragment } from 'react';
export function checkStatusOfTheApplication(
stateHistory: ApplicationState[] | [],
) {
if (stateHistory.length === 0) {
return ApplicationStatus.Empty;
}
if (stateHistory[0].stateId === undefined) {
return ApplicationStatus.Empty;
}
return stateHistory[0].stateId;
}
export function RenderWorkspacesWithApps({
userData,
query,
}: {
userData: UserData | null;
query: string;
}) {
return (
<div>
{userData?.workspaces
.filter((workspace) =>
workspace.applications.map((app) =>
app.name.toLowerCase().includes(query.toLowerCase()),
),
)
.sort((w1, w2) =>
// sort alphabetical order (A-Z)
w1.name.localeCompare(w2.name),
)
.map((workspace) => {
// early exit if no applications are available
if (workspace.applications.length === 0) {
return null;
}
const workspaceProjects = workspace.applications
.filter((app) =>
app.name.toLowerCase().includes(query.toLowerCase()),
)
.sort((appA, appB) => {
// sort apps based on either:
// 1. When the app was recently deployed, if there is any deployments available
// 2. When the app was created
const appASort =
appA.deployments.length > 0
? new Date(appA.deployments[0].deploymentEndedAt)
: new Date(appA.createdAt);
const appBSort =
appB.deployments.length > 0
? new Date(appB.deployments[0].deploymentEndedAt)
: new Date(appB.createdAt);
if (appASort > appBSort) {
return -1;
}
return 1;
});
return (
<div key={workspace.slug} className="my-8">
<NavLink href={`/${workspace.slug}`} passHref>
<Link
href={`${workspace.slug}`}
className="mb-1.5 block font-medium"
underline="none"
sx={{ color: 'text.primary' }}
>
{workspace.name}
</Link>
</NavLink>
<List className="grid grid-flow-row border-y">
{workspaceProjects.map((app, index) => {
const [latestDeployment] = app.deployments;
return (
<Fragment key={app.slug}>
<ListItem.Root
secondaryAction={
<div className="grid grid-flow-col gap-px">
{latestDeployment && (
<div className="mr-2 flex self-center align-middle">
<StatusCircle
status={
latestDeployment.deploymentStatus as DeploymentStatus
}
/>
</div>
)}
<StateBadge
state={checkStatusOfTheApplication(app.appStates)}
desiredState={app.desiredState}
title={getApplicationStatusString(
checkStatusOfTheApplication(app.appStates),
)}
/>
</div>
}
>
<NavLink
href={`${workspace?.slug}/${app.slug}`}
passHref
>
<ListItem.Button className="rounded-none">
<ListItem.Avatar>
<div className="h-10 w-10 overflow-hidden rounded-lg">
<Image
src="/logos/new.svg"
alt="Nhost Logo"
width={40}
height={40}
/>
</div>
</ListItem.Avatar>
<ListItem.Text
primary={app.name}
secondary={
<DeploymentStatusMessage
appCreatedAt={app.createdAt}
deployment={latestDeployment}
/>
}
/>
</ListItem.Button>
</NavLink>
</ListItem.Root>
{index < workspaceProjects.length - 1 && (
<Divider component="li" />
)}
</Fragment>
);
})}
</List>
</div>
);
})}
<FindOldApps />
</div>
);
}

View File

@@ -1,10 +1,10 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import Text from '@/ui/v2/Text';
import { triggerToast } from '@/utils/toast';
import { useRestoreApplicationDatabaseMutation } from '@/utils/__generated__/graphql';
import { triggerToast } from '@/utils/toast';
import { formatISO9075 } from 'date-fns';
import { useState } from 'react';
@@ -28,7 +28,7 @@ export function RestoreBackupModal({
const [isSure, setIsSure] = useState(false);
const [mutationIsCompleted, setMutationIsCompleted] = useState(false);
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [restoreApplicationDatabase, { loading }] =
useRestoreApplicationDatabaseMutation();
@@ -39,7 +39,7 @@ export function RestoreBackupModal({
await restoreApplicationDatabase({
variables: {
backupId,
appId: currentApplication.id,
appId: currentProject.id,
},
});
} catch (error) {
@@ -53,9 +53,9 @@ export function RestoreBackupModal({
if (mutationIsCompleted) {
return (
<Box className="w-modal p-6 rounded-lg">
<Box className="w-modal rounded-lg p-6">
<div className="flex flex-col">
<Text className="text-center font-medium text-lg">
<Text className="text-center text-lg font-medium">
The backup has been restored successfully.
</Text>
@@ -68,7 +68,7 @@ export function RestoreBackupModal({
}
return (
<Box className="w-modal px-6 py-6 text-left rounded-lg">
<Box className="w-modal rounded-lg px-6 py-6 text-left">
<div className="flex flex-col">
<Text className="text-center text-lg font-medium">
Restore Database Backup

View File

@@ -1,5 +1,6 @@
import { ChangePlanModal } from '@/components/applications/ChangePlanModal';
import { useDialog } from '@/components/common/DialogProvider';
import { useIsCurrentUserOwner } from '@/features/projects/common/hooks/useIsCurrentUserOwner';
import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
@@ -19,30 +20,37 @@ export function UnlockFeatureByUpgrading({
className,
...props
}: UnlockFeatureByUpgradingProps) {
const { openAlertDialog } = useDialog();
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={() => {
openAlertDialog({
title: 'Upgrade your plan.',
payload: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
hidePrimaryAction: true,
hideSecondaryAction: true,
hideTitle: true,
},
});
}}
>
Upgrade
</Button>
{!isOwner && (
<Text component="span" color="secondary" className="text-sm">
Ask an owner of this workspace to upgrade the project.
</Text>
)}
</Text>
{isOwner && (
<Button
variant="borderless"
onClick={() => {
openDialog({
component: <ChangePlanModal />,
props: {
PaperProps: { className: 'p-0 max-w-xl w-full' },
},
});
}}
>
Upgrade
</Button>
)}
</Alert>
</div>
);

View File

@@ -1,5 +1,5 @@
import type { ConnectGithubModalState } from '@/components/applications/ConnectGithubModal';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { FormProvider, useForm } from 'react-hook-form';
import { EditRepositorySettingsModal } from './EditRepositorySettingsModal';
@@ -21,13 +21,13 @@ export function EditRepositorySettings({
selectedRepoId,
handleSelectAnotherRepository,
}: EditRepositorySettingsProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const form = useForm<EditRepositorySettingsFormData>({
reValidateMode: 'onSubmit',
defaultValues: {
productionBranch: currentApplication.repositoryProductionBranch || 'main',
repoBaseFolder: currentApplication.nhostBaseFolder,
productionBranch: currentProject?.repositoryProductionBranch || 'main',
repoBaseFolder: currentProject?.nhostBaseFolder,
},
});

View File

@@ -2,8 +2,8 @@ import type { EditRepositorySettingsFormData } from '@/components/applications/g
import { useDialog } from '@/components/common/DialogProvider';
import ErrorBoundaryFallback from '@/components/common/ErrorBoundaryFallback';
import GithubIcon from '@/components/icons/GithubIcon';
import { useUpdateAppMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useUpdateApplicationMutation } from '@/generated/graphql';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
import { discordAnnounce } from '@/utils/discordAnnounce';
@@ -27,9 +27,9 @@ export function EditRepositorySettingsModal({
const isNotCompleted = !watch('productionBranch') || !watch('repoBaseFolder');
const { closeAlertDialog } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateApp, { loading }] = useUpdateAppMutation();
const [updateApp, { loading }] = useUpdateApplicationMutation();
const client = useApolloClient();
@@ -37,10 +37,10 @@ export function EditRepositorySettingsModal({
data: EditRepositorySettingsFormData,
) => {
try {
if (!currentApplication.githubRepository || selectedRepoId) {
if (!currentProject.githubRepository || selectedRepoId) {
await updateApp({
variables: {
id: currentApplication.id,
appId: currentProject.id,
app: {
githubRepositoryId: selectedRepoId,
repositoryProductionBranch: data.productionBranch,
@@ -51,7 +51,7 @@ export function EditRepositorySettingsModal({
} else {
await updateApp({
variables: {
id: currentApplication.id,
appId: currentProject.id,
app: {
repositoryProductionBranch: data.productionBranch,
nhostBaseFolder: data.repoBaseFolder,
@@ -69,7 +69,7 @@ export function EditRepositorySettingsModal({
triggerToast('GitHub repository settings successfully updated.');
} catch (error) {
await discordAnnounce(
`Error while trying to edit repository GitHub integration: ${currentApplication.slug}.`,
`Error while trying to edit repository GitHub integration: ${currentProject.slug}.`,
);
throw error;
}

View File

@@ -1,4 +1,4 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Option from '@/ui/v2/Option';
@@ -19,12 +19,12 @@ export interface UserSelectProps {
}
export function UserSelect({ onUserChange, ...props }: UserSelectProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const userApplicationClient = useRemoteApplicationGQLClient();
const { data, loading, error } = useRemoteAppGetUsersCustomQuery({
client: userApplicationClient,
variables: { where: {}, limit: 250, offset: 0 },
skip: !currentApplication,
skip: !currentProject,
});
if (loading) {

View File

@@ -1,6 +1,6 @@
import NavLink from '@/components/common/NavLink';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import Text from '@/ui/v2/Text';
@@ -10,8 +10,7 @@ export interface BreadcrumbsProps extends BoxProps {}
export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
const isPlatform = useIsPlatform();
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
if (!isPlatform) {
return (
@@ -61,16 +60,16 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
</>
)}
{currentApplication && (
{currentProject && (
<>
<Text color="disabled">/</Text>
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}`}
href={`/${currentWorkspace.slug}/${currentProject.slug}`}
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
{currentApplication.name}
{currentProject.name}
</NavLink>
</>
)}

View File

@@ -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';

View File

@@ -2,8 +2,8 @@ import AudioPreview from '@/components/icons/AudioPreview';
import { FileIcon } from '@/components/icons/FileIcon';
import PDFPreview from '@/components/icons/PDFPreview';
import VideoPreview from '@/components/icons/VideoPreview';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useAppClient } from '@/hooks/useAppClient';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Modal } from '@/ui/Modal';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
@@ -166,7 +166,7 @@ export default function DataGridPreviewCell<TData extends object>({
value: { fetchBlob, id, mimeType, alt, blob },
fallbackPreview = null,
}: DataGridPreviewCellProps<TData>) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const appClient = useAppClient();
const { objectUrl, loading, error } = useBlob({ fetchBlob, blob, mimeType });
const [showModal, setShowModal] = useState(false);
@@ -205,7 +205,7 @@ export default function DataGridPreviewCell<TData extends object>({
}
const { presignedUrl } = await appClient.storage
.setAdminSecret(currentApplication.config?.hasura.adminSecret)
.setAdminSecret(currentProject?.config?.hasura.adminSecret)
.getPresignedUrl({ fileId: id });
if (!presignedUrl) {

View File

@@ -22,7 +22,7 @@ export interface OpenDialogOptions {
/**
* Title of the dialog.
*/
title: ReactNode;
title?: ReactNode;
/**
* Component to render inside the dialog skeleton.
*/

View File

@@ -1,3 +1,4 @@
export * from './DialogContext';
export { default as DialogContext } from './DialogContext';
export { default as DialogProvider } from './DialogProvider';
export { default as useDialog } from './useDialog';

View File

@@ -3,7 +3,6 @@ import FeedbackForm from '@/components/common/FeedbackForm';
import NavLink from '@/components/common/NavLink';
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
import { Nav } from '@/components/dashboard/Nav';
import { useUserDataContext } from '@/context/workspace1-context';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import useProjectRoutes from '@/hooks/common/useProjectRoutes';
import { useNavigationVisible } from '@/hooks/useNavigationVisible';
@@ -19,6 +18,7 @@ import List from '@/ui/v2/List';
import type { ListItemButtonProps } from '@/ui/v2/ListItem';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import { useApolloClient } from '@apollo/client';
import { useSignOut } from '@nhost/nextjs';
import getConfig from 'next/config';
import { useRouter } from 'next/router';
@@ -88,7 +88,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
const [menuOpen, setMenuOpen] = useState(false);
const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
const { signOut } = useSignOut();
const { setUserContext } = useUserDataContext();
const apolloClient = useApolloClient();
const router = useRouter();
const { publicRuntimeConfig } = getConfig();
@@ -236,7 +236,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
setShowChangePasswordModal(true);
}}
>
Change password
Change Password
</ListItem.Button>
</ListItem.Root>
@@ -248,13 +248,13 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
color="secondary"
className="justify-start border-none px-2 py-2.5 text-[16px]"
onClick={async () => {
setUserContext({ workspaces: [] });
setMenuOpen(false);
await apolloClient.clearStore();
await signOut();
await router.push('/signin');
}}
>
Sign out
Sign Out
</ListItem.Button>
</ListItem.Root>
</List>

View File

@@ -7,9 +7,8 @@ import Button from '@/ui/v2/Button';
import { Dropdown, useDropdown } from '@/ui/v2/Dropdown';
import PowerIcon from '@/ui/v2/icons/PowerIcon';
import Text from '@/ui/v2/Text';
import { nhost } from '@/utils/nhost';
import { useApolloClient } from '@apollo/client';
import { useUserData } from '@nhost/nextjs';
import { useSignOut, useUserData } from '@nhost/nextjs';
import getConfig from 'next/config';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -22,8 +21,9 @@ function AccountMenuContent({
onChangePasswordClick,
}: AccountMenuContentProps) {
const user = useUserData();
const { signOut } = useSignOut();
const router = useRouter();
const client = useApolloClient();
const apolloClient = useApolloClient();
const { handleClose } = useDropdown();
const { publicRuntimeConfig } = getConfig();
@@ -37,12 +37,10 @@ function AccountMenuContent({
/>
<Text variant="h3" component="h2" className="text-center">
{nhost.auth.getUser()?.displayName}
{user?.displayName}
</Text>
<Text className="text-center font-medium">
{nhost.auth.getUser()?.email}
</Text>
<Text className="text-center font-medium">{user?.email}</Text>
</div>
<div className="grid grid-flow-row gap-2">
@@ -57,17 +55,13 @@ function AccountMenuContent({
Change Password
</Button>
<Button color="error" disabled>
Remove Account
</Button>
<Button
variant="outlined"
color="secondary"
onClick={async () => {
await nhost.auth.signOut();
router.push('/signin');
await client.resetStore();
await apolloClient.clearStore();
await signOut();
await router.push('/signin');
}}
endIcon={<PowerIcon className="mr-1 h-4 w-4" />}
>

View File

@@ -1,13 +0,0 @@
import type { ReactNode } from 'react';
interface ContainerIndexApplicationsProps {
children?: ReactNode | ReactNode[];
}
export function ContainerIndexApplications({
children,
}: ContainerIndexApplicationsProps) {
return <div className="flex flex-col font-display md:w-app">{children}</div>;
}
export default ContainerIndexApplications;

View File

@@ -22,6 +22,13 @@ export function CountrySelector({ value, onChange }: CountrySelectorProps) {
value={value || null}
onChange={(_event, inputValue) => onChange(inputValue as string)}
placeholder="Select Country"
slotProps={{
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
},
}}
>
{countries?.map((country) => (
<Option key={country.name} value={country.code}>

View File

@@ -1,55 +0,0 @@
import { useWorkspaceContext } from '@/context/workspace-context';
import { useUserDataContext } from '@/context/workspace1-context';
import Button from '@/ui/v2/Button';
import Text from '@/ui/v2/Text';
import { darken } from '@mui/system';
import Link from 'next/link';
export function NoApplications() {
const { userContext } = useUserDataContext();
const { workspaceContext } = useWorkspaceContext();
return (
<div className="noapps mt-4 h-80 rounded-md text-center font-display font-normal">
<div className="pt-12">
<Text
className="text-center text-2xl font-semibold"
sx={{ color: 'common.white' }}
>
Welcome to Nhost!
</Text>
<Text className="mt-2" sx={{ color: 'common.white' }}>
Let&apos;s set up your first backend - the Nhost way.
</Text>
<div className="inline-block pt-10">
<Link href="/new" passHref>
<Button
sx={{
backgroundColor: (theme) =>
`${theme.palette.common.white} !important`,
color: (theme) => `${theme.palette.common.black} !important`,
'&:hover': {
backgroundColor: (theme) =>
`${darken(theme.palette.common.white, 0.1)} !important`,
},
}}
disabled={
!workspaceContext.id && userContext.workspaces.length === 0
}
>
Create Your First Project
</Button>
</Link>
</div>
<div>
<Text className="mt-9 opacity-40" sx={{ color: 'common.white' }}>
Looking for your old projects? They&apos;re still on
console.nhost.io during this beta.
</Text>
</div>
</div>
</div>
);
}
export default NoApplications;

View File

@@ -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';

View File

@@ -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';

View File

@@ -9,11 +9,11 @@ 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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import useTablePath from '@/hooks/useTablePath';
import type {
DataBrowserGridColumn,
@@ -163,8 +163,8 @@ export default function DataBrowserGrid({
const isSchemaEditable = !isSchemaLocked(schemaSlug as string);
const { openDrawer, openAlertDialog } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const isGitHubConnected = !!currentApplication?.githubRepository;
const { currentProject } = useCurrentWorkspaceAndProject();
const isGitHubConnected = !!currentProject?.githubRepository;
const limit = 25;
const [currentOffset, setCurrentOffset] = useState<number | null>(

View File

@@ -1,8 +1,8 @@
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import useDataGridConfig from '@/hooks/useDataGridConfig';
import type { DataBrowserGridColumn } from '@/types/dataBrowser';
import type { BoxProps } from '@/ui/v2/Box';
@@ -50,8 +50,8 @@ export default function DataBrowserGridControls({
onInsertColumnClick,
...props
}: DataBrowserGridControlsProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const isGitHubConnected = !!currentApplication?.githubRepository;
const { currentProject } = useCurrentWorkspaceAndProject();
const isGitHubConnected = !!currentProject?.githubRepository;
const queryClient = useQueryClient();
const { openAlertDialog } = useDialog();

View File

@@ -3,10 +3,10 @@ import FormActivityIndicator from '@/components/common/FormActivityIndicator';
import InlineCode from '@/components/common/InlineCode';
import NavLink from '@/components/common/NavLink';
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
import useDeleteTableWithToastMutation from '@/hooks/dataBrowser/useDeleteTableMutation/useDeleteTableWithToastMutation';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
@@ -74,8 +74,8 @@ function DataBrowserSidebarContent({
}: Pick<DataBrowserSidebarProps, 'onSidebarItemClick'>) {
const queryClient = useQueryClient();
const { openDrawer, openAlertDialog } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const isGitHubConnected = !!currentApplication?.githubRepository;
const { currentProject } = useCurrentWorkspaceAndProject();
const isGitHubConnected = !!currentProject?.githubRepository;
const router = useRouter();
const {
@@ -516,7 +516,7 @@ export default function DataBrowserSidebar({
...props
}: DataBrowserSidebarProps) {
const isPlatform = useIsPlatform();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [expanded, setExpanded] = useState(false);
@@ -547,7 +547,7 @@ export default function DataBrowserSidebar({
document.removeEventListener('keydown', closeSidebarWhenEscapeIsPressed);
}, []);
if (isPlatform && !currentApplication?.config?.hasura.adminSecret) {
if (isPlatform && !currentProject?.config?.hasura.adminSecret) {
return null;
}

View File

@@ -1,7 +1,7 @@
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common';
import type {
@@ -13,9 +13,6 @@ import { Alert } from '@/ui/Alert';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import FullPermissionIcon from '@/ui/v2/icons/FullPermissionIcon';
import NoPermissionIcon from '@/ui/v2/icons/NoPermissionIcon';
import PartialPermissionIcon from '@/ui/v2/icons/PartialPermissionIcon';
import Link from '@/ui/v2/Link';
import Table from '@/ui/v2/Table';
import TableBody from '@/ui/v2/TableBody';
@@ -24,6 +21,9 @@ import TableContainer from '@/ui/v2/TableContainer';
import TableHead from '@/ui/v2/TableHead';
import TableRow from '@/ui/v2/TableRow';
import Text from '@/ui/v2/Text';
import FullPermissionIcon from '@/ui/v2/icons/FullPermissionIcon';
import NoPermissionIcon from '@/ui/v2/icons/NoPermissionIcon';
import PartialPermissionIcon from '@/ui/v2/icons/PartialPermissionIcon';
import { useGetRemoteAppRolesQuery } from '@/utils/__generated__/graphql';
import NavLink from 'next/link';
import { useState } from 'react';
@@ -61,8 +61,7 @@ export default function EditPermissionsForm({
const [action, setAction] = useState<DatabaseAction>();
const { closeDrawerWithDirtyGuard } = useDialog();
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const client = useRemoteApplicationGQLClient();
const {
@@ -330,7 +329,7 @@ export default function EditPermissionsForm({
<Alert className="text-left">
Please go to the{' '}
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/roles-and-permissions`}
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/roles-and-permissions`}
passHref
>
<Link

View File

@@ -1,18 +1,18 @@
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Autocomplete from '@/ui/v2/Autocomplete';
import Button from '@/ui/v2/Button';
import IconButton from '@/ui/v2/IconButton';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import XIcon from '@/ui/v2/icons/XIcon';
import InputLabel from '@/ui/v2/InputLabel';
import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import XIcon from '@/ui/v2/icons/XIcon';
import { useGetRolesPermissionsQuery } from '@/utils/__generated__/graphql';
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
import { useTheme } from '@mui/material';
import clsx from 'clsx';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
@@ -50,10 +50,10 @@ export default function ColumnPresetsSection({
error: tableError,
} = useTableQuery([`default.${schema}.${table}`], { schema, table });
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data: permissionVariablesData } = useGetRolesPermissionsQuery({
variables: { appId: currentApplication?.id },
skip: !currentApplication?.id,
variables: { appId: currentProject?.id },
skip: !currentProject?.id,
});
const {
setValue,

View File

@@ -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';

View File

@@ -1,12 +1,12 @@
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
import PlusIcon from '@/ui/v2/icons/PlusIcon';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { useMemo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
@@ -69,7 +69,7 @@ export default function RuleGroupEditor({
sx,
...props
}: RuleGroupEditorProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const form = useFormContext();
const { control, getValues } = form;
@@ -127,7 +127,7 @@ export default function RuleGroupEditor({
depth > 6 && { backgroundColor: 'secondary.800' },
]}
>
<div className="grid grid-flow-row gap-4 lg:gap-2 py-4">
<div className="grid grid-flow-row gap-4 py-4 lg:gap-2">
{(rules as (Rule & { id: string })[]).map((rule, ruleIndex) => (
<div className="grid grid-cols-[70px_1fr] gap-2" key={rule.id}>
<div>
@@ -188,13 +188,13 @@ export default function RuleGroupEditor({
<Text>
This rule group contains one or more objects (e.g: _exists) that
are not supported by our dashboard yet.{' '}
{currentApplication && (
{currentProject && (
<span>
Please{' '}
<Link
href={`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region?.awsName,
currentProject.subdomain,
currentProject.region,
'hasura',
)}/console/data/default/schema/${schema}/tables/${table}/permissions`}
underline="hover"
@@ -212,8 +212,8 @@ export default function RuleGroupEditor({
</div>
{!disabled && (
<div className="grid grid-flow-row lg:grid-flow-col lg:justify-between gap-2 pb-2">
<div className="grid grid-flow-row lg:grid-flow-col gap-2 lg:justify-start">
<div className="grid grid-flow-row gap-2 pb-2 lg:grid-flow-col lg:justify-between">
<div className="grid grid-flow-row gap-2 lg:grid-flow-col lg:justify-start">
<Button
startIcon={<PlusIcon />}
variant="borderless"

View File

@@ -3,15 +3,15 @@ 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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import type { InputProps } from '@/ui/v2/Input';
import { inputClasses } from '@/ui/v2/Input';
import Option from '@/ui/v2/Option';
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
import { useGetRolesPermissionsQuery } from '@/utils/__generated__/graphql';
import getAllPermissionVariables from '@/utils/settings/getAllPermissionVariables';
import { useController, useFormContext, useWatch } from 'react-hook-form';
import useRuleGroupEditor from './useRuleGroupEditor';
@@ -96,7 +96,7 @@ export default function RuleValueInput({
helperText,
}: RuleValueInputProps) {
const { schema, table, disabled } = useRuleGroupEditor();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const { setValue } = useFormContext();
const inputName = `${name}.value`;
const operator: HasuraOperator = useWatch({ name: `${name}.operator` });
@@ -118,8 +118,8 @@ export default function RuleValueInput({
loading,
error: customClaimsError,
} = useGetRolesPermissionsQuery({
variables: { appId: currentApplication?.id },
skip: !isHasuraInput || !currentApplication?.id,
variables: { appId: currentProject?.id },
skip: !isHasuraInput || !currentProject?.id,
});
if (operator === '_is_null') {

View File

@@ -1,19 +1,19 @@
import NavLink from '@/components/common/NavLink';
import AppDeploymentDuration from '@/components/deployments/AppDeploymentDuration';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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 ArrowCounterclockwiseIcon from '@/ui/v2/icons/ArrowCounterclockwiseIcon';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import { ListItem } from '@/ui/v2/ListItem';
import Tooltip from '@/ui/v2/Tooltip';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
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 getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
@@ -44,8 +44,7 @@ export default function DeploymentListItem({
showRedeploy,
disableRedeploy,
}: DeploymentListItemProps) {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const relativeDateOfDeployment = deployment.deploymentStartedAt
? formatDistanceToNowStrict(parseISO(deployment.deploymentStartedAt), {
@@ -61,7 +60,7 @@ export default function DeploymentListItem({
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 rounded-none p-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
href={`/${currentWorkspace.slug}/${currentProject.slug}/deployments/${deployment.id}`}
aria-label={commitMessage || 'No commit message'}
>
<div className="grid grid-flow-col items-center justify-center gap-2 self-center">
@@ -108,7 +107,7 @@ export default function DeploymentListItem({
const insertDeploymentPromise = insertDeployment({
variables: {
object: {
appId: currentApplication?.id,
appId: currentProject?.id,
commitMessage: deployment.commitMessage,
commitSHA: deployment.commitSHA,
commitUserAvatarUrl: deployment.commitUserAvatarUrl,

View File

@@ -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';

View File

@@ -7,15 +7,15 @@ 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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import useFiles from '@/hooks/useFiles';
import useFilesAggregate from '@/hooks/useFilesAggregate';
import { getHasuraAdminSecret } from '@/utils/env';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import type { Files } from '@/utils/__generated__/graphql';
import { Order_By as OrderBy } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env';
import { showLoadingToast, triggerToast } from '@/utils/toast';
import debounce from 'lodash.debounce';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
@@ -31,7 +31,7 @@ export type FilesDataGridProps = Partial<DataGridProps<StoredFile>>;
export default function FilesDataGrid(props: FilesDataGridProps) {
const router = useRouter();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const appClient = useAppClient();
const [searchString, setSearchString] = useState<string | null>(null);
const [currentOffset, setCurrentOffset] = useState<number | null>(
@@ -263,7 +263,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
.setAdminSecret(
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentApplication.config?.hasura.adminSecret,
: currentProject.config?.hasura.adminSecret,
)
.upload({
file,

View File

@@ -1,8 +1,8 @@
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import useDataGridConfig from '@/hooks/useDataGridConfig';
import type { FileUploadButtonProps } from '@/ui/FileUploadButton';
import FileUploadButton from '@/ui/FileUploadButton';
@@ -12,9 +12,9 @@ import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import type { InputProps } from '@/ui/v2/Input';
import Input from '@/ui/v2/Input';
import type { Files } from '@/utils/__generated__/graphql';
import { getHasuraAdminSecret } from '@/utils/env';
import { triggerToast } from '@/utils/toast';
import type { Files } from '@/utils/__generated__/graphql';
import type { PropsWithoutRef } from 'react';
import { useState } from 'react';
import type { Row } from 'react-table';
@@ -38,7 +38,7 @@ export default function FilesDataGridControls({
...props
}: FilesDataGridControlsProps) {
const { openAlertDialog } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const appClient = useAppClient();
const [deleteLoading, setDeleteLoading] = useState(false);
@@ -73,7 +73,7 @@ export default function FilesDataGridControls({
const storageWithAdminSecret = appClient.storage.setAdminSecret(
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: currentApplication.config?.hasura.adminSecret,
: currentProject.config?.hasura.adminSecret,
);
// note: this is not an optimal solution, but we don't have a better way

View File

@@ -1,14 +0,0 @@
import { RenderWorkspacesWithApps } from '@/components/applications/RenderWorkspacesWithApps';
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
export function AllWorkspacesApplications({
userData,
query,
}: {
userData: UserData | null;
query: string;
}) {
return <RenderWorkspacesWithApps query={query} userData={userData} />;
}
export default AllWorkspacesApplications;

View File

@@ -1,47 +0,0 @@
import { LoadingScreen } from '@/components/common/LoadingScreen';
import { ContainerIndexApplications } from '@/components/dashboard/ContainerIndexApplications';
import { NoApplications } from '@/components/dashboard/NoApplications';
import { AllWorkspacesApplications } from '@/components/home/AllWorkspaceApplications';
import { IndexHeaderApps } from '@/components/home/IndexHeaderApps';
import { useUserDataContext } from '@/context/workspace1-context';
import { useCheckApplications } from '@/hooks/useCheckApplications';
import type { UserData } from '@/hooks/useGetAllUserWorkspacesAndApplications';
import { useEffect, useState } from 'react';
export function Applications() {
const [filtered, setFiltered] = useState<UserData | null>(null);
const [query, setQuery] = useState('');
const { userContext } = useUserDataContext();
useEffect(() => {
setFiltered(userContext);
}, [userContext]);
const { loading, error, noApplications } = useCheckApplications();
if (loading) {
return <LoadingScreen />;
}
if (error) {
throw error;
}
if (noApplications) {
return (
<ContainerIndexApplications>
<NoApplications />
</ContainerIndexApplications>
);
}
return (
<ContainerIndexApplications>
<IndexHeaderApps query={query} setQuery={setQuery} />
<AllWorkspacesApplications query={query} userData={filtered} />
</ContainerIndexApplications>
);
}
export default Applications;

View File

@@ -3,14 +3,14 @@ import Form from '@/components/common/Form';
import type { DialogFormProps } from '@/types/common';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import { slugifyString } from '@/utils/helpers';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import {
refetchGetOneUserQuery,
GetAllWorkspacesAndProjectsDocument,
useInsertWorkspaceMutation,
useUpdateWorkspaceMutation,
} from '@/utils/__generated__/graphql';
import { slugifyString } from '@/utils/helpers';
import getServerError from '@/utils/settings/getServerError';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router';
@@ -85,11 +85,7 @@ export default function EditWorkspaceNameForm({
const currentUser = useUserData();
const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
const [updateWorkspaceName] = useUpdateWorkspaceMutation({
refetchQueries: [
refetchGetOneUserQuery({
userId: currentUser.id,
}),
],
refetchQueries: [GetAllWorkspacesAndProjectsDocument],
awaitRefetchQueries: true,
ignoreResults: true,
});
@@ -196,7 +192,7 @@ export default function EditWorkspaceNameForm({
}
await client.refetchQueries({
include: ['getOneUser'],
include: [GetAllWorkspacesAndProjectsDocument],
});
// The form has been submitted, it's not dirty anymore

View File

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

View File

@@ -1,25 +0,0 @@
import Link from '@/ui/v2/Link';
import Text from '@/ui/v2/Text';
export function FindOldApps() {
return (
<div className="mt-4">
<Text className="font-medium" color="secondary">
Looking for your old apps? They&apos;re still on{' '}
<span className="pb-0.25">
<Link
href="https://console.nhost.io"
target="_blank"
rel="noreferrer"
underline="hover"
>
console.nhost.io
</Link>
</span>{' '}
during this beta.
</Text>
</div>
);
}
export default FindOldApps;

View File

@@ -1,51 +0,0 @@
import { useUI } from '@/context/UIContext';
import Button from '@/ui/v2/Button';
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
import SearchIcon from '@/ui/v2/icons/SearchIcon';
import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import Link from 'next/link';
interface IndexHeaderAppsProps {
query?: any;
setQuery?: any;
}
export function IndexHeaderApps({ query, setQuery }: IndexHeaderAppsProps) {
const { maintenanceActive } = useUI();
return (
<div className="mx-auto mb-6 grid w-full grid-flow-col place-content-between items-center py-2">
<Text variant="h2" component="h1" className="hidden md:block">
My Projects
</Text>
<Input
placeholder="Find Project"
startAdornment={
<SearchIcon
className="ml-2 -mr-1 h-4 w-4 shrink-0"
sx={{ color: 'text.disabled' }}
/>
}
value={query}
onChange={(event) => {
setQuery(event.target.value);
}}
/>
<Link href="/new" passHref>
<Button
variant="outlined"
color="secondary"
startIcon={<PlusCircleIcon />}
disabled={maintenanceActive}
>
New Project
</Button>
</Link>
</div>
);
}
export default IndexHeaderApps;

View File

@@ -1,4 +1,8 @@
import { useGetWorkspaceMemberInvitesToManageQuery } from '@/generated/graphql';
import {
GetAllWorkspacesAndProjectsDocument,
GetWorkspaceMemberInvitesToManageDocument,
useGetWorkspaceMemberInvitesToManageQuery,
} from '@/generated/graphql';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useSubmitState } from '@/hooks/useSubmitState';
import Box from '@/ui/v2/Box';
@@ -29,7 +33,7 @@ export function InviteAnnounce() {
variables: {
userId: user?.id,
},
skip: !isPlatform,
skip: !isPlatform || !user,
});
useEffect(() => {
@@ -114,7 +118,10 @@ export function InviteAnnounce() {
// just refetch all data
await client.refetchQueries({
include: ['getOneUser', 'getWorkspaceMemberInvitesToManage'],
include: [
GetAllWorkspacesAndProjectsDocument,
GetWorkspaceMemberInvitesToManageDocument,
],
});
setIgnoreState({

View File

@@ -1,27 +0,0 @@
import { Resource } from '@/components/home/Resource';
import Text from '@/ui/v2/Text';
export default function Resources() {
return (
<div>
<Text color="disabled">Resources</Text>
<div className="mt-4 flex flex-col space-y-1">
<Resource
text="Documentation"
logo="Question"
link="https://docs.nhost.io"
/>
<Resource
text="Javascript Client"
logo="js"
link="https://docs.nhost.io/reference/javascript/"
/>
<Resource
text="Nhost CLI"
logo="CLI"
link="https://docs.nhost.io/platform/cli"
/>
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useInsertFeedbackOneMutation } from '@/generated/graphql';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Avatar } from '@/ui/Avatar';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
@@ -8,7 +8,7 @@ import { useUserData } from '@nhost/nextjs';
import * as React from 'react';
export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertFeedback, { loading }] = useInsertFeedbackOneMutation();
const user = useUserData();
@@ -16,7 +16,7 @@ export function SendFeedback({ setFeedbackSent, feedback, setFeedback }: any) {
e.preventDefault();
const feedbackWithProjectInfo = [
currentApplication && `Project ID: ${currentApplication.id}`,
currentProject && `Project ID: ${currentProject.id}`,
typeof window !== 'undefined' && `URL: ${window.location.href}`,
feedback,
]

View File

@@ -1,56 +0,0 @@
import { Resources } from '@/components/home';
import GithubIcon from '@/components/icons/GithubIcon';
import { WorkspaceSection } from '@/components/workspace/WorkspaceSection';
import Button from '@/ui/v2/Button';
import Image from 'next/image';
import Link from 'next/link';
export default function Sidebar() {
return (
<div className="mt-2 grid w-full grid-flow-row content-start gap-8 md:ml-10 md:grid md:w-workspaceSidebar">
<WorkspaceSection />
<Resources />
<div className="grid grid-flow-row gap-2">
<Link
href="https://github.com/nhost/nhost"
passHref
target="_blank"
rel="noreferrer noopener"
>
<Button
className="grid grid-flow-col gap-1"
variant="outlined"
color="secondary"
startIcon={<GithubIcon />}
>
Star us on GitHub
</Button>
</Link>
<Link
href="https://discord.com/invite/9V7Qb2U"
passHref
target="_blank"
rel="noreferrer noopener"
>
<Button
className="grid grid-flow-col gap-1"
variant="outlined"
color="secondary"
aria-labelledby="discord-button-label"
>
<Image
src="/assets/brands/discord.svg"
alt="Discord Logo"
width={24}
height={24}
/>
<span id="discord-button-label">Join Discord</span>
</Button>
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,165 @@
import { useDialog } from '@/components/common/DialogProvider';
import { EditWorkspaceNameForm } from '@/components/home/EditWorkspaceNameForm';
import { Resource } from '@/components/home/Resource';
import GithubIcon from '@/components/icons/GithubIcon';
import type { Workspace } from '@/types/application';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
import Image from 'next/image';
import NavLink from 'next/link';
import { twMerge } from 'tailwind-merge';
export interface SidebarProps extends BoxProps {
/**
* List of workspaces to be displayed.
*/
workspaces: Workspace[];
}
export default function Sidebar({
className,
workspaces,
...props
}: SidebarProps) {
const { openDialog } = useDialog();
return (
<Box
component="aside"
className={twMerge(
'grid w-full grid-flow-row content-start gap-8 md:grid',
className,
)}
{...props}
>
<section className="grid grid-flow-row gap-2">
<Text color="secondary">My Workspaces</Text>
{workspaces.length > 0 ? (
<List className="grid grid-flow-row gap-2">
{workspaces.map(({ id, name, slug }) => (
<ListItem.Root key={id}>
<NavLink href={`/${slug}`} passHref>
<ListItem.Button
dense
aria-label={`View ${name}`}
className="!p-1"
>
<ListItem.Avatar className="h-8 w-8">
<div className="inline-block h-8 w-8 overflow-hidden rounded-lg">
<Image
src="/logos/new.svg"
alt="Nhost Logo"
width={32}
height={32}
/>
</div>
</ListItem.Avatar>
<ListItem.Text primary={name} />
</ListItem.Button>
</NavLink>
</ListItem.Root>
))}
</List>
) : (
<ActivityIndicator
label="Creating your first workspace..."
className="py-1"
/>
)}
<Button
variant="borderless"
color="secondary"
startIcon={<PlusCircleIcon />}
className="justify-self-start"
onClick={() => {
openDialog({
title: (
<span className="grid grid-flow-row">
<span>New Workspace</span>
<Text variant="subtitle1" component="span">
Invite team members to workspaces to work collaboratively.
</Text>
</span>
),
component: <EditWorkspaceNameForm />,
});
}}
>
New Workspace
</Button>
</section>
<section className="grid grid-flow-row gap-2">
<Text color="secondary">Resources</Text>
<div className="grid grid-flow-row gap-2">
<Resource
text="Documentation"
logo="Question"
link="https://docs.nhost.io"
/>
<Resource
text="JavaScript Client"
logo="js"
link="https://docs.nhost.io/reference/javascript/"
/>
<Resource
text="Nhost CLI"
logo="CLI"
link="https://docs.nhost.io/platform/cli"
/>
</div>
</section>
<section className="grid grid-flow-row gap-2">
<NavLink
href="https://github.com/nhost/nhost"
passHref
target="_blank"
rel="noreferrer noopener"
>
<Button
className="grid grid-flow-col gap-1"
variant="outlined"
color="secondary"
startIcon={<GithubIcon />}
>
Star us on GitHub
</Button>
</NavLink>
<NavLink
href="https://discord.com/invite/9V7Qb2U"
passHref
target="_blank"
rel="noreferrer noopener"
>
<Button
className="grid grid-flow-col gap-1"
variant="outlined"
color="secondary"
aria-labelledby="discord-button-label"
>
<Image
src="/assets/brands/discord.svg"
alt="Discord Logo"
width={24}
height={24}
/>
<span id="discord-button-label">Join Discord</span>
</Button>
</NavLink>
</section>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,225 @@
import DeploymentStatusMessage from '@/components/deployments/DeploymentStatusMessage';
import { useUI } from '@/context/UIContext';
import type { ApplicationState, Workspace } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import StateBadge from '@/ui/StateBadge';
import type { DeploymentStatus } from '@/ui/StatusCircle';
import { StatusCircle } from '@/ui/StatusCircle';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import type { ButtonProps } from '@/ui/v2/Button';
import Button from '@/ui/v2/Button';
import type { InputProps } from '@/ui/v2/Input';
import Input from '@/ui/v2/Input';
import Link from '@/ui/v2/Link';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
import SearchIcon from '@/ui/v2/icons/SearchIcon';
import { getApplicationStatusString } from '@/utils/helpers';
import { Divider } from '@mui/material';
import debounce from 'lodash.debounce';
import Image from 'next/image';
import NavLink from 'next/link';
import type { ChangeEvent, PropsWithoutRef } from 'react';
import { Fragment, useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface WorkspaceAndProjectListProps extends BoxProps {
/**
* List of workspaces to be displayed.
*/
workspaces: Workspace[];
/**
* Props to be passed to individual slots.
*/
slotProps?: {
root?: BoxProps;
header?: BoxProps;
search?: PropsWithoutRef<InputProps>;
button?: PropsWithoutRef<ButtonProps>;
};
}
function checkStatusOfTheApplication(stateHistory: ApplicationState[] | []) {
if (stateHistory.length === 0) {
return ApplicationStatus.Empty;
}
if (stateHistory[0].stateId === undefined) {
return ApplicationStatus.Empty;
}
return stateHistory[0].stateId;
}
export default function WorkspaceAndProjectList({
workspaces,
className,
slotProps = {},
...props
}: WorkspaceAndProjectListProps) {
const [query, setQuery] = useState('');
const { maintenanceActive } = useUI();
const handleQueryChange = debounce((event: ChangeEvent<HTMLInputElement>) => {
slotProps?.search?.onChange?.(event);
setQuery(event.target.value);
}, 500);
const filteredWorkspaces = workspaces
.map((workspace) => ({
...workspace,
projects: workspace.projects.filter((project) =>
project.name.toLowerCase().includes(query.toLowerCase()),
),
}))
.filter((workspace) => workspace.projects.length > 0);
return (
<Box
{...props}
{...slotProps.root}
className={twMerge(
'grid grid-flow-row content-start gap-4',
className,
slotProps.root?.className,
)}
>
<Box
{...slotProps.header}
className={twMerge(
'grid grid-flow-col place-content-between items-center',
slotProps.header?.className,
)}
>
<Text variant="h2" component="h1" className="hidden md:block">
My Projects
</Text>
<Input
placeholder="Find Project"
startAdornment={
<SearchIcon
className="ml-2 -mr-1 h-4 w-4 shrink-0"
sx={{ color: 'text.disabled' }}
/>
}
{...slotProps.search}
onChange={handleQueryChange}
/>
<NavLink href="/new" passHref>
<Button
variant="outlined"
color="secondary"
startIcon={<PlusCircleIcon />}
disabled={maintenanceActive}
{...slotProps.button}
>
New Project
</Button>
</NavLink>
</Box>
<Box className="my-8 grid grid-flow-row gap-8">
{filteredWorkspaces.map((workspace) => (
<div key={workspace.slug}>
<NavLink href={`/${workspace.slug}`} passHref>
<Link
href={`${workspace.slug}`}
className="mb-1.5 block font-medium"
underline="none"
sx={{ color: 'text.primary' }}
>
{workspace.name}
</Link>
</NavLink>
<List className="grid grid-flow-row border-y">
{workspace.projects.map((project, index) => {
const [latestDeployment] = project.deployments;
return (
<Fragment key={project.slug}>
<ListItem.Root
secondaryAction={
<div className="grid grid-flow-col gap-px">
{latestDeployment && (
<div className="mr-2 flex self-center align-middle">
<StatusCircle
status={
latestDeployment.deploymentStatus as DeploymentStatus
}
/>
</div>
)}
<StateBadge
state={checkStatusOfTheApplication(
project.appStates,
)}
desiredState={project.desiredState}
title={getApplicationStatusString(
checkStatusOfTheApplication(project.appStates),
)}
/>
</div>
}
>
<NavLink
href={`${workspace?.slug}/${project.slug}`}
passHref
>
<ListItem.Button className="rounded-none">
<ListItem.Avatar>
<div className="h-10 w-10 overflow-hidden rounded-lg">
<Image
src="/logos/new.svg"
alt="Nhost Logo"
width={40}
height={40}
/>
</div>
</ListItem.Avatar>
<ListItem.Text
primary={project.name}
secondary={
<DeploymentStatusMessage
appCreatedAt={project.createdAt}
deployment={latestDeployment}
/>
}
/>
</ListItem.Button>
</NavLink>
</ListItem.Root>
{index < workspace.projects.length - 1 && (
<Divider component="li" role="listitem" />
)}
</Fragment>
);
})}
</List>
</div>
))}
</Box>
<Text className="font-medium" color="secondary">
Looking for your old apps? They&apos;re still on{' '}
<Link
href="https://console.nhost.io"
target="_blank"
rel="noreferrer"
underline="always"
>
console.nhost.io
</Link>{' '}
during this beta.
</Text>
</Box>
);
}

View File

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

View File

@@ -1,3 +0,0 @@
export { FindOldApps } from './FindOldApps';
export { IndexHeaderApps } from './IndexHeaderApps';
export { default as Resources } from './Resources';

View File

@@ -2,13 +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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetAllUserWorkspacesAndApplications } from '@/hooks/useGetAllUserWorkspacesAndApplications';
import { useNavigationVisible } from '@/hooks/useNavigationVisible';
import useNotFoundRedirect from '@/hooks/useNotFoundRedirect';
import { useSetAppWorkspaceContextFromUserContext } from '@/hooks/useSetAppWorkspaceContextFromUserContext';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import { NextSeo } from 'next-seo';
@@ -30,8 +28,7 @@ function ProjectLayoutContent({
...mainContainerProps
} = {},
}: ProjectLayoutProps) {
const { currentApplication, currentWorkspace } =
useCurrentWorkspaceAndApplication();
const { currentProject, loading, error } = useCurrentWorkspaceAndProject();
const router = useRouter();
const shouldDisplayNav = useNavigationVisible();
@@ -49,8 +46,6 @@ function ProjectLayoutContent({
),
);
useGetAllUserWorkspacesAndApplications(false);
useSetAppWorkspaceContextFromUserContext();
useNotFoundRedirect();
useEffect(() => {
@@ -63,10 +58,14 @@ function ProjectLayoutContent({
}
}, [isPlatform, isRestrictedPath, router]);
if (!currentWorkspace || !currentApplication || isRestrictedPath) {
if (isRestrictedPath || loading) {
return <LoadingScreen />;
}
if (error) {
throw error;
}
if (!isPlatform) {
return (
<>
@@ -104,7 +103,7 @@ function ProjectLayoutContent({
>
{children}
<NextSeo title={currentApplication.name} />
<NextSeo title={currentProject?.name} />
</Box>
</>
);

View File

@@ -4,7 +4,6 @@ import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
import BaseLayout from '@/components/layout/BaseLayout';
import Container from '@/components/layout/Container';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCleanWorkspaceContext } from '@/hooks/use-cleanWorkspaceContext';
import Box from '@/ui/v2/Box';
import ThemeProvider from '@/ui/v2/ThemeProvider';
import GlobalStyles from '@mui/material/GlobalStyles';
@@ -22,7 +21,6 @@ export default function UnauthenticatedLayout({
const router = useRouter();
const isPlatform = useIsPlatform();
const { isAuthenticated, isLoading } = useAuthenticationStatus();
useCleanWorkspaceContext();
useEffect(() => {
if (!isPlatform || (!isLoading && isAuthenticated)) {

View File

@@ -1,5 +1,5 @@
import LogsDatePicker from '@/components/logs/LogsDatePicker';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
@@ -127,8 +127,8 @@ export default function LogsHeader({
onServiceChange,
...props
}: LogsHeaderProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const applicationCreationDate = new Date(currentApplication.createdAt);
const { currentProject } = useCurrentWorkspaceAndProject();
const applicationCreationDate = new Date(currentProject.createdAt);
/**
* Will subtract the `customInterval` time in minutes from the current date.

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,9 @@
import { UserDataProvider } from '@/context/workspace1-context';
import type { Project } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import type { Workspace } from '@/types/workspace';
import nhostGraphQLLink from '@/utils/msw/mocks/graphql/nhostGraphQLLink';
import { render, screen, waitForElementToBeRemoved } 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({
@@ -37,54 +33,10 @@ 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)),
),
nhostGraphQLLink.operation(async (_req, res, ctx) =>
res(
ctx.data({
deployments: [],
}),
),
),
);
beforeAll(() => {
@@ -93,48 +45,89 @@ beforeAll(() => {
server.listen();
});
afterEach(() => server.resetHandlers());
afterEach(() => {
server.resetHandlers(
rest.get('https://local.graphql.nhost.run/v1', (_req, res, ctx) =>
res(ctx.status(200)),
),
);
queryClient.clear();
});
afterAll(() => {
server.close();
vi.restoreAllMocks();
});
test('should render an empty state when GitHub is not connected', () => {
render(
<UserDataProvider
initialWorkspaces={[
{
...mockWorkspace,
applications: [{ ...mockApplication, githubRepository: null }],
},
]}
>
<OverviewDeployments />
</UserDataProvider>,
test('should render an empty state when GitHub is not connected', async () => {
server.use(
rest.post('https://local.graphql.nhost.run/v1', async (req, res, ctx) => {
const { operationName } = await req.json();
if (operationName === 'GetWorkspaceAndProject') {
return res(
ctx.json({
data: {
workspaces: [
{
...mockWorkspace,
projects: [{ ...mockApplication, githubRepository: null }],
},
],
projects: [{ ...mockApplication, githubRepository: null }],
},
}),
);
}
return res(
ctx.json({
data: {
deployments: [],
},
}),
);
}),
);
expect(screen.getByText(/no deployments/i)).toBeInTheDocument();
render(<OverviewDeployments />);
expect(await screen.findByText(/no deployments/i)).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /connect to github/i }),
await screen.findByRole('button', { name: /connect to github/i }),
).toBeInTheDocument();
});
test('should render an empty state when GitHub is connected, but there are no deployments', async () => {
render(
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
<OverviewDeployments />
</UserDataProvider>,
server.use(
rest.post('https://local.graphql.nhost.run/v1', async (_req, res, ctx) => {
const { operationName } = await _req.json();
if (operationName === 'GetWorkspaceAndProject') {
return res(
ctx.json({
data: {
workspaces: [mockWorkspace],
projects: [mockApplication],
},
}),
);
}
return res(ctx.json({ data: { deployments: [] } }));
}),
);
expect(screen.getByText(/^deployments$/i)).toBeInTheDocument();
expect(screen.getByRole('link', { name: /view all/i })).toBeInTheDocument();
render(<OverviewDeployments />);
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(await screen.findByText(/^deployments$/i)).toBeInTheDocument();
expect(
await screen.findByRole('link', { name: /view all/i }),
).toBeInTheDocument();
expect(screen.getByText(/no deployments/i)).toBeInTheDocument();
expect(screen.getByText(/test\/git-project/i)).toBeInTheDocument();
expect(screen.getByRole('link', { name: /edit/i })).toHaveAttribute(
expect(await screen.findByText(/no deployments/i)).toBeInTheDocument();
expect(await screen.findByText(/test\/git-project/i)).toBeInTheDocument();
expect(await screen.findByRole('link', { name: /edit/i })).toHaveAttribute(
'href',
'/test-workspace/test-application/settings/git',
);
@@ -142,103 +135,124 @@ test('should render an empty state when GitHub is connected, but there are no de
test('should render a list of deployments', async () => {
server.use(
nhostGraphQLLink.operation(async (req, res, ctx) => {
const requestPayload = await req.json();
rest.post('https://local.graphql.nhost.run/v1', async (_req, res, ctx) => {
const { operationName } = await _req.json();
if (requestPayload.operationName === 'ScheduledOrPendingDeploymentsSub') {
return res(ctx.data({ deployments: [] }));
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
return res(ctx.json({ data: { deployments: [] } }));
}
return res(
ctx.data({
deployments: [
{
id: '1',
commitSHA: 'abc123',
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
deploymentStatus: 'DEPLOYED',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
}),
);
}),
);
render(
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
<OverviewDeployments />
</UserDataProvider>,
);
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(screen.getByText(/test commit message/i)).toBeInTheDocument();
expect(screen.getByLabelText(/avatar/i)).toHaveStyle(
'background-image: url(http://images.example.com/avatar.png)',
);
expect(
screen.getByRole('link', {
name: /test commit message/i,
}),
).toHaveAttribute('href', '/test-workspace/test-application/deployments/1');
expect(screen.getByText(/5m 0s/i)).toBeInTheDocument();
expect(screen.getByText(/live/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /redeploy/i })).not.toBeDisabled();
});
test('should disable redeployments if a deployment is already in progress', async () => {
server.use(
nhostGraphQLLink.operation(async (req, res, ctx) => {
const requestPayload = await req.json();
if (requestPayload.operationName === 'ScheduledOrPendingDeploymentsSub') {
if (operationName === 'GetWorkspaceAndProject') {
return res(
ctx.data({
deployments: [
{
id: '2',
commitSHA: 'abc234',
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
deploymentEndedAt: null,
deploymentStatus: 'PENDING',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
ctx.json({
data: {
workspaces: [mockWorkspace],
projects: [mockApplication],
},
}),
);
}
return res(
ctx.data({
deployments: [
{
id: '1',
commitSHA: 'abc123',
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
deploymentStatus: 'DEPLOYED',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
ctx.json({
data: {
deployments: [
{
id: '1',
commitSHA: 'abc123',
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
deploymentStatus: 'DEPLOYED',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
},
}),
);
}),
);
render(
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
<OverviewDeployments />
</UserDataProvider>,
render(<OverviewDeployments />);
expect(await screen.findByText(/test commit message/i)).toBeInTheDocument();
expect(await screen.findByLabelText(/avatar/i)).toHaveStyle(
'background-image: url(http://images.example.com/avatar.png)',
);
expect(
await screen.findByRole('link', {
name: /test commit message/i,
}),
).toHaveAttribute('href', '/test-workspace/test-application/deployments/1');
expect(await screen.findByText(/5m 0s/i)).toBeInTheDocument();
expect(await screen.findByText(/live/i)).toBeInTheDocument();
expect(
await screen.findByRole('button', { name: /redeploy/i }),
).not.toBeDisabled();
});
test('should disable redeployments if a deployment is already in progress', async () => {
server.use(
rest.post('https://local.graphql.nhost.run/v1', async (req, res, ctx) => {
const { operationName } = await req.json();
if (operationName === 'ScheduledOrPendingDeploymentsSub') {
return res(
ctx.json({
data: {
deployments: [
{
id: '2',
commitSHA: 'abc234',
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
deploymentEndedAt: null,
deploymentStatus: 'PENDING',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
},
}),
);
}
if (operationName === 'GetWorkspaceAndProject') {
return res(
ctx.json({
data: {
workspaces: [mockWorkspace],
projects: [mockApplication],
},
}),
);
}
return res(
ctx.json({
data: {
deployments: [
{
id: '1',
commitSHA: 'abc123',
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
deploymentStatus: 'DEPLOYED',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
},
}),
);
}),
);
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(screen.getByRole('button', { name: /redeploy/i })).toBeDisabled();
render(<OverviewDeployments />);
expect(
await screen.findByRole('button', { name: /redeploy/i }),
).toBeDisabled();
});

View File

@@ -2,28 +2,26 @@ 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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import Divider from '@/ui/v2/Divider';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import List from '@/ui/v2/List';
import Text from '@/ui/v2/Text';
import { getLastLiveDeployment } from '@/utils/helpers';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import {
useGetDeploymentsSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/utils/__generated__/graphql';
import { getLastLiveDeployment } from '@/utils/helpers';
import NavLink from 'next/link';
import { Fragment } from 'react';
function OverviewDeploymentsTopBar() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { githubRepository } = currentApplication || {};
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const isGitHubConnected = !!currentProject?.githubRepository;
return (
<div className="grid grid-flow-col place-content-between items-center gap-2 pb-4">
@@ -32,10 +30,10 @@ function OverviewDeploymentsTopBar() {
</Text>
<NavLink
href={`/${currentWorkspace?.slug}/${currentApplication?.slug}/deployments`}
href={`/${currentWorkspace?.slug}/${currentProject?.slug}/deployments`}
passHref
>
<Button variant="borderless" disabled={!githubRepository}>
<Button variant="borderless" disabled={!isGitHubConnected}>
View all
<ChevronRightIcon className="ml-1 inline-block h-4 w-4" />
</Button>
@@ -45,11 +43,10 @@ function OverviewDeploymentsTopBar() {
}
function OverviewDeploymentList() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const { data, loading } = useGetDeploymentsSubSubscription({
variables: {
id: currentApplication?.id,
id: currentProject?.id,
limit: 5,
offset: 0,
},
@@ -60,7 +57,7 @@ function OverviewDeploymentList() {
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({
variables: {
appId: currentApplication?.id,
appId: currentProject?.id,
},
});
@@ -102,12 +99,12 @@ function OverviewDeploymentList() {
>
<GithubIcon className="h-4 w-4 self-center" />
<Text variant="body1" className="self-center font-normal">
{currentApplication?.githubRepository?.fullName}
{currentProject?.githubRepository?.fullName}
</Text>
</Box>
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/git`}
passHref
>
<Button variant="borderless" size="small">
@@ -145,14 +142,17 @@ function OverviewDeploymentList() {
}
export default function OverviewDeployments() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { currentProject, loading } = useCurrentWorkspaceAndProject();
const { openGitHubModal } = useGitHubModal();
const { maintenanceActive } = useUI();
const isGitHubConnected = !!currentProject?.githubRepository;
const { githubRepository } = currentApplication || {};
if (loading) {
return <ActivityIndicator label="Loading project info..." delay={1000} />;
}
// GitHub repo connected. Show deployments
if (githubRepository) {
if (isGitHubConnected) {
return (
<div className="flex flex-col">
<OverviewDeploymentsTopBar />

View File

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

View File

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

View File

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

View File

@@ -1,141 +0,0 @@
import { useDialog } from '@/components/common/DialogProvider';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { ApplicationStatus } from '@/types/application';
import { Alert } from '@/ui/Alert';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import Text from '@/ui/v2/Text';
import { triggerToast } from '@/utils/toast';
import { useUpdateApplicationMutation } from '@/utils/__generated__/graphql';
const migrationSteps = [
{
title: 'Your project will be paused',
},
{
title: 'Your database will be migrated to its own dedicated instance',
},
{
title: 'Your project will be resumed',
},
];
export default function OverviewMigration() {
const { openAlertDialog } = useDialog();
const [updateApplication] = useUpdateApplicationMutation({
refetchQueries: ['getOneUser'],
});
const { currentApplication } = useCurrentWorkspaceAndApplication();
return (
<div className="pb-12">
<div className="flex flex-col gap-2">
<Text variant="h3">
Migrate Database
<span className="relative -top-0.5 ml-2 self-center">
<Chip label="New" color="primary" size="small" />
</span>
</Text>
<Text variant="subtitle1" className="!font-medium">
Migrate your project&apos;s data to its own Postgres instance and get
root access to your database.
</Text>
</div>
<div className="mt-6 flex flex-row place-content-between rounded-lg">
<Button
variant="outlined"
color="secondary"
className="w-full border-1 hover:border-1"
onClick={() => {
openAlertDialog({
title: 'Migrate Database',
payload: (
<div className="flex flex-col gap-6 pb-8">
<Text>
Your project&apos;s data will be moved to a new and
dedicated Postgres instance with root access.
</Text>
<div className="flex flex-col gap-4">
<Text>Steps to migrate:</Text>
<div className="grid grid-rows-3 gap-4">
{migrationSteps.map((step, index) => (
<div key={step.title} className="col-span-1">
<div className="flex h-11 flex-row gap-3">
<div className="flex items-center">
<Box
className="flex h-8 w-8 flex-col items-center justify-center self-center rounded-md align-middle font-semibold"
sx={{ backgroundColor: 'grey.200' }}
>
<Text
component="span"
className="font-semibold"
color="secondary"
>
{index + 1}
</Text>
</Box>
</div>
<div className="flex w-[312px] items-center">
<Text>{step.title}</Text>
</div>
</div>
</div>
))}
</div>
</div>
<Alert className="text-left">
You can expect some downtime while we are moving your data
around. The time to migrate is dependent on your database
size.
</Alert>
</div>
),
props: {
contentProps: {
className: 'py-0',
},
PaperProps: {
className: 'max-w-[29.25rem] mx-auto p-6 rounded-lg',
},
primaryButtonText: 'Start Migration',
onPrimaryAction: async () => {
try {
await updateApplication({
variables: {
appId: currentApplication.id,
app: {
desiredState: ApplicationStatus.Migrating,
},
},
});
localStorage.setItem(
`migration-${currentApplication.id}`,
new Date().toISOString(),
);
triggerToast(`${currentApplication.name} set to migrate.`);
} catch (e) {
triggerToast(
`Error trying to migrate ${currentApplication.name}`,
);
}
},
actionsProps: {
className: 'flex flex-row-reverse place-content-between',
},
},
});
}}
>
Start Migrating
</Button>
</div>
</div>
);
}

View File

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

View File

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

View File

@@ -1,28 +1,27 @@
import GithubIcon from '@/components/icons/GithubIcon';
import { useUI } from '@/context/UIContext';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
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';
import NavLink from 'next/link';
export default function OverviewRepository() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { currentWorkspace, currentProject } = useCurrentWorkspaceAndProject();
const { maintenanceActive } = useUI();
return (
<div>
<Text variant="h3">Repository</Text>
<Text variant="subtitle1" className="mt-2 !font-medium">
{!currentApplication.githubRepository
{!currentProject.githubRepository
? 'Connect your project with a GitHub repository to create your first deployment.'
: 'GitHub is connected.'}
</Text>
{!currentApplication.githubRepository ? (
{!currentProject.githubRepository ? (
<div className="mt-6 flex flex-row place-content-between rounded-lg">
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/git`}
passHref
>
<Button
@@ -47,12 +46,12 @@ export default function OverviewRepository() {
>
<GithubIcon className="h-4 w-4 self-center" />
<Text variant="body1" className="self-center font-normal">
{currentApplication.githubRepository.fullName}
{currentProject.githubRepository.fullName}
</Text>
</Box>
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}/settings/git`}
href={`/${currentWorkspace.slug}/${currentProject.slug}/settings/git`}
passHref
>
<Button

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