Compare commits

..

200 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
d9fd1a54a5 Merge pull request #2192 from nhost/changeset-release/main
chore: update versions
2023-08-23 11:19:48 +01:00
github-actions[bot]
a19b85c8ac chore: update versions 2023-08-23 09:45:31 +00:00
Hassan Ben Jobrane
4e1aaca0ee Merge pull request #2194 from nhost/feat/toggle-av
feat: toggle av
2023-08-23 10:42:20 +01:00
Hassan Ben Jobrane
34ef37cdce Merge pull request #2190 from dddenis/fix/storage-upload-status-error
fix(hasura-storage-js): fix upload response status code check
2023-08-23 10:40:29 +01:00
Hassan Ben Jobrane
5d6b655cb1 fix: make sure AV turns off correctly 2023-08-23 01:40:01 +01:00
Hassan Ben Jobrane
074a0fa111 chore: add changeset 2023-08-22 18:32:34 +01:00
Hassan Ben Jobrane
403d839fca chore: cleanup 2023-08-22 18:30:26 +01:00
Hassan Ben Jobrane
4e3098240b feat(settings): add toggle av settings 2023-08-22 18:28:27 +01:00
Hassan Ben Jobrane
07fda9bbb3 Merge pull request #2193 from nhost/fix/distinguish-not-uploaded-files
fix: grey out not uploaded files
2023-08-22 13:11:52 +01:00
Hassan Ben Jobrane
4a7ede11e9 chore: add changeset 2023-08-22 11:33:42 +01:00
Hassan Ben Jobrane
482ae4c4f1 fix: grey not uploaded files 2023-08-22 11:25:23 +01:00
Denis Goncharenko
08fe4cd65f fix(hasura-storage-js): update upload response error details 2023-08-22 11:34:41 +02:00
Hassan Ben Jobrane
5781721bca Merge pull request #2188 from nhost/feat/one-click-run-service
feat: add support for template run services
2023-08-22 10:18:54 +01:00
Denis Goncharenko
39de0063bf chore: add changeset 2023-08-21 20:49:56 +02:00
Hassan Ben Jobrane
202b647234 chore: add changeset 2023-08-21 15:45:43 +01:00
Hassan Ben Jobrane
51c163a268 fix: copy complete link to config 2023-08-21 15:43:46 +01:00
Hassan Ben Jobrane
6e802c9938 fix: handle the case where the config is not set in the URL 2023-08-21 13:38:38 +01:00
Hassan Ben Jobrane
9a46104e37 feat: replace project selector with a searchable list 2023-08-21 13:26:27 +01:00
Hassan Ben Jobrane
655b317c39 fix: keep image field when copying config to clipboard 2023-08-21 13:25:57 +01:00
Hassan Ben Jobrane
d3ad7c9d4a fix: handle error when navigating back when service form is still open 2023-08-21 13:25:01 +01:00
Hassan Ben Jobrane
ece08d3efd feat: add ability to copy current service config from the editor 2023-08-21 10:47:54 +01:00
Hassan Ben Jobrane
3493442c2d fix: fix command import when value is null 2023-08-21 10:47:26 +01:00
Denis Goncharenko
632a79b9e4 fix(hasura-storage-js): fix upload response status code check 2023-08-20 14:15:24 +02:00
Hassan Ben Jobrane
4a4d85757a fix: use serviceId to determine whether to create or update service 2023-08-19 19:33:45 +01:00
Hassan Ben Jobrane
88a01004b7 feat: parse base64 encoded config from query param 2023-08-19 18:50:46 +01:00
David Barroso
73230eb35a chore: fix and deploy react example (#2183) 2023-08-19 07:57:42 +02:00
Hassan Ben Jobrane
5cf9dd9bc2 Merge pull request #2173 from nhost/changeset-release/main
chore: update versions
2023-08-14 11:34:45 +01:00
github-actions[bot]
27e74c10d7 chore: update versions 2023-08-10 11:05:13 +00:00
Hassan Ben Jobrane
bd807a5ee1 Merge pull request #2171 from nhost/feat/run-pricing
feat: add pricing info and confirmation dialog
2023-08-10 12:01:04 +01:00
Hassan Ben Jobrane
4093e03a13 Merge pull request #2170 from nhost/feat/multiline-env-input
fix: nhost run enhancements
2023-08-10 12:00:53 +01:00
Hassan Ben Jobrane
29076d0304 Merge pull request #2169 from nhost/fix/null-values-services-form
fix(services): handle null values when editing a service
2023-08-10 12:00:43 +01:00
Hassan Ben Jobrane
ab83fa6b5e fix: make sure vCPUs are shown correctly 2023-08-09 16:20:45 +01:00
Hassan Ben Jobrane
b20761e976 chore: add changeset 2023-08-09 14:55:31 +01:00
Hassan Ben Jobrane
a445e5b786 feat: add pricing info and confirmation dialog 2023-08-09 14:53:05 +01:00
Hassan Ben Jobrane
90df6d81d8 chore: add changeset 2023-08-08 18:04:16 +01:00
Hassan Ben Jobrane
aa85084675 chore: add changeset 2023-08-08 18:02:35 +01:00
Hassan Ben Jobrane
07ad470c0c fix: query service logs correctly 2023-08-08 18:00:25 +01:00
Hassan Ben Jobrane
fa6b58a9c5 feat: enable multiline support for environment value input 2023-08-08 17:50:07 +01:00
Hassan Ben Jobrane
acf55376ba fix(services): handle null values when editing a service 2023-08-08 15:42:09 +01:00
Hassan Ben Jobrane
b0a9798b04 Merge pull request #2168 from nhost/changeset-release/main
chore: update versions
2023-08-08 11:17:25 +01:00
github-actions[bot]
3952e87f01 chore: update versions 2023-08-07 17:53:16 +00:00
Hassan Ben Jobrane
b95ccf873d Merge pull request #2167 from nhost/fix/announcement-banner-text
fix: make announcement text white in dark mode
2023-08-07 18:50:41 +01:00
Hassan Ben Jobrane
8d7f84b8da chore: add changeset 2023-08-07 18:33:17 +01:00
Hassan Ben Jobrane
bd1b69bd75 fix: make announcement text white in dark mode 2023-08-07 12:17:18 +01:00
Hassan Ben Jobrane
84d5436634 Merge pull request #2165 from nhost/changeset-release/main
chore: update versions
2023-08-07 12:09:08 +01:00
github-actions[bot]
2325766c1d chore: update versions 2023-08-04 16:06:40 +00:00
Hassan Ben Jobrane
2c355eaae4 Merge pull request #2162 from nhost/fix/dedicated-resources-modal
fix: show zero values when dedicated resources is disabled
2023-08-04 17:03:08 +01:00
Hassan Ben Jobrane
9e26ed767e Merge pull request #2161 from nhost/fix/announcement-banner
fix: make announcement close properly
2023-08-04 17:02:58 +01:00
Hassan Ben Jobrane
abdb6c56f4 chore: increase e2e CI timeout 2023-08-03 17:18:03 +01:00
Hassan Ben Jobrane
3b75bfce27 chore: add changeset 2023-08-03 17:03:01 +01:00
Hassan Ben Jobrane
f498190758 chore: add changeset 2023-08-03 17:00:10 +01:00
Hassan Ben Jobrane
b4158fa513 fix: show zero values when dedicated resources is disabled 2023-08-03 16:54:12 +01:00
Hassan Ben Jobrane
3d1a177632 fix: make announcement close properly 2023-08-03 15:17:06 +01:00
Hassan Ben Jobrane
0675a213b5 Merge pull request #2155 from nhost/changeset-release/main
chore: update versions
2023-08-01 14:28:53 +01:00
github-actions[bot]
a8ff383490 chore: update versions 2023-08-01 13:09:10 +00:00
David Barroso
960d815f68 chore(docs): added Nhost Run documentation (#2125)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2023-08-01 15:05:56 +02:00
Hassan Ben Jobrane
edf2b4e93f Merge pull request #2150 from nhost/feat/tweak-copy-service-form
feat(services): consistent naming for compute
2023-08-01 14:04:22 +01:00
Hassan Ben Jobrane
fe240542a4 Merge pull request #2152 from nhost/fix/services-form
fix(services): fix when config is null
2023-08-01 14:04:09 +01:00
Hassan Ben Jobrane
c7752c0657 Merge pull request #2154 from nhost/feat/run-announcement
feat(run): add annoucement for nhost run
2023-08-01 14:03:55 +01:00
Hassan Ben Jobrane
d1e2b1c75a chore: tweak announcement copy 2023-08-01 14:03:25 +01:00
Hassan Ben Jobrane
bcdab66bf8 chore: add changeset 2023-08-01 13:26:43 +01:00
Hassan Ben Jobrane
7636f40030 chore: remove unused 2023-08-01 13:25:28 +01:00
Hassan Ben Jobrane
e643bd3620 chore: add changeset 2023-08-01 13:23:13 +01:00
Hassan Ben Jobrane
311c7756d7 chore: add changeset 2023-08-01 13:21:17 +01:00
David Barroso
f967a2e596 chore(docs): added note about storage not being able to be downsized (#2153) 2023-08-01 14:16:16 +02:00
Hassan Ben Jobrane
4c4b253a71 chore: fix lockfile 2023-08-01 13:07:16 +01:00
Hassan Ben Jobrane
0f5f8c0d90 chore: revert lockfile 2023-08-01 13:00:37 +01:00
Hassan Ben Jobrane
37a7fc05d5 feat(run): add annoucement for nhost run 2023-08-01 12:37:23 +01:00
Hassan Ben Jobrane
bf93d87b36 fix(services): fix when config is null 2023-07-31 18:46:39 +01:00
Hassan Ben Jobrane
efb3dc7294 feat(services): consistent naming for compute 2023-07-27 17:24:42 +01:00
Hassan Ben Jobrane
42bd7807b2 Merge pull request #2148 from nhost/chore/increase-ci-timeout
chore: increase timeout for Publish to Docker Hub
2023-07-25 16:08:34 +01:00
Hassan Ben Jobrane
eea59bd202 chore: increase timeout for Publish to Docker Hub 2023-07-25 15:36:15 +01:00
Hassan Ben Jobrane
7248eb733f Merge pull request #2146 from nhost/changeset-release/main
chore: update versions
2023-07-25 13:12:50 +01:00
github-actions[bot]
fceb6a4a89 chore: update versions 2023-07-25 11:52:49 +00:00
Hassan Ben Jobrane
b10eca09a8 Merge pull request #2126 from nhost/renovate/turbo-monorepo
chore(deps): update dependency turbo to v1.10.11
2023-07-25 12:49:57 +01:00
Hassan Ben Jobrane
4799b65e96 Merge pull request #2144 from nhost/feat/add-run-services-to-logs
feat: add run/services to the logs
2023-07-25 12:24:13 +01:00
Hassan Ben Jobrane
067eb9d6a9 Merge pull request #2143 from nhost/feat/change-grafana-url
feat: change grafana url to point to dashboards
2023-07-25 12:24:03 +01:00
Hassan Ben Jobrane
219d5ecdcf Merge pull request #2140 from nhost/fix/create-service-form
fix: tweaking the create service form
2023-07-25 12:23:47 +01:00
Hassan Ben Jobrane
9073182d51 chore: add changeset 2023-07-25 12:20:15 +01:00
Hassan Ben Jobrane
bdb5783e79 chore: sync turbo version in Dockerfile 2023-07-25 12:19:09 +01:00
Hassan Ben Jobrane
ece717d6e0 chore: add changeset 2023-07-25 12:01:53 +01:00
Hassan Ben Jobrane
b135ef695c chore: add changeset 2023-07-25 11:38:00 +01:00
Hassan Ben Jobrane
82b3353110 chore: add changeset 2023-07-25 11:34:20 +01:00
Hassan Ben Jobrane
3f165a85e3 fix: grafana urls in generateAppServiceUrl.test 2023-07-25 11:14:58 +01:00
renovate[bot]
aa4018909f chore(deps): update dependency turbo to v1.10.11 2023-07-24 23:03:50 +00:00
Hassan Ben Jobrane
98397e3ccd feat: add run/services to the logs 2023-07-24 18:00:22 +01:00
Hassan Ben Jobrane
911e7112c9 feat: change grafana url to point to dashboards 2023-07-24 17:32:46 +01:00
Hassan Ben Jobrane
e62402ecfc feat(services): add toolip content 2023-07-24 10:24:24 +01:00
Hassan Ben Jobrane
9190dd726d Merge pull request #2138 from nhost/changeset-release/main
chore: update versions
2023-07-23 21:31:33 +01:00
github-actions[bot]
ae093283d0 chore: update versions 2023-07-23 20:13:51 +00:00
Hassan Ben Jobrane
875327fbea Merge pull request #2136 from tawsbob/bug/users-page-limit
fix: back to previous limit
2023-07-23 21:11:11 +01:00
Hassan Ben Jobrane
3d5c34f4ce chore: add changeset 2023-07-23 20:53:14 +01:00
Dellean Santos
58c2a20532 fix: back to previous limit 2023-07-23 16:17:10 -03:00
Hassan Ben Jobrane
6c90cb5024 fix: make command optional and set min replicas 0 2023-07-22 19:58:34 +01:00
Hassan Ben Jobrane
7e37570587 Merge pull request #2133 from nhost/chore/ci-trigger-release-manually
chore(ci): add support for triggering release manually
2023-07-21 19:27:04 +01:00
Hassan Ben Jobrane
87d225a840 chore(ci): trigger release manually 2023-07-21 19:08:34 +01:00
Hassan Ben Jobrane
7b0de27c80 Merge pull request #2132 from nhost/changeset-release/main
chore: update versions
2023-07-21 17:43:45 +01:00
github-actions[bot]
564fc76195 chore: update versions 2023-07-21 16:24:17 +00:00
Hassan Ben Jobrane
2ed4f40c12 Merge pull request #2120 from nhost/feat/services
feat: services
2023-07-21 17:21:44 +01:00
Hassan Ben Jobrane
d67a023e21 feat(services): block services for free apps 2023-07-21 16:51:58 +01:00
Hassan Ben Jobrane
c99d117d1c chore: add changeset 2023-07-21 15:40:08 +01:00
Hassan Ben Jobrane
a497a6ba0a feat(services): mod port url if values are empty 2023-07-21 12:57:18 +01:00
Hassan Ben Jobrane
160cd08cc7 feat(services): fix e2e nav test 2023-07-21 10:35:51 +01:00
Hassan Ben Jobrane
120151c40c feat(services): run pnpm install 2023-07-20 19:14:47 +01:00
Hassan Ben Jobrane
9dc16f29b3 feat(services): add services pagination 2023-07-20 19:10:10 +01:00
Hassan Ben Jobrane
964fc5644a feat(services): put services behind a feature flag 2023-07-20 19:09:48 +01:00
Hassan Ben Jobrane
2f907fc68f feat(services): tweak service form on mobile 2023-07-20 12:41:35 +01:00
Hassan Ben Jobrane
fe6cadc2cd feat(services): tweak services list 2023-07-20 11:40:45 +01:00
Hassan Ben Jobrane
338c8e5a80 feat(services): fix command on initialData 2023-07-19 17:55:47 +01:00
Hassan Ben Jobrane
e6f3a1a39d feat(services): tweak compute section 2023-07-19 16:20:52 +01:00
Hassan Ben Jobrane
a168faeb69 feat(services): tweak service form 2023-07-19 14:00:25 +01:00
Hassan Ben Jobrane
b1628c59b5 feat(services): refactor command field 2023-07-19 13:36:37 +01:00
Hassan Ben Jobrane
32a2f5db9a feat(services): fix replicas form section 2023-07-19 11:57:10 +01:00
Hassan Ben Jobrane
818a48f74d feat(services): refactor edit service 2023-07-19 10:58:56 +01:00
Hassan Ben Jobrane
bed377d05f feat(services): add service details page 2023-07-18 16:19:48 +01:00
Hassan Ben Jobrane
709a616cfa feat(services): fix e2e nav item count 2023-07-17 19:59:24 +01:00
Hassan Ben Jobrane
860e2d877c feat(services): fix linter errors 2023-07-17 19:40:21 +01:00
Hassan Ben Jobrane
5c6b2f88b9 fix: export component properly 2023-07-17 18:43:41 +01:00
Hassan Ben Jobrane
f151a0e872 feat(services): tweaks + show URL for ports/image 2023-07-17 17:01:28 +01:00
Hassan Ben Jobrane
4a84bbb410 feat(service): tweaks + add service details page 2023-07-17 16:10:18 +01:00
Hassan Ben Jobrane
fa3a50e323 feat(service): add GiB to storage capacity field 2023-07-16 23:48:48 +01:00
Hassan Ben Jobrane
398152358c feat(service): tweak compute form section 2023-07-16 23:43:44 +01:00
Hassan Ben Jobrane
34ae9046f3 feat(services): add fetch and delete services 2023-07-16 23:32:24 +01:00
Hassan Ben Jobrane
a478689587 feat(services): fix compute section 2023-07-14 19:54:24 +01:00
Hassan Ben Jobrane
9dbc0607dc feat(services): hook up create service to the api 2023-07-14 19:43:08 +01:00
Hassan Ben Jobrane
7455efdd53 feat(services): tweak create service form 2023-07-14 15:20:14 +01:00
Hassan Ben Jobrane
d0aff6141f feat(services): add info tooltip to each section 2023-07-14 13:59:41 +01:00
Hassan Ben Jobrane
aed0c4f82a feat(services): add create service form 2023-07-14 12:20:34 +01:00
Hassan Ben Jobrane
74d4276c1a feat(services): add new page for services 2023-07-13 11:55:20 +01:00
Hassan Ben Jobrane
1e98130aa1 Merge pull request #2113 from nhost/changeset-release/main
chore: update versions
2023-07-12 20:34:35 +01:00
github-actions[bot]
52e9b510da chore: update versions 2023-07-12 19:22:34 +00:00
Hassan Ben Jobrane
ece197eb6b Merge pull request #2116 from nhost/renovate/prettier-plugin-tailwindcss-0.x
chore(deps): update dependency prettier-plugin-tailwindcss to ^0.4.0
2023-07-12 20:20:50 +01:00
Hassan Ben Jobrane
d14e112bff chore: add changeset 2023-07-12 17:29:12 +01:00
renovate[bot]
83884f04a5 chore(deps): update dependency prettier-plugin-tailwindcss to ^0.4.0 2023-07-12 16:10:24 +00:00
Hassan Ben Jobrane
977de21e86 Merge pull request #2117 from nhost/chore/add-hasura-auth-version
chore: add hasura-auth version 0.20.2
2023-07-12 17:07:27 +01:00
Hassan Ben Jobrane
462a60a8f8 chore: fix hasura-auth version 2023-07-12 16:45:01 +01:00
Hassan Ben Jobrane
9aa4371ef4 chore: add changeset 2023-07-12 16:45:01 +01:00
Hassan Ben Jobrane
f0feddd83f chore: add hasura-auth version 0.20.2 2023-07-12 16:45:01 +01:00
Hassan Ben Jobrane
0748cab125 Merge pull request #2087 from nhost/renovate/vite-plugin-dts-3.x
chore(deps): update dependency vite-plugin-dts to v3
2023-07-12 16:41:59 +01:00
Hassan Ben Jobrane
27885491ee chore: fix test project subdomain 2023-07-12 13:55:14 +01:00
Hassan Ben Jobrane
a36bdbf907 chore: uncomment setting preview URL 2023-07-12 13:53:42 +01:00
Hassan Ben Jobrane
d3e8bb94ae chore: add changeset 2023-07-11 16:39:26 +01:00
Hassan Ben Jobrane
645595ee43 Revert "chore: increase playwright timeout"
This reverts commit 72d1e94cb3.
2023-07-11 16:36:46 +01:00
Hassan Ben Jobrane
4d82bc5609 Revert "chore: playwright: increase number of workers"
This reverts commit b4c10f9f8a.
2023-07-11 16:17:30 +01:00
Hassan Ben Jobrane
fdf1e555d8 chore: ci: comment Fetch Dashboard Preview URL 2023-07-11 16:13:50 +01:00
Hassan Ben Jobrane
90c694cbba chore: ci: Comment step Set Dashboard Preview URL 2023-07-11 15:51:03 +01:00
Hassan Ben Jobrane
3262fa7b37 chore: teardown: run playwright in slowMo 2023-07-11 15:09:25 +01:00
Hassan Ben Jobrane
ab43fe567f chore: fix inserting sql in hasura page 2023-07-11 14:47:21 +01:00
Hassan Ben Jobrane
b4c10f9f8a chore: playwright: increase number of workers 2023-07-11 14:26:22 +01:00
Hassan Ben Jobrane
f4c6e7cfab chore: bring back raw_sql fill 2023-07-11 13:58:41 +01:00
Hassan Ben Jobrane
72d1e94cb3 chore: increase playwright timeout 2023-07-11 13:36:44 +01:00
Hassan Ben Jobrane
82d221a48d Revert "chore: increase CI e2e timeout"
This reverts commit 3fe46771b9.
2023-07-11 11:57:36 +01:00
Hassan Ben Jobrane
3fe46771b9 chore: increase CI e2e timeout 2023-07-11 11:06:09 +01:00
Hassan Ben Jobrane
a1c487aa21 chore: fix lock file 2023-07-11 01:55:39 +01:00
Hassan Ben Jobrane
cf455608e2 chore: fix e2e tests 2023-07-11 01:40:17 +01:00
Hassan Ben Jobrane
5dac12dd41 chore: use node v18 2023-07-10 18:15:22 +01:00
Hassan Ben Jobrane
2389b46e0d chore: update node to v18 2023-07-10 16:40:41 +01:00
renovate[bot]
6fe2d22d0e chore(deps): update dependency vite-plugin-dts to v3 2023-07-10 15:23:12 +00:00
Hassan Ben Jobrane
0b439149e4 Merge pull request #2106 from nhost/renovate/pluralize-0.x
chore(deps): update dependency @types/pluralize to ^0.0.30
2023-07-10 16:19:28 +01:00
Hassan Ben Jobrane
a9d7da8af7 chore: add changeset 2023-07-10 16:09:14 +01:00
renovate[bot]
3ecc21a45e chore(deps): update dependency @types/pluralize to ^0.0.30 2023-07-10 14:30:41 +00:00
Hassan Ben Jobrane
aa19e85cdc Merge pull request #2088 from nhost/renovate/turbo-monorepo
chore(deps): update dependency turbo to v1.10.7
2023-07-10 15:28:49 +01:00
Hassan Ben Jobrane
26c650227d Merge pull request #2111 from nhost/fix/tweak-config-warning
fix: tweak warning in dark mode
2023-07-10 15:16:25 +01:00
Hassan Ben Jobrane
face99ccde chore: add changeset 2023-07-10 15:07:25 +01:00
Hassan Ben Jobrane
49bcc525ad chore: bump turbo version in Dockerfile 2023-07-10 15:07:25 +01:00
renovate[bot]
533563c893 chore(deps): update dependency turbo to v1.10.7 2023-07-10 15:07:25 +01:00
Hassan Ben Jobrane
cfe527307e chore: add changeset 2023-07-10 15:05:22 +01:00
Hassan Ben Jobrane
1e36c6706d Revert "chore: use node 18 for GH actions"
This reverts commit 6e40b114fc.
2023-07-10 15:01:46 +01:00
Hassan Ben Jobrane
6e40b114fc chore: use node 18 for GH actions 2023-07-10 13:58:11 +01:00
Hassan Ben Jobrane
77acf1385d Revert "chore: increase ci timeout"
This reverts commit cec7edd2d5.
2023-07-10 12:23:22 +01:00
Hassan Ben Jobrane
cec7edd2d5 chore: increase ci timeout 2023-07-10 10:50:55 +01:00
Hassan Ben Jobrane
9dbbdb3121 fix: show only when a repo is connected 2023-07-07 19:24:09 +01:00
Hassan Ben Jobrane
79d2602648 fix: tweak warning in dark mode 2023-07-07 18:52:24 +01:00
Hassan Ben Jobrane
b0363a4f4c Merge pull request #2110 from nhost/changeset-release/main
chore: update versions
2023-07-07 17:18:43 +01:00
github-actions[bot]
17045b2018 chore: update versions 2023-07-07 16:07:03 +00:00
Hassan Ben Jobrane
c49cc11862 Merge pull request #2108 from nhost/feat/fix-hasura-storage-file-upload
fix(hasura-storage-js): fix file upload
2023-07-07 17:05:33 +01:00
Hassan Ben Jobrane
c83fe7d776 chore(e2e): change e2e tests timeout 2023-07-07 16:48:49 +01:00
Hassan Ben Jobrane
235b4c7405 chore: wrap secret values in quotes 2023-07-07 16:08:24 +01:00
Hassan Ben Jobrane
c2c0fbd33a chore(e2e): increase timeout 2023-07-07 15:19:02 +01:00
Hassan Ben Jobrane
300e3f49e0 chore: add changeset 2023-07-07 14:21:12 +01:00
Hassan Ben Jobrane
a95a77886b fix(hasura-storage-js): fix file upload 2023-07-07 10:44:42 +01:00
Stephan van Opstal
1f3f683202 Update serverless-functions.mdx (#2105)
Please correct me if I'm wrong but I believe the endpoints in the docs
are wrong.
2023-07-07 08:36:57 +02:00
github-actions[bot]
4c67fd23c4 chore: update versions (#2101)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@0.19.0

### Minor Changes

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

### Patch Changes

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

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-06 20:15:13 +02:00
Hassan Ben Jobrane
93d8d71e34 Merge pull request #2102 from nhost/feat/pull-config-warning
feat(settings): add warning to pull config
2023-07-06 15:06:08 +01:00
Hassan Ben Jobrane
47bda15ff2 chore: add changeset 2023-07-06 14:39:27 +01:00
Hassan Ben Jobrane
4563488b5d feat(settings): show alert when there's a repo 2023-07-06 14:35:31 +01:00
Hassan Ben Jobrane
8fd35f3fea feat(settings): add warning to pull config 2023-07-06 14:26:14 +01:00
David Barroso
9c61c69a7b chore(dashboard):add postgres 14.6-20230705-1 to the version selector (#2100) 2023-07-06 15:24:06 +02:00
github-actions[bot]
030ad4621e chore: update versions (#2098)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@0.18.0

### Minor Changes

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

## @nhost/docs@0.4.0

### Minor Changes

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

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


# Releases
## @nhost/dashboard@0.17.19

### Patch Changes

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

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-29 11:19:53 +02:00
Szilárd Dóró
f866120a65 fix(dashboard): use dynamic validation schema for password editing (#2082)
Fixes #2081
2023-06-29 10:52:40 +02:00
159 changed files with 5371 additions and 461 deletions

View File

@@ -26,10 +26,10 @@ runs:
path: ${{ steps.pnpm-cache-dir.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-node-
- name: Use Node.js v16
- name: Use Node.js v18
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- shell: bash
name: Install packages
run: pnpm install --frozen-lockfile

View File

@@ -10,6 +10,7 @@ on:
- '**.md'
- '!.changeset/**'
- 'LICENSE'
workflow_dispatch:
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
@@ -99,7 +100,7 @@ jobs:
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v4
timeout-minutes: 60
timeout-minutes: 90
with:
context: .
file: ./dashboard/Dockerfile

View File

@@ -146,7 +146,7 @@ jobs:
run: echo "NHOST_TEST_DASHBOARD_URL=https://${{ steps.fetch-dashboard-preview-url.outputs.preview_url }}" >> $GITHUB_ENV
# * Run the `ci` script of the current package of the matrix. Dependencies build is cached by Turborepo
- name: Run e2e tests
timeout-minutes: 7
timeout-minutes: 15
run: pnpm --filter="${{ matrix.package.name }}" run e2e
- id: file-name
if: ${{ failure() }}

View File

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

View File

@@ -7,7 +7,8 @@ import baseLibConfig from './vite.lib.config'
export default defineConfig({
...baseLibConfig,
optimizeDeps: {
include: ['react/jsx-runtime']
include: ['react/jsx-runtime'],
exclude: ['react-hook-form']
},
plugins: [react({ jsxRuntime: 'classic' }), ...baseLibConfig.plugins]
})

View File

@@ -1,5 +1,114 @@
# @nhost/dashboard
## 0.20.7
### Patch Changes
- 4a7ede11e: fix: distinguish files that were not uploaded
- 202b64723: feat(nhost-run): add support for one-click-install run services
- 074a0fa11: feat(dashboard): add settings toggle to enable/disable antivirus
- @nhost/react-apollo@5.0.33
- @nhost/nextjs@1.13.35
## 0.20.6
### Patch Changes
- b20761e97: feat(services): add pricing info and confirmation dialog
- 90df6d81d: fix(services): handle null values when editing a service
- aa8508467: fix: query service logs correctly
feat: enable multiline support for environment value input
## 0.20.5
### Patch Changes
- 8d7f84b8d: fix: make announcement adapt to theme
## 0.20.4
### Patch Changes
- 3b75bfce2: fix: make announcement close properly
- f49819075: fix: show correct values when dedicated resources are disabled
## 0.20.3
### Patch Changes
- e643bd362: fix(services): fix errors when config is null
- bcdab66bf: feat: add annoucement for nhost run
- f967a2e59: added note about storage not being able to be downsized
- 311c7756d: chore(services): consistent naming for compute
## 0.20.2
### Patch Changes
- 9073182d5: chore(dashboard): bump `turbo` to 1.10.11
- ece717d6e: feat(logs): show services in the logs page
- 82b335311: feat(metrics): change grafana link to point to the dashboards
- b135ef695: fix(services): set command as optional and set min replicas to 0
## 0.20.1
### Patch Changes
- 3d5c34f4c: fix(auth): fix users pagination limit
## 0.20.0
### Minor Changes
- c99d117d1: feat(services): add support for custom services
## 0.19.2
### Patch Changes
- face99ccd: chore(deps): bump turbo version
- cfe527307: style: tweak pull config warning in dark mode
- a9d7da8af: chore(deps): update dependency @types/pluralize to ^0.0.30
- 9aa4371ef: chore: add hasura-auth version 0.21.2
- d14e112bf: chore(deps): update dependency prettier-plugin-tailwindcss to ^0.4.0
- d3e8bb94a: chore(deps): update dependency vite-plugin-dts to v3
## 0.19.1
### Patch Changes
- @nhost/react-apollo@5.0.32
- @nhost/nextjs@1.13.34
## 0.19.0
### Minor Changes
- 9c61c69a7: chore(dashboard):add postgres 14.6-20230705-1 to the version selector
### Patch Changes
- 47bda15ff: feat(settings): add warning to pull config
## 0.18.0
### Minor Changes
- ee0b9b8ed: chore(dashboard):add hasura v2.28.2 and v2.29.0 to the version selector
## 0.17.20
### Patch Changes
- @nhost/react-apollo@5.0.31
- @nhost/nextjs@1.13.33
## 0.17.19
### Patch Changes
- f866120a6: fix(users): use the password length from the config
## 0.17.18
### Patch Changes

View File

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

View File

@@ -9,7 +9,7 @@ import { openProject } from '@/e2e/utils';
import { chromium } from '@playwright/test';
async function globalTeardown() {
const browser = await chromium.launch();
const browser = await chromium.launch({ slowMo: 1000 });
const context = await browser.newContext({
baseURL: TEST_DASHBOARD_URL,
@@ -46,18 +46,23 @@ async function globalTeardown() {
await hasuraPage.locator('a', { hasText: /data/i }).click();
await hasuraPage.getByRole('link', { name: /sql/i }).click();
await hasuraPage.locator('#raw_sql > textarea').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 $$;
`);
// Set the value of the Ace code editor using JavaScript evaluation in the browser context
await hasuraPage.evaluate(() => {
const editor = ace.edit('raw_sql');
editor.setValue(`
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();

View File

@@ -0,0 +1,5 @@
query InitQuery {
root {
enableServices
}
}

5
dashboard/hypertune.json Normal file
View File

@@ -0,0 +1,5 @@
{
"projectId": 2596,
"token": "U2FsdGVkX19+V8BJnVR0xLEC+42OW5qZl/A0i6beAaRmJoIhFh5Yf6eIKBzLbV9h",
"outputDirectoryPath": "src/hypertune"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.17.18",
"version": "0.20.7",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -54,6 +54,7 @@
"graphql-request": "^6.0.0",
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.11.2",
"hypertune": "^1.4.4",
"just-kebab-case": "^4.1.1",
"lodash.debounce": "^4.0.8",
"next": "^12.3.1",
@@ -65,12 +66,14 @@
"react-error-boundary": "^4.0.0",
"react-hook-form": "^7.42.1",
"react-hot-toast": "^2.4.0",
"react-intersection-observer": "^9.5.2",
"react-is": "18.2.0",
"react-loading-skeleton": "^2.2.0",
"react-merge-refs": "^1.1.0",
"react-syntax-highlighter": "^15.4.5",
"react-table": "^7.8.0",
"sharp": "^0.32.0",
"shell-quote": "^1.8.1",
"slugify": "^1.6.5",
"stripe": "^10.17.0",
"tailwind-merge": "^1.8.0",
@@ -101,12 +104,15 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
"@types/ace": "^0.0.48",
"@types/bcryptjs": "^2.4.2",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.29",
"@types/pluralize": "^0.0.30",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react-table": "^7.7.12",
"@types/shell-quote": "^1.7.1",
"@types/testing-library__jest-dom": "^5.14.5",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^5.43.0",
@@ -136,7 +142,7 @@
"postcss": "^8.4.19",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^3.2.0",
"prettier-plugin-tailwindcss": "^0.3.0",
"prettier-plugin-tailwindcss": "^0.4.0",
"react-date-fns-hooks": "^0.9.4",
"require-from-string": "^2.0.2",
"snake-case": "^3.0.4",

View File

@@ -0,0 +1,62 @@
import { Button } from '@/components/ui/v2/Button';
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { XIcon } from '@/components/ui/v2/icons/XIcon';
import { Text } from '@/components/ui/v2/Text';
import { forwardRef, type ForwardedRef } from 'react';
import { twMerge } from 'tailwind-merge';
import AnnouncementContainer, {
type AnnouncementContainerProps,
} from './AnnouncementContainer';
export interface AnnouncementProps extends AnnouncementContainerProps {
/**
* Function called when the announcement is closed.
*/
onClose?: VoidFunction;
/**
* The href to use for the announcement link.
*/
href: string;
}
function Announcement(
{ children, slotProps, onClose, href, ...props }: AnnouncementProps,
ref: ForwardedRef<HTMLDivElement>,
) {
return (
<AnnouncementContainer
{...props}
ref={ref}
className="grid grid-flow-col justify-between gap-4"
slotProps={{
root: {
...(slotProps?.root || {}),
className: twMerge('w-full py-1.5', slotProps?.root?.className),
},
}}
>
<span />
<div className="flex items-center self-center truncate">
<a href={href}>
<Text className="cursor-pointer truncate hover:underline">
{children}
</Text>
</a>
<ArrowRightIcon className="ml-1 h-4 w-4 text-white" />
</div>
<Button
variant="borderless"
onClick={onClose}
aria-label="Close announcement"
size="small"
className="rounded-sm p-1"
>
<XIcon className="opacity-65 h-4 w-4" />
</Button>
</AnnouncementContainer>
);
}
export default forwardRef(Announcement);

View File

@@ -0,0 +1,66 @@
import {
createElement,
forwardRef,
type DetailedHTMLProps,
type ElementType,
type ForwardedRef,
type HTMLProps,
type PropsWithoutRef,
} from 'react';
import { twMerge } from 'tailwind-merge';
export interface AnnouncementContainerProps
extends PropsWithoutRef<
DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>
> {
/**
* Custom component to render as.
*/
component?: ElementType<any>;
/**
* Props passed to component slots.
*/
slotProps?: {
/**
* Props passed to the root component.
*/
root?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
/**
* Props passed to the content component.
*/
content?: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement>;
};
}
function AnnouncementContainer(
{
component = 'div',
className,
children,
slotProps,
...props
}: AnnouncementContainerProps,
ref: ForwardedRef<HTMLDivElement>,
) {
return createElement(
component,
{
...props,
...(slotProps?.root || {}),
ref,
className: twMerge('w-full overflow-hidden', slotProps?.root?.className),
},
<div
{...(slotProps?.content || {})}
className={twMerge(
'mx-auto max-w-7xl px-5',
className,
slotProps?.content?.className,
)}
>
{children}
</div>,
);
}
export default forwardRef(AnnouncementContainer);

View File

@@ -0,0 +1,92 @@
import { Divider } from '@/components/ui/v2/Divider';
import {
createContext,
useEffect,
useMemo,
useState,
type PropsWithChildren,
type ReactNode,
} from 'react';
import { useInView } from 'react-intersection-observer';
import Announcement from './Announcement';
interface AnnouncementType {
id: string;
content: ReactNode;
href: string;
}
export interface AnnouncementContextProps {
/**
* The announcement to show.
*/
announcement?: AnnouncementType;
/**
* Whether or not to show the announcement.
*/
showAnnouncement?: boolean;
/**
* Function to close the announcement.
*/
handleClose?: () => void;
/**
* Whether or not the announcement is in view.
*/
inView?: boolean;
}
// Note: You can define the active announcement here.
const announcement: AnnouncementType = {
id: 'nhost-run',
href: 'https://discord.com/invite/9V7Qb2U',
content:
'Now you can bring custom and third-party OSS services to run alongside your Nhost projects',
};
export const AnnouncementContext = createContext<AnnouncementContextProps>({});
export default function AnnouncementProvider({ children }: PropsWithChildren) {
const { ref, inView } = useInView();
const [showAnnouncement, setShowAnnouncement] = useState(false);
useEffect(() => {
if (
typeof window === 'undefined' ||
!announcement ||
window.localStorage.getItem(announcement.id) === '1'
) {
return;
}
setShowAnnouncement(true);
}, []);
function handleClose() {
setShowAnnouncement(false);
window.localStorage.setItem(announcement?.id, '1');
}
const announcementValue = useMemo(
() => ({ showAnnouncement, announcement, handleClose, inView }),
[inView, showAnnouncement],
);
return (
<AnnouncementContext.Provider value={announcementValue}>
{announcement && showAnnouncement && (
<>
<Announcement
ref={ref}
href={announcement.href}
onClose={handleClose}
>
{announcement.content}
</Announcement>
<Divider />
</>
)}
{children}
</AnnouncementContext.Provider>
);
}

View File

@@ -0,0 +1,3 @@
export * from './Announcement';
export * from './AnnouncementProvider';
export { default as useAnnouncement } from './useAnnouncement';

View File

@@ -0,0 +1,14 @@
import { useContext } from 'react';
import { AnnouncementContext } from './AnnouncementProvider';
export default function useAnnouncement() {
const context = useContext(AnnouncementContext);
if (!context) {
throw new Error(
'useAnnouncement must be used within an AnnouncementProvider',
);
}
return context;
}

View File

@@ -15,6 +15,7 @@ export type PaginationProps = DetailedHTMLProps<
* Total number of pages.
*/
totalNrOfPages: number;
/**
* Number of total elements per page.
*/
@@ -23,6 +24,10 @@ export type PaginationProps = DetailedHTMLProps<
* Total number of elements.
*/
totalNrOfElements: number;
/**
* Label of the elements displayed ex: pages, users...
*/
itemsLabel: string;
/**
* Current page number.
*/
@@ -64,6 +69,7 @@ export default function Pagination({
elementsPerPage,
onPageChange,
totalNrOfElements,
itemsLabel,
...props
}: PaginationProps) {
return (
@@ -132,7 +138,7 @@ export default function Pagination({
{totalNrOfElements < currentPageNumber * elementsPerPage
? totalNrOfElements
: currentPageNumber * elementsPerPage}{' '}
of {totalNrOfElements} users
of {totalNrOfElements} {itemsLabel}
</Text>
</div>
</div>

View File

@@ -178,6 +178,22 @@ export default function DataGridBody<T extends object>({
}
}
const getBackgroundCellColor = (
row: Row<T>,
column: DataBrowserGridColumn<T>,
) => {
// Grey out files not uploaded
if (!row.values.isUploaded) {
return 'grey.200';
}
if (column.isDisabled) {
return 'grey.100';
}
return 'background.paper';
};
return (
<div {...getTableBodyProps()} ref={bodyRef} {...props}>
{rows.length === 0 && !loading && (
@@ -260,9 +276,7 @@ export default function DataGridBody<T extends object>({
})}
cell={cell}
sx={{
backgroundColor: column.isDisabled
? 'grey.100'
: 'background.paper',
backgroundColor: getBackgroundCellColor(row, column),
color: isCellDisabled ? 'text.secondary' : 'text.primary',
}}
className={twMerge(

View File

@@ -54,7 +54,7 @@ export default function Header({ className, ...props }: HeaderProps) {
sx={{ backgroundColor: 'background.paper' }}
{...props}
>
<div className="grid grid-flow-col items-center gap-3">
<div className="grid grid-flow-col items-center gap-3 ">
<NavLink href="/" className="w-12">
<Logo className="mx-auto cursor-pointer" />
</NavLink>

View File

@@ -114,7 +114,7 @@ export default function SettingsContainer({
<Box
{...root}
className={twMerge(
'grid grid-flow-row gap-4 rounded-lg border-1 py-4',
'grid grid-flow-row gap-4 overflow-hidden rounded-lg border-1 py-4',
root?.className || rootClassName,
)}
>

View File

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

View File

@@ -0,0 +1,40 @@
import type { IconProps } from '@/components/ui/v2/icons';
function CubeIcon(props: IconProps) {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14 11.0826V4.91742C14 4.8287 13.9764 4.74158 13.9316 4.665C13.8868 4.58841 13.8225 4.52513 13.7451 4.48163L8.24513 1.38788C8.17029 1.34578 8.08587 1.32367 8 1.32367C7.91413 1.32367 7.82971 1.34578 7.75487 1.38788L2.25487 4.48163C2.17754 4.52513 2.11318 4.58841 2.0684 4.665C2.02361 4.74158 2 4.8287 2 4.91742V11.0826C2 11.1713 2.02361 11.2584 2.0684 11.335C2.11318 11.4116 2.17754 11.4749 2.25487 11.5184L7.75487 14.6121C7.82971 14.6542 7.91413 14.6763 8 14.6763C8.08587 14.6763 8.17029 14.6542 8.24513 14.6121L13.7451 11.5184C13.8225 11.4749 13.8868 11.4116 13.9316 11.335C13.9764 11.2584 14 11.1713 14 11.0826Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.9311 4.66414L8.0594 8.00001L2.06934 4.66357"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8.05916 8L8.00049 14.6763"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
CubeIcon.displayName = 'NhostCubeIcon';
export default CubeIcon;

View File

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

View File

@@ -0,0 +1,27 @@
import type { IconProps } from '@/components/ui/v2/icons';
import { SvgIcon } from '@/components/ui/v2/icons/SvgIcon';
function ServicesIcon(props: IconProps) {
return (
<SvgIcon
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Services"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.89295 4.15125H9.21701C9.28097 4.15125 9.33291 4.09959 9.33326 4.03565V2.8556C9.33291 2.79163 9.28097 2.73999 9.21701 2.73999H7.89295C7.82909 2.73999 7.77734 2.79174 7.77734 2.8556V4.03562C7.77734 4.09948 7.82911 4.15125 7.89295 4.15125ZM5.53406 5.84862H4.21001C4.14594 5.84826 4.09411 5.79643 4.09375 5.73236V4.55298C4.09411 4.48902 4.14606 4.43738 4.21001 4.43738H5.53406C5.5979 4.43738 5.64967 4.48912 5.64967 4.55298V5.73236C5.64967 5.79631 5.59801 5.84826 5.53406 5.84862ZM14.6307 6.48419C15.4316 6.48419 15.8114 6.77094 15.8521 6.80325L16 6.92016L15.9386 7.09971C15.8408 7.34738 15.69 7.57067 15.4968 7.75398C15.2062 8.04139 14.6791 8.38436 13.8221 8.38436H13.6839C13.337 9.26145 12.8707 10.2484 12.0879 11.1345C11.6196 11.6644 11.0689 12.1152 10.457 12.4696C9.71438 12.8901 8.90665 13.1835 8.06725 13.3376C7.4634 13.45 6.85036 13.5056 6.23616 13.5036C4.87658 13.5036 3.67717 13.2453 2.93893 12.7932C2.28012 12.3908 1.77374 11.7333 1.43337 10.8407C1.13576 10.0277 0.989105 9.1673 1.00063 8.30169C1.00204 8.04363 1.21146 7.83507 1.46954 7.83472H11.3503C11.471 7.8302 12.0678 7.77917 12.4399 7.57185C12.1318 7.08484 12.0446 6.51519 12.188 5.9087C12.2639 5.59123 12.3932 5.28898 12.5703 5.01479L12.7118 4.81068L12.9268 4.93471L12.9269 4.93473C12.9668 4.9583 13.8447 5.47632 13.9996 6.53843C14.2082 6.50325 14.4192 6.48511 14.6307 6.48419ZM3.7092 7.54529H2.38514C2.32128 7.54529 2.26953 7.49353 2.26953 7.42967V6.25029V6.24964C2.26953 6.1858 2.32128 6.13403 2.38514 6.13403H3.7092H3.70985C3.77369 6.13439 3.82516 6.18643 3.8248 6.25029V7.42969C3.8248 7.49353 3.77306 7.54529 3.7092 7.54529ZM4.21003 7.54529H5.53409C5.59794 7.54529 5.64969 7.49353 5.64969 7.42969V6.25029C5.65005 6.18643 5.59858 6.13439 5.53472 6.13403H5.53407H4.21001C4.14579 6.13403 4.09375 6.18607 4.09375 6.25029V7.42967C4.09413 7.49363 4.14606 7.54529 4.21003 7.54529ZM7.38597 7.54529H6.06191C5.99808 7.54529 5.94631 7.49353 5.94629 7.42967V6.25029V6.24964C5.94629 6.1858 5.99803 6.13403 6.06189 6.13403H7.38595H7.3866C7.45046 6.13439 7.50193 6.18643 7.50157 6.25029V7.42969C7.50157 7.49353 7.44983 7.54529 7.38597 7.54529ZM7.89295 7.54529H9.21701C9.28097 7.54529 9.33291 7.49365 9.33326 7.42969V6.25029C9.33326 6.18607 9.28122 6.13403 9.21701 6.13403H7.89295C7.82909 6.13403 7.77734 6.1858 7.77734 6.24964V6.25029V7.42967C7.77734 7.49353 7.82911 7.54529 7.89295 7.54529ZM6.06189 5.84862H7.38595C7.4499 5.84826 7.50156 5.79631 7.50156 5.73236V4.55298C7.50156 4.48912 7.44979 4.43738 7.38595 4.43738H6.06189C5.99804 4.43738 5.94629 4.48915 5.94629 4.55298V5.73236C5.94629 5.79631 5.99795 5.84826 6.06189 5.84862ZM9.21701 5.84862H7.89295C7.82901 5.84826 7.77734 5.79631 7.77734 5.73236V4.55298C7.77734 4.48915 7.82909 4.43738 7.89295 4.43738H9.21701C9.28097 4.43738 9.33291 4.48902 9.33326 4.55298V5.73236C9.33291 5.79643 9.28108 5.84826 9.21701 5.84862ZM11.0637 7.54529H9.73963C9.67579 7.54529 9.62402 7.49353 9.62402 7.42967V6.25029V6.24964C9.62402 6.1858 9.67579 6.13403 9.73963 6.13403H11.0637C11.1279 6.13403 11.1799 6.18607 11.1799 6.25029V7.42969C11.1796 7.49365 11.1277 7.54529 11.0637 7.54529Z"
fill="currentColor"
/>
</SvgIcon>
);
}
ServicesIcon.displayName = 'NhostServicesIcon';
export default ServicesIcon;

View File

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

View File

@@ -29,6 +29,7 @@ export type AuthServiceVersionFormValues = Yup.InferType<
>;
const AVAILABLE_AUTH_VERSIONS = [
'0.21.2',
'0.20.1',
'0.20.0',
'0.19.3',

View File

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

View File

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

View File

@@ -31,6 +31,8 @@ export type HasuraServiceVersionFormValues = Yup.InferType<
>;
const AVAILABLE_HASURA_VERSIONS = [
'v2.29.0-ce',
'v2.28.2-ce',
'v2.27.0-ce',
'v2.25.1-ce',
'v2.25.0-ce',

View File

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

View File

@@ -0,0 +1,12 @@
import { useEffect, useState } from 'react';
export default function useHostName() {
const [hostName, setHostName] = useState('');
useEffect(() => {
const { port, hostname, protocol } = window.location;
setHostName(`${protocol}//${hostname}:${port}`);
}, []);
return hostName;
}

View File

@@ -8,11 +8,13 @@ import { GraphQLIcon } from '@/components/ui/v2/icons/GraphQLIcon';
import { HasuraIcon } from '@/components/ui/v2/icons/HasuraIcon';
import { HomeIcon } from '@/components/ui/v2/icons/HomeIcon';
import { RocketIcon } from '@/components/ui/v2/icons/RocketIcon';
import { ServicesIcon } from '@/components/ui/v2/icons/ServicesIcon';
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
import type { SvgIconProps } from '@/components/ui/v2/icons/SvgIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useHypertune } from '@/hooks/useHypertune';
import type { ReactElement } from 'react';
export interface ProjectRoute {
@@ -56,8 +58,26 @@ export interface ProjectRoute {
export default function useProjectRoutes() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const { currentProject, loading: currentProjectLoading } =
useCurrentWorkspaceAndProject();
const {
currentWorkspace,
currentProject,
loading: currentProjectLoading,
} = useCurrentWorkspaceAndProject();
const hypertune = useHypertune();
const enableServices =
currentWorkspace &&
hypertune
.root({
context: {
workSpace: {
id: currentWorkspace.id,
},
},
})
.enableServices({})
.get(false);
const nhostRoutes: ProjectRoute[] = [
{
@@ -98,7 +118,7 @@ export default function useProjectRoutes() {
},
];
const allRoutes: ProjectRoute[] = [
let allRoutes: ProjectRoute[] = [
{
relativePath: '/',
exact: true,
@@ -136,9 +156,19 @@ export default function useProjectRoutes() {
label: 'Storage',
icon: <StorageIcon />,
},
...nhostRoutes,
];
if (enableServices) {
allRoutes.push({
relativePath: '/services',
exact: false,
label: 'Run',
icon: <ServicesIcon />,
});
}
allRoutes = [...allRoutes, ...nhostRoutes];
return {
nhostRoutes,
allRoutes,

View File

@@ -69,7 +69,7 @@ test('should generate a per service subdomain in remote mode', () => {
);
expect(generateAppServiceUrl('test', region, 'grafana')).toBe(
'https://test.grafana.eu-west-1.nhost.run',
'https://test.grafana.eu-west-1.nhost.run/dashboards',
);
});
@@ -102,7 +102,7 @@ test('should generate staging subdomains in staging environment', () => {
);
expect(generateAppServiceUrl('test', stagingRegion, 'grafana')).toBe(
'https://test.grafana.eu-west-1.staging.nhost.run',
'https://test.grafana.eu-west-1.staging.nhost.run/dashboards',
);
});
@@ -120,7 +120,7 @@ test('should generate no slug for Hasura and Grafana neither in local mode nor i
'https://test.hasura.eu-west-1.staging.nhost.run',
);
expect(generateAppServiceUrl('test', stagingRegion, 'grafana')).toBe(
'https://test.grafana.eu-west-1.staging.nhost.run',
'https://test.grafana.eu-west-1.staging.nhost.run/dashboards',
);
process.env.NEXT_PUBLIC_ENV = 'production';
@@ -129,7 +129,7 @@ test('should generate no slug for Hasura and Grafana neither in local mode nor i
'https://test.hasura.eu-west-1.nhost.run',
);
expect(generateAppServiceUrl('test', region, 'grafana')).toBe(
'https://test.grafana.eu-west-1.nhost.run',
'https://test.grafana.eu-west-1.nhost.run/dashboards',
);
});

View File

@@ -102,5 +102,11 @@ export default function generateAppServiceUrl(
.filter(Boolean)
.join('.');
return `https://${constructedDomain}${remoteBackendSlugs[service]}`;
let url = `https://${constructedDomain}${remoteBackendSlugs[service]}`;
if (service === 'grafana') {
url = `${url}/dashboards`;
}
return url;
}

View File

@@ -10,6 +10,7 @@ import type { LogsCustomInterval } from '@/features/projects/logs/utils/constant
import { LOGS_AVAILABLE_INTERVALS } from '@/features/projects/logs/utils/constants/intervals';
import type { AvailableLogsService } from '@/features/projects/logs/utils/constants/services';
import { LOGS_AVAILABLE_SERVICES } from '@/features/projects/logs/utils/constants/services';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { subMinutes } from 'date-fns';
import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge';
@@ -132,6 +133,37 @@ export default function LogsHeader({
const { currentProject } = useCurrentWorkspaceAndProject();
const applicationCreationDate = new Date(currentProject.createdAt);
const [runServices, setRunServices] = useState<
{
label: string;
value: string;
}[]
>([]);
const { data, loading } = useGetRunServicesQuery({
variables: {
appID: currentProject.id,
resolve: false,
limit: 1000,
offset: 0,
},
});
useEffect(() => {
if (!loading) {
const services = data.app?.runServices ?? [];
setRunServices(
services
.filter((s) => !!s.config?.name)
.map((s) => ({
label: s.config.name,
value: `run-${s.config.name}`,
})),
);
}
}, [loading, data]);
/**
* Will subtract the `customInterval` time in minutes from the current date.
*/
@@ -181,15 +213,17 @@ export default function LogsHeader({
root: { className: 'min-h-[initial] h-9 leading-[initial]' },
}}
>
{LOGS_AVAILABLE_SERVICES.map(({ value, label }) => (
<Option
key={value}
value={value}
className="text-sm+ font-medium"
>
{label}
</Option>
))}
{[...LOGS_AVAILABLE_SERVICES, ...runServices].map(
({ value, label }) => (
<Option
key={value}
value={value}
className="text-sm+ font-medium"
>
{label}
</Option>
),
)}
</Select>
</Box>
</Box>

View File

@@ -68,11 +68,15 @@ export default function ResourcesConfirmationDialog({
const totalBillableVCPU = formValues.enabled ? billableResources.vcpu : 0;
const totalBillableMemory = formValues.enabled ? billableResources.memory : 0;
const updatedPrice =
Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
) + proPlan.price;
const { enabled } = formValues;
const updatedPrice = enabled
? Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE,
) + proPlan.price
: proPlan.price;
if (!loading && !proPlan) {
return (
@@ -86,18 +90,30 @@ export default function ResourcesConfirmationDialog({
throw error;
}
const databaseVCPU = enabled ? formValues.database.vcpu : 0;
const databaseMemory = enabled ? formValues.database.memory : 0;
const hasuraVCPU = enabled ? formValues.hasura.vcpu : 0;
const hasuraMemory = enabled ? formValues.hasura.memory : 0;
const authVCPU = enabled ? formValues.auth.vcpu : 0;
const authMemory = enabled ? formValues.auth.memory : 0;
const storageVCPU = enabled ? formValues.storage.vcpu : 0;
const storageMemory = enabled ? formValues.storage.memory : 0;
const databaseResources = `${prettifyVCPU(
formValues.database.vcpu,
)} vCPU + ${prettifyMemory(formValues.database.memory)}`;
const hasuraResources = `${prettifyVCPU(
formValues.hasura.vcpu,
)} vCPU + ${prettifyMemory(formValues.hasura.memory)}`;
const authResources = `${prettifyVCPU(
formValues.auth.vcpu,
)} vCPU + ${prettifyMemory(formValues.auth.memory)}`;
databaseVCPU,
)} vCPU + ${prettifyMemory(databaseMemory)}`;
const hasuraResources = `${prettifyVCPU(hasuraVCPU)} vCPU + ${prettifyMemory(
hasuraMemory,
)}`;
const authResources = `${prettifyVCPU(authVCPU)} vCPU + ${prettifyMemory(
authMemory,
)}`;
const storageResources = `${prettifyVCPU(
formValues.storage.vcpu,
)} vCPU + ${prettifyMemory(formValues.storage.memory)}`;
storageVCPU,
)} vCPU + ${prettifyMemory(storageMemory)}`;
return (
<div className="grid grid-flow-row gap-6 px-6 pb-6">

View File

@@ -48,6 +48,21 @@ export const MIN_SERVICE_VCPU = 0.25 * RESOURCE_VCPU_MULTIPLIER;
*/
export const MAX_SERVICE_VCPU = 7 * RESOURCE_VCPU_MULTIPLIER;
/**
* Best resource utilization ration for CPU-Memory.
*/
export const MEM_CPU_RATIO = 2.048;
/**
* Minimum storage capacity (Gib)
*/
export const MIN_STORAGE_CAPACITY = 1;
/**
* Maximum storage capacity (Gib)
*/
export const MAX_STORAGE_CAPACITY = 1000;
/**
* The minimum amount of memory that has to be allocated per service.
*/
@@ -135,3 +150,9 @@ export const resourceSettingsValidationSchema = Yup.object({
export type ResourceSettingsFormValues = Yup.InferType<
typeof resourceSettingsValidationSchema
>;
export const MIN_SERVICES_CPU = Math.floor(128 / MEM_CPU_RATIO);
export const MIN_SERVICES_MEM = 128;
export const MAX_SERVICES_CPU = 7000;
export const MAX_SERVICES_MEM = Math.floor(MAX_SERVICES_CPU * MEM_CPU_RATIO);
export const COST_PER_VCPU = 0.05;

View File

@@ -0,0 +1,477 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Form } from '@/components/form/Form';
import { Alert } from '@/components/ui/v2/Alert';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useHostName } from '@/features/projects/common/hooks/useHostName';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import {
COST_PER_VCPU,
MAX_SERVICES_CPU,
MAX_SERVICES_MEM,
MAX_SERVICE_REPLICAS,
MIN_SERVICES_CPU,
MIN_SERVICES_MEM,
} from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { ComputeFormSection } from '@/features/services/components/ServiceForm/components/ComputeFormSection';
import { EnvironmentFormSection } from '@/features/services/components/ServiceForm/components/EnvironmentFormSection';
import { PortsFormSection } from '@/features/services/components/ServiceForm/components/PortsFormSection';
import { ReplicasFormSection } from '@/features/services/components/ServiceForm/components/ReplicasFormSection';
import { StorageFormSection } from '@/features/services/components/ServiceForm/components/StorageFormSection';
import type { DialogFormProps } from '@/types/common';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import {
useInsertRunServiceConfigMutation,
useInsertRunServiceMutation,
useReplaceRunServiceConfigMutation,
type ConfigRunServiceConfigInsertInput,
} from '@/utils/__generated__/graphql';
import type { ApolloError } from '@apollo/client';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { parse } from 'shell-quote';
import * as Yup from 'yup';
import { ServiceConfirmationDialog } from './components/ServiceConfirmationDialog';
export enum PortTypes {
HTTP = 'http',
TCP = 'tcp',
UDP = 'udp',
}
export const validationSchema = Yup.object({
name: Yup.string().required('The name is required.'),
image: Yup.string().label('Image to run'),
command: Yup.string(),
environment: Yup.array().of(
Yup.object().shape({
name: Yup.string().required(),
value: Yup.string().required(),
}),
),
compute: Yup.object({
cpu: Yup.number().min(MIN_SERVICES_CPU).max(MAX_SERVICES_CPU).required(),
memory: Yup.number().min(MIN_SERVICES_MEM).max(MAX_SERVICES_MEM).required(),
}),
replicas: Yup.number().min(0).max(MAX_SERVICE_REPLICAS).required(),
ports: Yup.array().of(
Yup.object().shape({
port: Yup.number().required(),
type: Yup.mixed<PortTypes>().oneOf(Object.values(PortTypes)).required(),
publish: Yup.boolean().default(false),
}),
),
storage: Yup.array().of(
Yup.object()
.shape({
name: Yup.string().required(),
path: Yup.string().required(),
capacity: Yup.number().nonNullable().required(),
})
.required(),
),
});
export type ServiceFormValues = Yup.InferType<typeof validationSchema>;
export interface ServiceFormProps extends DialogFormProps {
/**
* To use in conjunction with initialData to allow for updating the service
*/
serviceID?: string;
/**
* if there is initialData then it's an update operation
*/
initialData?: ServiceFormValues;
/**
* Function to be called when the operation is cancelled.
*/
onCancel?: VoidFunction;
/**
* Function to be called when the submit is successful.
*/
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
}
export default function ServiceForm({
serviceID,
initialData,
onSubmit,
onCancel,
location,
}: ServiceFormProps) {
const hostName = useHostName();
const { onDirtyStateChange, openDialog, closeDialog } = useDialog();
const [insertRunService] = useInsertRunServiceMutation();
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertRunServiceConfig] = useInsertRunServiceConfigMutation();
const [replaceRunServiceConfig] = useReplaceRunServiceConfigMutation();
const [createServiceFormError, setCreateServiceFormError] =
useState<Error | null>(null);
const form = useForm<ServiceFormValues>({
defaultValues: initialData ?? {
compute: {
cpu: 62,
memory: 128,
},
replicas: 1,
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
const {
watch,
register,
formState: { errors, isSubmitting, dirtyFields },
} = form;
const formValues = watch();
const serviceImage = watch('image');
const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => {
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
const getFormattedConfig = (values: ServiceFormValues) => {
const config: ConfigRunServiceConfigInsertInput = {
name: values.name,
image: {
image: values.image,
},
command: parse(values.command).map((item) => item.toString()),
resources: {
compute: {
cpu: values.compute.cpu,
memory: values.compute.memory,
},
storage: values.storage.map((item) => ({
name: item.name,
path: item.path,
capacity: item.capacity,
})),
replicas: values.replicas,
},
environment: values.environment.map((item) => ({
name: item.name,
value: item.value,
})),
ports: values.ports.map((item) => ({
port: item.port,
type: item.type,
publish: item.publish,
})),
};
return config;
};
const createOrUpdateService = async (values: ServiceFormValues) => {
const config = getFormattedConfig(values);
if (serviceID) {
// Update service config
await replaceRunServiceConfig({
variables: {
appID: currentProject.id,
serviceID,
config,
},
});
} else {
// Insert service config
const {
data: {
insertRunService: { id: newServiceID },
},
} = await insertRunService({
variables: {
object: {
appID: currentProject.id,
},
},
});
await insertRunServiceConfig({
variables: {
appID: currentProject.id,
serviceID: newServiceID,
config: {
...config,
image: {
// If the image field left empty then we auto-populate following this format
// registry.<region>.<nhost_domain>/<service_id>
image:
values.image.length > 0
? values.image
: `registry.${currentProject.region.awsName}.${currentProject.region.domain}/${newServiceID}`,
},
},
},
});
}
};
const handleSubmit = async (values: ServiceFormValues) => {
try {
await toast.promise(
createOrUpdateService(values),
{
loading: 'Configuring the service...',
success: `The service has been configured successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while configuring the service. Please try again.'
);
},
},
getToastStyleProps(),
);
// await refetchWorkspaceAndProject();
// refestch the services
onSubmit?.();
} catch {
// Note: The toast will handle the error.
}
};
const handleConfirm = (values: ServiceFormValues) => {
openDialog({
title: 'Confirm Resources',
component: (
<ServiceConfirmationDialog
formValues={values}
onCancel={closeDialog}
onSubmit={async () => {
await handleSubmit(formValues);
}}
/>
),
});
};
const pricingExplanation = () => {
const vCPUs = `${formValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER} vCPUs`;
const mem = `${formValues.compute.memory} MiB Mem`;
let details = `${vCPUs} + ${mem}`;
if (formValues.replicas > 1) {
details = `(${details}) x ${formValues.replicas} replicas`;
}
return `Approximate cost for ${details}`;
};
const copyConfig = () => {
const config = getFormattedConfig(formValues);
const base64Config = btoa(JSON.stringify(config));
const link = `${hostName}/run-one-click-install?config=${base64Config}`;
copy(link, 'Service Config');
};
return (
<FormProvider {...form}>
<Form
onSubmit={handleConfirm}
className="grid grid-flow-row gap-4 px-6 pb-6"
>
<Input
{...register('name')}
id="name"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Name</Text>
<Tooltip title="Name of the service, must be unique per project.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder="Service name"
hideEmptyHelperText
error={!!errors.name}
helperText={errors?.name?.message}
fullWidth
autoComplete="off"
autoFocus
/>
<Input
{...register('image')}
id="image"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Image</Text>
<Tooltip
title={
<span>
Image to use, it can be hosted on any public registry or it
can use the{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/registry"
className="underline"
>
Nhost registry
</a>
. Image needs to support arm.
</span>
}
>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder="To automatically fill the private registry, leave it blank."
hideEmptyHelperText
error={!!errors.image}
helperText={errors?.image?.message}
fullWidth
autoComplete="off"
/>
{/* This shows only when trying to edit a service */}
{serviceID && serviceImage && (
<InfoCard
title="Private registry"
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
/>
)}
<Input
{...register('command')}
id="command"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Command</Text>
<Tooltip title="Command to run when to start the service. This is optional as the image may already have a baked-in command.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
}
placeholder="$ npm start"
hideEmptyHelperText
error={!!errors.command}
helperText={errors?.command?.message}
fullWidth
autoComplete="off"
/>
<Alert
severity="info"
className="flex items-center justify-between space-x-2"
>
<span>{pricingExplanation()}</span>
<b>
$
{parseFloat(
(
formValues.compute.cpu *
formValues.replicas *
COST_PER_VCPU
).toFixed(2),
)}
</b>
</Alert>
<ComputeFormSection />
<ReplicasFormSection />
<EnvironmentFormSection />
<PortsFormSection />
<StorageFormSection />
{createServiceFormError && (
<Alert
severity="error"
className="grid grid-flow-col items-center justify-between px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {createServiceFormError.message}
</span>
<Button
variant="borderless"
color="error"
size="small"
onClick={() => {
setCreateServiceFormError(null);
}}
>
Clear
</Button>
</Alert>
)}
<div className="grid grid-flow-row gap-2">
<div className="grid grid-cols-2 gap-2">
<Button
type="submit"
disabled={isSubmitting}
startIcon={<PlusIcon />}
>
{serviceID ? 'Update' : 'Create'}
</Button>
<Button
color="secondary"
variant="outlined"
disabled={isSubmitting}
onClick={copyConfig}
startIcon={<CopyIcon />}
>
Copy one-click install link
</Button>
</div>
<Button variant="outlined" color="secondary" onClick={onCancel}>
Cancel
</Button>
</div>
</Form>
</FormProvider>
);
}

View File

@@ -0,0 +1,103 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { ArrowLeftIcon } from '@/components/ui/v2/icons/ArrowLeftIcon';
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { Slider } from '@/components/ui/v2/Slider';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import {
MAX_SERVICES_MEM,
MEM_CPU_RATIO,
MIN_SERVICES_MEM,
} from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
import { useFormContext, useWatch } from 'react-hook-form';
export default function ComputeFormSection() {
const { setValue } = useFormContext<ServiceFormValues>();
const formValues = useWatch<ServiceFormValues>();
const handleSliderUpdate = (value: string) => {
const updatedMem = parseFloat(value);
if (Number.isNaN(updatedMem) || updatedMem < MIN_SERVICES_MEM) {
return;
}
setValue('compute.memory', Math.floor(updatedMem), { shouldDirty: true });
setValue('compute.cpu', Math.floor(updatedMem / MEM_CPU_RATIO), {
shouldDirty: true,
});
};
const incrementCompute = () => {
const newMemoryValue = formValues.compute.memory + 128;
setValue('compute.memory', newMemoryValue);
setValue('compute.cpu', Math.floor(newMemoryValue / MEM_CPU_RATIO));
};
const decrementCompute = () => {
const newMemoryValue = formValues.compute.memory - 128;
setValue('compute.memory', newMemoryValue);
setValue('compute.cpu', Math.floor(newMemoryValue / MEM_CPU_RATIO));
};
return (
<Box className="space-y-4 rounded border-1 p-4">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
vCPUs: {formValues.compute.cpu / 1000} / Memory:{' '}
{formValues.compute.memory}
</Text>
<Tooltip
title={
<span>
Compute resources dedicated for the service. Refer to{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/resources"
className="underline"
>
resources
</a>{' '}
for more information.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Box className="flex flex-row items-center justify-between space-x-4">
<Button
disabled={formValues.compute.memory <= MIN_SERVICES_MEM}
variant="outlined"
onClick={decrementCompute}
>
<ArrowLeftIcon className="h-4 w-4" />
</Button>
<Slider
value={Number(formValues.compute.memory)}
onChange={(_event, value) => handleSliderUpdate(value.toString())}
max={MAX_SERVICES_MEM}
min={MIN_SERVICES_MEM}
step={256}
aria-label="Compute resources"
marks
/>
<Button
disabled={formValues.compute.memory >= MAX_SERVICES_MEM}
variant="outlined"
onClick={incrementCompute}
>
<ArrowRightIcon className="h-4 w-4" />
</Button>
</Box>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,97 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
import { useState } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
export default function EnvironmentFormSection() {
const {
register,
formState: { errors },
} = useFormContext<ServiceFormValues>();
const [focusedInput, setFocusedInput] = useState<string>(null);
const { fields, append, remove } = useFieldArray({
name: 'environment',
});
return (
<Box className="space-y-4 rounded border-1 p-4">
<Box className="flex flex-row items-center justify-between ">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
Environment
</Text>
<Tooltip
title={
<span>
Environment variables to add to the service. Other than the ones
specified here only <code>NHOST_SUBDOMAIN</code> and{' '}
<code>NHOST_REGION</code> are added automatically to the
service.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Button
variant="borderless"
onClick={() => append({ name: '', value: '' })}
>
<PlusIcon className="h-5 w-5" />
</Button>
</Box>
<Box className="flex flex-col space-y-4">
{fields.map((field, index) => (
<Box key={field.id} className="flex w-full items-center space-x-2">
<div className="flex w-full flex-col space-y-2">
<Input
{...register(`environment.${index}.name`)}
id={`${field.id}-name`}
placeholder={`Key ${index}`}
className="w-full"
hideEmptyHelperText
error={!!errors?.environment?.at(index)}
helperText={errors?.environment?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register(`environment.${index}.value`)}
id={`${field.id}-value`}
placeholder={`Value ${index}`}
className="w-full"
hideEmptyHelperText
error={!!errors?.environment?.at(index)}
helperText={errors?.environment?.at(index)?.message}
fullWidth
autoComplete="off"
multiline
maxRows={focusedInput === `${field.id}-value` ? 1000 : 1}
onFocusCapture={() => setFocusedInput(`${field.id}-value`)}
onBlurCapture={() => setFocusedInput(null)}
/>
</div>
<Button
variant="borderless"
className=""
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-6 w-4" />
</Button>
</Box>
))}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,2 @@
// eslint-disable-next-line import/no-cycle
export { default as EnvironmentFormSection } from './EnvironmentFormSection';

View File

@@ -0,0 +1,156 @@
import { ControlledSwitch } from '@/components/form/ControlledSwitch';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import {
PortTypes,
type ServiceFormValues,
} from '@/features/services/components/ServiceForm';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
export default function PortsFormSection() {
const form = useFormContext<ServiceFormValues>();
const { currentProject } = useCurrentWorkspaceAndProject();
const {
register,
setValue,
formState: { errors },
} = form;
const { fields, append, remove } = useFieldArray({
name: 'ports',
});
const formValues = useWatch<ServiceFormValues>();
const onChangePortType = (value: string | undefined, index: number) =>
setValue(`ports.${index}.type`, value as PortTypes);
const showURL = (index: number) =>
formValues.ports[index]?.type === PortTypes.HTTP &&
formValues.ports[index]?.publish;
const getPortURL = (_port: string | number, _name: string) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
const name = _name && _name.length > 0 ? _name : '[name]';
return `https://${currentProject?.subdomain}-${name}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
};
return (
<Box className="space-y-4 rounded border-1 p-4">
<Box className="flex flex-row items-center justify-between ">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
Ports
</Text>
<Tooltip
title={
<span>
Network ports to configure for the service. Refer to{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/networking"
className="underline"
>
Networking
</a>{' '}
for more information.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Button
variant="borderless"
onClick={() => append({ port: null, type: null, publish: false })}
>
<PlusIcon className="h-5 w-5" />
</Button>
</Box>
<Box className="flex flex-col space-y-4">
{fields.map((field, index) => (
<Box key={field.id} className="flex flex-col space-y-2">
<Box className="flex w-full flex-col space-y-2 xs+:flex-row xs+:space-x-2 xs+:space-y-0">
<Input
{...register(`ports.${index}.port`)}
id={`${field.id}-port`}
placeholder="Port"
className="w-full"
hideEmptyHelperText
error={!!errors?.ports?.at(index)}
helperText={errors?.ports?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<Select
fullWidth
value={formValues.ports.at(index)?.type || ''}
onChange={(_event, inputValue) =>
onChangePortType(inputValue as string, index)
}
placeholder="Select port type"
slotProps={{
listbox: { className: 'min-w-0 w-full' },
popper: {
disablePortal: false,
className: 'z-[10000] w-[270px] w-full',
},
}}
>
{['http', 'tcp', 'udp']?.map((portType) => (
<Option key={portType} value={portType}>
{portType}
</Option>
))}
</Select>
<ControlledSwitch
{...register(`ports.${index}.publish`)}
disabled={false} // TODO turn off and disable if the port is not http
label={
<Text variant="subtitle1" component="span">
Publish
</Text>
}
/>
<Button
variant="borderless"
className=""
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</Box>
{showURL(index) && (
<InfoCard
title="URL"
value={getPortURL(
formValues.ports[index]?.port,
formValues.name,
)}
/>
)}
</Box>
))}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,2 @@
/* eslint-disable import/no-cycle */
export { default as PortsFormSection } from './PortsFormSection';

View File

@@ -0,0 +1,61 @@
import { Box } from '@/components/ui/v2/Box';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { Slider } from '@/components/ui/v2/Slider';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { MAX_SERVICE_REPLICAS } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
import { useFormContext, useWatch } from 'react-hook-form';
export default function ReplicasFormSection() {
const { setValue } = useFormContext<ServiceFormValues>();
const { replicas } = useWatch<ServiceFormValues>();
const handleReplicasChange = (value: string) => {
const updatedReplicas = parseInt(value, 10);
setValue('replicas', updatedReplicas, { shouldDirty: true });
// TODO Trigger revalidate storage
};
return (
<Box className="space-y-4 rounded border-1 p-4">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
Replicas ({replicas})
</Text>
<Tooltip
title={
<span>
Number of replicas for the service. Multiple replicas can process
requests/work in parallel. You can set replicas to 0 to pause the
service. Refer to{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/resources"
className="underline"
>
resources
</a>{' '}
for more information.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Slider
value={replicas}
onChange={(_event, value) => handleReplicasChange(value.toString())}
min={0}
max={MAX_SERVICE_REPLICAS}
step={1}
aria-label="Replicas"
marks
/>
</Box>
);
}

View File

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

View File

@@ -0,0 +1,87 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Divider } from '@/components/ui/v2/Divider';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
export interface ServiceConfirmationDialogProps {
/**
* The updated resources that the user has selected.
*/
formValues: ServiceFormValues;
/**
* Function to be called when the user clicks the cancel button.
*/
onCancel: () => void;
/**
* Function to be called when the user clicks the confirm button.
*/
onSubmit: () => Promise<void>;
}
export default function ServiceConfirmationDialog({
formValues,
onCancel,
onSubmit,
}: ServiceConfirmationDialogProps) {
const approximatePriceForService = parseFloat(
(formValues.compute.cpu * formValues.replicas * COST_PER_VCPU).toFixed(2),
);
return (
<div className="grid grid-flow-row gap-6 px-6 pb-6">
<Box className="grid grid-flow-row gap-4">
<Box className="grid grid-flow-row gap-1.5">
<Box className="grid grid-flow-col items-center justify-between gap-2">
<Box className="grid grid-flow-row gap-0.5">
<Text color="secondary">vCPUs</Text>
</Box>
<Text>{formValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER}</Text>
</Box>
<Box className="grid grid-flow-col items-center justify-between gap-2">
<Box className="grid grid-flow-row gap-0.5">
<Text color="secondary">Memory</Text>
</Box>
<Text>{formValues.compute.memory} MiB</Text>
</Box>
<Box className="grid grid-flow-col items-center justify-between gap-2">
<Box className="grid grid-flow-row gap-0.5">
<Text color="secondary">Replicas</Text>
</Box>
<Text>{formValues.replicas}</Text>
</Box>
</Box>
<Divider />
<Box className="grid grid-flow-col justify-between gap-2">
<Box className="grid grid-flow-col items-center gap-1.5">
<Text className="font-medium">Approximate Cost</Text>
<Tooltip title="$0.0012/minute for every 1 vCPU and 2 GiB of RAM">
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Text>${approximatePriceForService}/mo</Text>
</Box>
</Box>
<Box className="grid grid-flow-row gap-2">
<Button color="primary" onClick={onSubmit} autoFocus>
Confirm
</Button>
<Button variant="borderless" color="secondary" onClick={onCancel}>
Cancel
</Button>
</Box>
</div>
);
}

View File

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

View File

@@ -0,0 +1,147 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import {
MAX_STORAGE_CAPACITY,
MIN_STORAGE_CAPACITY,
} from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import type { ServiceFormValues } from '@/features/services/components/ServiceForm';
import { useFieldArray, useFormContext } from 'react-hook-form';
export default function StorageFormSection() {
const {
register,
setValue,
formState: { errors },
} = useFormContext<ServiceFormValues>();
const { fields, append, remove } = useFieldArray({
name: 'storage',
});
const checkBounds = (value: string, index: number) => {
const storageCapacity = parseInt(value, 10);
if (Number.isNaN(storageCapacity)) {
setValue(`storage.${index}.capacity`, 1);
}
if (storageCapacity > MAX_STORAGE_CAPACITY) {
setValue(`storage.${index}.capacity`, MAX_STORAGE_CAPACITY);
}
if (storageCapacity < MIN_STORAGE_CAPACITY) {
setValue(`storage.${index}.capacity`, MIN_STORAGE_CAPACITY);
}
};
return (
<Box className="space-y-4 rounded border-1 p-4">
<Box className="flex flex-row items-center justify-between ">
<Box className="flex flex-row items-center space-x-2">
<Text variant="h4" className="font-semibold">
Storage
</Text>
<Tooltip
title={
<span>
By default, services do not have persistent storage. You can add
SSD disks to the service here. It is important to note that
capacity can not be decreased after creation, only expanded. Refer to{' '}
<a
target="_blank"
rel="noopener noreferrer"
href="https://docs.nhost.io/run/storage"
className="underline"
>
Storage
</a>{' '}
for more information.
</span>
}
>
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
</Tooltip>
</Box>
<Button
variant="borderless"
onClick={() => append({ name: '', capacity: 1, path: '' })}
>
<PlusIcon className="h-5 w-5" />
</Button>
</Box>
<Box className="flex flex-col space-y-4">
{fields.map((field, index) => (
<Box
key={field.id}
className="flex w-full flex-col space-y-2 xs+:flex-row xs+:space-y-0 xs+:space-x-2"
>
<Input
{...register(`storage.${index}.name`)}
id={`${field.id}-name`}
label={!index && 'Name'}
placeholder="Name"
className="w-full"
hideEmptyHelperText
error={!!errors?.storage?.at(index)}
helperText={errors?.storage?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<Input
{...register(`storage.${index}.capacity`, {
onBlur: (event) => checkBounds(event.target.value, index),
})}
id={`${field.id}-capacity`}
label={!index && 'Capacity'}
type="number"
placeholder="Capacity"
className="w-full"
hideEmptyHelperText
error={!!errors?.storage?.at(index)}
helperText={errors?.storage?.at(index)?.message}
fullWidth
autoComplete="off"
endAdornment={
<Text sx={{ color: 'grey.500' }} className="pr-2">
GiB
</Text>
}
/>
<Input
{...register(`storage.${index}.path`)}
id={`${field.id}-path`}
label={!index && 'Path'}
placeholder="Path"
className="w-full"
hideEmptyHelperText
error={!!errors?.storage?.at(index)}
helperText={errors?.storage?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<Button
variant="borderless"
className=""
color="error"
onClick={() => remove(index)}
>
<TrashIcon className="h-4 w-4" />
</Button>
</Box>
))}
</Box>
</Box>
);
}

View File

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

View File

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

View File

@@ -0,0 +1,219 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Box } from '@/components/ui/v2/Box';
import { Divider } from '@/components/ui/v2/Divider';
import { Dropdown } from '@/components/ui/v2/Dropdown';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { CubeIcon } from '@/components/ui/v2/icons/CubeIcon';
import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon';
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
ServiceForm,
type PortTypes,
} from '@/features/services/components/ServiceForm';
import { getToastStyleProps } from '@/utils/constants/settings';
import { copy } from '@/utils/copy';
import {
useDeleteRunServiceConfigMutation,
useDeleteRunServiceMutation,
} from '@/utils/__generated__/graphql';
import type { ApolloError } from '@apollo/client';
import { formatDistanceToNow } from 'date-fns';
import type { RunService } from 'pages/[workspaceSlug]/[appSlug]/services';
import { toast } from 'react-hot-toast';
interface ServicesListProps {
/**
* The run services fetched from entering the users page.
*/
services: RunService[];
/**
* Function to be called after a submitting the form for either creating or updating a service.
*
* @example onDelete={() => refetch()}
*/
onCreateOrUpdate?: () => Promise<any>;
/**
* Function to be called after a successful delete action.
*
*/
onDelete?: () => Promise<any>;
}
export default function ServicesList({
services,
onCreateOrUpdate,
onDelete,
}: ServicesListProps) {
const { openDrawer } = useDialog();
const [deleteRunService] = useDeleteRunServiceMutation();
const { currentProject } = useCurrentWorkspaceAndProject();
const [deleteRunServiceConfig] = useDeleteRunServiceConfigMutation();
const deleteServiceAndConfig = async (appID: string, serviceID: string) => {
await deleteRunService({ variables: { serviceID } });
await deleteRunServiceConfig({ variables: { appID, serviceID } });
await onDelete?.();
};
const viewService = async (service: RunService) => {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<Text>Edit {service.config?.name ?? 'unset'}</Text>
</Box>
),
component: (
<ServiceForm
serviceID={service.id}
initialData={{
...service.config,
image: service.config?.image?.image,
command: service.config?.command?.join(' '),
ports: service.config?.ports?.map((item) => ({
port: item.port,
type: item.type as PortTypes,
publish: item.publish,
})),
compute: service.config?.resources?.compute ?? {
cpu: 62,
memory: 128,
},
replicas: service.config?.resources?.replicas,
storage: service.config?.resources?.storage,
}}
onSubmit={() => onCreateOrUpdate()}
/>
),
});
};
const deleteService = async (serviceID: string) => {
await toast.promise(
deleteServiceAndConfig(currentProject.id, serviceID),
{
loading: 'Deleteing the service...',
success: `The service has been deleted successfully.`,
error: (arg: ApolloError) => {
// we need to get the internal error message from the GraphQL error
const { internal } = arg.graphQLErrors[0]?.extensions || {};
const { message } = (internal as Record<string, any>)?.error || {};
// we use the default Apollo error message if we can't find the
// internal error message
return (
message ||
arg.message ||
'An error occurred while deleting the service. Please try again.'
);
},
},
getToastStyleProps(),
);
};
return (
<Box className="flex flex-col">
{services.map((service) => (
<Box
key={service.id}
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
sx={{
[`&:hover`]: {
backgroundColor: 'action.hover',
},
}}
>
<Box
onClick={() => viewService(service)}
className="flex w-full flex-row justify-between"
sx={{
backgroundColor: 'transparent',
}}
>
<div className="flex flex-1 flex-row items-center space-x-4">
<CubeIcon className="h-5 w-5" />
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{service.config?.name ?? 'unset'}
</Text>
<Tooltip title={service.updatedAt}>
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
Deployed {formatDistanceToNow(new Date(service.updatedAt))}{' '}
ago
</span>
</Tooltip>
</div>
</div>
<div className="hidden flex-row items-center space-x-2 md:flex">
<Text variant="subtitle1" className="font-mono text-xs">
{service.id}
</Text>
<IconButton
variant="borderless"
color="secondary"
onClick={(event) => {
copy(service.id, 'Service Id');
event.stopPropagation();
}}
aria-label="Service Id"
>
<CopyIcon className="h-4 w-4" />
</IconButton>
</div>
</Box>
<Dropdown.Root>
<Dropdown.Trigger
asChild
hideChevron
onClick={(event) => event.stopPropagation()}
>
<IconButton
variant="borderless"
color="secondary"
aria-label="More options"
onClick={(event) => event.stopPropagation()}
>
<DotsHorizontalIcon />
</IconButton>
</Dropdown.Trigger>
<Dropdown.Content
menu
PaperProps={{ className: 'w-52' }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<Dropdown.Item
onClick={() => viewService(service)}
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<UserIcon className="h-4 w-4" />
<Text className="font-medium">View Service</Text>
</Dropdown.Item>
<Divider component="li" />
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() => deleteService(service.id)}
>
<TrashIcon className="h-4 w-4" />
<Text className="font-medium" color="error">
Delete Service
</Text>
</Dropdown.Item>
</Dropdown.Content>
</Dropdown.Root>
</Box>
))}
</Box>
);
}

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ query GetStorageSettings($appId: uuid!) {
__typename
storage {
version
antivirus {
server
}
}
}
}

View File

@@ -0,0 +1,5 @@
mutation deleteRunService($serviceID: uuid!) {
deleteRunService(id: $serviceID) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation deleteRunServiceConfig($appID: uuid!, $serviceID: uuid!) {
deleteRunServiceConfig(appID: $appID, serviceID: $serviceID) {
name
}
}

View File

@@ -0,0 +1,33 @@
query getRunService($id: uuid!, $resolve: Boolean!) {
runService(id: $id) {
id
config(resolve: $resolve) {
name
image {
image
}
command
resources {
compute {
cpu
memory
}
storage {
name
path
capacity
}
replicas
}
environment {
name
value
}
ports {
port
type
publish
}
}
}
}

View File

@@ -0,0 +1,48 @@
query getRunServices(
$appID: uuid!
$resolve: Boolean!
$limit: Int!
$offset: Int!
) {
app(id: $appID) {
runServices(limit: $limit, offset: $offset) {
id
createdAt
updatedAt
config(resolve: $resolve) {
name
image {
image
}
command
resources {
compute {
cpu
memory
}
storage {
name
path
capacity
}
replicas
}
environment {
name
value
}
ports {
port
type
publish
}
}
}
runServices_aggregate {
aggregate {
count
}
}
}
}

View File

@@ -0,0 +1,6 @@
mutation insertRunService($object: run_service_insert_input!) {
insertRunService(object: $object) {
id
appID
}
}

View File

@@ -0,0 +1,13 @@
mutation insertRunServiceConfig(
$appID: uuid!
$serviceID: uuid!
$config: ConfigRunServiceConfigInsertInput!
) {
insertRunServiceConfig(
appID: $appID
serviceID: $serviceID
config: $config
) {
name
}
}

View File

@@ -0,0 +1,13 @@
mutation replaceRunServiceConfig(
$appID: uuid!
$serviceID: uuid!
$config: ConfigRunServiceConfigInsertInput!
) {
replaceRunServiceConfig(
appID: $appID
serviceID: $serviceID
config: $config
) {
__typename
}
}

View File

@@ -0,0 +1,13 @@
mutation updateRunServiceConfig(
$appID: uuid!
$serviceID: uuid!
$config: ConfigRunServiceConfigUpdateInput!
) {
updateRunServiceConfig(
appID: $appID
serviceID: $serviceID
config: $config
) {
name
}
}

View File

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

View File

@@ -0,0 +1,14 @@
import hypertune from '@/hypertune/hypertune';
import { useEffect, useState } from 'react';
export default function useHypertune() {
const [, setIsInitialized] = useState<boolean>(hypertune.isInitialized());
useEffect(() => {
hypertune.waitForInitialization().then(() => {
setIsInitialized(true);
});
}, []);
return hypertune;
}

View File

@@ -0,0 +1,5 @@
import { initializeHypertune } from './project_2596';
const hypertune = initializeHypertune({});
export default hypertune;

View File

@@ -0,0 +1,107 @@
/* eslint-disable */
import * as sdk from "hypertune";
const projectId = 2596;
const businessToken = `U2FsdGVkX19+V8BJnVR0xLEC+42OW5qZl/A0i6beAaRmJoIhFh5Yf6eIKBzLbV9h`;
const queryCode = `query InitQuery {
root {
enableServices
}
}
`;
const query = {"Query":{"objectTypeName":"Query","selection":{"root":{"fieldArguments":{"__isPartialObject__":true},"fieldQuery":{"Root":{"objectTypeName":"Root","selection":{"enableServices":{"fieldArguments":{},"fieldQuery":null}}}}}}}};
const fallbackInitData: sdk.FallbackInitData & { [key: string]: unknown } = {"commitId":3297,"reducedExpression":{"id":"caxyeQqTKX3UGOXClvbnW","logs":{"events":{},"exposures":{},"evaluations":{}},"type":"ObjectExpression","fields":{"root":{"id":"PoMWxsy7KbW9fCq5XXvx4","body":{"id":"IUICRjZ7iSnh9k0cWBmnd","logs":{"events":{},"exposures":{},"evaluations":{}},"type":"ObjectExpression","fields":{"enableServices":{"id":"7WZWy2AIy_q9Vbz4cn9KB","logs":{"evaluations":{"XNOtHkUBpglrY1nkYa_bf":1},"events":{},"exposures":{}},"type":"BooleanExpression","value":true,"valueType":{"type":"BooleanValueType"}}},"valueType":{"type":"ObjectValueType","objectTypeName":"Root"},"objectTypeName":"Root"},"logs":{"events":{},"exposures":{},"evaluations":{}},"type":"FunctionExpression","valueType":{"type":"FunctionValueType","returnValueType":{"type":"ObjectValueType","objectTypeName":"Root"},"parameterValueTypes":[{"type":"ObjectValueType","objectTypeName":"Query_root_args"}]},"parameters":[{"id":"Ygjhl2LqjiwcousTABFQz","name":"rootArgs"}]}},"metadata":{"permissions":{"user":{},"group":{"team":{"write":"allow"}}}},"valueType":{"type":"ObjectValueType","objectTypeName":"Query"},"objectTypeName":"Query"},"splits":{},"eventTypes":{},"commitConfig":{"splitConfig":{}},"initLogId":0,"commitHash":"4178461588049503","sdkConfig":{"hashPollInterval":1000,"flushLogsInterval":1000,"maxLogsPerFlush":1},"query":{"Query":{"objectTypeName":"Query","selection":{"root":{"fieldArguments":{"__isPartialObject__":true},"fieldQuery":{"Root":{"objectTypeName":"Root","selection":{"enableServices":{"fieldArguments":{},"fieldQuery":null}}}}}}}}};
export function initializeHypertune(
variableValues: Rec,
options: sdk.InitializeOptions = {}
): QueryNode {
const defaultOptions = { businessToken, query, fallbackInitData };
return sdk.initialize(
QueryNode,
projectId,
queryCode,
variableValues,
{ ...defaultOptions, ...options }
);
}
// Enum types
// Input object types
export type Rec = {
//
};
export type Rec2 = {
context: Rec3;
//
};
export type Rec3 = {
workSpace: Rec4;
//
};
export type Rec4 = {
id: string;
//
};
// Enum node classes
// Fragment node classes
export class QueryNode extends sdk.Node {
typeName = "Query" as const;
root(args: Rec2): RootNode {
const props0 = this.getField("root", args);
const expression0 = props0.expression;
if (
expression0 &&
expression0.type === "ObjectExpression"
&& expression0.objectTypeName === "Root"
) {
return new RootNode(props0);
}
const node = new RootNode(props0);
node._logUnexpectedTypeError();
return node;
}
}
export class RootNode extends sdk.Node {
typeName = "Root" as const;
enableServices(args: Rec): sdk.BooleanNode {
const props0 = this.getField("enableServices", args);
const expression0 = props0.expression;
if (
expression0 &&
expression0.type === "BooleanExpression"
) {
return new sdk.BooleanNode(props0);
}
const node = new sdk.BooleanNode(props0);
node._logUnexpectedTypeError();
return node;
}
}

View File

@@ -0,0 +1,262 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Pagination } from '@/components/common/Pagination';
import { Container } from '@/components/layout/Container';
import { ProjectLayout } from '@/components/layout/ProjectLayout';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { CubeIcon } from '@/components/ui/v2/icons/CubeIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { ServicesIcon } from '@/components/ui/v2/icons/ServicesIcon';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import {
ServiceForm,
type PortTypes,
} from '@/features/services/components/ServiceForm';
import ServicesList from '@/features/services/components/ServicesList/ServicesList';
import { useRouter } from 'next/router';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type ReactElement,
} from 'react';
export type RunService = Omit<
GetRunServicesQuery['app']['runServices'][0],
'__typename'
>;
export type RunServiceConfig = Omit<
GetRunServicesQuery['app']['runServices'][0]['config'],
'__typename'
>;
export default function ServicesPage() {
const limit = useRef(25);
const router = useRouter();
const { openDrawer, openAlertDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlanFree = currentProject.plan.isFree;
const [currentPage, setCurrentPage] = useState(
parseInt(router.query.page as string, 10) || 1,
);
const [nrOfPages, setNrOfPages] = useState(0);
const offset = useMemo(() => currentPage - 1, [currentPage]);
const {
data,
loading,
refetch: refetchServices,
} = useGetRunServicesQuery({
variables: {
appID: currentProject.id,
resolve: false,
limit: limit.current,
offset,
},
});
useEffect(() => {
if (loading) {
return;
}
const userCount = data?.app?.runServices_aggregate.aggregate.count ?? 0;
setNrOfPages(Math.ceil(userCount / limit.current));
}, [data, loading]);
const services = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
const checkConfigFromQuery = useCallback(
(base64Config: string) => {
if (router.query?.config) {
try {
const decodedConfig = atob(base64Config);
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<Text>Create a new run service</Text>
</Box>
),
component: (
<ServiceForm
initialData={{
...parsedConfig,
compute: parsedConfig?.resources?.compute ?? {
cpu: 62,
memory: 128,
},
image: parsedConfig?.image?.image,
command: parsedConfig?.command?.join(' '),
ports: parsedConfig?.ports.map((item) => ({
port: item.port,
type: item.type as PortTypes,
publish: item.publish,
})),
replicas: parsedConfig?.resources?.replicas,
storage: parsedConfig?.resources?.storage,
}}
onSubmit={refetchServices}
/>
),
});
} catch (error) {
openAlertDialog({
title: 'Configuration not set properly',
payload: 'The service configuration was not properly encoded',
props: {
primaryButtonText: 'Ok',
hideSecondaryAction: true,
},
});
}
}
},
[router.query.config, openDrawer, refetchServices, openAlertDialog],
);
useEffect(() => {
if (router.query?.config) {
checkConfigFromQuery(router.query?.config as string);
}
}, [checkConfigFromQuery, router.query]);
const openCreateServiceDialog = () => {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<Text>Create a new service</Text>
</Box>
),
component: <ServiceForm onSubmit={refetchServices} />,
});
};
if (isPlanFree) {
return (
<Container>
<UpgradeNotification
message="Unlock Nhost Run by upgrading your project to the Pro plan."
className="mt-4"
/>
</Container>
);
}
if (data?.app.runServices.length === 0 && !loading) {
return (
<Container className="mx-auto max-w-9xl space-y-5 overflow-x-hidden">
<div className="flex flex-row place-content-end">
<Button
variant="contained"
color="primary"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Add service
</Button>
</div>
<Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
<ServicesIcon className="h-10 w-10" />
<div className="flex flex-col space-y-1">
<Text className="text-center font-medium" variant="h3">
No custom services are available
</Text>
<Text variant="subtitle1" className="text-center">
All your projects custom services will be listed here.
</Text>
</div>
<div className="flex flex-row place-content-between rounded-lg ">
<Button
variant="contained"
color="primary"
className="w-full"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Add service
</Button>
</div>
</Box>
</Container>
);
}
return (
<div className="flex flex-col">
<Box className="flex flex-row place-content-end border-b-1 p-4">
<Button
variant="contained"
color="primary"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Add service
</Button>
</Box>
<Box className="space-y-4">
<ServicesList
services={services}
onDelete={() => refetchServices()}
onCreateOrUpdate={() => refetchServices()}
/>
<Pagination
className="px-2"
totalNrOfPages={nrOfPages}
currentPageNumber={currentPage}
totalNrOfElements={
data?.app?.runServices_aggregate.aggregate.count ?? 0
}
itemsLabel="services"
elementsPerPage={limit.current}
onPrevPageClick={async () => {
setCurrentPage((page) => page - 1);
if (currentPage - 1 !== 1) {
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage - 1 },
});
}
}}
onNextPageClick={async () => {
setCurrentPage((page) => page + 1);
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage + 1 },
});
}}
onPageChange={async (page) => {
setCurrentPage(page);
await router.push({
pathname: router.pathname,
query: { ...router.query, page },
});
}}
/>
</Box>
</div>
);
}
ServicesPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -3,6 +3,7 @@ import { SettingsLayout } from '@/components/layout/SettingsLayout';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { StorageServiceVersionSettings } from '@/features/storage/settings/components/HasuraServiceVersionSettings';
import { HasuraStorageAVSettings } from '@/features/storage/settings/components/HasuraStorageAVSettings';
import { useGetStorageSettingsQuery } from '@/utils/__generated__/graphql';
import type { ReactElement } from 'react';
@@ -34,6 +35,7 @@ export default function StorageSettingsPage() {
rootClassName="bg-transparent"
>
<StorageServiceVersionSettings />
<HasuraStorageAVSettings />
</Container>
);
}

View File

@@ -347,6 +347,7 @@ export default function UsersPage() {
.count
: dataRemoteAppUsers?.usersAggregate?.aggregate?.count
}
itemsLabel="users"
elementsPerPage={
searchString
? dataRemoteAppUsers?.filteredUsersAggreggate.aggregate

View File

@@ -1,3 +1,4 @@
import AnnouncementProvider from '@/components/common/Announcement/AnnouncementProvider';
import { DialogProvider } from '@/components/common/DialogProvider';
import { UIProvider } from '@/components/common/UIProvider';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
@@ -105,7 +106,9 @@ function MyApp({
>
<RetryableErrorBoundary>
<DialogProvider>
{getLayout(<Component {...pageProps} />)}
<AnnouncementProvider>
{getLayout(<Component {...pageProps} />)}
</AnnouncementProvider>
</DialogProvider>
</RetryableErrorBoundary>
</ThemeProvider>

View File

@@ -0,0 +1,218 @@
import { useDialog } from '@/components/common/DialogProvider';
import { AuthenticatedLayout } from '@/components/layout/AuthenticatedLayout';
import { Container } from '@/components/layout/Container';
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Input } from '@/components/ui/v2/Input';
import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import {
useGetAllWorkspacesAndProjectsQuery,
type GetAllWorkspacesAndProjectsQuery,
} from '@/utils/__generated__/graphql';
import { Divider } from '@mui/material';
import { useUserData } from '@nhost/nextjs';
import debounce from 'lodash.debounce';
import Image from 'next/image';
import { useRouter } from 'next/router';
import type { ChangeEvent, ReactElement } from 'react';
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
type Workspace = Omit<
GetAllWorkspacesAndProjectsQuery['workspaces'][0],
'__typename'
>;
export default function SelectWorkspaceAndProject() {
const user = useUserData();
const router = useRouter();
const { openAlertDialog } = useDialog();
const { data, loading } = useGetAllWorkspacesAndProjectsQuery({
skip: !user,
});
const workspaces: Workspace[] = data?.workspaces || [];
const projects = workspaces.flatMap((workspace) =>
workspace.projects.map((project) => ({
workspaceName: workspace.name,
projectName: project.name,
value: `${workspace.slug}/${project.slug}`,
isFree: project.plan.isFree,
})),
);
const [filter, setFilter] = useState('');
const handleFilterChange = useMemo(
() =>
debounce((event: ChangeEvent<HTMLInputElement>) => {
setFilter(event.target.value);
}, 200),
[],
);
useEffect(() => () => handleFilterChange.cancel(), [handleFilterChange]);
const checkConfigFromQuery = useCallback(
(base64Config: string) => {
try {
JSON.parse(atob(base64Config));
} catch (error) {
openAlertDialog({
title: 'Configuration not set properly',
payload:
'Either the link is wrong or the configuration is not properly encoded',
props: {
primaryButtonText: 'Ok',
hideSecondaryAction: true,
onPrimaryAction: async () => {
await router.push('/');
},
},
});
}
},
[openAlertDialog, router],
);
useEffect(() => {
checkConfigFromQuery(router.query?.config as string);
}, [checkConfigFromQuery, router.query]);
const goToServices = async (project: {
workspaceName: string;
projectName: string;
value: string;
isFree: boolean;
}) => {
if (!project) {
openAlertDialog({
title: 'Please select a workspace and a project',
payload:
'You must select a workspace and a project before proceeding to create the run service',
props: {
primaryButtonText: 'Ok',
hideSecondaryAction: true,
},
});
return;
}
if (project.isFree) {
openAlertDialog({
title: 'The project must have a pro plan',
payload: 'Creating run services is only availabel for pro projects',
props: {
primaryButtonText: 'Ok',
hideSecondaryAction: true,
},
});
return;
}
await router.push({
pathname: `/${project.value}/services`,
// Keep the same query params that got us here
query: router.query,
});
};
const projectsToDisplay = filter
? projects.filter((project) =>
project.projectName.toLowerCase().includes(filter.toLowerCase()),
)
: projects;
if (loading) {
return (
<ActivityIndicator
delay={500}
label="Loading workspaces and projects..."
/>
);
}
return (
<Container>
<div className="mx-auto grid max-w-[760px] grid-flow-row gap-4 py-6 sm:py-14">
<Text variant="h2" component="h1" className="">
New Run Service
</Text>
<InfoCard
title="Please select the workspace and the project where you want to create the service"
disableCopy
value=""
/>
<div>
<div className="mb-2 flex w-full">
<Input
placeholder="Search..."
onChange={handleFilterChange}
fullWidth
autoFocus
/>
</div>
<RetryableErrorBoundary>
{projectsToDisplay.length === 0 ? (
<Box className="h-import py-2">
<Text variant="subtitle2">No results found.</Text>
</Box>
) : (
<List className="h-import overflow-y-auto">
{projectsToDisplay.map((project, index) => (
<Fragment key={project.value}>
<ListItem.Root
className="grid grid-flow-col justify-start gap-2 py-2.5"
secondaryAction={
<Button
variant="borderless"
color="primary"
onClick={() => goToServices(project)}
>
Proceed
</Button>
}
>
<ListItem.Avatar>
<span className="inline-block h-6 w-6 overflow-hidden rounded-md">
<Image
src="/logos/new.svg"
alt="Nhost Logo"
width={24}
height={24}
/>
</span>
</ListItem.Avatar>
<ListItem.Text
primary={project.projectName}
secondary={`${project.workspaceName} / ${project.projectName}`}
/>
</ListItem.Root>
{index < projects.length - 1 && <Divider component="li" />}
</Fragment>
))}
</List>
)}
</RetryableErrorBoundary>
</div>
</div>
</Container>
);
}
SelectWorkspaceAndProject.getLayout = function getLayout(page: ReactElement) {
return (
<AuthenticatedLayout title="New Run Service">{page}</AuthenticatedLayout>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,10 @@ module.exports = {
extend: {
colors: {
github: '#24292E;',
brown: '#382D22',
copper: '#DD792D',
paper: '#171d26',
divider: '#2f363d',
},
boxShadow: {
outline: 'inset 0 0 0 2px rgba(0, 82, 205, 0.6)',

View File

@@ -18,6 +18,7 @@
"noImplicitAny": false,
"baseUrl": "./src",
"useUnknownInCatchVariables": false,
"types": ["@types/ace"],
"paths": {
"@/tests/*": ["tests/*"],
"@/e2e/*": ["../e2e/*"],
@@ -28,7 +29,8 @@
"@/styles/*": ["styles/*"],
"@/data/*": ["data/*"],
"@/generated/*": ["utils/__generated__/*"],
"@/features/*": ["features/*"]
"@/features/*": ["features/*"],
"@/hypertune/*": ["hypertune/*"]
},
"incremental": true
},

View File

@@ -1,5 +1,23 @@
# @nhost/docs
## 0.5.0
### Minor Changes
- 960d815f6: added docs for Nhost Run
## 0.4.0
### Minor Changes
- c6fa8da6d: fix(docs): remove outdated reference/cli
## 0.3.5
### Patch Changes
- 923abd365: chore(deps): update dependency @tsconfig/docusaurus to v2
## 0.3.4
### Patch Changes

View File

@@ -35,4 +35,4 @@ sudo nhost sw upgrade
- [Local Development](/cli/local-development)
- [Migrate to Nhost Config](/cli/migrate-config)
- [Multiple Projects in Parallel](/cli/multiple-projects)
- [CLI commands reference](/reference/cli)
- [CLI Documentation](/cli)

View File

@@ -28,6 +28,7 @@ Learn more about the product and features of Nhost.
- [Authentication](/authentication)
- [Storage](/storage)
- [Serverless Functions](/serverless-functions)
- [Run](/run)
## Architecture

View File

@@ -1,16 +0,0 @@
---
title: 'down'
sidebar_position: 3
---
Delete all containers created by `nhost up`
```bash
nhost down
```
To delete all containers **and the local database**, append `--data` to the command.
```bash
nhost down --data
```

View File

@@ -1,22 +0,0 @@
---
title: 'Global Flags'
sidebar_position: 9
---
### `--debug`, `-d`
Turn on debug output.
```bash
nhost up --debug
nhost init -d
```
### `--log-file`, `-f`
Save output to a given file.
```bash
nhost up -d --log-file some-file.txt
nhost logs -f some-file.txt
```

View File

@@ -1,6 +0,0 @@
---
title: 'CLI'
sidebar_position: 1
---
This section is a reference for the commands available in the [Nhost CLI](/cli).

View File

@@ -1,24 +0,0 @@
---
title: 'init'
sidebar_position: 1
---
Initialize a local Nhost project.
```
nhost init
```
If you have an existing Nhost project in Nhost Cloud that you want to use as a starting point for local development and for the [Git-based workflow](/platform/git), run `nhost init --remote`.
The `nhost init --remote` command does the following:
- Creates a new local Nhost project.
- Pulls the database migrations and Hasura metadata from the Nhost Cloud project.
- Resets the remote Nhost Cloud project's database migrations.
:::warning
The `nhost init --remote` command should only be run **once**. Running it multiple times will reset the remote Nhost Cloud project's database migrations which can cause migration conflict issues in your development team.
:::

View File

@@ -1,10 +0,0 @@
---
title: 'link'
sidebar_position: 4
---
Link the local Nhost project in your working directory to a project in Nhost Cloud.
```bash
nhost link
```

View File

@@ -1,10 +0,0 @@
---
title: 'list'
sidebar_position: 7
---
List projects in Nhost Cloud.
```bash
nhost list
```

View File

@@ -1,10 +0,0 @@
---
title: 'login'
sidebar_position: 5
---
Authenticate the CLI with your Nhost user.
```bash
nhost login
```

View File

@@ -1,10 +0,0 @@
---
title: 'logout'
sidebar_position: 6
---
Remove authentication for the CLI.
```bash
nhost logout
```

View File

@@ -1,10 +0,0 @@
---
title: 'logs'
sidebar_position: 9
---
View logs of all services.
```bash
nhost logs
```

View File

@@ -1,19 +0,0 @@
---
title: 'up'
sidebar_position: 2
---
To launch the development environment for your project, use the command `nhost up`. Once the environment is running, this command will
- Apply database migrations.
- Apply Hasura metadata.
```bash
nhost up
```
If it's the first time you start the project, [seed data](/database#seed-data) will be applied.
## Stop
Use `ctrl+c` to stop the development environment.

View File

@@ -1,10 +0,0 @@
---
title: 'upgrade'
sidebar_position: 8
---
Upgrade the CLI to the latest version.
```bash
nhost upgrade
```

View File

@@ -30,7 +30,3 @@ In this section:
- [Getting started](/reference/vue)
- [Protecting routes](/reference/vue/protecting-routes)
- [Apollo GraphQL](/reference/vue/apollo)
### Nhost CLI
- [CLI overview](/reference/cli)

View File

@@ -0,0 +1,37 @@
---
title: Getting Started
sidebar_label: Getting Started
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
To start with Nhost Run, you will need to create an Nhost project first. Then you can click on `Run` in the sidebar:
![click on run](/img/run/getting_started_1.png)
Then on `New Service`:
![click on New Service](/img/run/getting_started_2.png)
Now you can fill your [service configuration](/run/configuration):
![click on New Service](/img/run/getting_started_3.png)
As you configure the `Ports` section you can take note of the generated URL. You can find more information about this section under [Networking](/run/networking).
![copy the URL](/img/run/getting_started_4.png)
Once you are done configuring your service you can click on `Create`:
![click on create](/img/run/getting_started_5.png)
Now wait for the service to finish updating:
![wait for the service to finish updating](/img/run/getting_started_6.png)
Finally you can visit the URL you copied before:
![visit url](/img/run/getting_started_7.png)
And profit!

View File

@@ -0,0 +1,59 @@
---
title: Configuration
sidebar_label: Configuration
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
To configure a service you can use either the dashboard or a TOML configuration file with the following format:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![configuration](/img/run/configuration.png)
</TabItem>
<TabItem value="toml" label="toml">
```
name="mynodeapp"
[image]
image="registry.eu-central-1.nhost.run/b719c4a4-6fb2-4feb-8220-c7f01ef5bfe8:0.1.1"
[[environment]]
name="ENV"
value="prod"
[[ports]]
type="http"
port=3000
publish=true
[resources]
replicas=1
[resources.compute]
cpu=62
memory=128
[[resources.storage]]
name="data"
path="/var/lib/data"
capacity=1
```
</TabItem>
</Tabs>
::::info
Head to [CLI & CI deployments](/run/ci) for more details on how to deploy using a configuration file
::::
The `name` of the service is used as an identifier and to generate URLs when exposing the service to the Internet. You can use any container image publicly available or you can push your own to the [Nhost registry](/run/registry).
All environment variables set here are exclusive to this service and will not be shared with other services or with the Nhost stack. If you are using a configuration file secrets are supported.
For more details about the `Ports` section head to [networking](/run/networking). You can also head to [resources](/run/resources) for more information about replicas, compute, and storage.

View File

@@ -0,0 +1,119 @@
---
title: Networking
sidebar_label: Networking
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
Nhost Run services and the main Nhost stack share the same network, facilitating direct and efficient communication. This network connectivity offers low latency, high throughput, and eliminates egress costs.
## Connecting to the Nhost stack
To connect your service to the Nhost stack, use the following information:
- postgres: `postgres://postgres:<password>@postgres-service:5432/<subdomain>?sslmode=disable`
- hasura: base URL is `http://hasura-service:8080`
- hasura-auth: base URL is `http://hasura-auth-service:4000`
- hasura-storage: base URL is `http://hasura-storage-service:5000`
## Connecting to your service internally
To connect to your own service internally from another service, follow these steps:
1. Expose the desired port(s).
Example for Redis service:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![exposing a port](/img/run/configure-port.png)
</TabItem>
<TabItem value="toml" label="toml">
```
[[ports]]
type = "tcp"
port = 6379
publish = false
```
</TabItem>
</Tabs>
2. Once the port is exposed, you can connect to the service using its name and the corresponding port.
Example: `redis://user:password@redis:6379`
If needed, you can open internally more than one port by repeating the block for each one of them:
Example for a service exposing the ports tcp/3000 and udp/4000:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![exposing multiple ports](/img/run/configure-multiple-ports.png)
</TabItem>
<TabItem value="toml" label="toml">
```
[[ports]]
type = "tcp"
port = 3000
publish = false
[[ports]]
type = "udp"
port = 4000
publish = false
```
</TabItem>
</Tabs>
## Exposing Your Service to the Internet
To expose your service to the internet, follow these steps:
1. Update your configuration with the relevant port information:
Example for a nodejs service exposing an API on port 3000:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![expose http port](/img/run/configure-http-port.png)
</TabItem>
<TabItem value="toml" label="toml">
```
[[ports]]
type = "http"
port = 3000
publish = true
```
</TabItem>
</Tabs>
:::info
Currently, only services of type `http` can be exposed to the internet.
:::
2. Once the service of type `http` is published, you can connect to it using a URL with the following format:
`https://<subdomain>-<svc_name>-<port>.svc.<region>.nhost.run`
For example:
`https://zlbmqjfczuwqvsquujno-mysvc-3000.svc.eu-central-1.nhost.run`

View File

@@ -0,0 +1,106 @@
---
title: Resources
sidebar_label: Resources
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
## Compute
You can configure compute resources for your service like this:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![compute slider](/img/run/resources_1.png)
</TabItem>
<TabItem value="toml" label="toml">
```
[resources.compute]
cpu = 62
memory = 128
```
</TabItem>
</Tabs>
If you are configuring the resources via the configuration file instead of using the dashboard, keep in mind that cpu and memory allocation need to follow the following rules:
* The value of the `memory` parameter must be exactly 2,048 times the value of the cpu parameter.
* The value of the memory parameter must be within the range of 128 to 7,168 (inclusive).
Don't forget you can use `nhost run config-validate` command to verify your values are correct.
## Storage
By default a container's disk is ephemeral so data isn't persisted across reboots. If your service needs persistent storage you can attach one or more SSD disks by adding the following configuration:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![storage](/img/run/resources_2.png)
</TabItem>
<TabItem value="toml" label="toml">
``` toml
[[resources.storage]]
name="database"
path="/var/lib/db"
capacity=10
[[resources.storage]]
name="data"
path="/mnt/data"
capacity=1
```
</TabItem>
</Tabs>
With the above configuration, two disks will be provided. The first disk named `database` will have a capacity of 10 GiB and will be mounted at `/var/lib/db`. The second disk named `data` will have a capacity of 1 GiB and will be mounted at ``/mnt/data`.`
:::warning
Please note that renaming a disk will result in the destruction of the old disk and the creation of a new one, potentially leading to data loss.
:::
:::warning
Volume capacity can not be decreased. Once a volume with a given size has been created, its capacity can only by expanded.
:::
It's important to note that disks are unique to each service and cannot be shared across multiple services.
## Pausing a service
To pause a service, simply set its number of replicas to `0`:
<Tabs groupId="package-manager">
<TabItem value="dashboard" label="dashboard" default>
![pausing a service](/img/run/resources_3.png)
</TabItem>
<TabItem value="toml" label="toml">
```toml
[resources]
replicas = 0
```
</TabItem>
</Tabs>
When you pause a service, it will cease running while keeping any associated disk and registry intact.
:::info
While the service is paused, computing costs will not be charged. However, storage costs will continue to apply.
:::

View File

@@ -0,0 +1,96 @@
---
title: Registry
sidebar_label: Registry
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
## Creating a private repository for your image
We provide a private image registry you can push images to with each service. To make use of it you can start by creating a service and configuring it:
![configure service](/img/run/registry_1.png)
Note that we are leaving the Image empty, this will auto-populate the service with the provisioned registry. We are also setting `Replicas` to 0 to avoid starting the service and incurring costs before we have pushed the image.
Now you can click on Create:
![create service](/img/run/registry_2.png)
Now you can click on the newly created service:
![click on the new service](/img/run/registry_3.png)
And copy the image:
![copy the image](/img/run/registry_4.png)
## Configuring docker to use Nhost's registry
The CLI can configure docker automatically to be able to push and pull images from the docker registry. To do so you need to run the following command:
```
$ nhost docker-credentials configure
Installing credentials helper for docker in /usr/local/bin/docker-credential-nhost-login
I need root privileges to install the file. Please, enter your password.
Password:
- I am about to configure docker to authenticate with Nhost's registry. This will modify your docker config file on /Users/dbarroso/.docker/config.json. Should I continue? [y/N] y
````
After you have configured the credentials helper with the command above docker should be able to interact with your images normally:
```
$ docker buildx build \
--push \
--platform linux/amd64,linux/arm64 \
-t registry.eu-central-1.nhost.run/f02bb536-f785-4732-9eb8-d1d3664d7949:123 \
.
[+] Building 8.7s (11/11) FINISHED docker-container:focused_dirac
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 96B 0.0s
=> [linux/arm64 internal] load metadata for docker.io/library/node:18-slim 6.4s
=> [linux/amd64 internal] load metadata for docker.io/library/node:18-slim 6.4s
=> [linux/arm64 1/2] FROM docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
=> => resolve docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 28B 0.0s
=> [linux/amd64 1/2] FROM docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
=> => resolve docker.io/library/node:18-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
=> CACHED [linux/arm64 2/2] ADD app.js app.js 0.0s
=> CACHED [linux/amd64 2/2] ADD app.js app.js 0.0s
=> exporting to image 2.2s
=> => exporting layers 0.0s
=> => exporting manifest sha256:b38199ab8a25d3c765cd763be8af6ea6b3542455f2e7ad37f92924538dbc2af7 0.0s
=> => exporting config sha256:d66a5274b034a3b5698044a03bbec53dccc9e4f5f774701c59604fa17c8f0ff3 0.0s
=> => exporting attestation manifest sha256:c5487067b094ab1d7a81a216595a0f773c55ceec95319e844cded94247e4d341 0.0s
=> => exporting manifest sha256:f1c56d22755cae4d73ee432f11d248ca10ae4b89be3d56f0ffe95e5d7ce10542 0.0s
=> => exporting config sha256:a532f91c5f9899a150b9c981933fa3b163879dcb5ceb48ebc132f72b564a5878 0.0s
=> => exporting attestation manifest sha256:9e1a0207de26c335df4cee8398ada86f7b6a68ebb86e39550fe862416a10df84 0.0s
=> => exporting manifest list sha256:f0d8521804f16280642e99c8c25bbd66f659a9a9bd7bb72cf0726dd98d9bfb00 0.0s
=> => pushing layers 1.2s
=> => pushing manifest for registry.eu-central-1.nhost.run/f02bb536-f785-4732-9eb8-d1d3664d7949:123@sha256:f0d8521804f16280642e99c8c25bbd66f659a9a9bd7bb72cf0726dd98d9bfb00 1.0s
=> [auth] sharing credentials for registry.eu-central-1.nhost.run 0.0s
```
:::info
The credentials helper will authenticate requests with the logged in user, so don't forget to authenticate with your user before trying to push or pull images. You can log in by running the command `nhost login`.
:::
## Updating the image in the service configuration
After you have pushed your image you can click on your service again and update the configuration:
![configure service](/img/run/registry_5.png)
Notice we added the tag `:123` to the image that was already pre-populated and that we increased replicas to `1` to unpause the service. Don't forget to copy the URL where we are exposing the service. Now you can click on update:
![update service](/img/run/registry_6.png)
Wait a few seconds until the project is done updating the new service and visit the URL we copied before:
![visit url](/img/run/registry_7.png)

53
docs/docs/run/7_ci.mdx Normal file
View File

@@ -0,0 +1,53 @@
---
title: CLI & CI Deployments
sidebar_label: CLI & CI Deployments
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
## Deploying with the CLI
Below you can find a simple script to deploy a Nhost Run service using the CLI. This script can be leveraged on any CI or environment as long as you can install the Nhost CLI and have access to docker buildx.
```bash
#!/bin/sh
SERVICE_ID="2503b290-249c-42f5-b89e-fd9a98980e22"
IMAGE="registry.eu-central-1.nhost.run/2503b290-249c-42f5-b89e-fd9a98980e22"
PAT="this-is-my-pat"
VERSION="1.0.0"
CONFIGURATION_FILE="nhost-service.toml"
# this only needs to be done once in each environment
# but usually CIs start with a clean environment each time
#
# you can also login with your regular email/password
# credentials if you prefer
nhost login --pat $PAT
# this only needs to be done once in each environment
# but usually CIs start with a clean environment each time
nhost docker-credentials configure --no-interactive
docker buildx build \
--push \
--platform linux/amd64,linux/arm64 \
-t $IMAGE:$VERSION \
.
nhost run config-deploy \
--config $CONFIGURATION \
--service-id $SERVICE_ID
```
::::info
You can create a PAT by heading to the dashboard -> Account Settings -> Create Personal Access Token
::::
## Deploy from Github Actions
If you prefer to deploy from GitHub actions we support a few GitHub actions you can use to build your own workflows. We also have an already-made workflow you can leverage and there is a hello-world application you can check for a more thorough example and a step-by-step demo:
1. [Github actions](https://github.com/marketplace?type=actions&query=nhost+)
2. [Workflows](https://github.com/nhost-actions/workflows#build-and-release-nhost-runyaml)
3. [Hello World](https://github.com/nhost/nhost-run-hello-world)

View File

@@ -0,0 +1,4 @@
{
"label": "Run",
"position": 9
}

49
docs/docs/run/index.mdx Normal file
View File

@@ -0,0 +1,49 @@
---
title: Overview
sidebar_label: Overview
sidebar_position: 1
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
Nhost Run enables you to seamlessly incorporate your custom software within your project environment. With this feature, you can run various applications, including homegrown backend services, agents, extensions for your GraphQL API through remote schemas or actions, data-processing workloads, etc., all in close proximity to your database for low latency, speed, and efficiency.
![Nhost Architecture Diagram](/img/run/overview.png)
:::info
Currently Nhost Run is in private beta. If you are interested in using this service feel free to reach out to us via [email](mailto:support@nhost.io), [GitHub](https://github.com/nhost/nhost/issues), or [Discord](https://discord.com/invite/9V7Qb2U)
:::
## Use Cases
Nhost Run allows you to expand and truly customize your backend in multiple ways:
1. Homegrown Backend Services: Deploy and execute your custom backend services within your project environment.
2. GraphQL API Extensions: Extend your GraphQL API functionalities by incorporating remote schemas or actions.
3. Data-Processing Workloads: Execute data-processing tasks in close proximity to your database for enhanced efficiency.
4. OSS and third-party software: Redis, memcache, datadog agents, mysql, mongodb... anything your application needs.
## Advantages
Nhost Run offers several key advantages for running workloads alongside your project:
1. **Minimal Latency**: By running workloads alongside your project environment, Nhost Run reduces latency between services. This means that the communication and data exchange between different components of your project can occur quickly and efficiently.
2. **Improved Reliability**: Nhost Run eliminates the dependency on external internet connectivity. This increased reliability ensures that your workloads continue to function even in scenarios where internet access may be limited or disrupted.
3. **No Egress Costs**: With Nhost Run, you won't incur additional egress costs for transferring data between your project and the cloud infrastructure. This cost-saving benefit allows you to manage your expenses more effectively.
4. **Integrated operations**: Develop, build, manage and scale your own workloads the same way you can manage your Nhost Project.
By leveraging Nhost Run, you can optimize the performance, reliability, and cost-efficiency of your project by running workloads alongside it.
## Requirements
Nhost Run works with container images built for the **arm architecture**. Images can be pulled from the [Nhost's private registry](/run/registry) or from any other publicly available registry.
## Roadmap
Some missing functionality we are currently working on and should be added soon:
1. Custom domains
2. Run services with the CLI alongside your project
3. Ability to connect services to repositories for automated building and deployment (currently this needs to be done via a third party CI, see [Deployment via CI](/run/ci) for more details).
4. Expose TCP/UDP ports

View File

@@ -69,12 +69,12 @@ HTTP endpoints are automatically generated based on the file structure inside `f
Here's an example of four Serverless Functions with their files and their HTTP endpoints:
| File | HTTP Endpoint |
| --------------------------- | ----------------------------------------------------------------- |
| `functions/index.js` | `https://[project-subdomain].nhost.run/v1/functions/` |
| `functions/users/index.ts` | `https://[project-subdomain].nhost.run/v1/functions/users` |
| `functions/users/active.ts` | `https://[project-subdomain].nhost.run/v1/functions/users/active` |
| `functions/my-company.js` | `https://[project-subdomain].nhost.run/v1/functions/my-company` |
| File | HTTP Endpoint |
| --------------------------- | ------------------------------------------------------------------ |
| `functions/index.js` | `https://[subdomain].functions.[region].nhost.run/v1/` |
| `functions/users/index.ts` | `https://[subdomain].functions.[region].nhost.run/v1/users` |
| `functions/users/active.ts` | `https://[subdomain].functions.[region].nhost.run/v1/users/active` |
| `functions/my-company.js` | `https://[subdomain].functions.[region].nhost.run/v1/my-company` |
You can prepend files and folders with an underscore (`_`) to prevent them from being treated as Serverless Functions and
be turned into HTTP endpoints. This is useful if you have, for example, a utils file (`functions/_utils.js`) or a utils-f

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