Compare commits

..

252 Commits

Author SHA1 Message Date
Hassan Ben Jobrane
563a37e58d Merge pull request #2285 from nhost/changeset-release/main
chore: update versions
2023-09-29 18:05:40 +02:00
github-actions[bot]
bff23720ee chore: update versions 2023-09-29 15:43:43 +00:00
Hassan Ben Jobrane
02cbaeffd2 Merge pull request #2225 from nhost/feat/examples/nextjs-server-components
feat: quickstarts: draft for using server components
2023-09-29 17:40:52 +02:00
Hassan Ben Jobrane
9eb814c79a chore: update readme 2023-09-29 15:57:53 +01:00
Hassan Ben Jobrane
ebc5913bb3 chore: naming consistency 2023-09-29 15:57:53 +01:00
Hassan Ben Jobrane
4fe4a16964 chore: add changeset 2023-09-29 15:57:53 +01:00
Hassan Ben Jobrane
92c475b7a7 chore: add missing refreshToken 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
679b34b031 chore: cleanup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
d3186aefbd refactor: extract session middleware into helper function 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
fdecac9d69 refactor: add high order component for protected pages 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
5077283028 chore: merge oauth handling in middleware 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
f5f662aad1 chore: refactor server actions 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
735b779af7 chore: clean up database setup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
4418d6abcf chore: cleanup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
049e315c30 fix: set correct path on cookie on oauth signin 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
764597538b fix: make sure that hasura-storage-js works on EdgeRuntime 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
c8aea785cc fix: tweak todo item layout 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
e0e44b2ff4 fix: set same path for session cookie 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
12280f7c87 feat: pat list pagination 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
732a4f40ca wip 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
d67fd599e4 feat: todos CRUD 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
a41231927a feat: add signin with pat 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
42ec665950 fix: return refreshToken in getAuthenticationResult 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
7225712a30 chore: update hasura auth 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
6593fdd9bb fix: make sure refreshToken is returned after signin/signup 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
40039fece5 Revert "refactor: make sure to return refresh token"
This reverts commit b31b358ca1898bb4173954b8b33059d92cc8c126.
2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
e5fcfb3cd5 refactor: make sure to return refresh token 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
218ec314fb feat(quickstarts): refactor and organize signup/signin 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
9367e91d45 feat: examples: add other sign in methods
Add sign in with google and webauthn
2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
06c640be2c chore: delete unnecessary files 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
ae45be9816 feat(quickstarts): draft for using server components 2023-09-29 15:57:52 +01:00
Hassan Ben Jobrane
ec4be590d8 Merge pull request #2284 from nhost/chore/ci/fix-release
chore: ci: fix release worflow
2023-09-29 16:56:12 +02:00
Hassan Ben Jobrane
5c51653aa0 chore: fix release worflow 2023-09-29 13:29:44 +01:00
Hassan Ben Jobrane
7348c15ad1 Merge pull request #2281 from nhost/changeset-release/main
chore: update versions
2023-09-29 13:48:59 +02:00
Hassan Ben Jobrane
44831e32a7 Merge pull request #2282 from nhost/chore/ci/fix-release-workflow
chore(ci): fix release workflow
2023-09-29 13:47:21 +02:00
Hassan Ben Jobrane
ee0f837762 chore(ci): fix release workflow 2023-09-29 12:32:05 +01:00
github-actions[bot]
e040979e91 chore: update versions 2023-09-29 10:59:27 +00:00
Hassan Ben Jobrane
68100d63b9 Merge pull request #2267 from nhost/changeset-release/main
chore: update versions
2023-09-29 12:56:45 +02:00
github-actions[bot]
9b800046d7 chore: update versions 2023-09-29 10:37:52 +00:00
Hassan Ben Jobrane
807d8574b6 Merge pull request #2280 from nhost/feat/dashboard/multiline-variables-input
feat(dashboard): make env value input multiline
2023-09-29 12:34:59 +02:00
Hassan Ben Jobrane
77028e4eef Merge pull request #2265 from nhost/chore/ci/disable-changeset-github-releases
chore(ci): disable automatic GitHub releases
2023-09-29 12:34:36 +02:00
Hassan Ben Jobrane
e0d32aab33 Merge pull request #2235 from nhost/chore/ci/release/recorder-steps
chore(ci): run publish to vercel before docker build
2023-09-29 12:34:16 +02:00
Hassan Ben Jobrane
75c4c8ae36 chore: add changeset 2023-09-29 11:14:53 +01:00
Hassan Ben Jobrane
1d90639e46 feat: make env value input multiline 2023-09-29 11:12:35 +01:00
David Barroso
765b398b21 chore(docs): added jit settings documentation (#2274)
Solves #2273
2023-09-29 10:44:32 +02:00
David Barroso
30aae1557c chore(docs): minor fix to performance documentation (#2272) 2023-09-29 10:44:02 +02:00
David Barroso
a3efc1d131 chore (docs): added storage/antivirus documentation (#2268)
Rearranged the storage section a bit and added:

https://docs-git-docs-storage-nhost.vercel.app/storage/av
2023-09-28 07:27:57 +02:00
David Barroso
612d754965 chore: docs: added more docs (#2264)
https://docs-git-docs-postgres-nhost.vercel.app/database/settings
https://docs-git-docs-postgres-nhost.vercel.app/database/extensions
https://docs-git-docs-postgres-nhost.vercel.app/graphql/settings
https://docs-git-docs-postgres-nhost.vercel.app/database/performance

---------

Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2023-09-28 07:27:15 +02:00
Hassan Ben Jobrane
b2e5f30379 Merge pull request #2269 from nhost/fix/docs/mermaid-theme
fix(docs): mermaid diagrams dark mode
2023-09-27 16:28:53 +01:00
Hassan Ben Jobrane
3b3e83a218 chore: add changeset 2023-09-27 15:42:48 +01:00
Hassan Ben Jobrane
0d5231f1a1 fix(docs): correct rendering of mermaid diagrams in dark mode 2023-09-27 15:41:59 +01:00
Hassan Ben Jobrane
1a8332a3ca Merge pull request #2266 from nhost/fix/settings/compute-resources
fix: make sure dedicated resources pricing follows total resources
2023-09-27 10:06:48 +01:00
Hassan Ben Jobrane
7418105de2 chore: remove commented code 2023-09-26 19:29:48 +01:00
Hassan Ben Jobrane
425d485f85 chore: add changeset 2023-09-26 17:09:08 +01:00
Hassan Ben Jobrane
d8d25b3ea0 fix: make sure dedicated resources pricing follows total resources on the top section 2023-09-26 17:06:51 +01:00
Hassan Ben Jobrane
320513f6f5 chore(ci): disable automatic GitHub releases 2023-09-26 16:34:18 +01:00
Hassan Ben Jobrane
b37053376d Merge pull request #2261 from nhost/changeset-release/main
chore: update versions
2023-09-22 12:00:44 +01:00
github-actions[bot]
c21ba4aebd chore: update versions 2023-09-22 10:46:59 +00:00
Hassan Ben Jobrane
58948c50d4 Merge pull request #2260 from nhost/fix/graphql/remove-unused-fields
fix: dashboard: remove unused fields
2023-09-22 11:44:23 +01:00
Hassan Ben Jobrane
ae324f67fa chore: add changeset 2023-09-22 11:33:51 +01:00
Hassan Ben Jobrane
acabf2b168 fix(dashboard): remove unused fields 2023-09-22 11:31:57 +01:00
Hassan Ben Jobrane
73cb65b9be Merge pull request #2255 from nhost/changeset-release/main
chore: update versions
2023-09-21 14:48:34 +01:00
github-actions[bot]
5e7c8395c2 chore: update versions 2023-09-21 11:33:11 +00:00
David Barroso
c2837209e6 chore: added codeql to CI (#2252) 2023-09-21 13:30:29 +02:00
Hassan Ben Jobrane
638710ea29 Merge pull request #2257 from nhost/fix/run/service-details
fix: dashboard: run: show correct private registry
2023-09-21 12:29:42 +01:00
Hassan Ben Jobrane
a79fddbafb fix(dashboard/run): remove image prop 2023-09-21 11:23:39 +01:00
Hassan Ben Jobrane
ab6a8f2add Merge pull request #2256 from nhost/feat/dashboard/query-software-versions
feat: dashboard: query software versions from platform
2023-09-21 11:12:52 +01:00
Hassan Ben Jobrane
69a5661bcf fix(dashboard/run): remove unused image prop 2023-09-21 10:37:18 +01:00
Hassan Ben Jobrane
0886118f9d Merge pull request #2254 from nhost/chore/run/remove-feature-flag
chore: remove run feature flag
2023-09-21 10:31:09 +01:00
Hassan Ben Jobrane
34fc08ca7c chore: add changeset 2023-09-21 10:18:05 +01:00
Hassan Ben Jobrane
153de22713 fix(dashboard): show correct private registry when showing service details 2023-09-21 10:16:52 +01:00
Hassan Ben Jobrane
bf4a1f6c2a chore: add changeset 2023-09-20 15:26:22 +01:00
Hassan Ben Jobrane
2a67d0f872 feat(dashboard): fetch Storage versions from platform 2023-09-20 15:23:17 +01:00
Hassan Ben Jobrane
b156c7b72e feat(dashboard): fetch Auth versions from platform 2023-09-20 15:22:55 +01:00
Hassan Ben Jobrane
b484b04ae2 feat(dashboard): fetch Hasura versions from platform 2023-09-20 15:18:40 +01:00
Hassan Ben Jobrane
2e55c7f46a feat(dashboard): fetch postgres versions from platform 2023-09-20 14:06:19 +01:00
Hassan Ben Jobrane
2d983e6ab1 feat(dashboard): codegen graphql query for getting software versions 2023-09-20 14:05:41 +01:00
Hassan Ben Jobrane
df5b4302c3 chore: add changeset 2023-09-20 12:12:42 +01:00
Hassan Ben Jobrane
828aed2df9 chore: remove run feature flag 2023-09-20 12:12:42 +01:00
Hassan Ben Jobrane
310df10892 Merge pull request #2251 from nhost/chore/replace-feedback
chore(dashboard): replace feedback form with contact us
2023-09-20 12:08:52 +01:00
Hassan Ben Jobrane
555fba4400 chore: re-arrange text content 2023-09-20 11:43:27 +01:00
Hassan Ben Jobrane
885d10620a chore: add changeset 2023-09-20 11:24:20 +01:00
Hassan Ben Jobrane
a8370f5aaa chore: fix nav test 2023-09-20 11:03:43 +01:00
Hassan Ben Jobrane
bd07905846 chore(dashboard): replace feedback form with contact us 2023-09-19 19:25:53 +01:00
Nuno Pato
47a2164549 Merge pull request #2250 from nhost/changeset-release/main
chore: update versions
2023-09-19 16:57:24 +00:00
github-actions[bot]
a96c79de00 chore: update versions 2023-09-19 16:42:43 +00:00
Nuno Pato
596d0666fc Merge pull request #2249 from nhost/chore/docs-run-public-beta
chore: docs: Run is now in public beta
2023-09-19 16:40:04 +00:00
Nuno Pato
9aaa407d29 Run is now in public beta 2023-09-19 16:01:57 +00:00
Hassan Ben Jobrane
1767b2f105 Merge pull request #2248 from nhost/changeset-release/main
chore: update versions
2023-09-18 20:17:34 +01:00
github-actions[bot]
c99c5c4191 chore: update versions 2023-09-18 19:03:08 +00:00
Hassan Ben Jobrane
d845da2503 Merge pull request #2246 from nhost/fix/one-click-install
fix: run: center the loading indicator
2023-09-18 20:00:37 +01:00
Hassan Ben Jobrane
9f1ba1686c Merge pull request #2247 from nhost/feat/run/delete-service-confirmation-dialog
feat: run: delete service confirmation dialog
2023-09-18 19:57:21 +01:00
Hassan Ben Jobrane
48b09a58ff fix: typo
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2023-09-18 18:54:37 +01:00
Hassan Ben Jobrane
2169908883 chore: add changeset 2023-09-18 18:48:06 +01:00
Hassan Ben Jobrane
ed16c8b5de chore: add changeset 2023-09-18 18:45:11 +01:00
Hassan Ben Jobrane
c618503376 feat(run): add a confirmation dialog when deleting a run service 2023-09-18 18:45:03 +01:00
Hassan Ben Jobrane
f306c3940c fix: center the loading indicator 2023-09-18 16:53:15 +01:00
Hassan Ben Jobrane
ef125216bb Merge pull request #2242 from nhost/changeset-release/main
chore: update versions
2023-09-15 17:33:37 +01:00
github-actions[bot]
fb43fefb5c chore: update versions 2023-09-15 16:22:32 +00:00
Hassan Ben Jobrane
73744c90f0 Merge pull request #2241 from nhost/feat/node18-announcement
feat: add node18 announcement banner
2023-09-15 17:19:27 +01:00
Hassan Ben Jobrane
9fbea9787e chore: add changeset 2023-09-15 16:47:21 +01:00
Hassan Ben Jobrane
e5f54bc197 feat: add node18 announcement banner 2023-09-15 16:43:59 +01:00
Hassan Ben Jobrane
10a6ae4853 Merge pull request #2233 from nhost/changeset-release/main
chore: update versions
2023-09-13 17:11:54 +01:00
github-actions[bot]
d6ca1c7cfd chore: update versions 2023-09-13 13:59:01 +00:00
Hassan Ben Jobrane
bb85a95eda Merge pull request #2236 from nhost/fix/run/subdomain-optional
fix: run: handle subdomain nullability
2023-09-13 14:56:22 +01:00
Hassan Ben Jobrane
e84acf4692 chore: add changeset 2023-09-13 13:06:05 +01:00
Hassan Ben Jobrane
2f20a70a28 fix(run): subdomain is not set when creating a new service 2023-09-13 13:00:18 +01:00
Hassan Ben Jobrane
e622ca0d83 chore(ci): run publish to vercel before docker build 2023-09-12 15:09:34 +01:00
David Barroso
819e1e97dc chore (docs): update fqdn format for nhost run (#2232) 2023-09-12 14:54:39 +02:00
Hassan Ben Jobrane
7c1cca0a43 Merge pull request #2231 from nhost/changeset-release/main
chore: update versions
2023-09-12 13:09:07 +01:00
github-actions[bot]
0f51f4e868 chore: update versions 2023-09-12 12:05:55 +00:00
Hassan Ben Jobrane
97a6fcead9 Merge pull request #2230 from nhost/feat/run/copy-urls-dialog
feat(run): add dialog to copy service urls
2023-09-12 13:03:17 +01:00
Hassan Ben Jobrane
b7c799d62c chore: add changeset 2023-09-12 12:04:41 +01:00
Hassan Ben Jobrane
18b14b27fd refactor: pass service data directly to the details dialog 2023-09-12 12:01:20 +01:00
Hassan Ben Jobrane
67a867c93a feat: add dialog to copy service urls 2023-09-11 19:52:51 +01:00
David Barroso
0a1fb12467 feat: observability: add egress/requests metrics to general dashboard (#2227) 2023-09-06 18:03:25 +02:00
Hassan Ben Jobrane
78467ee348 Merge pull request #2219 from nhost/changeset-release/main
chore: update versions
2023-09-04 11:58:34 +01:00
github-actions[bot]
c24eef0db9 chore: update versions 2023-09-04 10:24:35 +00:00
Hassan Ben Jobrane
2159b8171e Merge pull request #2218 from nhost/fix/format-functions-execution
fix: dashboard: usage stats
2023-09-04 11:21:25 +01:00
Hassan Ben Jobrane
8903e6abd9 chore: add changeset 2023-09-02 15:18:54 +01:00
Hassan Ben Jobrane
7290260990 fix: show correct egress volume limit 2023-09-02 15:15:43 +01:00
Hassan Ben Jobrane
06529a1ea4 fix: round up functions duration 2023-09-02 15:14:29 +01:00
Hassan Ben Jobrane
607d89e2aa Merge pull request #2215 from nhost/changeset-release/main
chore: update versions
2023-09-01 19:13:41 +01:00
github-actions[bot]
0cca72311c chore: update versions 2023-09-01 15:26:44 +00:00
Hassan Ben Jobrane
a6525b6467 Merge pull request #2214 from nhost/feat/update-usage-metrics
feat(dashboard): update usage metrics
2023-09-01 16:24:06 +01:00
Hassan Ben Jobrane
387be37b6e chore: remove redundant egress card 2023-09-01 15:30:01 +01:00
Hassan Ben Jobrane
c8fd8bbcc7 fix: update storage upper limit for pro plan 2023-09-01 13:01:27 +01:00
Hassan Ben Jobrane
bfb34bad00 fix: use correct value for functions duration 2023-09-01 12:28:19 +01:00
Hassan Ben Jobrane
666a75a233 chore: add changeset 2023-09-01 12:26:41 +01:00
Hassan Ben Jobrane
3b050217df feat(dashboard): tweak usage metrics 2023-09-01 12:25:15 +01:00
Hassan Ben Jobrane
0ed4481615 feat(dashboard): update usage metrics 2023-09-01 11:14:49 +01:00
Hassan Ben Jobrane
ac3f12c878 Merge pull request #2211 from nhost/changeset-release/main
chore: update versions
2023-08-31 12:29:34 +01:00
github-actions[bot]
65cabb089f chore: update versions 2023-08-31 11:01:17 +00:00
Hassan Ben Jobrane
2905beb0a1 Merge pull request #2212 from nhost/fix/hasura-storage-js-edge-runtime
fix(hasura-storage-js): swap fetch when running on edge runtime
2023-08-31 11:58:43 +01:00
Hassan Ben Jobrane
83fee54460 chore: add changeset 2023-08-31 11:11:44 +01:00
Hassan Ben Jobrane
82898b6dae fix(hasura-storage-js): swap fetch when running on edge runtime 2023-08-31 11:09:37 +01:00
Hassan Ben Jobrane
500f76a38d Merge pull request #2208 from nhost/fix/user-auth-locales
fix: remove hardcoded locales
2023-08-30 10:31:43 +01:00
Hassan Ben Jobrane
5e1e80aa8b chore: add changeset 2023-08-29 20:05:29 +01:00
Hassan Ben Jobrane
6d0a126907 fix: remove hardcoded locales 2023-08-29 13:32:12 +01:00
Hassan Ben Jobrane
1b7dcf2121 Merge pull request #2207 from nhost/changeset-release/main
chore: update versions
2023-08-28 16:40:51 +01:00
github-actions[bot]
2b9205b6cf chore: update versions 2023-08-28 15:16:01 +00:00
Hassan Ben Jobrane
bdc4d4a88c Merge pull request #2206 from nhost/fix/stripe-graphql-js
fix(stripe-graphql-js): fix stripe GraphQL extension export issue in serverless functions
2023-08-28 16:12:13 +01:00
Hassan Ben Jobrane
45759c4d4c chore: add changeset 2023-08-28 15:49:17 +01:00
Hassan Ben Jobrane
5f9886577a fix: import 2023-08-28 15:47:49 +01:00
Hassan Ben Jobrane
fa65496327 fix(stripe-extension): return yoga instance instead of node http server 2023-08-28 15:24:56 +01:00
Hassan Ben Jobrane
03777680c1 chore: add STRIPE_SECRET_KEY 2023-08-26 16:51:31 +01:00
Hassan Ben Jobrane
72c81207ff Merge pull request #2201 from nhost/chore/add-missing-changeset
chore: add missing changeset
2023-08-24 16:47:41 +01:00
Hassan Ben Jobrane
5ca2a394e8 chore: sync version in package.json 2023-08-24 16:30:18 +01:00
Hassan Ben Jobrane
e63b8da58a chore: add missing changeset 2023-08-24 16:27:38 +01:00
Hassan Ben Jobrane
bf8543cd34 Merge pull request #2195 from nhost/changeset-release/main
chore: update versions
2023-08-24 13:57:34 +01:00
github-actions[bot]
8a557bbd02 chore: update versions 2023-08-24 12:21:34 +00:00
Hassan Ben Jobrane
327e30b859 Merge pull request #2200 from nhost/chore/ignore-version-update-sveltekit-example
chore: sveltekit-example: changeset ignore dep version update
2023-08-24 13:18:25 +01:00
Hassan Ben Jobrane
bbfaf9732b chore: sveltekit-example: ignore changeset dep version update 2023-08-24 12:44:16 +01:00
Hassan Ben Jobrane
c064a53256 Merge pull request #2199 from nhost/chore/fix-dep-version
chore: fix dep version for sveltekit example
2023-08-24 12:03:57 +01:00
Hassan Ben Jobrane
ebda86f1f0 chore: sync lockfile 2023-08-24 11:53:41 +01:00
Hassan Ben Jobrane
8948be9d3d chore: fix dep version for sveltekit example 2023-08-24 11:50:47 +01:00
Hassan Ben Jobrane
54e9b141f1 Merge pull request #2191 from nhost/dbarroso/react-example
chore: react-apollo-example: add profile to allowedUrls
2023-08-24 10:56:45 +01:00
Hassan Ben Jobrane
dba71483df chore: add changeset 2023-08-24 10:41:58 +01:00
Hassan Ben Jobrane
77ef68232a Merge pull request #2197 from nhost/fix/webauthn-error-handling
fix(hasura-auth-js): make sure CodifiedError works on non v8 browsers
2023-08-24 10:26:46 +01:00
Hassan Ben Jobrane
8fbc7f9f95 Merge pull request #2198 from nhost/chore/remove-facebook-login
chore(react-apollo-example): remove facebook login
2023-08-24 10:26:31 +01:00
Hassan Ben Jobrane
ca9f0f6ae9 chore: show error toast when adding a security key fails 2023-08-23 23:48:45 +01:00
Hassan Ben Jobrane
e819903f1b chore: add changeset 2023-08-23 17:00:30 +01:00
Hassan Ben Jobrane
f780b17581 chore: remove facebook login from react apollo example 2023-08-23 16:59:44 +01:00
Hassan Ben Jobrane
032c0bd217 chore: add changeset 2023-08-23 16:51:14 +01:00
Hassan Ben Jobrane
5d278709cb fix(hasura-auth-js): make sure CodifiedError works on non v8 browsers 2023-08-23 16:25:57 +01:00
Hassan Ben Jobrane
3a012e089a Merge pull request #2182 from nhost/feat/add-sveltekit-example
feat: add sveltekit example
2023-08-23 12:14:38 +01:00
Hassan Ben Jobrane
7aed620e12 chore: fix tests 2023-08-23 11:39:29 +01:00
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
dd0a5cf3c1 chore: fix lock file 2023-08-22 16:55:28 +01:00
Hassan Ben Jobrane
5187fd3a4b chore: dashboard tests 2023-08-22 16:49:26 +01:00
Hassan Ben Jobrane
d8dfd6bf80 Revert "chore: add missing dep for vitest"
This reverts commit 6ea6ad61db.
2023-08-22 16:16:20 +01:00
Hassan Ben Jobrane
6ea6ad61db chore: add missing dep for vitest 2023-08-22 16:04:24 +01:00
Hassan Ben Jobrane
fd0b904ed4 chore: fix dashboard e2e tests 2023-08-22 15:41:36 +01:00
Hassan Ben Jobrane
8989e314a6 fix: ignore conflict with linting and sveltekit build 2023-08-22 14:40:18 +01:00
Hassan Ben Jobrane
5b5a1219c5 fix: make sure linting runs correctly 2023-08-22 14:31:45 +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
2fa828fef1 chore: cleanup .gitignore file 2023-08-22 13:10:59 +01:00
Hassan Ben Jobrane
d5ec69ac37 chore(examples-sveltekit): add a basic test 2023-08-22 13:07:06 +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
David Barroso
09fc852c3a asd 2023-08-21 13:13:41 +02: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
27e1c90624 fix: change env to dynamic 2023-08-18 18:26:09 +01:00
Hassan Ben Jobrane
1cc53d550a chore: add changeset 2023-08-18 18:04:12 +01:00
Hassan Ben Jobrane
22d3f71e02 fix: make sure to include lib folder in sveltekit example 2023-08-18 17:58:29 +01:00
Hassan Ben Jobrane
010b816866 chore: fix README 2023-08-18 17:36:10 +01:00
Hassan Ben Jobrane
4a6e62e673 feat: add sveltekit example 2023-08-18 17:16:13 +01: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
356 changed files with 11372 additions and 1331 deletions

View File

@@ -6,5 +6,5 @@
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
"ignore": ["@nhost-examples/sveltekit"]
}

View File

@@ -42,6 +42,7 @@ jobs:
commit: 'chore: update versions'
title: 'chore: update versions'
publish: pnpm run release
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -62,12 +63,39 @@ jobs:
uses: ./.github/workflows/dashboard.yaml
secrets: inherit
publish-vercel:
name: Publish to Vercel
runs-on: ubuntu-latest
needs:
- test
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- name: Setup Vercel CLI
run: pnpm add -g vercel
- name: Trigger a Vercel deployment
env:
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
run: |
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
publish-docker:
name: Publish to Docker Hub
runs-on: ubuntu-latest
needs:
- test
- version
- publish-vercel
steps:
- name: Checkout repository
uses: actions/checkout@v3
@@ -124,32 +152,6 @@ jobs:
if: failure()
run: git push --delete origin ${{ env.DASHBOARD_PACKAGE }}@${{ needs.version.outputs.dashboardVersion }}
publish-vercel:
name: Publish to Vercel
runs-on: ubuntu-latest
needs:
- publish-docker
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Node and dependencies
uses: ./.github/actions/install-dependencies
with:
TURBO_TOKEN: ${{ env.TURBO_TOKEN }}
TURBO_TEAM: ${{ env.TURBO_TEAM }}
- name: Setup Vercel CLI
run: pnpm add -g vercel
- name: Trigger a Vercel deployment
env:
VERCEL_ORG_ID: ${{ secrets.DASHBOARD_VERCEL_TEAM_ID }}
VERCEL_PROJECT_ID: ${{ secrets.DASHBOARD_VERCEL_PROJECT_ID }}
run: |
vercel pull --environment=production --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel build --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
vercel deploy --prebuilt --prod --token=${{ secrets.DASHBOARD_VERCEL_DEPLOY_TOKEN }}
bump-cli:
name: Bump Dashboard version in the Nhost CLI
runs-on: ubuntu-latest

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() }}

56
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: "CodeQL"
on:
push: {}
pull_request: {}
schedule:
- cron: '20 23 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2

3
.gitignore vendored
View File

@@ -19,10 +19,8 @@ logs/
coverage/
dist/
umd/
lib/
node_modules/
tmp/
.docz/
.pnpm-store
.turbo
.env
@@ -32,7 +30,6 @@ out/
# Custom
*.min.js
*.map
todo.md
# Config files that are not part of the repository root anymore. Should be removed in the future.
/.eslintignore

View File

@@ -1,5 +1,132 @@
# @nhost/dashboard
## 0.20.20
### Patch Changes
- @nhost/react-apollo@5.0.36
- @nhost/nextjs@1.13.38
## 0.20.19
### Patch Changes
- 75c4c8ae3: feat(dashboard): make env value input multiline
## 0.20.18
### Patch Changes
- 425d485f8: fix(dashboard): make sure dedicated resources pricing follows total resources
## 0.20.17
### Patch Changes
- ae324f67f: fix(dashboard): remove unused graphql fields
## 0.20.16
### Patch Changes
- df5b4302c: chore(dashboard): remove run feature flag
- bf4a1f6c2: feat(dashboard): fetch auth, postgres, hasura and storage versions from dashboard
- 34fc08ca7: fix(dashboard/run): show correct private registry in service details
- 885d10620: chore(dashboard): change feedback to contact us
## 0.20.15
### Patch Changes
- ed16c8b5d: feat(run): add a confirmation dialog when deleting a run service
- 216990888: fix(run): center loading indicator when selecting a project
## 0.20.14
### Patch Changes
- 9fbea9787: feat: add node18 announcement
## 0.20.13
### Patch Changes
- e84acf469: fix(run): handle subdomain undefined error when creating a new service
## 0.20.12
### Patch Changes
- b7c799d62: feat(run): add dialog to copy registry and URLs
## 0.20.11
### Patch Changes
- 8903e6abd: fix(dashboard): show correct egress limit in usage stats
## 0.20.10
### Patch Changes
- 666a75a23: feat(dashboard): add functions execution time and egress volume to usage stats
## 0.20.9
### Patch Changes
- 5e1e80aa8: fix(dashboard): show correct locales in user details
- @nhost/react-apollo@5.0.35
- @nhost/nextjs@1.13.37
## 0.20.8
### Patch Changes
- @nhost/react-apollo@5.0.34
- @nhost/nextjs@1.13.36
## 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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.20.2",
"version": "0.20.20",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -66,6 +66,7 @@
"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",
@@ -105,6 +106,7 @@
"@testing-library/user-event": "^14.4.3",
"@types/ace": "^0.0.48",
"@types/bcryptjs": "^2.4.2",
"@types/jest": "^29.5.3",
"@types/lodash.debounce": "^4.0.7",
"@types/node": "^16.11.7",
"@types/pluralize": "^0.0.30",

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: 'node-18',
href: 'https://github.com/nhost/nhost/discussions/2239',
content:
"Starting October 1st, we're upgrading to Node.js 18 for improved performance, security, and stability. Learn more.",
};
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

@@ -0,0 +1,70 @@
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ContactUsProps
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {}
export default function FeedbackForm({ className, ...props }: ContactUsProps) {
return (
<div
className={twMerge(
'grid max-w-md grid-flow-row gap-2 py-4 px-5',
className,
)}
{...props}
>
<Text variant="h3" component="h2">
Contact us
</Text>
<Text>
To report issues with Nhost, please open a GitHub issue in the{' '}
<Link
href="https://github.com/nhost/nhost/issues/new"
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
nhost/nhost
</Link>{' '}
repository.
</Text>
<Text>
For issues related to the CLI, please visit the{' '}
<Link
href="https://github.com/nhost/cli/issues/new"
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
nhost/cli
</Link>{' '}
repository.
</Text>
<Text>
If you need assistance or have any questions, feel free to join us on{' '}
<Link
href="https://discord.com/invite/9V7Qb2U"
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
Discord
</Link>
. Alternatively, if you prefer, you can also open a{' '}
<Link
href="https://github.com/nhost/nhost/discussions/new/choose"
target="_blank"
rel="noopener noreferrer"
underline="hover"
>
GitHub discussion
</Link>
.
</Text>
<Text>We&apos;re here to help, so don&apos;t hesitate to reach out!</Text>
</div>
);
}

View File

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

View File

@@ -1,148 +0,0 @@
import { Avatar } from '@/components/ui/v2/Avatar';
import { Button } from '@/components/ui/v2/Button';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useInsertFeedbackOneMutation } from '@/utils/__generated__/graphql';
import { useUserData } from '@nhost/nextjs';
import Image from 'next/image';
import type { DetailedHTMLProps, HTMLProps } from 'react';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';
export interface FeedbackFormProps
extends DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> {}
// TODO: Use `react-hook-form` here instead of the custom form implementation
export default function FeedbackForm({
className,
...props
}: FeedbackFormProps) {
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertFeedback, { loading }] = useInsertFeedbackOneMutation();
const user = useUserData();
const [feedback, setFeedback] = useState('');
const [feedbackSent, setFeedbackSent] = useState(false);
function handleClose() {
setTimeout(() => {
setFeedbackSent(false);
}, 500);
}
async function handleSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
e.preventDefault();
const feedbackWithProjectInfo = [
currentProject && `Project ID: ${currentProject.id}`,
typeof window !== 'undefined' && `URL: ${window.location.href}`,
feedback,
]
.filter(Boolean)
.join('\n\n');
try {
await insertFeedback({
variables: {
feedback: {
feedback: feedbackWithProjectInfo,
},
},
});
setFeedbackSent(true);
setFeedback('');
} catch (error) {
// TODO: Display error to user and use a logging solution
}
}
if (feedbackSent) {
return (
<div
className={twMerge(
'grid max-w-md grid-flow-row justify-center gap-4 py-4 px-5 text-center',
className,
)}
{...props}
>
<Image
src="/assets/FeedbackReceived.svg"
alt="Light bulb with a checkmark"
width={72}
height={72}
/>
<div className="grid grid-flow-row gap-2">
<Text variant="h3" component="h2" className="text-center">
Feedback Received
</Text>
<Text>
Thanks for sending us your thoughts! Feel free to send more feedback
as you explore the beta, and stay tuned for updates.
</Text>
</div>
<Button
variant="outlined"
color="secondary"
className="mt-2 text-sm+ font-normal"
onClick={handleClose}
>
Go Back
</Button>
</div>
);
}
return (
<div
className={twMerge(
'grid max-w-md grid-flow-row gap-2 py-4 px-5',
className,
)}
{...props}
>
<Text variant="h3" component="h2">
Leave Feedback
</Text>
<Text>
Nhost is still in beta and not everything is in place yet, but we&apos;d
love to know what you think of it so far.
</Text>
<form onSubmit={handleSubmit} className="grid grid-flow-row gap-2">
<div className="grid grid-flow-col place-content-between gap-2">
<Text className="font-medium">
What do you think we should improve?
</Text>
<Avatar
className="h-6 w-6 rounded-full"
alt={user?.displayName}
src={user?.avatarUrl}
>
{user?.displayName}
</Avatar>
</div>
<Input
multiline
value={feedback}
onChange={(event) => setFeedback(event.target.value)}
placeholder="Your feedback"
rows={6}
required
fullWidth
hideEmptyHelperText
/>
<Button type="submit" disabled={!feedback} loading={loading}>
Send Feedback
</Button>
</form>
</div>
);
}

View File

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

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

@@ -1,4 +1,4 @@
import { FeedbackForm } from '@/components/common/FeedbackForm';
import { ContactUs } from '@/components/common/ContactUs';
import { NavLink } from '@/components/common/NavLink';
import { AccountMenu } from '@/components/layout/AccountMenu';
import { Breadcrumbs } from '@/components/layout/Breadcrumbs';
@@ -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>
@@ -75,14 +75,14 @@ export default function Header({ className, ...props }: HeaderProps) {
hideChevron
className="rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
>
Feedback
Contact us
</Dropdown.Trigger>
<Dropdown.Content
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
>
<FeedbackForm className="max-w-md" />
<ContactUs className="max-w-md" />
</Dropdown.Content>
</Dropdown.Root>
)}

View File

@@ -1,4 +1,4 @@
import { FeedbackForm } from '@/components/common/FeedbackForm';
import { ContactUs } from '@/components/common/ContactUs';
import { NavLink } from '@/components/common/NavLink';
import { ThemeSwitcher } from '@/components/common/ThemeSwitcher';
import { Nav } from '@/components/presentational/Nav';
@@ -171,7 +171,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
className="w-full"
role={undefined}
>
<ListItem.Text>Feedback</ListItem.Text>
<ListItem.Text>Contact us</ListItem.Text>
</ListItem.Button>
</ListItem.Root>
</Dropdown.Trigger>
@@ -180,7 +180,7 @@ export default function MobileNav({ className, ...props }: MobileNavProps) {
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<FeedbackForm className="max-w-md" />
<ContactUs className="max-w-md" />
</Dropdown.Content>
</Dropdown.Root>
)}

View File

@@ -7,14 +7,15 @@ import MaterialLinearProgress, {
export interface LinearProgressProps extends MaterialLinearProgressProps {}
const LinearProgress = styled(MaterialLinearProgress)(({ theme }) => ({
const LinearProgress = styled(MaterialLinearProgress)(({ theme, value }) => ({
height: 12,
borderRadius: 1,
[`&.${linearProgressClasses.colorPrimary}`]: {
backgroundColor: theme.palette.grey[300],
},
[`& .${linearProgressClasses.bar}`]: {
backgroundColor: theme.palette.primary.main,
backgroundColor:
value >= 100 ? theme.palette.error.dark : theme.palette.primary.main,
},
}));

View File

@@ -7,7 +7,9 @@ import { filterOptions } from '@/components/ui/v2/Autocomplete';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetAuthenticationSettingsDocument,
Software_Type_Enum,
useGetAuthenticationSettingsQuery,
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
@@ -28,15 +30,6 @@ export type AuthServiceVersionFormValues = Yup.InferType<
typeof validationSchema
>;
const AVAILABLE_AUTH_VERSIONS = [
'0.21.2',
'0.20.1',
'0.20.0',
'0.19.3',
'0.19.2',
'0.19.1',
];
export default function AuthServiceVersionSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -49,9 +42,16 @@ export default function AuthServiceVersionSettings() {
fetchPolicy: 'cache-only',
});
const { data: authVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Auth,
},
});
const { version } = data?.config?.auth || {};
const versions = authVersionsData?.softwareVersions || [];
const availableVersions = Array.from(
new Set(AVAILABLE_AUTH_VERSIONS).add(version),
new Set(versions.map((el) => el.version)).add(version),
)
.sort()
.reverse()

View File

@@ -38,6 +38,10 @@ query GetAuthenticationSettings($appId: uuid!) {
default
rating
}
locale {
allowed
default
}
}
version
}

View File

@@ -24,6 +24,7 @@ import { copy } from '@/utils/copy';
import { getServerError } from '@/utils/getServerError';
import {
RemoteAppGetUsersDocument,
useGetProjectLocalesQuery,
useGetRolesPermissionsQuery,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
@@ -146,6 +147,14 @@ export default function EditUserForm({
dataRoles?.config?.auth?.user?.roles?.allowed,
);
const { data } = useGetProjectLocalesQuery({
variables: {
appId: currentProject?.id,
},
});
const allowedLocales = data?.config?.auth?.user?.locale?.allowed || [];
/**
* This will change the `disabled` field in the user to its opposite.
* If the user is disabled, it will be enabled and vice versa.
@@ -374,12 +383,11 @@ export default function EditUserForm({
error={!!errors.locale}
helperText={errors?.locale?.message}
>
<Option key="en" value="en">
en
</Option>
<Option key="fr" value="fr">
fr
</Option>
{allowedLocales.map((locale) => (
<Option key={locale} value={locale}>
{locale}
</Option>
))}
</ControlledSelect>
</Box>
<Box

View File

@@ -2,8 +2,9 @@ import permissionVariablesQuery from '@/tests/msw/mocks/graphql/permissionVariab
import hasuraMetadataQuery from '@/tests/msw/mocks/rest/hasuraMetadataQuery';
import tableQuery from '@/tests/msw/mocks/rest/tableQuery';
import { render, screen } from '@/tests/testUtils';
import '@testing-library/jest-dom';
import { setupServer } from 'msw/node';
import { test, vi } from 'vitest';
import { afterAll, afterEach, beforeAll, test, vi } from 'vitest';
import ColumnAutocomplete from './ColumnAutocomplete';
const server = setupServer(

View File

@@ -7,7 +7,9 @@ import { filterOptions } from '@/components/ui/v2/Autocomplete';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetPostgresSettingsDocument,
Software_Type_Enum,
useGetPostgresSettingsQuery,
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
@@ -30,15 +32,6 @@ export type DatabaseServiceVersionFormValues = Yup.InferType<
typeof validationSchema
>;
const AVAILABLE_POSTGRES_VERSIONS = [
'14.6-20230705-1',
'14.6-20230613-1',
'14.6-20230525',
'14.6-20230406-2',
'14.6-20230406-1',
'14.6-20230404',
];
export default function DatabaseServiceVersionSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -51,9 +44,16 @@ export default function DatabaseServiceVersionSettings() {
fetchPolicy: 'cache-only',
});
const { data: databaseVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.PostgreSql,
},
});
const { version } = data?.config?.postgres || {};
const databaseVersions = databaseVersionsData?.softwareVersions || [];
const availableVersions = Array.from(
new Set(AVAILABLE_POSTGRES_VERSIONS).add(version),
new Set(databaseVersions.map((el) => el.version)).add(version),
)
.sort()
.reverse()

View File

@@ -7,7 +7,9 @@ import { filterOptions } from '@/components/ui/v2/Autocomplete';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetHasuraSettingsDocument,
Software_Type_Enum,
useGetHasuraSettingsQuery,
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { getToastStyleProps } from '@/utils/constants/settings';
@@ -30,16 +32,6 @@ export type HasuraServiceVersionFormValues = Yup.InferType<
typeof validationSchema
>;
const AVAILABLE_HASURA_VERSIONS = [
'v2.29.0-ce',
'v2.28.2-ce',
'v2.27.0-ce',
'v2.25.1-ce',
'v2.25.0-ce',
'v2.24.1-ce',
'v2.15.2',
];
export default function HasuraServiceVersionSettings() {
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
@@ -53,9 +45,16 @@ export default function HasuraServiceVersionSettings() {
fetchPolicy: 'cache-only',
});
const { data: hasuraVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Hasura,
},
});
const { version } = data?.config?.hasura || {};
const versions = hasuraVersionsData?.softwareVersions || [];
const availableVersions = Array.from(
new Set(AVAILABLE_HASURA_VERSIONS).add(version),
new Set(versions.map((el) => el.version)).add(version),
)
.sort()
.reverse()

View File

@@ -1,4 +1,4 @@
import { FeedbackForm } from '@/components/common/FeedbackForm';
import { ContactUs } from '@/components/common/ContactUs';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Button } from '@/components/ui/v2/Button';
import { Dropdown } from '@/components/ui/v2/Dropdown';
@@ -99,7 +99,7 @@ export default function AppLoader({
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<FeedbackForm />
<ContactUs />
</Dropdown.Content>
</Dropdown.Root>
)}

View File

@@ -1,4 +1,4 @@
import { FeedbackForm } from '@/components/common/FeedbackForm';
import { ContactUs } from '@/components/common/ContactUs';
import { Container } from '@/components/layout/Container';
import { Modal } from '@/components/ui/v1/Modal';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
@@ -250,7 +250,7 @@ export default function ApplicationErrored() {
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<FeedbackForm />
<ContactUs />
</Dropdown.Content>
</Dropdown.Root>

View File

@@ -1,4 +1,4 @@
import { FeedbackForm } from '@/components/common/FeedbackForm';
import { ContactUs } from '@/components/common/ContactUs';
import { Container } from '@/components/layout/Container';
import { Modal } from '@/components/ui/v1/Modal';
import { Button } from '@/components/ui/v2/Button';
@@ -65,7 +65,7 @@ export default function ApplicationUnknown() {
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
transformOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<FeedbackForm />
<ContactUs />
</Dropdown.Content>
</Dropdown.Root>

View File

@@ -0,0 +1,116 @@
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { getToastStyleProps } from '@/utils/constants/settings';
import {
useDeleteRunServiceConfigMutation,
useDeleteRunServiceMutation,
} from '@/utils/__generated__/graphql';
import type { ApolloError } from '@apollo/client';
import { type RunService } from 'pages/[workspaceSlug]/[appSlug]/services';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface DeleteServiceModalProps {
service: RunService;
onDelete?: () => Promise<any>;
close: () => void;
}
export default function DeleteServiceModal({
service,
onDelete,
close,
}: DeleteServiceModalProps) {
const [remove, setRemove] = useState(false);
const [loadingRemove, setLoadingRemove] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [deleteRunService] = useDeleteRunServiceMutation();
const [deleteRunServiceConfig] = useDeleteRunServiceConfigMutation();
const deleteServiceAndConfig = async () => {
await deleteRunService({ variables: { serviceID: service.id } });
await deleteRunServiceConfig({
variables: { appID: currentProject.id, serviceID: service.id },
});
await onDelete?.();
close();
};
async function handleClick() {
setLoadingRemove(true);
await toast.promise(
deleteServiceAndConfig(),
{
loading: 'Deleting 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={twMerge('w-full rounded-lg p-6 text-left')}>
<div className="grid grid-flow-row gap-1">
<Text variant="h3" component="h2">
Delete Service {service?.config?.name}
</Text>
<Text variant="subtitle2">
Are you sure you want to delete this service?
</Text>
<Text
variant="subtitle2"
className="font-bold"
sx={{ color: (theme) => `${theme.palette.error.main} !important` }}
>
This cannot be undone.
</Text>
<Box className="my-4">
<Checkbox
id="accept-1"
label={`I'm sure I want to delete ${service?.config?.name}`}
className="py-2"
checked={remove}
onChange={(_event, checked) => setRemove(checked)}
aria-label="Confirm Delete Project #1"
/>
</Box>
<div className="grid grid-flow-row gap-2">
<Button
color="error"
onClick={handleClick}
disabled={!remove}
loading={loadingRemove}
>
Delete Service
</Button>
<Button variant="outlined" color="secondary" onClick={close}>
Cancel
</Button>
</div>
</div>
</Box>
);
}

View File

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

View File

@@ -111,7 +111,6 @@ export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndP
awsName: null,
domain: null,
},
isProvisioned: true,
createdAt: new Date().toISOString(),
desiredState: ApplicationStatus.Live,
featureFlags: [],

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

@@ -14,7 +14,6 @@ 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 {
@@ -58,26 +57,8 @@ export interface ProjectRoute {
export default function useProjectRoutes() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const {
currentWorkspace,
currentProject,
loading: currentProjectLoading,
} = useCurrentWorkspaceAndProject();
const hypertune = useHypertune();
const enableServices =
currentWorkspace &&
hypertune
.root({
context: {
workSpace: {
id: currentWorkspace.id,
},
},
})
.enableServices({})
.get(false);
const { currentProject, loading: currentProjectLoading } =
useCurrentWorkspaceAndProject();
const nhostRoutes: ProjectRoute[] = [
{
@@ -118,7 +99,7 @@ export default function useProjectRoutes() {
},
];
let allRoutes: ProjectRoute[] = [
const allRoutes: ProjectRoute[] = [
{
relativePath: '/',
exact: true,
@@ -156,18 +137,14 @@ export default function useProjectRoutes() {
label: 'Storage',
icon: <StorageIcon />,
},
];
if (enableServices) {
allRoutes.push({
{
relativePath: '/services',
exact: false,
label: 'Run',
icon: <ServicesIcon />,
});
}
allRoutes = [...allRoutes, ...nhostRoutes];
},
...nhostRoutes,
];
return {
nhostRoutes,

View File

@@ -128,6 +128,8 @@ export default function BaseEnvironmentVariableForm({
error={!!errors.value}
helperText={errors?.value?.message}
fullWidth
multiline
rows={5}
autoComplete="off"
autoFocus={mode === 'edit'}
/>

View File

@@ -154,10 +154,12 @@ export default function LogsHeader({
const services = data.app?.runServices ?? [];
setRunServices(
services.map((s) => ({
label: s.config.name,
value: s.config.name,
})),
services
.filter((s) => !!s.config?.name)
.map((s) => ({
label: s.config.name,
value: `run-${s.config.name}`,
})),
);
}
}, [loading, data]);

View File

@@ -41,11 +41,6 @@ export default function OverviewMetrics() {
numberOfDecimals: 0,
}),
},
{
label: 'Egress Volume',
tooltip: 'Amount of data your services have sent to users',
value: prettifySize(data?.egressVolume?.value || 0),
},
{
label: 'Logs',
tooltip: 'Amount of logs stored',

View File

@@ -96,7 +96,7 @@ export function OverviewUsageMetrics() {
remoteAppMetricsData?.filesAggregate?.aggregate?.sum?.size || 0;
const totalStorage = currentProject?.plan?.isFree
? 1 * 1000 ** 3 // 1 GB
: 10 * 1000 ** 3; // 10 GB
: 50 * 1000 ** 3; // 10 GB
// metrics for users
const usedUsers = remoteAppMetricsData?.usersAggregate?.aggregate?.count || 0;
@@ -105,6 +105,16 @@ export function OverviewUsageMetrics() {
// metrics for functions
const usedFunctions = functionsInfoData?.app.metadataFunctions.length || 0;
const totalFunctions = currentProject?.plan?.isFree ? 10 : 50;
const usedFunctionsDuration = projectMetrics?.functionsDuration.value || 0;
const totalFunctionsDuration = currentProject?.plan?.isFree
? 3600 // 1 hour
: 3600 * 10; // 10 hours
// metrics for egress
const usedEgressVolume = projectMetrics?.egressVolume.value || 0;
const totalEgressVolume = currentProject?.plan?.isFree
? 5 * 1000 ** 3 // 5 GB
: 50 * 1000 ** 3; // 50 GB
if (metricsLoading) {
return (
@@ -112,7 +122,9 @@ export function OverviewUsageMetrics() {
<UsageProgress label="Database" percentage={0} />
<UsageProgress label="Storage" percentage={0} />
<UsageProgress label="Users" percentage={0} />
<UsageProgress label="Functions" percentage={0} />
<UsageProgress label="Number of Functions" percentage={0} />
<UsageProgress label="Functions Execution Time" percentage={0} />
<UsageProgress label="Egress Volume" percentage={0} />
</div>
);
}
@@ -139,6 +151,18 @@ export function OverviewUsageMetrics() {
used={usedFunctions}
percentage={100}
/>
<UsageProgress
label="Functions"
used={usedFunctionsDuration}
percentage={100}
/>
<UsageProgress
label="Egress"
used={usedEgressVolume}
percentage={100}
/>
</div>
);
}
@@ -167,11 +191,25 @@ export function OverviewUsageMetrics() {
/>
<UsageProgress
label="Functions"
label="Number of Functions"
used={usedFunctions}
total={totalFunctions}
percentage={(usedFunctions / totalFunctions) * 100}
/>
<UsageProgress
label="Functions Execution Time"
used={Math.trunc(usedFunctionsDuration)}
total={`${totalFunctionsDuration} seconds`}
percentage={(usedFunctionsDuration / totalFunctionsDuration) * 100}
/>
<UsageProgress
label="Egress Volume"
used={prettifySize(usedEgressVolume)}
total={prettifySize(totalEgressVolume)}
percentage={(usedEgressVolume / totalEgressVolume) * 100}
/>
</div>
);
}

View File

@@ -1,4 +1,5 @@
import type { PermissionVariable } from '@/types/application';
import { expect, test } from 'vitest';
import getAllPermissionVariables from './getAllPermissionVariables';
test('should convert permission variable object to array', () => {

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

@@ -4,7 +4,6 @@ import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { Slider, sliderClasses } from '@/components/ui/v2/Slider';
import { Text } from '@/components/ui/v2/Text';
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
import { calculateBillableResources } from '@/features/projects/resources/settings/utils/calculateBillableResources';
import { getAllocatedResources } from '@/features/projects/resources/settings/utils/getAllocatedResources';
import { prettifyMemory } from '@/features/projects/resources/settings/utils/prettifyMemory';
import { prettifyVCPU } from '@/features/projects/resources/settings/utils/prettifyVCPU';
@@ -63,34 +62,7 @@ export default function TotalResourcesFormFragment({
(formValues.totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE;
const billableResources = calculateBillableResources(
{
replicas: formValues.database?.replicas,
vcpu: formValues.database?.vcpu,
memory: formValues.database?.memory,
},
{
replicas: formValues.hasura?.replicas,
vcpu: formValues.hasura?.vcpu,
memory: formValues.hasura?.memory,
},
{
replicas: formValues.auth?.replicas,
vcpu: formValues.auth?.vcpu,
memory: formValues.auth?.memory,
},
{
replicas: formValues.storage?.replicas,
vcpu: formValues.storage?.vcpu,
memory: formValues.storage?.memory,
},
);
const updatedPrice =
Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE,
) + proPlan.price;
const updatedPrice = priceForTotalAvailableVCPU + proPlan.price;
const { vcpu: allocatedVCPU, memory: allocatedMemory } =
getAllocatedResources(formValues);

View File

@@ -1,4 +1,4 @@
import { test } from 'vitest';
import { expect, test } from 'vitest';
import getAllocatedResources from './getAllocatedResources';
test('should return the total number of allocated resources', () => {

View File

@@ -155,3 +155,4 @@ 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

@@ -3,13 +3,17 @@ 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,
@@ -22,7 +26,9 @@ import { PortsFormSection } from '@/features/services/components/ServiceForm/com
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,
@@ -36,6 +42,8 @@ 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';
import { ServiceDetailsDialog } from './components/ServiceDetailsDialog';
export enum PortTypes {
HTTP = 'http',
@@ -87,7 +95,7 @@ export interface ServiceFormProps extends DialogFormProps {
/**
* if there is initialData then it's an update operation
*/
initialData?: ServiceFormValues;
initialData?: ServiceFormValues & { subdomain?: string }; // subdomain is only set on the backend
/**
* Function to be called when the operation is cancelled.
@@ -106,11 +114,16 @@ export default function ServiceForm({
onCancel,
location,
}: ServiceFormProps) {
const { onDirtyStateChange } = useDialog();
const hostName = useHostName();
const { onDirtyStateChange, openDialog, closeDialog } = useDialog();
const [insertRunService] = useInsertRunServiceMutation();
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertRunServiceConfig] = useInsertRunServiceConfigMutation();
const [replaceRunServiceConfig] = useReplaceRunServiceConfigMutation();
const [detailsServiceId, setDetailsServiceId] = useState('');
const [detailsServiceSubdomain, setDetailsServiceSubdomain] = useState(
initialData?.subdomain,
);
const [createServiceFormError, setCreateServiceFormError] =
useState<Error | null>(null);
@@ -133,6 +146,8 @@ export default function ServiceForm({
formState: { errors, isSubmitting, dirtyFields },
} = form;
const formValues = watch();
const serviceImage = watch('image');
const isDirty = Object.keys(dirtyFields).length > 0;
@@ -141,7 +156,7 @@ export default function ServiceForm({
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
const createOrUpdateService = async (values: ServiceFormValues) => {
const getFormattedConfig = (values: ServiceFormValues) => {
const config: ConfigRunServiceConfigInsertInput = {
name: values.name,
image: {
@@ -171,7 +186,13 @@ export default function ServiceForm({
})),
};
if (initialData) {
return config;
};
const createOrUpdateService = async (values: ServiceFormValues) => {
const config = getFormattedConfig(values);
if (serviceID) {
// Update service config
await replaceRunServiceConfig({
variables: {
@@ -180,11 +201,13 @@ export default function ServiceForm({
config,
},
});
setDetailsServiceId(serviceID);
} else {
// Insert service config
const {
data: {
insertRunService: { id: newServiceID },
insertRunService: { id: newServiceID, subdomain },
},
} = await insertRunService({
variables: {
@@ -211,6 +234,9 @@ export default function ServiceForm({
},
},
});
setDetailsServiceId(newServiceID);
setDetailsServiceSubdomain(subdomain);
}
};
@@ -238,18 +264,75 @@ export default function ServiceForm({
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);
}}
/>
),
});
};
useEffect(() => {
(async () => {
if (detailsServiceId) {
openDialog({
title: 'Service Details',
component: (
<ServiceDetailsDialog
serviceID={detailsServiceId}
subdomain={detailsServiceSubdomain}
ports={formValues.ports}
/>
),
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
})();
}, [detailsServiceId, detailsServiceSubdomain, formValues, openDialog]);
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={handleSubmit}
onSubmit={handleConfirm}
className="grid grid-flow-row gap-4 px-6 pb-6"
>
<Input
@@ -346,6 +429,23 @@ export default function ServiceForm({
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 />
@@ -378,9 +478,24 @@ export default function ServiceForm({
</Alert>
)}
<div className="grid grid-flow-row gap-2">
<Button type="submit" disabled={isSubmitting}>
{initialData ? 'Update' : 'Create'}
</Button>
<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

View File

@@ -48,7 +48,8 @@ export default function ComputeFormSection() {
<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">
CPU: {formValues.compute.cpu} / Memory: {formValues.compute.memory}
vCPUs: {formValues.compute.cpu / 1000} / Memory:{' '}
{formValues.compute.memory}
</Text>
<Tooltip

View File

@@ -7,6 +7,7 @@ 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() {
@@ -15,6 +16,8 @@ export default function EnvironmentFormSection() {
formState: { errors },
} = useFormContext<ServiceFormValues>();
const [focusedInput, setFocusedInput] = useState<string>(null);
const { fields, append, remove } = useFieldArray({
name: 'environment',
});
@@ -49,42 +52,42 @@ export default function EnvironmentFormSection() {
<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(`environment.${index}.name`)}
id={`${field.id}-name`}
label={!index && '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`}
label={!index && 'Value'}
placeholder={`Value ${index}`}
className="w-full"
hideEmptyHelperText
error={!!errors?.environment?.at(index)}
helperText={errors?.environment?.at(index)?.message}
fullWidth
autoComplete="off"
/>
<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-4 w-4" />
<TrashIcon className="h-6 w-4" />
</Button>
</Box>
))}

View File

@@ -32,20 +32,20 @@ export default function PortsFormSection() {
name: 'ports',
});
const formValues = useWatch<ServiceFormValues>();
const formValues = useWatch<ServiceFormValues & { subdomain: string }>();
const onChangePortType = (value: string | undefined, index: number) =>
setValue(`ports.${index}.type`, value as PortTypes);
const showURL = (index: number) =>
formValues.subdomain &&
formValues.ports[index]?.type === PortTypes.HTTP &&
formValues.ports[index]?.publish;
const getPortURL = (_port: string | number, _name: string) => {
const getPortURL = (_port: string | number, subdomain: 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 `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
};
return (
@@ -144,7 +144,7 @@ export default function PortsFormSection() {
title="URL"
value={getPortURL(
formValues.ports[index]?.port,
formValues.name,
formValues.subdomain,
)}
/>
)}

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,75 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import type { ConfigRunServicePort } from '@/utils/__generated__/graphql';
export interface ServiceDetailsDialogProps {
/**
* The id of the service
*/
serviceID: string;
/**
* The subdomain of the service
*/
subdomain: string;
/**
* The service ports
* We use partial here because `port` is set as required in ConfigRunServicePort
*/
ports: Partial<ConfigRunServicePort>[];
}
export default function ServiceDetailsDialog({
serviceID,
subdomain,
ports,
}: ServiceDetailsDialogProps) {
const { currentProject } = useCurrentWorkspaceAndProject();
const { closeDialog } = useDialog();
const getPortURL = (_port: string | number) => {
const port = Number(_port) > 0 ? Number(_port) : '[port]';
return `https://${subdomain}-${port}.svc.${currentProject?.region.awsName}.${currentProject?.region.domain}`;
};
return (
<div className="flex flex-col gap-4 px-6 pb-6">
<div className="flex flex-col gap-2">
<Text color="secondary">Private registry</Text>
<InfoCard
title=""
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
/>
</div>
{ports?.length > 0 && (
<div className="flex flex-col gap-2">
<Text color="secondary">Ports</Text>
{ports
.filter((port) => port.publish)
.map((port) => (
<InfoCard
title={`${port.type}:${port.port}`}
value={getPortURL(port.port)}
/>
))}
</div>
)}
<Button
className="w-full"
color="primary"
onClick={() => closeDialog()}
autoFocus
>
OK
</Button>
</div>
);
}

View File

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

View File

@@ -52,7 +52,8 @@ export default function StorageFormSection() {
title={
<span>
By default, services do not have persistent storage. You can add
SSD disks to the service here. Refer to{' '}
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"

View File

@@ -10,21 +10,14 @@ 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 { DeleteServiceModal } from '@/features/projects/common/components/DeleteServiceModal';
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 {
/**
@@ -51,30 +44,14 @@ export default function ServicesList({
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 { openDrawer, openDialog, closeDialog } = useDialog();
const viewService = async (service: RunService) => {
const {
image,
command,
ports,
resources: { compute, replicas, storage },
} = service.config;
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<Text>Edit {service.config.name}</Text>
<Text>Edit {service.config?.name ?? 'unset'}</Text>
</Box>
),
component: (
@@ -82,16 +59,20 @@ export default function ServicesList({
serviceID={service.id}
initialData={{
...service.config,
image: image.image,
command: command?.join(' '),
ports: ports.map((item) => ({
image: service.config?.image?.image,
subdomain: service.subdomain,
command: service.config?.command?.join(' '),
ports: service.config?.ports?.map((item) => ({
port: item.port,
type: item.type as PortTypes,
publish: item.publish,
})),
compute,
replicas,
storage,
compute: service.config?.resources?.compute ?? {
cpu: 62,
memory: 128,
},
replicas: service.config?.resources?.replicas,
storage: service.config?.resources?.storage,
}}
onSubmit={() => onCreateOrUpdate()}
/>
@@ -99,28 +80,16 @@ export default function ServicesList({
});
};
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(),
);
const deleteService = async (service: RunService) => {
openDialog({
component: (
<DeleteServiceModal
service={service}
close={closeDialog}
onDelete={onDelete}
/>
),
});
};
return (
@@ -146,7 +115,7 @@ export default function ServicesList({
<CubeIcon className="h-5 w-5" />
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{service.config.name}
{service.config?.name ?? 'unset'}
</Text>
<Tooltip title={service.updatedAt}>
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
@@ -207,7 +176,7 @@ export default function ServicesList({
<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)}
onClick={() => deleteService(service)}
>
<TrashIcon className="h-4 w-4" />
<Text className="font-medium" color="error">

View File

@@ -7,6 +7,8 @@ import { filterOptions } from '@/components/ui/v2/Autocomplete';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import {
GetStorageSettingsDocument,
Software_Type_Enum,
useGetSoftwareVersionsQuery,
useGetStorageSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
@@ -30,8 +32,6 @@ export type StorageServiceVersionFormValues = Yup.InferType<
typeof validationSchema
>;
const AVAILABLE_STORAGE_VERSIONS = ['0.3.5', '0.3.4', '0.3.3', '0.3.2'];
export default function StorageServiceVersionSettings() {
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -44,9 +44,16 @@ export default function StorageServiceVersionSettings() {
fetchPolicy: 'cache-only',
});
const { data: storageVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Storage,
},
});
const { version } = data?.config?.storage || {};
const versions = storageVersionsData?.softwareVersions || [];
const availableVersions = Array.from(
new Set(AVAILABLE_STORAGE_VERSIONS).add(version),
new Set(versions.map((el) => el.version)).add(version),
)
.sort()
.reverse()

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,12 @@
query getProjectLocales($appId: uuid!) {
config(appID: $appId, resolve: true) {
auth {
user {
locale {
allowed
default
}
}
}
}
}

View File

@@ -17,6 +17,9 @@ query GetProjectMetrics(
) {
value
}
functionsDuration: getFunctionsDuration(appID: $appId, from: $from, to: $to) {
value
}
postgresVolumeCapacity: getPostgresVolumeCapacity(appID: $appId) {
value
}

View File

@@ -4,7 +4,6 @@ fragment Project on apps {
name
repositoryProductionBranch
subdomain
isProvisioned
createdAt
desiredState
nhostBaseFolder

View File

@@ -0,0 +1,9 @@
query getSoftwareVersions($software: software_type_enum!) {
softwareVersions(
where: { software: { _eq: $software } }
order_by: { version: desc }
) {
version
software
}
}

View File

@@ -1,6 +1,7 @@
query getRunService($id: uuid!, $resolve: Boolean!) {
runService(id: $id) {
id
subdomain
config(resolve: $resolve) {
name
image {

View File

@@ -9,6 +9,7 @@ query getRunServices(
id
createdAt
updatedAt
subdomain
config(resolve: $resolve) {
name
image {

View File

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

View File

@@ -13,20 +13,35 @@ import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import { ServiceForm } from '@/features/services/components/ServiceForm';
import {
ServiceForm,
type PortTypes,
} from '@/features/services/components/ServiceForm';
import ServicesList from '@/features/services/components/ServicesList/ServicesList';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
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 } = useDialog();
const { openDrawer, openAlertDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlanFree = currentProject.plan.isFree;
@@ -66,6 +81,63 @@ export default function ServicesPage() {
[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: (

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

@@ -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,220 @@
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 (
<div className="flex w-full justify-center">
<ActivityIndicator
delay={500}
label="Loading workspaces and projects..."
/>
</div>
);
}
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>
);
};

View File

@@ -48,7 +48,6 @@ export const mockApplication: Project = {
slug: 'test-application',
appStates: [],
subdomain: '',
isProvisioned: true,
region: {
awsName: 'us-east-1',
city: 'New York',

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,8 @@ module.exports = {
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

@@ -1,5 +1,39 @@
# @nhost/docs
## 0.6.2
### Patch Changes
- 4fe4a1696: return `refreshToken` immediately after signIn and signUp
## 0.6.1
### Patch Changes
- 612d75496: updated postgres and graphql documentation
- 3b3e83a21: fix(docs): correct rendering of mermaid diagrams in dark mode
- 765b398b2: added jit settings documentation
- 30aae1557: minor fix to performance documentation
- a3efc1d13: docs: added storage/antivirus documentation
## 0.6.0
### Minor Changes
- 9aaa407d2: Fix messaging around Run's private beta
## 0.5.1
### Patch Changes
- 819e1e97d: update fqdn format for nhost run
## 0.5.0
### Minor Changes
- 960d815f6: added docs for Nhost Run
## 0.4.0
### Minor Changes

View File

@@ -0,0 +1,159 @@
---
title: 'Extensions'
sidebar_position: 4
---
## postgis
PostGIS extends the capabilities of the PostgreSQL relational database by adding support storing, indexing and querying geographic data.
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION postgis;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION postgis;
```
### Resources
* [Official website](https://postgis.net)
## pgvector
Open-source vector similarity search for Postgres. Store your vectors with the rest of your data. Supports:
* exact and approximate nearest neighbor search
* L2 distance, inner product, and cosine distance
* any language with a Postgres client
Plus ACID compliance, point-in-time recovery, JOINs, and all of the other great features of Postgres
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION vector;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION vector;
```
### Resources
* [GitHub](https://github.com/pgvector/pgvector)
## pg_cron
pg_cron is a simple cron-based job scheduler for PostgreSQL (10 or higher) that runs inside the database as an extension. It uses the same syntax as regular cron, but it allows you to schedule PostgreSQL commands directly from the database. You can also use '[1-59] seconds' to schedule a job based on an interval.
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION pg_cron;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION pg_cron;
```
### Resources
* [GitHub](https://github.com/citusdata/pg_cron)
## hypopg
HypoPG is a PostgreSQL extension adding support for hypothetical indexes.
An hypothetical -- or virtual -- index is an index that doesn't really exists, and thus doesn't cost CPU, disk or any resource to create. They're useful to know if specific indexes can increase performance for problematic queries, since you can know if PostgreSQL will use these indexes or not without having to spend resources to create them.
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION hypopg;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION hypopg;
```
### Resources
* [GitHub](https://github.com/HypoPG/hypopg)
* [Documentation](https://hypopg.readthedocs.io)
## timescaledb
TimescaleDB is an open-source database designed to make SQL scalable for time-series data. It is engineered up from PostgreSQL and packaged as a PostgreSQL extension, providing automatic partitioning across time and space (partitioning key), as well as full SQL support.
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION timescaledb;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION timescaledb;
```
### Resources
* [GitHub](https://github.com/timescale/timescaledb)
* [Documentation](https://docs.timescale.com)
* [Website](https://www.timescale.com)
## pg_stat_statements
The pg_stat_statements module provides a means for tracking planning and execution statistics of all SQL statements executed by a server.
### Managing
To install the extension you can create a migration with the following contents:
```
SET ROLE postgres;
CREATE EXTENSION pg_stat_statements;
```
To uninstall it, you can use the following migration:
```
SET ROLE postgres;
DROP EXTENSION pg_stat_statements;
```
### Resources
* [Documentation](https://www.postgresql.org/docs/14/pgstatstatements.html)

View File

@@ -0,0 +1,89 @@
---
title: 'Performance'
sidebar_position: 4
---
Ensuring a healthy and performant PostgreSQL service is crucial as it directly impacts the overall response time and stability of your backend. Since Postgres serves as the centerpiece of your backend, prioritize the optimization and maintenance of your Postgres service to achieve the desired performance and reliability.
In case your Postgres service is not meeting your performance expectations, you can explore the following options:
1. Consider upgrading your [dedicated compute](/platform/compute) resources to provide more processing power and memory to the Postgres server.
2. Fine-tune the configuration parameters of Postgres to optimize its performance. Adjust settings such as `shared_buffers`, `work_mem`, and `effective_cache_size` to better align with your workload and server resources.
3. Identify and analyze slow-performing queries using tools like query logs or query monitoring extensions. Optimize or rewrite these queries to improve their efficiency.
4. Evaluate the usage of indexes in your database. Identify queries that could benefit from additional indexes and strategically add them to improve query performance.
By implementing these steps, you can effectively address performance concerns and enhance the overall performance of your Postgres service.
## Upgrade to our latest postgres image
Before trying anything else, always upgrade to our latest postgres image first. You can find our availables images in the dashbhoard, under your database settings.
## Upgrading dedicated compute
Increasing CPU and memory is the simplest way to address performance issues. You can read more about compute resources [here](/platform/compute).
## Fine-tune configuration parameters
When optimizing your Postgres setup, you can consider adjusting various Postgres settings. You can find a list of these parameters [here](/database/settings). Keep in mind that the optimal values for these parameters will depend on factors such as available resources, workload, and data distribution.
To help you get started, you can use [pgtune](https://pgtune.leopard.in.ua) as a reference tool. Pgtune can generate recommended configuration settings based on your system specifications. By providing information about your system, it can suggest parameter values that may be a good starting point for optimization.
However, it's important to note that the generated settings from pgtune are not guaranteed to be the best for your specific environment. It's always recommended to review and customize the suggested settings based on your particular requirements, performance testing, and ongoing monitoring of your Postgres database.
## Identifying slow queries
Monitoring slow queries is a highly effective method for tackling performance issues. Several tools leverage [pg_stat_statements](https://www.postgresql.org/docs/14/pgstatstatements.html), a PostgreSQL extension, to provide constant monitoring. You can employ these tools to identify and address slow queries in real-time.
### pghero
[PgHero](https://github.com/ankane/pghero) is one of such tools you can use to idenfity and address slow queries. You can easily run pghero alongside your postgres with [Nhost Run](/run):
1. First, make sure the extension [pg_stat_statements](/database/extensions#pg_stat_statements) is enabled.
2. Click on this [one-click install link](https://app.nhost.io:/run-one-click-install?config=eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbeyJuYW1lIjoiREFUQUJBU0VfVVJMIiwidmFsdWUiOiJwb3N0Z3JlczovL3Bvc3RncmVzOltQQVNTV09SRF1AcG9zdGdyZXMtc2VydmljZTo1NDMyL1tTVUJET01BSU5dP3NzbG1vZGU9ZGlzYWJsZSJ9LHsibmFtZSI6IlBHSEVST19VU0VSTkFNRSIsInZhbHVlIjoiW1VTRVJdIn0seyJuYW1lIjoiUEdIRVJPX1BBU1NXT1JEIiwidmFsdWUiOiJbUEFTU1dPUkRdIn1dLCJwb3J0cyI6W3sicG9ydCI6ODA4MCwidHlwZSI6Imh0dHAiLCJwdWJsaXNoIjp0cnVlfV19)
3. Select your project:
![select your project](/img/database/performance/pghero_01.png)
4. Replace the placeholders with your postgres password, subdomain and a user and password to protect your pghero service. Finally, click on create.
![fill run service details](/img/database/performance/pghero_02.png)
5. After confirming the service, copy the URL:
![run service details](/img/database/performance/pghero_03.png)
6. Finally, you can open the link you just copied to access pghero:
![pghero](/img/database/performance/pghero_04.png)
:::info
When you create a new service, it can take a few minutes for the DNS (Domain Name System) to propagate. If your browser displays an error stating that it couldn't find the server or website, simply wait for a couple of minutes and then try again.
:::
After successfully setting up pghero, it will begin displaying slow queries, suggesting index proposals, and offering other valuable information. Utilize this data to enhance your service's performance.
## Adding indexes
Indexes can significantly enhance the speed of data retrieval. However, it's essential to be aware that they introduce additional overhead during mutations. Therefore, understanding your workload is crucial before opting to add an index.
There are tools you can use to help analyze your workload and detect missing indexes.
### pghero
[PgHero](https://github.com/ankane/pghero), in addition to help with slow queries, can also help finding missing and duplicate indexes. See previous section on how to deploy pghero with [Nhost Run](/run).
### dexter
[Dexter](https://github.com/ankane/dexter) can leverage both [pg_stat_statements](https://www.postgresql.org/docs/14/pgstatstatements.html) and [hypopg](https://hypopg.readthedocs.io/en/rel1_stable/) to find and evaluate indexes. You can run dexter directly from your machine:
1. Enable [hypopg](/database/extensions#hypopg)
2. Execute the command `docker run --rm -it ankane/dexter [POSTGRES_CONN_STRING] --pg-stat-statements`
```
$ docker run --rm -it ankane/dexter [POSTGRES_CONN_STRING] --pg-stat-statements
Processing 1631 new query fingerprints
No new indexes found
```

View File

@@ -0,0 +1,84 @@
---
title: 'Settings'
sidebar_position: 3
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
Below you can find the official schema (cue) and an example to configure your postgres database:
<Tabs groupId="package-manager">
<TabItem value="schema" label="schema">
```cue
#Postgres: {
version: string | *"14.6-20230705-1"
// Resources for the service, optional
resources?: #Resources & {
replicas: 1
}
// postgres settings of the same name in camelCase, optional
settings?: {
jit: "off" | "on" | *"on"
maxConnections: int32 | *100
sharedBuffers: string | *"128MB"
effectiveCacheSize: string | *"4GB"
maintenanceWorkMem: string | *"64MB"
checkpointCompletionTarget: number | *0.9
walBuffers: int32 | *-1
defaultStatisticsTarget: int32 | *100
randomPageCost: number | *4.0
effectiveIOConcurrency: int32 | *1
workMem: string | *"4MB"
hugePages: string | *"try"
minWalSize: string | *"80MB"
maxWalSize: string | *"1GB"
maxWorkerProcesses: int32 | *8
maxParallelWorkersPerGather: int32 | *2
maxParallelWorkers: int32 | *8
maxParallelMaintenanceWorkers: int32 | *2
}
}
```
</TabItem>
<TabItem value="toml" label="toml" default>
```toml
[postgres]
version = '14.6-20230925-1'
[postgres.resources.compute]
cpu = 1000
memory = 2048
[postgres.settings]
jit = "off"
maxConnections = 100
sharedBuffers = '256MB'
effectiveCacheSize = '768MB'
maintenanceWorkMem = '64MB'
checkpointCompletionTarget = 0.9
walBuffers = -1
defaultStatisticsTarget = 100
randomPageCost = 1.1
effectiveIOConcurrency = 200
workMem = '1310kB'
hugePages = 'off'
minWalSize = '80MB'
maxWalSize = '1GB'
maxWorkerProcesses = 8
maxParallelWorkersPerGather = 2
maxParallelWorkers = 8
maxParallelMaintenanceWorkers = 2
```
</TabItem>
</Tabs>
:::info
At the time of writing this document postgres settings are only supported via the [configuration file](https://nhost.io/blog/config).
:::

View File

@@ -144,6 +144,14 @@ One of the most common permission requirements is that authenticated users shoul
1. Select the **columns** you want the user to be able to read. In our case, we'll allow the user to read all columns.
1. Click **Save**.
## Known issues
### Permissions are slow
In certain situations, permission checks can cause significant delays. One way to identify this issue is by comparing the execution time of a GraphQL query when performed as an admin versus as a regular user. To resolve such cases, disabling the Just-in-Time (JIT) compilation in [Postgres](/database/settings) can be beneficial.
[Github issue](https://github.com/hasura/graphql-engine/issues/3672)
## Next Steps
Hasura has more in-depth documentation related to permissions that you can learn from:

View File

@@ -0,0 +1,174 @@
---
title: 'Settings'
sidebar_position: 3
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
Below you can find the official schema (cue) and an example to configure your graphql service:
<Tabs groupId="package-manager">
<TabItem value="schema" label="schema">
```cue
// Configuration for hasura service
#Hasura: {
// Version of hasura, you can see available versions in the URL below:
// https://hub.docker.com/r/hasura/graphql-engine/tags
version: string | *"v2.33.4-ce"
// JWT Secrets configuration
jwtSecrets: [#JWTSecret]
// Admin secret
adminSecret: string
// Webhook secret
webhookSecret: string
// Configuration for hasura services
// Reference: https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/
settings: {
// HASURA_GRAPHQL_CORS_DOMAIN
corsDomain: [...#Url] | *["*"]
// HASURA_GRAPHQL_DEV_MODE
devMode: bool | *true
// HASURA_GRAPHQL_ENABLE_ALLOWLIST
enableAllowList: bool | *false
// HASURA_GRAPHQL_ENABLE_CONSOLE
enableConsole: bool | *true
// HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS
enableRemoteSchemaPermissions: bool | *false
// HASURA_GRAPHQL_ENABLED_APIS
enabledAPIs: [...#HasuraAPIs] | *["metadata", "graphql", "pgdump", "config"]
// HASURA_GRAPHQL_LIVE_QUERIES_MULTIPLEXED_REFETCH_INTERVAL
liveQueriesMultiplexedRefetchInterval: uint32 | *1000
}
logs: {
// HASURA_GRAPHQL_LOG_LEVEL
level: "debug" | "info" | "error" | *"warn"
}
events: {
// HASURA_GRAPHQL_EVENTS_HTTP_POOL_SIZE
httpPoolSize: uint32 & >=1 & <=100 | *100
}
// Resources for the service
resources?: #Resources
}
```
</TabItem>
<TabItem value="toml" label="toml" default>
```toml
[hasura]
version = ''
adminSecret = 'adminsecret'
webhookSecret = 'webhooksecret'
[[hasura.jwtSecrets]]
type = 'HS256'
key = 'secret'
[hasura.settings]
corsDomain = ['*']
devMode = false
enableAllowList = true
enableConsole = true
enableRemoteSchemaPermissions = true
enabledAPIs = ['metadata']
liveQueriesMultiplexedRefetchInterval = 1000
[hasura.logs]
level = 'warn'
[hasura.events]
httpPoolSize = 10
[hasura.resources]
replicas = 1
[hasura.resources.compute]
cpu = 500
memory = 1024
```
</TabItem>
</Tabs>
### JWT Secret
All formats supported by [hasura](https://hasura.io/docs/latest/auth/authentication/jwt/) should be supported:
<Tabs groupId="package-manager">
<TabItem value="schema" label="schema" default>
```cue
#JWTSecret:
({
type: "HS384" | "HS512" | "RS256" | "RS384" | "RS512" | "Ed25519" | *"HS256"
key: string
} |
{
jwk_url: #Url | *null
}) &
{
claims_format?: "stringified_json" | *"json"
audience?: string
issuer?: string
allowed_skew?: uint32
header?: string
} & {
claims_map?: [...#ClaimMap]
} &
({
claims_namespace: string | *"https://hasura.io/jwt/claims"
} |
{
claims_namespace_path: string
} | *{})
#ClaimMap: {
claim: string
{
value: string
} | {
path: string
default?: string
}
} & {
}
```
</TabItem>
<TabItem value="toml" label="toml">
```toml
# example 1
[[hasura.jwtSecrets]]
type = 'HS256'
key = 'secret'
# example 2
[[hasura.jwtSecrets]]
jwk_url = 'https:/....'
# example 3
[[hasura.jwtSecrets]]
jwk_url = "https://......"
issuer = "https://my-auth-server.com"
[[hasura.jwtSecrets.claims_map]]
claim = "x-some-claim"
value = "some-value"
[[hasura.jwtSecrets.claims_map]]
claim = "x-other-claim"
path = "$.user.claim.id"
default = "default-value"
```
</TabItem>
</Tabs>

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

@@ -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://<run_service_subdomain>-<port>.svc.<region>.nhost.run`
For example:
`https://zlbmqjfczuwqvsquujno-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 public beta. If you find any bugs or if you have any feedback and suggestions, please 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

@@ -0,0 +1,4 @@
{
"label": "Storage",
"position": 7
}

44
docs/docs/storage/av.mdx Normal file
View File

@@ -0,0 +1,44 @@
---
title: Antivirus
sidebar_label: Antivirus
sidebar_position: 2
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
Integration with [clamav](https://www.clamav.net) antivirus relies on an external [clamd](https://docs.clamav.net/manual/Usage/Scanning.html#clamd) service. When a file is uploaded `hasura-storage` will create the file metadata first and then check if the file is clean with `clamd` via its TCP socket. If the file is clean the rest of the process will continue as usual. If a virus is found details about the virus will be added to the `virus` table and the rest of the process will be aborted.
``` mermaid
sequenceDiagram
actor User
User ->> storage: upload file
storage ->>clamav: check for virus
alt virus found
storage-->s3: abort upload
storage->>graphql: insert row in virus table
else virus not found
storage->>s3: upload
storage->>graphql: update metadata
end
```
To enable the antivirus you need to follow the next steps:
1. Deploy using [Nhost Run](/run) a dedicated instance of `clamd` with this [one-click install link](https://app.nhost.io:/run-one-click-install?config=eyJuYW1lIjoiY2xhbWF2IiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vbmhvc3QvY2xhbWF2OjAuMS4xIn0sImNvbW1hbmQiOltdLCJyZXNvdXJjZXMiOnsiY29tcHV0ZSI6eyJjcHUiOjEwMDAsIm1lbW9yeSI6MjA0OH0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbXSwicG9ydHMiOlt7InBvcnQiOiIzMzEwIiwidHlwZSI6InRjcCIsInB1Ymxpc2giOmZhbHNlfV19).
2. Select the project:
![select project](/img/storage/av_01.png)
3. Click on "Create":
![click on create](/img/storage/av_02.png)
4. Make sure you are running **at least** storage version 0.4.0 and enable the antivirus:
![update settings](/img/storage/av_03.png)
5. Wait for the service to update and try to upload a sample virus file like [eicar](https://www.eicar.org/download-anti-malware-testfile/)
![upload virus](/img/storage/av_04.png)
6. If the setup is working the upload should fail
![upload fails](/img/storage/av_05.png)
7. You can also head to hasura and verify entries were added to the `virus` table:
![click on create](/img/storage/av_06.png)
That entry should have useful information about like the filename, the virus found and the user session. In addition, the information on that table can be used a source for events.

View File

@@ -0,0 +1,57 @@
---
title: "Example: CRM System"
sidebar_label: "Example: CRM System"
sidebar_position: 3
---
Let's say you want to build a CRM system and you want to store files for customers. This is one way how you could do that.
Start with, you would have two tables:
1. `customers` - Customer data.
2. `customer_files` - What file belongs to what customer
```text
- customers
- id
- name
- address
customer_files
- id
- customer_id (Foreign Key to `customers.id`)
- file_id (Foreign Key to `storage.files.id`)
```
You would also create a [Hasura Relationship](https://hasura.io/docs/latest/graphql/core/databases/postgres/schema/table-relationships/index/) (GraphQL relationship) between between `customers` and `customer_files` and between `customer_files` and `storage.files`.
With the two tables and GraphQL relationships in place, you can query customers and the customer's files like this:
```graphql
query {
customers {
# customers table
id
name
customer_files {
# customer_files table
id
file {
# storage.files table
id
name
size
mimeType
}
}
}
}
```
The file upload process would be as follows:
1. Upload a file.
2. Get the returned file id.
3. Insert (GraphQL Mutation) the file `id` and the customer's `id` into the `customer_files` table.
This would allow you to upload and download files belonging to specific customers in your CRM system.

View File

@@ -1,7 +1,7 @@
---
title: 'Storage'
sidebar_position: 7
title: 'Overview'
image: /img/og/storage.png
sidebar_position: 1
---
import Tabs from '@theme/Tabs'
@@ -195,57 +195,3 @@ Image Transformation works on both public and pre-signed URLs.
```text
https://[subdomain].storage.[region].nhost.run/v1/files/08e6fa32-0880-4d0e-a832-278198acb363?w=500
```
## Example: CRM System
Let's say you want to build a CRM system and you want to store files for customers. This is one way how you could do that.
Start with, you would have two tables:
1. `customers` - Customer data.
2. `customer_files` - What file belongs to what customer
```text
- customers
- id
- name
- address
customer_files
- id
- customer_id (Foreign Key to `customers.id`)
- file_id (Foreign Key to `storage.files.id`)
```
You would also create a [Hasura Relationship](https://hasura.io/docs/latest/graphql/core/databases/postgres/schema/table-relationships/index/) (GraphQL relationship) between between `customers` and `customer_files` and between `customer_files` and `storage.files`.
With the two tables and GraphQL relationships in place, you can query customers and the customer's files like this:
```graphql
query {
customers {
# customers table
id
name
customer_files {
# customer_files table
id
file {
# storage.files table
id
name
size
mimeType
}
}
}
}
```
The file upload process would be as follows:
1. Upload a file.
2. Get the returned file id.
3. Insert (GraphQL Mutation) the file `id` and the customer's `id` into the `customer_files` table.
This would allow you to upload and download files belonging to specific customers in your CRM system.

View File

@@ -26,6 +26,10 @@ const config = {
favicon: 'img/favicon.png',
organizationName: 'nhost',
projectName: 'docs',
markdown: {
mermaid: true
},
themes: ['@docusaurus/theme-mermaid'],
scripts: [
{ src: 'https://plausible.io/js/script.js', defer: true, 'data-domain': 'docs.nhost.io' }
],
@@ -44,7 +48,6 @@ const config = {
routeBasePath: '/',
breadcrumbs: false,
sidebarPath: require.resolve('./sidebars.js'),
remarkPlugins: [require('mdx-mermaid')],
editUrl: 'https://github.com/nhost/nhost/edit/main/docs/'
},
theme: {
@@ -177,6 +180,7 @@ const config = {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
defaultLanguage: 'javascript',
additionalLanguages: ['cue', 'toml'],
magicComments: [
{
className: 'code-block-error-line',

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.4.0",
"version": "0.6.2",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@@ -19,12 +19,13 @@
"@docusaurus/core": "2.4.1",
"@docusaurus/plugin-sitemap": "2.4.1",
"@docusaurus/preset-classic": "2.4.1",
"@docusaurus/theme-mermaid": "2.4.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-image-zoom": "^0.1.1",
"mdx-mermaid": "^1.3.2",
"mermaid": "^9.0.0",
"prism-react-renderer": "^1.3.5",
"prismjs": "^1.29.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"unist-util-visit": "^2.0.0"

View File

@@ -44,8 +44,27 @@ const sidebars = {
}
]
},
'storage',
{
type: 'category',
label: 'Storage',
items: [
{
type: 'autogenerated',
dirName: 'storage'
}
]
},
'serverless-functions',
{
type: 'category',
label: 'Run',
items: [
{
type: 'autogenerated',
dirName: 'run'
}
]
},
{
type: 'category',
label: 'CLI',

View File

@@ -0,0 +1,2 @@
import type * as PrismNamespace from 'prismjs';
export default function prismIncludeLanguages(PrismObject: typeof PrismNamespace): void;

View File

@@ -0,0 +1,20 @@
/*global globalThis*/
import siteConfig from '@generated/docusaurus.config'
export default function prismIncludeLanguages(PrismObject) {
const {
themeConfig: { prism }
} = siteConfig
const { additionalLanguages } = prism
// Prism components work on the Prism instance on the window, while prism-
// react-renderer uses its own Prism instance. We temporarily mount the
// instance onto window, import components to enhance it, then remove it to
// avoid polluting global namespace.
// You can mutate PrismObject: registering plugins, deleting languages... As
// long as you don't re-assign it
globalThis.Prism = PrismObject
additionalLanguages.forEach((lang) => {
// eslint-disable-next-line global-require, import/no-dynamic-require
require(`prismjs/components/prism-${lang}`)
})
delete globalThis.Prism
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

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