Compare commits

..

68 Commits

Author SHA1 Message Date
Szilárd Dóró
89b70eb93c Merge pull request #1627 from nhost/changeset-release/main
chore: update versions
2023-02-17 13:09:56 +01:00
github-actions[bot]
860d872d07 chore: update versions 2023-02-17 11:18:24 +00:00
Szilárd Dóró
b9917c0c69 Merge pull request #1628 from nhost/chore/react-monorepo-18
chore(deps): bump `react` to v18
2023-02-17 12:16:58 +01:00
Szilárd Dóró
bf1e4071db chore(deps): bump react to v18 2023-02-17 10:36:25 +01:00
Szilárd Dóró
e5601581f5 Merge pull request #1624 from nhost/renovate-changesets
chore: create changesest from Renovate bumps
2023-02-17 10:03:53 +01:00
Szilárd Dóró
5013213bc3 fix changesets 2023-02-17 10:03:22 +01:00
szilarddoro
8be094be54 fix(deps): update docusaurus monorepo to v2.3.1 2023-02-17 08:56:50 +00:00
Szilárd Dóró
43e5221119 Merge pull request #1559 from nhost/renovate/docusaurus-monorepo
fix(deps): update docusaurus monorepo to v2.3.1
2023-02-17 09:55:40 +01:00
Szilárd Dóró
6f8feaffc5 Merge pull request #1518 from nhost/renovate/commander-10.x
fix(deps): update dependency commander to v10
2023-02-17 09:30:45 +01:00
Szilárd Dóró
cf347341d4 Merge pull request #1620 from nhost/changeset-release/main
chore: update versions
2023-02-16 09:54:38 +01:00
renovate[bot]
5262fac6d5 fix(deps): update docusaurus monorepo to v2.3.1 2023-02-16 08:39:58 +00:00
github-actions[bot]
d6c670a78b chore: update versions 2023-02-16 08:32:50 +00:00
Szilárd Dóró
197e406209 Merge pull request #1614 from nhost/fix/deployments-are-undefined
fix(dashboard): don't break the UI when deployments are unavailable
2023-02-16 09:30:53 +01:00
Szilárd Dóró
5fc78964dc fix(dashboard): fix deployments on the deployments page 2023-02-15 18:32:55 +01:00
Szilárd Dóró
1bb71df7a1 Merge pull request #1616 from nhost/changeset-release/main
chore: update versions
2023-02-15 15:30:21 +01:00
Szilárd Dóró
5880f0cd17 chore(hasura-auth-js): bump msw version 2023-02-15 15:14:15 +01:00
github-actions[bot]
d7404e2247 chore: update versions 2023-02-15 14:04:47 +00:00
Szilárd Dóró
2aa91cf266 Merge pull request #1613 from nhost/fix/header-navlinks
fix(dashboard): fix header link color
2023-02-15 15:03:27 +01:00
Szilárd Dóró
b2b17d71aa fix(dashboard): fix build error 2023-02-15 14:16:54 +01:00
Szilárd Dóró
0785a41070 fix lockfile 2023-02-15 13:52:52 +01:00
Szilárd Dóró
c6c8569088 chore(dashboard): extended deployment tests 2023-02-15 13:51:56 +01:00
Szilárd Dóró
c38f60740e chore(dashboard): added deployment list tests 2023-02-15 13:45:32 +01:00
Szilárd Dóró
a37a430b9f fix(dashboard): don't break UI when deployments are unavailable 2023-02-15 11:42:28 +01:00
Szilárd Dóró
7b970e6858 fix(dashboard): fix header link color 2023-02-15 09:58:53 +01:00
Szilárd Dóró
5bdb2c605d Merge pull request #1612 from nhost/changeset-release/main 2023-02-15 09:08:22 +01:00
Johan Eliasson
a08cfc696d Merge pull request #1611 from nhost/docs-ytg89ji
CLI docs updates.
2023-02-15 08:09:41 +01:00
Johan Eliasson
9c4223c547 Update docs/docs/reference/cli/up.mdx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-02-15 08:09:12 +01:00
github-actions[bot]
7de3ed5323 chore: update versions 2023-02-14 15:52:32 +00:00
Szilárd Dóró
83f4e32972 Merge pull request #1610 from nhost/feat/new-sign-in-page 2023-02-14 16:50:48 +01:00
Szilárd Dóró
7b86f38ba3 fix(dashboard): add autofocus to reset input 2023-02-14 16:37:23 +01:00
Szilárd Dóró
a689b0dbe0 fix(dashboard): fix auth pages button feedback 2023-02-14 12:04:21 +01:00
Szilárd Dóró
14235d08bc chore(dashboard): add autofocus to sign in and sign up input fields 2023-02-14 11:55:56 +01:00
Szilárd Dóró
e0a987248f fix(dashboard): fix mobile responsive layout 2023-02-14 11:29:53 +01:00
Szilárd Dóró
1303fc60ac feat(dashboard): update sign in and sign up pages 2023-02-14 11:24:12 +01:00
Johan Eliasson
172cab87c7 updates 2023-02-14 09:38:26 +01:00
Johan Eliasson
a463d5c3bf updates 2023-02-14 08:47:47 +01:00
Szilárd Dóró
041385f19a chore(dashboard): cleanup
- reset original page route structure
2023-02-13 14:08:43 +01:00
Szilárd Dóró
47949ed4af Update logo.svg 2023-02-13 11:57:10 +01:00
Szilárd Dóró
6a52a267ff Update logo.svg 2023-02-13 11:56:27 +01:00
Szilárd Dóró
f33242f2c4 chore(dashboard): add changeset 2023-02-13 11:51:03 +01:00
Szilárd Dóró
b9013e2f6e feat(dashboard): add reset password page 2023-02-13 11:50:22 +01:00
Szilárd Dóró
bbe6fb39c8 feat(dashboard): update sign in page 2023-02-13 11:35:31 +01:00
Szilárd Dóró
955e3e1316 feat(dashboard): finalize sign up pages 2023-02-13 11:07:11 +01:00
Szilárd Dóró
2dba783ad3 feat(dashboard): updated sign up pages 2023-02-13 10:59:40 +01:00
Szilárd Dóró
dbc2fadd87 Merge pull request #1606 from nhost/changeset-release/main
chore: update versions
2023-02-10 14:57:35 +01:00
github-actions[bot]
4578065807 chore: update versions 2023-02-10 12:30:32 +00:00
Szilárd Dóró
feb0c20eb3 Merge pull request #1574 from nhost/fix/dashboard-dark-mode
fix(dashboard): use correct theme color in dark mode
2023-02-10 13:29:17 +01:00
Szilárd Dóró
e3efed65a4 Merge pull request #1605 from nhost/changeset-release/main
chore: update versions
2023-02-09 14:54:24 +01:00
github-actions[bot]
194dec1f00 chore: update versions 2023-02-09 13:22:00 +00:00
Szilárd Dóró
0a1b36e600 Merge pull request #1604 from nhost/fix/logs-allow-hasura
fix(dashboard): re-enable Hasura on logs page
2023-02-09 14:20:41 +01:00
Szilárd Dóró
902f486bbe fix(dashboard): re-enable Hasura on logs page 2023-02-09 14:18:52 +01:00
Szilárd Dóró
fa544cc696 Merge pull request #1601 from nhost/changeset-release/main
chore: update versions
2023-02-09 11:45:11 +01:00
Johan Eliasson
7f959b3544 Merge pull request #1545 from nhost/docs-general-9ashd9asd
docs(graphql): updates
2023-02-09 08:18:43 +01:00
github-actions[bot]
bf5056f14b chore: update versions 2023-02-08 17:10:41 +00:00
Szilárd Dóró
b3e674ced0 Merge pull request #1600 from nhost/fix/permissions
fix(dashboard): apply select permissions properly
2023-02-08 18:09:25 +01:00
Szilárd Dóró
1f9720fa25 fix(dashboard): apply select permissions properly 2023-02-08 15:42:15 +01:00
Szilárd Dóró
747bc1104a Merge pull request #1599 from nhost/changeset-release/main
chore: update versions
2023-02-08 10:38:40 +01:00
github-actions[bot]
887c168b1b chore: update versions 2023-02-08 09:37:42 +00:00
Szilárd Dóró
038d903555 Merge pull request #1598 from nhost/country-code-fix
Use country code instead of name
2023-02-08 10:36:31 +01:00
Szilárd Dóró
deb14b510b chore(dashboard): add changeset 2023-02-08 10:35:52 +01:00
Johan Eliasson
8ee23c9303 use code 2023-02-08 10:22:56 +01:00
Pilou
bca1835ecd Merge pull request #1586 from nhost/docs/typo
Correct useMultipleFilesUpload inline documentation
2023-02-06 14:28:28 +01:00
Pierre-Louis Mercereau
eb55408f85 typo 2023-02-06 10:36:46 +01:00
Szilárd Dóró
e9c8909c6b fix(dashboard): use correct theme color in dark mode 2023-02-01 18:31:04 +01:00
Johan Eliasson
d539a103d9 Update docs/docs/graphql/permissions.mdx
Co-authored-by: Szilárd Dóró <doroszilard@icloud.com>
2023-02-01 09:38:31 +01:00
Johan Eliasson
1bc3c30f85 formatting 2023-01-27 22:26:29 +01:00
Johan Eliasson
4be6406a1e updates 2023-01-27 22:18:44 +01:00
renovate[bot]
88a992ba36 fix(deps): update dependency commander to v10 2023-01-14 04:54:59 +00:00
96 changed files with 2706 additions and 2451 deletions

View File

@@ -1,5 +1,59 @@
# @nhost/dashboard
## 0.11.11
### Patch Changes
- bf1e4071: chore(dashboard): bump `react-is` version to `18.2.0`
- Updated dependencies [bf1e4071]
- Updated dependencies [5013213b]
- @nhost/nextjs@1.13.5
- @nhost/react-apollo@4.13.5
## 0.11.10
### Patch Changes
- a37a430b: fix(dashboard): don't break UI when deployments are unavailable
- @nhost/react-apollo@4.13.4
- @nhost/nextjs@1.13.4
## 0.11.9
### Patch Changes
- 7b970e68: fix(dashboard): fix header link color
## 0.11.8
### Patch Changes
- f33242f2: feat(dashboard): add new sign up, sign in and reset password pages
## 0.11.7
### Patch Changes
- e9c8909c: fix(dashboard): use correct theme color in dark mode
## 0.11.6
### Patch Changes
- 902f486b: fix(dashboard): re-enable Hasura on logs page
## 0.11.5
### Patch Changes
- 1f9720fa: fix(dashboard): apply select permissions properly
## 0.11.4
### Patch Changes
- deb14b51: fix(dashboard): don't break billing form
## 0.11.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.11.3",
"version": "0.11.11",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -67,7 +67,7 @@
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.39.5",
"react-hot-toast": "^2.4.0",
"react-is": "17.0.2",
"react-is": "18.2.0",
"react-loading-skeleton": "^2.2.0",
"react-merge-refs": "^1.1.0",
"react-syntax-highlighter": "^15.4.5",
@@ -129,7 +129,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^21.0.0",
"lint-staged": ">=13",
"msw": "^0.49.0",
"msw": "^1.0.1",
"msw-storybook-addon": "^1.6.3",
"postcss": "^8.4.19",
"prettier": "^2.7.1",

View File

@@ -1,17 +0,0 @@
<svg width="142" height="48" viewBox="0 0 142 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M75.985 22.8273H83.1731V17.1251H86.9756V31.7529H83.1731V26.0506H75.985V31.7529H72.3164V17.1251H75.985V22.8273ZM101.48 17.1251H95.6099C93.9842 17.1251 92.9947 17.4319 92.2458 18.1792C91.4999 18.9235 91.2137 19.8684 91.2137 21.5362V27.3478C91.2137 29.0157 91.4999 29.9575 92.2458 30.7048C92.9917 31.4522 93.9811 31.759 95.6099 31.759H101.48C103.105 31.759 104.095 31.4522 104.844 30.7048C105.593 29.9605 105.876 29.0157 105.876 27.3478V21.5362C105.876 19.8684 105.59 18.9266 104.844 18.1792C104.095 17.4319 103.105 17.1251 101.48 17.1251ZM102.204 27.4329C102.204 28.2896 101.918 28.5296 100.886 28.5296H96.2036C95.1715 28.5296 94.8853 28.2896 94.8853 27.4329V21.4451C94.8853 20.5884 95.1715 20.3484 96.2036 20.3484H100.886C101.942 20.3484 102.204 20.5671 102.204 21.4451V27.4329ZM114.972 22.9792H120.224C121.85 22.9792 122.861 23.2861 123.588 24.0334C124.246 24.6927 124.62 25.7651 124.62 26.9286V27.8066C124.62 28.9701 124.246 30.0213 123.588 30.7018C122.861 31.4491 121.871 31.756 120.224 31.756H110.202V28.5327H119.609C120.641 28.5327 120.927 28.2927 120.927 27.4359V26.9985C120.927 26.1418 120.641 25.9018 119.609 25.9018H114.357C112.729 25.9018 111.718 25.5919 110.993 24.8476C110.336 24.1884 109.961 23.1159 109.961 21.9524V21.0744C109.961 19.9109 110.333 18.8597 110.993 18.1792C111.721 17.4319 112.707 17.1251 114.357 17.1251H123.856V20.3484H114.972C113.94 20.3484 113.654 20.5884 113.654 21.4451V21.8825C113.654 22.7392 113.94 22.9792 114.972 22.9792ZM126.946 20.3484V17.1251H141.891V20.3484H136.265V31.7529H132.594V20.3484H126.946Z" fill="#21324B"/>
<path d="M63.5321 17.0096H57.6624C56.0366 17.0096 55.0471 17.3164 54.2982 18.0638C53.5523 18.8081 53.2661 19.7529 53.2661 21.4177V23.6385V24.322V31.7499H56.9378V24.3251V23.6415V21.3327C56.9378 20.4759 57.224 20.2359 58.256 20.2359H62.9385C63.9949 20.2359 64.2567 20.4547 64.2567 21.3327V23.6415V24.3251V31.7529H67.9284V24.3251V23.6415V21.4208C67.9284 19.7529 67.6422 18.8111 66.8963 18.0668C66.1504 17.3164 65.1609 17.0096 63.5321 17.0096Z" fill="#0052CD"/>
<g clip-path="url(#clip1)">
<path d="M40.8114 10.2866L24.0819 0.647089C22.581 -0.215696 20.7178 -0.215696 19.2138 0.647089C17.7129 1.51291 16.7813 3.12304 16.7813 4.85165V6.10937L15.6913 5.48051C14.1904 4.61772 12.3272 4.61772 10.8232 5.48051C9.32224 6.34633 8.39063 7.95646 8.39063 9.6881V10.9458L7.3007 10.317C5.79976 9.45418 3.93653 9.45418 2.43255 10.317C0.931612 11.1828 0 12.7929 0 14.5246V44.7281C0 45.597 0.505383 46.4051 1.29086 46.7818C2.0733 47.1615 3.02318 47.0582 3.70515 46.5205L12.0014 39.9919L24.7944 47.362C25.1475 47.5656 25.5433 47.6658 25.9391 47.6658C26.3349 47.6658 26.7307 47.5625 27.0838 47.362C27.7901 46.9549 28.2285 46.1985 28.2285 45.3843V27.2081C28.2285 24.2248 26.6211 21.4481 24.0332 19.9565L19.8379 17.5382V4.85468C19.8379 4.21063 20.185 3.60911 20.7452 3.28709C21.3054 2.96506 21.9995 2.96506 22.5597 3.28709L39.2892 12.9235C40.9363 13.8714 41.9592 15.6425 41.9592 17.5382V40.1711C41.9592 40.8152 41.6121 41.4167 41.052 41.7387L36.6192 44.2937V22.3716C36.6192 19.3884 35.0117 16.6116 32.4239 15.12L22.1243 9.18684V12.7018L30.8986 17.757C32.5456 18.7048 33.5686 20.4729 33.5686 22.3716V45.6091C33.5686 46.4203 34.007 47.1797 34.7133 47.5868C35.0665 47.7904 35.4623 47.8906 35.8581 47.8906C36.2538 47.8906 36.6496 47.7873 37.0028 47.5868L42.5803 44.3727C44.0812 43.5068 45.0128 41.8967 45.0128 40.1651V17.5322C45.0068 14.558 43.3993 11.7782 40.8114 10.2866ZM22.5019 22.5934C24.1489 23.5413 25.1719 25.3094 25.1719 27.2081V44.0689L14.577 37.9656L17.9777 35.2921C19.156 34.3656 19.8318 32.9772 19.8318 31.4795V21.0592L22.5019 22.5934ZM16.7813 19.2972V31.4734C16.7813 32.0324 16.5286 32.5519 16.0902 32.8952L3.05058 43.1544V14.5215C3.05058 13.8775 3.39766 13.2759 3.95784 12.9539C4.51803 12.6319 5.21217 12.6319 5.77236 12.9539L8.39063 14.4608V36.0456L11.4412 33.6456V9.6881C11.4412 9.04405 11.7883 8.44253 12.3485 8.12051C12.9087 7.79848 13.6028 7.79848 14.163 8.12051L16.7813 9.62734V15.7792L13.7307 14.0203V17.5382L16.7813 19.2972Z" fill="#0052CD"/>
</g>
</g>
<defs>
<clipPath id="clip0">
<rect width="141.873" height="48" fill="white"/>
</clipPath>
<clipPath id="clip1">
<rect width="45.0128" height="47.8906" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1 +0,0 @@
<svg width="150" height="30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M123.477 14.573c0-2.369 1.633-3.936 3.759-3.936s3.758 1.567 3.758 3.936c0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.759-1.604-3.759-3.936Zm4.252 7.361c3.758 0 7.023-3.134 7.023-7.398 0-4.263-3.265-7.397-7.023-7.397-2.468 0-3.91 1.42-3.91 1.42V2h-3.797v19.789h2.772l.645-1.422c.038 0 1.671 1.567 4.29 1.567Zm-14.73-7.361c0 2.369-1.632 3.936-3.758 3.936-2.127 0-3.759-1.567-3.759-3.936 0-2.369 1.632-3.936 3.759-3.936 2.126 0 3.758 1.567 3.758 3.936Zm-4.252 7.361c2.62 0 4.252-1.567 4.252-1.567l.645 1.422h2.772V7.357h-2.772l-.645 1.421s-1.632-1.567-4.252-1.567c-3.759 0-7.024 3.134-7.024 7.398 0 4.264 3.265 7.325 7.024 7.325ZM98.952 2h-3.758v19.789h3.758V2Zm-7.365 5.175c-2.278 0-3.606 1.713-3.606 1.713l-.646-1.567h-2.771v14.431h3.758V14.1c0-1.895 1.29-3.134 3.113-3.134.835 0 1.291.146 1.291.146V7.357s-.493-.182-1.139-.182Zm-17.008 3.462c1.974 0 3.265 1.24 3.607 2.843h-7.214c.304-1.604 1.633-2.843 3.607-2.843Zm2.771 6.742s-.835 1.093-2.771 1.093-3.113-1.093-3.417-2.514H81.64s.152-.802.152-1.421c0-4.41-3.265-7.399-7.213-7.399-4.1 0-7.517 3.28-7.517 7.399 0 4.118 3.417 7.397 7.517 7.397 3.417 0 5.885-2.368 6.72-4.555H77.35Zm-23.575-2.806c0-2.369 1.632-3.936 3.758-3.936 2.126 0 3.758 1.567 3.758 3.936 0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.758-1.604-3.758-3.936Zm4.252-7.398c-2.62 0-4.252 1.567-4.252 1.567l-.646-1.421h-2.771v19.46h3.758v-6.268s1.48 1.421 3.91 1.421c3.759 0 7.024-3.134 7.024-7.398 0-4.263-3.265-7.361-7.023-7.361ZM39.081 21.169l-.341.802c-.456 1.093-1.291 1.895-2.962 1.895v2.988s.646.146 1.291.146c2.468 0 3.759-1.239 5.24-4.701.341-.802 6.036-14.905 6.036-14.905h-3.873l-3.416 9.438-3.797-9.475h-3.91l5.732 13.812ZM16 4.515v17.274h3.91v-7.216h7.365v7.216h3.91V4.515h-3.91v6.596H19.91V4.515H16Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -1 +0,0 @@
<svg width="150" height="30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M123.477 14.573c0-2.369 1.633-3.936 3.759-3.936s3.758 1.567 3.758 3.936c0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.759-1.604-3.759-3.936Zm4.252 7.361c3.758 0 7.023-3.134 7.023-7.398 0-4.263-3.265-7.397-7.023-7.397-2.468 0-3.91 1.42-3.91 1.42V2h-3.797v19.789h2.772l.645-1.422c.038 0 1.671 1.567 4.29 1.567Zm-14.73-7.361c0 2.369-1.632 3.936-3.758 3.936-2.127 0-3.759-1.567-3.759-3.936 0-2.369 1.632-3.936 3.759-3.936 2.126 0 3.758 1.567 3.758 3.936Zm-4.252 7.361c2.62 0 4.252-1.567 4.252-1.567l.645 1.422h2.772V7.357h-2.772l-.645 1.421s-1.632-1.567-4.252-1.567c-3.759 0-7.024 3.134-7.024 7.398 0 4.264 3.265 7.325 7.024 7.325ZM98.952 2h-3.758v19.789h3.758V2Zm-7.365 5.175c-2.278 0-3.606 1.713-3.606 1.713l-.646-1.567h-2.771v14.431h3.758V14.1c0-1.895 1.29-3.134 3.113-3.134.835 0 1.291.146 1.291.146V7.357s-.493-.182-1.139-.182Zm-17.008 3.462c1.974 0 3.265 1.24 3.607 2.843h-7.214c.304-1.604 1.633-2.843 3.607-2.843Zm2.771 6.742s-.835 1.093-2.771 1.093-3.113-1.093-3.417-2.514H81.64s.152-.802.152-1.421c0-4.41-3.265-7.399-7.213-7.399-4.1 0-7.517 3.28-7.517 7.399 0 4.118 3.417 7.397 7.517 7.397 3.417 0 5.885-2.368 6.72-4.555H77.35Zm-23.575-2.806c0-2.369 1.632-3.936 3.758-3.936 2.126 0 3.758 1.567 3.758 3.936 0 2.369-1.632 3.936-3.758 3.936-2.126-.037-3.758-1.604-3.758-3.936Zm4.252-7.398c-2.62 0-4.252 1.567-4.252 1.567l-.646-1.421h-2.771v19.46h3.758v-6.268s1.48 1.421 3.91 1.421c3.759 0 7.024-3.134 7.024-7.398 0-4.263-3.265-7.361-7.023-7.361ZM39.081 21.169l-.341.802c-.456 1.093-1.291 1.895-2.962 1.895v2.988s.646.146 1.291.146c2.468 0 3.759-1.239 5.24-4.701.341-.802 6.036-14.905 6.036-14.905h-3.873l-3.416 9.438-3.797-9.475h-3.91l5.732 13.812ZM16 4.515v17.274h3.91v-7.216h7.365v7.216h3.91V4.515h-3.91v6.596H19.91V4.515H16Z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="48" fill="none"><g clip-path="url(#a)"><path fill="#fff" fill-rule="evenodd" d="M75.985 22.827h7.188v-5.702h3.803v14.628h-3.803V26.05h-7.188v5.702h-3.669V17.125h3.669v5.702Zm25.495-5.702h-5.87c-1.626 0-2.615.307-3.364 1.054-.746.745-1.032 1.69-1.032 3.357v5.812c0 1.668.286 2.61 1.032 3.357.746.747 1.735 1.054 3.364 1.054h5.87c1.625 0 2.615-.307 3.364-1.054.749-.744 1.032-1.69 1.032-3.357v-5.812c0-1.668-.286-2.61-1.032-3.357-.749-.747-1.739-1.054-3.364-1.054Zm.724 10.308c0 .857-.286 1.097-1.318 1.097h-4.682c-1.032 0-1.319-.24-1.319-1.097v-5.988c0-.857.286-1.097 1.319-1.097h4.682c1.056 0 1.318.22 1.318 1.097v5.988Zm12.768-4.454h5.252c1.626 0 2.637.307 3.364 1.054.658.66 1.032 1.732 1.032 2.896v.878c0 1.163-.374 2.214-1.032 2.895-.727.747-1.717 1.054-3.364 1.054h-10.022v-3.223h9.407c1.032 0 1.318-.24 1.318-1.097v-.438c0-.856-.286-1.096-1.318-1.096h-5.252c-1.628 0-2.639-.31-3.364-1.054-.657-.66-1.032-1.732-1.032-2.896v-.878c0-1.163.372-2.214 1.032-2.895.728-.747 1.714-1.054 3.364-1.054h9.499v3.223h-8.884c-1.032 0-1.318.24-1.318 1.097v.438c0 .856.286 1.096 1.318 1.096Zm11.974-2.63v-3.224h14.945v3.223h-5.626v11.405h-3.671V20.348h-5.648Z" clip-rule="evenodd"/><path fill="#3888ff" d="M63.532 17.01h-5.87c-1.625 0-2.615.306-3.364 1.054-.746.744-1.032 1.689-1.032 3.354V31.75h3.672V21.332c0-.856.286-1.096 1.318-1.096h4.682c1.057 0 1.319.219 1.319 1.097v10.42h3.671V21.42c0-1.668-.286-2.61-1.032-3.354-.746-.75-1.735-1.057-3.364-1.057Z"/><g clip-path="url(#b)"><path fill="#3888ff" d="M40.811 10.287 24.081.647a4.901 4.901 0 0 0-4.867 0 4.868 4.868 0 0 0-2.433 4.205v1.257l-1.09-.628a4.901 4.901 0 0 0-4.868 0 4.869 4.869 0 0 0-2.432 4.207v1.258l-1.09-.629a4.901 4.901 0 0 0-4.868 0A4.869 4.869 0 0 0 0 14.525v30.203a2.29 2.29 0 0 0 1.29 2.054c.783.38 1.733.276 2.415-.261l8.296-6.53 12.793 7.371a2.29 2.29 0 0 0 1.145.304 2.288 2.288 0 0 0 2.29-2.281V27.207a8.392 8.392 0 0 0-4.196-7.252l-4.195-2.418V4.855a1.813 1.813 0 0 1 2.722-1.568l16.73 9.637a5.342 5.342 0 0 1 2.67 4.614v22.633c0 .644-.348 1.246-.908 1.568l-4.433 2.555V22.372a8.393 8.393 0 0 0-4.195-7.252l-10.3-5.933v3.515l8.775 5.055a5.338 5.338 0 0 1 2.67 4.615v23.237c0 .811.438 1.57 1.144 1.978a2.29 2.29 0 0 0 1.145.304c.396 0 .792-.104 1.145-.304l5.577-3.214a4.87 4.87 0 0 0 2.433-4.208V17.532a8.412 8.412 0 0 0-4.202-7.245Zm-18.31 12.306a5.339 5.339 0 0 1 2.67 4.615v16.86l-10.594-6.102 3.4-2.674a4.82 4.82 0 0 0 1.855-3.813V21.06l2.67 1.534Zm-5.72-3.296v12.176c0 .56-.252 1.079-.69 1.422L3.05 43.155V14.52a1.814 1.814 0 0 1 2.721-1.567l2.619 1.507v21.585l3.05-2.4V9.688a1.813 1.813 0 0 1 2.722-1.567l2.618 1.506v6.152l-3.05-1.759v3.518l3.05 1.76Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h141.873v48H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h45.013v47.891H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="48" fill="none"><g clip-path="url(#a)"><path fill="#21324B" fill-rule="evenodd" d="M75.985 22.827h7.188v-5.702h3.803v14.628h-3.803V26.05h-7.188v5.702h-3.669V17.125h3.669v5.702Zm25.495-5.702h-5.87c-1.626 0-2.615.307-3.364 1.054-.746.745-1.032 1.69-1.032 3.357v5.812c0 1.668.286 2.61 1.032 3.357.746.747 1.735 1.054 3.364 1.054h5.87c1.625 0 2.615-.307 3.364-1.054.749-.744 1.032-1.69 1.032-3.357v-5.812c0-1.668-.286-2.61-1.032-3.357-.749-.747-1.739-1.054-3.364-1.054Zm.724 10.308c0 .857-.286 1.097-1.318 1.097h-4.682c-1.032 0-1.319-.24-1.319-1.097v-5.988c0-.857.286-1.097 1.319-1.097h4.682c1.056 0 1.318.22 1.318 1.097v5.988Zm12.768-4.454h5.252c1.626 0 2.637.307 3.364 1.054.658.66 1.032 1.732 1.032 2.896v.878c0 1.163-.374 2.214-1.032 2.895-.727.747-1.717 1.054-3.364 1.054h-10.022v-3.223h9.407c1.032 0 1.318-.24 1.318-1.097v-.438c0-.856-.286-1.096-1.318-1.096h-5.252c-1.628 0-2.639-.31-3.364-1.054-.657-.66-1.032-1.732-1.032-2.896v-.878c0-1.163.372-2.214 1.032-2.895.728-.747 1.714-1.054 3.364-1.054h9.499v3.223h-8.884c-1.032 0-1.318.24-1.318 1.097v.438c0 .856.286 1.096 1.318 1.096Zm11.974-2.63v-3.224h14.945v3.223h-5.626v11.405h-3.671V20.348h-5.648Z" clip-rule="evenodd"/><path fill="#0052CD" d="M63.532 17.01h-5.87c-1.625 0-2.615.306-3.364 1.054-.746.744-1.032 1.689-1.032 3.354V31.75h3.672V21.332c0-.856.286-1.096 1.318-1.096h4.682c1.057 0 1.319.219 1.319 1.097v10.42h3.671V21.42c0-1.668-.286-2.61-1.032-3.354-.746-.75-1.735-1.057-3.364-1.057Z"/><g clip-path="url(#b)"><path fill="#0052CD" d="M40.811 10.287 24.081.647a4.901 4.901 0 0 0-4.867 0 4.868 4.868 0 0 0-2.433 4.205v1.257l-1.09-.628a4.901 4.901 0 0 0-4.868 0 4.869 4.869 0 0 0-2.432 4.207v1.258l-1.09-.629a4.901 4.901 0 0 0-4.868 0A4.869 4.869 0 0 0 0 14.525v30.203a2.29 2.29 0 0 0 1.29 2.054c.783.38 1.733.276 2.415-.261l8.296-6.53 12.793 7.371a2.29 2.29 0 0 0 1.145.304 2.288 2.288 0 0 0 2.29-2.281V27.207a8.392 8.392 0 0 0-4.196-7.252l-4.195-2.418V4.855a1.813 1.813 0 0 1 2.722-1.568l16.73 9.637a5.342 5.342 0 0 1 2.67 4.614v22.633c0 .644-.348 1.246-.908 1.568l-4.433 2.555V22.372a8.393 8.393 0 0 0-4.195-7.252l-10.3-5.933v3.515l8.775 5.055a5.338 5.338 0 0 1 2.67 4.615v23.237c0 .811.438 1.57 1.144 1.978a2.29 2.29 0 0 0 1.145.304c.396 0 .792-.104 1.145-.304l5.577-3.214a4.87 4.87 0 0 0 2.433-4.208V17.532a8.412 8.412 0 0 0-4.202-7.245Zm-18.31 12.306a5.339 5.339 0 0 1 2.67 4.615v16.86l-10.594-6.102 3.4-2.674a4.82 4.82 0 0 0 1.855-3.813V21.06l2.67 1.534Zm-5.72-3.296v12.176c0 .56-.252 1.079-.69 1.422L3.05 43.155V14.52a1.814 1.814 0 0 1 2.721-1.567l2.619 1.507v21.585l3.05-2.4V9.688a1.813 1.813 0 0 1 2.722-1.567l2.618 1.506v6.152l-3.05-1.759v3.518l3.05 1.76Z"/></g></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h141.873v48H0z"/></clipPath><clipPath id="b"><path fill="#fff" d="M0 0h45.013v47.891H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1 @@
<svg width="1003" height="644" fill="none" xmlns="http://www.w3.org/2000/svg"><g opacity=".24"><mask id="b" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="128" y="0" width="921" height="644"><ellipse cx="588.5" cy="322" rx="460.5" ry="322" fill="url(#a)" fill-opacity=".32"/></mask><g mask="url(#b)" stroke="#fff" stroke-opacity=".88"><path d="M141.5 17v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609m32-609v609M-51 33.5h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280m-1280 32h1280"/></g></g><defs><radialGradient id="a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 322 -460.5 0 588.5 322)"><stop/><stop offset="1" stop-opacity="0"/></radialGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg width="119" height="40" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)" fill="#fff"><path fill-rule="evenodd" clip-rule="evenodd" d="M63.32 19.023h5.99V14.27h3.17v12.19h-3.17V21.71h-5.99v4.752h-3.058v-12.19h3.057v4.752h.002Zm21.245-4.752h-4.89c-1.356 0-2.18.255-2.804.879-.622.62-.86 1.408-.86 2.798v4.842c0 1.39.237 2.175.86 2.797.62.623 1.446.88 2.803.88h4.891c1.356 0 2.18-.257 2.804-.88.625-.62.86-1.407.86-2.797v-4.842c0-1.39-.237-2.175-.86-2.8-.625-.622-1.448-.877-2.803-.877Zm.604 8.59c0 .713-.237.913-1.099.913h-3.9c-.86 0-1.1-.2-1.1-.913v-4.99c0-.713.24-.913 1.1-.913h3.9c.882 0 1.1.18 1.1.912v4.991Zm10.64-3.712h4.376c1.355 0 2.198.256 2.804.878.549.55.86 1.444.86 2.413v.732c0 .97-.312 1.845-.86 2.413-.606.622-1.431.879-2.804.879h-8.35v-2.688h7.838c.86 0 1.1-.2 1.1-.912v-.365c0-.714-.24-.914-1.1-.914h-4.375c-1.359 0-2.2-.259-2.804-.879-.549-.55-.861-1.442-.861-2.412v-.732c0-.97.31-1.846.861-2.412.606-.624 1.428-.879 2.803-.879h7.916v2.686h-7.404c-.86 0-1.099.2-1.099.913v.365c0 .715.24.915 1.1.915v-.001Zm9.979-2.192V14.27h12.454v2.688h-4.688v9.503h-3.06v-9.503h-4.707.001Z"/><path d="M52.946 14.175h-4.892c-1.355 0-2.18.255-2.803.879-.622.62-.86 1.407-.86 2.795v8.61h3.058v-8.684c0-.712.24-.912 1.1-.912h3.902c.88 0 1.098.182 1.098.912v8.685h3.06v-8.61c0-1.39-.238-2.175-.86-2.795-.622-.625-1.446-.881-2.803-.881v.001ZM34.01 8.573 20.069.538a4.085 4.085 0 0 0-4.058 0 4.057 4.057 0 0 0-2.026 3.503V5.09l-.909-.525a4.085 4.085 0 0 0-4.057 0 4.058 4.058 0 0 0-2.027 3.51v1.048l-.908-.524a4.085 4.085 0 0 0-4.056 0A4.058 4.058 0 0 0 0 12.104v25.17a1.907 1.907 0 0 0 3.087 1.494L10 33.325l10.663 6.143a1.936 1.936 0 0 0 1.907 0c.587-.34.954-.97.954-1.65V22.676a6.995 6.995 0 0 0-3.497-6.044l-3.496-2.015V4.046A1.513 1.513 0 0 1 18.8 2.738l13.941 8.031a4.452 4.452 0 0 1 2.225 3.845v18.861c0 .538-.29 1.038-.756 1.307l-3.694 2.128V18.643A6.994 6.994 0 0 0 27.02 12.6l-8.582-4.944v2.93l7.31 4.212a4.448 4.448 0 0 1 2.226 3.845v19.365c0 .675.365 1.308.953 1.648a1.936 1.936 0 0 0 1.908 0l4.649-2.678a4.058 4.058 0 0 0 2.027-3.507v-18.86a7.01 7.01 0 0 0-3.501-6.038ZM18.75 18.828a4.448 4.448 0 0 1 2.225 3.846v14.05l-8.828-5.086 2.834-2.228a4.018 4.018 0 0 0 1.545-3.177V17.55l2.225 1.279-.001-.001Zm-4.766-2.747v10.147c0 .466-.211.9-.576 1.185L2.54 35.963V12.1a1.512 1.512 0 0 1 2.268-1.306l2.183 1.256v17.988l2.542-2V8.073a1.511 1.511 0 0 1 2.268-1.307l2.182 1.255v5.128l-2.54-1.465v2.931l2.542 1.466h-.002Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h118.75v40H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -111,9 +111,9 @@ export default function AppDeployments(props: AppDeploymentsProps) {
throw error;
}
const { deployments } = deploymentPageData || {};
const { deployments } = deploymentPageData || { deployments: [] };
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData || {};
scheduledOrPendingDeploymentsData || { deployments: [] };
const latestDeployment = latestDeploymentData?.deployments[0];
const latestLiveDeployment = latestLiveDeploymentData?.deployments[0];
@@ -135,7 +135,7 @@ export default function AppDeployments(props: AppDeploymentsProps) {
deployment={deployment}
isLive={liveDeploymentId === deployment.id}
showRedeploy={latestDeployment.id === deployment.id}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
disableRedeploy={scheduledOrPendingDeployments?.length > 0}
/>
{index !== deployments.length - 1 && <Divider component="li" />}
@@ -143,7 +143,7 @@ export default function AppDeployments(props: AppDeploymentsProps) {
))}
</List>
<div className="mt-8 flex w-full justify-center">
<div className="grid grid-flow-col gap-2 items-center">
<div className="grid grid-flow-col items-center gap-2">
<NextPrevPageLink
direction="prev"
prevAllowed={page !== 1}

View File

@@ -58,7 +58,7 @@ export default function ApplicationErrored() {
const { currentDate } = useCurrentDate();
const user = useUserData();
const isOwner = currentWorkspace.members.some(
({ userId, type }) => userId === user.id && type === 'owner',
({ userId, type }) => userId === user?.id && type === 'owner',
);
const { appCreatedAt } = useAppCreatedAt();

View File

@@ -1,3 +1,4 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import Container from '@/components/layout/Container';
import { features } from '@/components/overview/features';
import { frameworks } from '@/components/overview/frameworks';
@@ -55,7 +56,9 @@ export default function ApplicationLive() {
<div className="grid grid-cols-1 gap-12 pt-3 lg:grid-cols-3">
<div className="order-2 grid grid-flow-row gap-12 lg:order-1 lg:col-span-2">
<OverviewDeployments />
<RetryableErrorBoundary>
<OverviewDeployments />
</RetryableErrorBoundary>
<OverviewDocumentation
title="Pick your favorite framework and start learning"

View File

@@ -18,7 +18,7 @@ export default function ApplicationUnknown() {
const [showDeleteModal, setShowDeleteModal] = useState(false);
const user = useUserData();
const isOwner = currentWorkspace.members.some(
({ userId, type }) => userId === user.id && type === 'owner',
({ userId, type }) => userId === user?.id && type === 'owner',
);
return (

View File

@@ -31,6 +31,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
<NavLink
href="/local/local"
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
local
</NavLink>
@@ -53,6 +54,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
<NavLink
href={`/${currentWorkspace.slug}`}
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
{currentWorkspace.name}
</NavLink>
@@ -66,6 +68,7 @@ export default function Breadcrumbs({ className, ...props }: BreadcrumbsProps) {
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}`}
className="truncate text-[13px] hover:underline sm:text-sm"
sx={{ color: 'text.primary' }}
>
{currentApplication.name}
</NavLink>

View File

@@ -2,13 +2,12 @@ import Breadcrumbs from '@/components/common/Breadcrumbs';
import FeedbackForm from '@/components/common/FeedbackForm';
import Logo from '@/components/common/Logo';
import MobileNav from '@/components/common/MobileNav';
import NavLink from '@/components/common/NavLink';
import ThemeSwitcher from '@/components/common/ThemeSwitcher';
import { AccountMenu } from '@/components/dashboard/AccountMenu';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import Box from '@/ui/v2/Box';
import { Dropdown } from '@/ui/v2/Dropdown';
import Link from '@/ui/v2/Link';
import NavLink from 'next/link';
import { useRouter } from 'next/router';
import type { DetailedHTMLProps, HTMLProps, PropsWithoutRef } from 'react';
import { twMerge } from 'tailwind-merge';
@@ -32,10 +31,8 @@ export default function Header({ className, ...props }: HeaderProps) {
{...props}
>
<div className="grid grid-flow-col items-center gap-3">
<NavLink href="/" passHref>
<Link href="/" className="w-12">
<Logo className="mx-auto cursor-pointer" />
</Link>
<NavLink href="/" className="w-12">
<Logo className="mx-auto cursor-pointer" />
</NavLink>
{(router.query.workspaceSlug || router.query.appSlug) && (
@@ -63,22 +60,17 @@ export default function Header({ className, ...props }: HeaderProps) {
)}
<NavLink
underline="none"
href="https://docs.nhost.io"
passHref
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
sx={{
color: 'text.primary',
'&:hover': { backgroundColor: 'grey.200' },
}}
target="_blank"
rel="noopener noreferrer"
>
<Link
underline="none"
href="https://docs.nhost.io"
className="mr-2 rounded-md px-2.5 py-1.5 text-sm motion-safe:transition-colors"
sx={{
color: 'text.primary',
'&:hover': { backgroundColor: 'grey.200' },
}}
>
Docs
</Link>
Docs
</NavLink>
{isPlatform ? <AccountMenu /> : <ThemeSwitcher className="w-52" />}

View File

@@ -1,12 +1,11 @@
import type { NavLinkProps } from '@/components/common/NavLink';
import NavLink from '@/components/common/NavLink';
import type { SvgIconProps } from '@/ui/v2/icons/SvgIcon';
import Link from '@/ui/v2/Link';
import NavLink from 'next/link';
import type { ForwardedRef, ReactElement } from 'react';
import type { ForwardedRef, PropsWithoutRef, ReactElement } from 'react';
import { cloneElement, forwardRef, isValidElement } from 'react';
import { twMerge } from 'tailwind-merge';
export interface IconLinkProps extends Omit<NavLinkProps, 'ref'> {
export interface IconLinkProps extends PropsWithoutRef<NavLinkProps> {
/**
* The icon to display.
*/
@@ -58,49 +57,49 @@ function IconLink(
}
return (
<NavLink ref={ref} passHref href={href} {...props}>
<Link
href={href}
underline="none"
className={twMerge(
'grid grid-flow-row justify-items-center gap-1 rounded-md py-2.5 px-0.5 text-center font-medium motion-safe:transition-colors',
className,
)}
sx={{
fontSize: (theme) => theme.typography.pxToRem(10),
lineHeight: (theme) => theme.typography.pxToRem(15),
backgroundColor: active ? 'primary.light' : 'transparent',
color: active ? 'primary.main' : 'text.primary',
[`&:hover`]: {
backgroundColor: active ? 'primary.light' : 'action.hover',
},
}}
>
{isValidElement(icon)
? cloneElement(icon, {
...icon.props,
className: twMerge('w-4 h-4', icon.props.className),
sx: [
...(Array.isArray(icon.props?.sx)
? icon.props.sx
: [icon.props?.sx]),
{
color: (theme) => {
if (active) {
return 'primary.main';
}
<NavLink
ref={ref}
href={href}
underline="none"
className={twMerge(
'grid grid-flow-row justify-items-center gap-1 rounded-md py-2.5 px-0.5 text-center font-medium motion-safe:transition-colors',
className,
)}
sx={{
fontSize: (theme) => theme.typography.pxToRem(10),
lineHeight: (theme) => theme.typography.pxToRem(15),
backgroundColor: active ? 'primary.light' : 'transparent',
color: active ? 'primary.main' : 'text.primary',
[`&:hover`]: {
backgroundColor: active ? 'primary.light' : 'action.hover',
},
}}
{...props}
>
{isValidElement(icon)
? cloneElement(icon, {
...icon.props,
className: twMerge('w-4 h-4', icon.props.className),
sx: [
...(Array.isArray(icon.props?.sx)
? icon.props.sx
: [icon.props?.sx]),
{
color: (theme) => {
if (active) {
return 'primary.main';
}
return theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.primary';
},
return theme.palette.mode === 'dark'
? 'text.secondary'
: 'text.primary';
},
],
})
: null}
},
],
})
: null}
{children}
</Link>
{children}
</NavLink>
);
}

View File

@@ -1,10 +1,50 @@
import type { ActivityIndicatorProps } from '@/ui/v2/ActivityIndicator';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import { twMerge } from 'tailwind-merge';
export function LoadingScreen() {
export interface LoadingScreenProps extends BoxProps {
/**
* Props passed to individual component slots.
*/
slotProps?: {
/**
* Props passed to the `<Box />` component.
*/
root?: BoxProps;
/**
* Props passed to the `<ActivityIndicator />` component.
*/
activityIndicator?: ActivityIndicatorProps;
};
}
export function LoadingScreen({
className,
slotProps = { root: {}, activityIndicator: {} },
...props
}: LoadingScreenProps) {
return (
<Box className="absolute top-0 left-0 bottom-0 right-0 flex items-center justify-center z-50 h-full w-full">
<ActivityIndicator circularProgressProps={{ className: 'w-5 h-5' }} />
<Box
className={twMerge(
'absolute top-0 left-0 bottom-0 right-0 z-50 flex h-full w-full items-center justify-center',
className,
slotProps?.root?.className,
)}
{...slotProps?.root}
{...props}
>
<ActivityIndicator
{...slotProps?.activityIndicator}
circularProgressProps={{
...slotProps?.activityIndicator?.circularProgressProps,
className: twMerge(
'w-5 h-5',
slotProps?.activityIndicator?.circularProgressProps?.className,
),
}}
/>
</Box>
);
}

View File

@@ -1,21 +1,27 @@
import Link from 'next/link';
import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
import type { LinkProps } from '@/ui/v2/Link';
import Link from '@/ui/v2/Link';
import NextLink from 'next/link';
import type { ForwardedRef, PropsWithoutRef } from 'react';
import { forwardRef } from 'react';
import { twMerge } from 'tailwind-merge';
export interface NavLinkProps
extends DetailedHTMLProps<HTMLProps<HTMLAnchorElement>, HTMLAnchorElement> {}
export interface NavLinkProps extends PropsWithoutRef<LinkProps> {
/**
* Determines whether or not the link should be disabled.
*/
disabled?: boolean;
}
function NavLink(
{ className, children, href, ...props }: NavLinkProps,
ref: ForwardedRef<HTMLAnchorElement>,
) {
return (
<Link href={href} passHref>
<a className={twMerge('font-display', className)} ref={ref} {...props}>
<NextLink href={href} passHref>
<Link className={twMerge('font-display', className)} ref={ref} {...props}>
{children}
</a>
</Link>
</Link>
</NextLink>
);
}

View File

@@ -32,11 +32,11 @@ function AccountMenuContent({
color="secondary"
className="absolute top-6 right-4 grid grid-flow-col items-center gap-px self-start font-medium"
onClick={async () => {
router.push('/signin');
await nhost.auth.signOut();
router.push('/signin');
await client.resetStore();
}}
endIcon={<PowerIcon className="w-4 h-4 mr-1" />}
endIcon={<PowerIcon className="mr-1 h-4 w-4" />}
>
Sign Out
</Button>

View File

@@ -24,7 +24,7 @@ export function CountrySelector({ value, onChange }: CountrySelectorProps) {
placeholder="Select Country"
>
{countries?.map((country) => (
<Option key={country.name} value={country.name}>
<Option key={country.name} value={country.code}>
{country.name}
</Option>
))}

View File

@@ -181,6 +181,7 @@ export default function EditPermissionsForm({
return (
<RolePermissionEditorForm
resourceVersion={metadata?.resourceVersion}
disabled={disabled}
schema={schema}
table={table}
@@ -203,7 +204,7 @@ export default function EditPermissionsForm({
sx={{ backgroundColor: 'background.default' }}
>
<div className="flex-auto">
<Box className="grid grid-flow-row gap-6 content-start overflow-y-auto p-6 border-b-1">
<Box className="grid grid-flow-row content-start gap-6 overflow-y-auto border-b-1 p-6">
<div className="grid grid-flow-row gap-2">
<Text component="h2" className="!font-bold">
Roles & Actions overview
@@ -215,24 +216,24 @@ export default function EditPermissionsForm({
</Text>
</div>
<div className="grid grid-flow-col gap-4 items-center justify-start">
<div className="grid grid-flow-col items-center justify-start gap-4">
<Text
variant="subtitle2"
className="grid items-center grid-flow-col gap-1"
className="grid grid-flow-col items-center gap-1"
>
full access <FullPermissionIcon />
</Text>
<Text
variant="subtitle2"
className="grid items-center grid-flow-col gap-1"
className="grid grid-flow-col items-center gap-1"
>
partial access <PartialPermissionIcon />
</Text>
<Text
variant="subtitle2"
className="grid items-center grid-flow-col gap-1"
className="grid grid-flow-col items-center gap-1"
>
no access <NoPermissionIcon />
</Text>
@@ -262,7 +263,7 @@ export default function EditPermissionsForm({
</TableRow>
</TableHead>
<TableBody className="rounded-sm+ block border-1">
<TableBody className="block rounded-sm+ border-1">
<RolePermissionsRow
name="admin"
disabled

View File

@@ -66,6 +66,10 @@ export interface RolePermissionEditorFormValues {
* Whether the mutation should be restricted to trusted backends.
*/
backendOnly?: boolean;
/**
* Computed fields to be allowed for the role.
*/
computedFields?: string[];
}
export interface RolePermissionEditorFormProps {
@@ -85,6 +89,10 @@ export interface RolePermissionEditorFormProps {
* The role that is being edited.
*/
role: string;
/**
* The resource version of the metadata.
*/
resourceVersion: number;
/**
* The action that is being edited.
*/
@@ -155,6 +163,7 @@ export default function RolePermissionEditorForm({
schema,
table,
role,
resourceVersion,
action,
onSubmit,
onCancel,
@@ -189,6 +198,7 @@ export default function RolePermissionEditorForm({
permission?.subscription_root_fields?.length > 0,
queryRootFields: permission?.query_root_fields || [],
subscriptionRootFields: permission?.subscription_root_fields || [],
computedFields: permission?.computed_fields || [],
columnPresets: getColumnPresets(permission?.set || {}),
backendOnly: permission?.backend_only || false,
},
@@ -213,13 +223,18 @@ export default function RolePermissionEditorForm({
action,
mode: permission ? 'update' : 'insert',
originalPermission: permission,
resourceVersion,
permission: {
set: convertToColumnPresetObject(values.columnPresets),
columns: values.columns,
limit: values.limit,
allow_aggregations: values.allowAggregations,
query_root_fields: values.queryRootFields,
subscription_root_fields: values.subscriptionRootFields,
query_root_fields:
values.queryRootFields.length > 0 ? values.queryRootFields : null,
subscription_root_fields:
values.subscriptionRootFields.length > 0
? values.subscriptionRootFields
: null,
filter:
action !== 'insert'
? convertToHasuraPermissions(values.filter as RuleGroup)
@@ -229,6 +244,10 @@ export default function RolePermissionEditorForm({
? convertToHasuraPermissions(values.filter as RuleGroup)
: permission?.check,
backend_only: values.backendOnly,
computed_fields:
permission?.computed_fields.length > 0
? permission?.computed_fields
: null,
},
});
@@ -266,6 +285,7 @@ export default function RolePermissionEditorForm({
const deletePermissionPromise = managePermission({
role,
action,
resourceVersion,
originalPermission: permission,
mode: 'delete',
});
@@ -331,10 +351,10 @@ export default function RolePermissionEditorForm({
className="flex flex-auto flex-col content-between overflow-hidden border-t-1"
sx={{ backgroundColor: 'background.default' }}
>
<div className="grid grid-flow-row gap-6 content-start flex-auto py-4 overflow-auto">
<div className="grid flex-auto grid-flow-row content-start gap-6 overflow-auto py-4">
<PermissionSettingsSection
title="Selected role & action"
className="justify-between grid-flow-col"
className="grid-flow-col justify-between"
>
<div className="grid grid-flow-col gap-4">
<Text>
@@ -387,7 +407,7 @@ export default function RolePermissionEditorForm({
{action !== 'select' && <BackendOnlySection disabled={disabled} />}
</div>
<Box className="grid flex-shrink-0 sm:grid-flow-col sm:justify-between gap-2 border-t-1 p-2">
<Box className="grid flex-shrink-0 gap-2 border-t-1 p-2 sm:grid-flow-col sm:justify-between">
<Button
variant="borderless"
color="secondary"
@@ -398,7 +418,7 @@ export default function RolePermissionEditorForm({
</Button>
{!disabled && (
<Box className="grid grid-flow-row sm:grid-flow-col gap-2">
<Box className="grid grid-flow-row gap-2 sm:grid-flow-col">
{Boolean(permission) && (
<Button
variant="outlined"

View File

@@ -11,7 +11,6 @@ import { inputClasses } from '@/ui/v2/Input';
import Option from '@/ui/v2/Option';
import getPermissionVariablesArray from '@/utils/settings/getPermissionVariablesArray';
import { useGetAppCustomClaimsQuery } from '@/utils/__generated__/graphql';
import clsx from 'clsx';
import { useController, useFormContext, useWatch } from 'react-hook-form';
import useRuleGroupEditor from './useRuleGroupEditor';
@@ -214,7 +213,7 @@ export default function RuleValueInput({
freeSolo={!isHasuraInput}
autoSelect={!isHasuraInput}
autoHighlight={isHasuraInput}
filterSelectedOptions
open
isOptionEqualToValue={(option, value) => {
if (typeof value === 'string') {
return option.value.toLowerCase() === (value as string).toLowerCase();
@@ -230,7 +229,7 @@ export default function RuleValueInput({
sx: sharedInputSx,
},
formControl: { className: '!bg-transparent' },
paper: { className: clsx(!isHasuraInput && 'hidden') },
paper: { className: 'empty:border-transparent' },
}}
fullWidth
loading={loading}

View File

@@ -58,9 +58,10 @@ export default function DeploymentListItem({
return (
<ListItem.Root>
<ListItem.Button
className="grid grid-flow-col items-center justify-between gap-2 px-2 py-2"
className="grid grid-flow-col items-center justify-between gap-2 rounded-none px-2 py-2"
component={NavLink}
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments/${deployment.id}`}
aria-label={commitMessage || 'No commit message'}
>
<div className="flex cursor-pointer flex-row items-center justify-center space-x-2 self-center">
<ListItem.Avatar>
@@ -83,10 +84,14 @@ export default function DeploymentListItem({
/>
</div>
<div className="grid grid-flow-col gap-2 items-center">
<div className="grid grid-flow-col items-center gap-2">
{showRedeploy && (
<Tooltip
title="Deployments cannot be re-triggered when a deployment is in progress."
title={
!disableRedeploy && !loading
? 'Deployments cannot be re-triggered when a deployment is in progress.'
: ''
}
hasDisabledChildren={disableRedeploy || loading}
disableHoverListener={!disableRedeploy}
>
@@ -123,9 +128,10 @@ export default function DeploymentListItem({
);
}}
startIcon={
<ArrowCounterclockwiseIcon className={twMerge('w-4 h-4')} />
<ArrowCounterclockwiseIcon className={twMerge('h-4 w-4')} />
}
className="rounded-full py-1 px-2 text-xs"
aria-label="Redeploy"
>
Redeploy
</Button>
@@ -133,7 +139,7 @@ export default function DeploymentListItem({
)}
{isLive && (
<div className="w-12 flex justify-end">
<div className="flex w-12 justify-end">
<Chip size="small" color="success" label="Live" />
</div>
)}

View File

@@ -45,12 +45,6 @@ export default function AuthenticatedLayout({
return;
}
if (router.pathname === '/') {
router.push('/signup');
return;
}
router.push('/signin');
}, [isLoading, isAuthenticated, router, isPlatform]);
@@ -84,7 +78,7 @@ export default function AuthenticatedLayout({
<Container
rootClassName="h-full"
className="grid justify-center max-w-md grid-flow-row gap-2 my-12 text-center"
className="my-12 grid max-w-md grid-flow-row justify-center gap-2 text-center"
>
<div className="mx-auto">
<Image
@@ -121,7 +115,7 @@ export default function AuthenticatedLayout({
}
return (
<BaseLayout className="flex flex-col h-full" {...props}>
<BaseLayout className="flex h-full flex-col" {...props}>
<Header className="flex max-h-[59px] flex-auto" />
<InviteAnnounce />

View File

@@ -2,9 +2,14 @@ import { LoadingScreen } from '@/components/common/LoadingScreen';
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import type { BaseLayoutProps } from '@/components/layout/BaseLayout';
import BaseLayout from '@/components/layout/BaseLayout';
import Container from '@/components/layout/Container';
import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useCleanWorkspaceContext } from '@/hooks/use-cleanWorkspaceContext';
import Box from '@/ui/v2/Box';
import ThemeProvider from '@/ui/v2/ThemeProvider';
import GlobalStyles from '@mui/material/GlobalStyles';
import { useAuthenticationStatus } from '@nhost/nextjs';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
@@ -28,14 +33,76 @@ export default function UnauthenticatedLayout({
if (!isPlatform || isLoading || isAuthenticated) {
return (
<BaseLayout {...props}>
<LoadingScreen />
<LoadingScreen
sx={{ backgroundColor: (theme) => theme.palette.background.default }}
slotProps={{
activityIndicator: {
className: 'text-white',
},
}}
/>
</BaseLayout>
);
}
return (
<BaseLayout {...props}>
<RetryableErrorBoundary>{children}</RetryableErrorBoundary>
</BaseLayout>
<ThemeProvider color="dark">
<BaseLayout {...props}>
<GlobalStyles
styles={{
'html, body': {
backgroundColor: `#000 !important`,
},
'#__next': {
overflow: 'auto',
},
}}
/>
<RetryableErrorBoundary>
<Box
className="flex min-h-screen items-center"
sx={{ backgroundColor: (theme) => theme.palette.common.black }}
>
<Container
rootClassName="bg-transparent h-full"
className="grid h-full w-full items-center justify-items-center gap-12 bg-transparent pt-8 pb-12 lg:grid-cols-2 lg:gap-4 lg:pt-0 lg:pb-0"
>
<div className="relative z-10 order-2 grid w-full max-w-[544px] grid-flow-row gap-12 lg:order-1">
{children}
</div>
<div className="relative z-0 order-1 flex h-full w-full items-center justify-center md:min-h-[150px] lg:order-2 lg:min-h-[none]">
<div className="absolute top-0 left-0 right-0 bottom-0 mx-auto flex h-full w-full max-w-xl items-center justify-center opacity-70">
<Image
priority
src="/assets/line-grid.svg"
width={1003}
height={644}
alt="Transparent lines"
objectFit="fill"
className="h-full w-full scale-[200%]"
/>
</div>
<Box
className="backface-hidden absolute left-0 right-0 z-0 mx-auto h-20 w-20 transform-gpu rounded-full opacity-80 blur-[56px]"
sx={{
backgroundColor: (theme) => theme.palette.primary.main,
}}
/>
<Image
src="/assets/logo.svg"
width={119}
height={40}
alt="Nhost Logo"
/>
</div>
</Container>
</Box>
</RetryableErrorBoundary>
</BaseLayout>
</ThemeProvider>
);
}

View File

@@ -184,7 +184,6 @@ export default function LogsHeader({
key={value}
value={value}
className="text-sm+ font-medium"
disabled={label === 'Hasura'}
>
{label}
</Option>

View File

@@ -0,0 +1,238 @@
import { UserDataProvider } from '@/context/workspace1-context';
import type { Application } from '@/types/application';
import { ApplicationStatus } from '@/types/application';
import type { Workspace } from '@/types/workspace';
import { render, screen, waitForElementToBeRemoved } from '@/utils/testUtils';
import { graphql, rest } from 'msw';
import { setupServer } from 'msw/node';
import { afterAll, beforeAll, vi } from 'vitest';
import OverviewDeployments from '.';
vi.mock('next/router', () => ({
useRouter: vi.fn().mockReturnValue({
basePath: '',
pathname: '/test-workspace/test-application',
route: '/[workspaceSlug]/[appSlug]',
asPath: '/test-workspace/test-application',
isLocaleDomain: false,
isReady: true,
isPreview: false,
query: {
workspaceSlug: 'test-workspace',
appSlug: 'test-application',
},
push: vi.fn(),
replace: vi.fn(),
reload: vi.fn(),
back: vi.fn(),
prefetch: vi.fn(),
beforePopState: vi.fn(),
events: {
on: vi.fn(),
off: vi.fn(),
emit: vi.fn(),
},
isFallback: false,
}),
}));
const mockApplication: Application = {
id: '1',
name: 'Test Application',
slug: 'test-application',
appStates: [],
hasuraGraphqlAdminSecret: 'nhost-admin-secret',
subdomain: '',
isProvisioned: true,
region: {
awsName: 'us-east-1',
city: 'New York',
countryCode: 'US',
id: '1',
},
createdAt: new Date().toISOString(),
deployments: [],
desiredState: ApplicationStatus.Live,
featureFlags: [],
providersUpdated: true,
githubRepository: { fullName: 'test/git-project' },
};
const mockWorkspace: Workspace = {
id: '1',
name: 'Test Workspace',
slug: 'test-workspace',
members: [],
applications: [mockApplication],
};
const mockGraphqlLink = graphql.link('http://localhost:1337/v1/graphql');
const server = setupServer(
rest.get('http://localhost:1337/v1/graphql', (req, res, ctx) =>
res(ctx.status(200)),
),
mockGraphqlLink.operation(async (req, res, ctx) =>
res(
ctx.data({
deployments: [],
}),
),
),
);
beforeAll(() => {
process.env.NEXT_PUBLIC_NHOST_PLATFORM = 'true';
process.env.NEXT_PUBLIC_ENV = 'production';
server.listen();
});
afterEach(() => server.resetHandlers());
afterAll(() => {
server.close();
vi.restoreAllMocks();
});
test('should render an empty state when GitHub is not connected', () => {
render(
<UserDataProvider
initialWorkspaces={[
{
...mockWorkspace,
applications: [{ ...mockApplication, githubRepository: null }],
},
]}
>
<OverviewDeployments />
</UserDataProvider>,
);
expect(screen.getByText(/no deployments/i)).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /connect to github/i }),
).toBeInTheDocument();
});
test('should render an empty state when GitHub is connected, but there are no deployments', async () => {
render(
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
<OverviewDeployments />
</UserDataProvider>,
);
expect(screen.getByText(/^deployments$/i)).toBeInTheDocument();
expect(screen.getByRole('link', { name: /view all/i })).toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(screen.getByText(/no deployments/i)).toBeInTheDocument();
expect(screen.getByText(/test\/git-project/i)).toBeInTheDocument();
expect(screen.getByRole('link', { name: /edit/i })).toHaveAttribute(
'href',
'/test-workspace/test-application/settings/git',
);
});
test('should render a list of deployments', async () => {
server.use(
mockGraphqlLink.operation(async (req, res, ctx) => {
const requestPayload = await req.json();
if (requestPayload.operationName === 'ScheduledOrPendingDeploymentsSub') {
return res(ctx.data({ deployments: [] }));
}
return res(
ctx.data({
deployments: [
{
id: '1',
commitSHA: 'abc123',
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
deploymentStatus: 'DEPLOYED',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
}),
);
}),
);
render(
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
<OverviewDeployments />
</UserDataProvider>,
);
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(screen.getByText(/test commit message/i)).toBeInTheDocument();
expect(screen.getByLabelText(/avatar/i)).toHaveStyle(
'background-image: url(http://images.example.com/avatar.png)',
);
expect(
screen.getByRole('link', {
name: /test commit message/i,
}),
).toHaveAttribute('href', '/test-workspace/test-application/deployments/1');
expect(screen.getByText(/5m 0s/i)).toBeInTheDocument();
expect(screen.getByText(/live/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /redeploy/i })).not.toBeDisabled();
});
test('should disable redeployments if a deployment is already in progress', async () => {
server.use(
mockGraphqlLink.operation(async (req, res, ctx) => {
const requestPayload = await req.json();
if (requestPayload.operationName === 'ScheduledOrPendingDeploymentsSub') {
return res(
ctx.data({
deployments: [
{
id: '2',
commitSHA: 'abc234',
deploymentStartedAt: '2021-08-02T00:00:00.000Z',
deploymentEndedAt: null,
deploymentStatus: 'PENDING',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
}),
);
}
return res(
ctx.data({
deployments: [
{
id: '1',
commitSHA: 'abc123',
deploymentStartedAt: '2021-08-01T00:00:00.000Z',
deploymentEndedAt: '2021-08-01T00:05:00.000Z',
deploymentStatus: 'DEPLOYED',
commitUserName: 'test.user',
commitUserAvatarUrl: 'http://images.example.com/avatar.png',
commitMessage: 'Test commit message',
},
],
}),
);
}),
);
render(
<UserDataProvider initialWorkspaces={[mockWorkspace]}>
<OverviewDeployments />
</UserDataProvider>,
);
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'));
expect(screen.getByRole('button', { name: /redeploy/i })).toBeDisabled();
});

View File

@@ -6,6 +6,7 @@ import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Divider from '@/ui/v2/Divider';
import ChevronRightIcon from '@/ui/v2/icons/ChevronRightIcon';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import List from '@/ui/v2/List';
import Text from '@/ui/v2/Text';
@@ -14,7 +15,6 @@ import {
useGetDeploymentsSubSubscription,
useScheduledOrPendingDeploymentsSubSubscription,
} from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import NavLink from 'next/link';
import { Fragment } from 'react';
@@ -22,16 +22,16 @@ function OverviewDeploymentsTopBar() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { githubRepository } = currentApplication;
const { githubRepository } = currentApplication || {};
return (
<div className="grid grid-flow-col gap-2 items-center place-content-between pb-4">
<div className="grid grid-flow-col place-content-between items-center gap-2 pb-4">
<Text variant="h3" className="font-medium">
Deployments
</Text>
<NavLink
href={`/${currentWorkspace.slug}/${currentApplication.slug}/deployments`}
href={`/${currentWorkspace?.slug}/${currentApplication?.slug}/deployments`}
passHref
>
<Button variant="borderless" disabled={!githubRepository}>
@@ -43,20 +43,12 @@ function OverviewDeploymentsTopBar() {
);
}
interface OverviewDeploymentsProps {
projectId: string;
githubRepository: { fullName: string };
}
function OverviewDeployments({
projectId,
githubRepository,
}: OverviewDeploymentsProps) {
function OverviewDeploymentList() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { data, loading } = useGetDeploymentsSubSubscription({
variables: {
id: projectId,
id: currentApplication?.id,
limit: 5,
offset: 0,
},
@@ -67,23 +59,23 @@ function OverviewDeployments({
loading: scheduledOrPendingDeploymentsLoading,
} = useScheduledOrPendingDeploymentsSubSubscription({
variables: {
appId: projectId,
appId: currentApplication?.id,
},
});
if (loading || scheduledOrPendingDeploymentsLoading) {
return (
<Box className="h-[323px] p-2 border-1 rounded-lg">
<Box className="h-[323px] rounded-lg border-1 p-2">
<ActivityIndicator label="Loading deployments..." />
</Box>
);
}
const { deployments } = data;
const { deployments } = data || { deployments: [] };
if (deployments.length === 0) {
if (!deployments?.length) {
return (
<Box className="grid grid-flow-row gap-5 items-center justify-items-center rounded-lg py-12 px-48 shadow-sm border-1">
<Box className="grid grid-flow-row items-center justify-items-center gap-5 overflow-hidden rounded-lg border-1 py-12 px-48 shadow-sm">
<RocketIcon
strokeWidth={1}
className="h-10 w-10"
@@ -100,16 +92,16 @@ function OverviewDeployments({
</div>
<Box
className="mt-6 flex flex-row place-content-between rounded-lg py-2 px-2 max-w-sm w-full"
className="mt-6 flex w-full max-w-sm flex-row place-content-between rounded-lg py-2 px-2"
sx={{ backgroundColor: 'grey.200' }}
>
<Box
className="grid grid-flow-col gap-1.5 ml-2"
className="ml-2 grid grid-flow-col gap-1.5"
sx={{ backgroundColor: 'transparent' }}
>
<GithubIcon className="h-4 w-4 self-center" />
<Text variant="body1" className="self-center font-normal">
{githubRepository.fullName}
{currentApplication?.githubRepository?.fullName}
</Text>
</Box>
@@ -128,20 +120,20 @@ function OverviewDeployments({
const liveDeploymentId = getLastLiveDeployment(deployments);
const { deployments: scheduledOrPendingDeployments } =
scheduledOrPendingDeploymentsData;
scheduledOrPendingDeploymentsData || { deployments: [] };
return (
<List
className="rounded-x-lg flex flex-col rounded-lg"
className="rounded-x-lg flex flex-col overflow-hidden rounded-lg"
sx={{ borderColor: 'grey.300', borderWidth: 1 }}
>
{deployments.map((deployment, index) => (
{deployments?.map((deployment, index) => (
<Fragment key={deployment.id}>
<DeploymentListItem
deployment={deployment}
isLive={deployment.id === liveDeploymentId}
showRedeploy={index === 0}
disableRedeploy={scheduledOrPendingDeployments.length > 0}
disableRedeploy={scheduledOrPendingDeployments?.length > 0}
/>
{index !== deployments.length - 1 && <Divider component="li" />}
@@ -151,21 +143,18 @@ function OverviewDeployments({
);
}
export default function OverviewDeploymentsPage() {
export default function OverviewDeployments() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const { openGitHubModal } = useGitHubModal();
const { githubRepository } = currentApplication;
const { githubRepository } = currentApplication || {};
// GitHub repo connected. Show deployments
if (githubRepository) {
return (
<div className="flex flex-col">
<OverviewDeploymentsTopBar />
<OverviewDeployments
projectId={currentApplication.id}
githubRepository={githubRepository}
/>
<OverviewDeploymentList />
</div>
);
}
@@ -175,7 +164,7 @@ export default function OverviewDeploymentsPage() {
<div className="flex flex-col">
<OverviewDeploymentsTopBar />
<Box className="grid grid-flow-row gap-5 items-center justify-items-center rounded-lg py-12 px-48 shadow-sm border-1">
<Box className="grid grid-flow-row items-center justify-items-center gap-5 rounded-lg border-1 py-12 px-48 shadow-sm">
<RocketIcon strokeWidth={1} className="h-10 w-10" />
<div className="grid grid-flow-row gap-1">

View File

@@ -59,6 +59,7 @@ export function Avatar({
<Box
style={Object.assign(style, { backgroundImage: `url(${avatarUrl})` })}
className={classes}
aria-label="Avatar"
{...rest}
/>
);

View File

@@ -63,7 +63,9 @@ function ActivityIndicator({
// We are rendering a span instead of null in order to keep the layout
// intact in certain cases (e.g. when elements have a "space-between"
// position).
return <span />;
return (
<span role="progressbar" aria-label="Activity indicator placeholder" />
);
}
return (

View File

@@ -1,16 +1,24 @@
import type { LinkProps as MaterialLinkProps } from '@mui/material/Link';
import MaterialLink from '@mui/material/Link';
import MaterialLink, {
linkClasses as materialLinkClasses,
} from '@mui/material/Link';
import type { ForwardedRef } from 'react';
import { forwardRef } from 'react';
export interface LinkProps extends MaterialLinkProps {}
export interface LinkProps extends MaterialLinkProps {
/**
* Controls when the link should have an underline.
* @default 'hover'
*/
underline?: MaterialLinkProps['underline'];
}
function Link(
{ children, ...props }: LinkProps,
{ children, underline = 'hover', ...props }: LinkProps,
ref: ForwardedRef<HTMLAnchorElement>,
) {
return (
<MaterialLink ref={ref} {...props}>
<MaterialLink underline={underline} ref={ref} {...props}>
{children}
</MaterialLink>
);
@@ -18,4 +26,6 @@ function Link(
Link.displayName = 'NhostLink';
export { materialLinkClasses as linkClasses };
export default forwardRef(Link);

View File

@@ -19,6 +19,10 @@ export type TextProps<
* @default 'primary'
*/
color?: 'primary' | 'secondary' | 'disabled' | 'error';
/**
* The component used for the root node.
*/
component?: D;
};
const textClasses = {

View File

@@ -4,23 +4,31 @@ import useColorPreference from '@/ui/v2/useColorPreference';
import CssBaseline from '@mui/material/CssBaseline';
import GlobalStyles from '@mui/material/GlobalStyles';
import { ThemeProvider as MaterialThemeProvider } from '@mui/material/styles';
import Head from 'next/head';
import type { PropsWithChildren } from 'react';
function ThemeProviderContent({ children }: PropsWithChildren<unknown>) {
function ThemeProviderContent({
children,
color: manualColor,
}: PropsWithChildren<{ color?: 'light' | 'dark' }>) {
const { color } = useColorPreference();
const theme = createTheme(color);
const theme = createTheme(manualColor || color);
return (
<MaterialThemeProvider theme={theme}>
<CssBaseline />
<GlobalStyles
styles={{
body: {
backgroundColor: theme.palette.background.default,
'html, body': {
backgroundColor: `${theme.palette.background.default} !important`,
},
}}
/>
<Head>
<meta name="theme-color" content={theme.palette.background.paper} />
</Head>
{children}
</MaterialThemeProvider>
);
@@ -33,17 +41,22 @@ export interface ThemeProviderProps extends PropsWithChildren<unknown> {
* @default 'color-mode'
*/
colorPreferenceStorageKey?: string;
/**
* Manually set the color preference.
*/
color?: 'light' | 'dark';
}
function ThemeProvider({
children,
color,
colorPreferenceStorageKey = 'color-preference',
}: ThemeProviderProps) {
return (
<ColorPreferenceProvider
colorPreferenceStorageKey={colorPreferenceStorageKey}
>
<ThemeProviderContent>{children}</ThemeProviderContent>
<ThemeProviderContent color={color}>{children}</ThemeProviderContent>
</ColorPreferenceProvider>
);
}

View File

@@ -25,10 +25,25 @@ export const UserDataContext = createContext<UserDataContent>({
setUserContext: () => {},
});
export function UserDataProvider({ children }: PropsWithChildren<unknown>) {
export interface UserDataProviderProps {
/**
* Initial workspaces to be used in the context.
*/
initialWorkspaces?: Workspace[];
/**
* Initial metadata to be used in the context.
*/
initialMetadata?: Record<string, any>;
}
export function UserDataProvider({
children,
initialWorkspaces,
initialMetadata,
}: PropsWithChildren<UserDataProviderProps>) {
const [userContext, setUserContext] = useState({
workspaces: [],
metadata: {},
workspaces: initialWorkspaces || [],
metadata: initialMetadata || {},
});
const value = useMemo(

View File

@@ -31,6 +31,7 @@ test('should throw an error if permission object is incorrectly provided', async
action: 'select',
role: 'user',
mode: 'update',
resourceVersion: 1,
}),
).rejects.toThrowError(
new Error(

View File

@@ -17,6 +17,10 @@ export interface ManagePermissionVariables {
* The action to manage the permission for.
*/
action: DatabaseAction;
/**
* The resource version of the metadata.
*/
resourceVersion: number;
/**
* Permission to insert or update.
*/
@@ -45,6 +49,7 @@ export default async function managePermission({
permission,
role,
action,
resourceVersion,
mode = 'update',
}: ManagePermissionOptions & ManagePermissionVariables) {
if (mode !== 'delete' && !permission) {
@@ -87,7 +92,7 @@ export default async function managePermission({
args,
type: 'bulk',
source: dataSource,
version: 1,
resource_version: resourceVersion,
}),
});

View File

@@ -18,6 +18,10 @@ export interface ManagePermissionMigrationVariables {
* The action to manage the permission for.
*/
action: DatabaseAction;
/**
* The resource version of the metadata.
*/
resourceVersion: number;
/**
* Permission to insert or update.
*/

View File

@@ -8,7 +8,12 @@ import fetch from 'cross-fetch';
export interface FetchMetadataOptions
extends Omit<MutationOrQueryBaseOptions, 'schema' | 'table'> {}
export interface FetchMetadataReturnType extends HasuraMetadataSource {}
export interface FetchMetadataReturnType extends Partial<HasuraMetadataSource> {
/**
* The resource version of the metadata.
*/
resourceVersion: number;
}
/**
* Fetch Hasura metadata using the Metadata API.
@@ -33,8 +38,9 @@ export default async function fetchMetadata({
}),
});
const responseData: Record<string, HasuraMetadata> | QueryError =
await response.json();
const responseData:
| { metadata: HasuraMetadata; resource_version: number }
| QueryError = await response.json();
if (!response.ok || 'error' in responseData) {
if ('internal' in responseData) {
@@ -48,9 +54,11 @@ export default async function fetchMetadata({
}
}
const { metadata } = responseData;
const { metadata, resource_version: resourceVersion } = responseData;
const currentSource =
metadata?.sources?.find((source) => source.name === dataSource) || null;
return (
metadata?.sources?.find((source) => source.name === dataSource) || null
);
return currentSource
? { ...currentSource, resourceVersion }
: { resourceVersion };
}

View File

@@ -35,15 +35,15 @@ export default function useNotFoundRedirect() {
}
if (noResolvedWorkspace && notIn404Already) {
router.push('/404');
router.replace('/404');
}
if (noResolvedApplication && notIn404Already) {
router.push('/404');
router.replace('/404');
}
if (isProjectUsingRDS && inSettingsDatabasePage) {
router.push('/404');
router.replace('/404');
}
}, [
updating,

View File

@@ -12,13 +12,11 @@ export function authProtected<P>(Comp: ComponentType<P>) {
const { isAuthenticated, isLoading } = useAuthenticationStatus();
useEffect(() => {
if (!isLoading) {
if (!isAuthenticated && router.pathname === '/') {
router.push('/signup');
} else if (!isAuthenticated) {
router.push('/signin');
}
if (isLoading || isAuthenticated) {
return;
}
router.push('/signin');
}, [isLoading, isAuthenticated, router]);
if (isLoading) {

View File

@@ -1,38 +1,67 @@
import Form from '@/components/common/Form';
import NavLink from '@/components/common/NavLink';
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Link from '@/ui/v2/Link';
import Input, { inputClasses } from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { useTheme } from '@mui/material';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material';
import { useResetPassword } from '@nhost/nextjs';
import Image from 'next/image';
import NavLink from 'next/link';
import type { ReactElement } from 'react';
import { useForm } from 'react-hook-form';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
type ResetPasswordFormProps = {
email: string;
};
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
});
function ResetPasswordForm() {
const { resetPassword, isSent, isLoading, isError, error } =
useResetPassword();
export type ResetPasswordFormValues = Yup.InferType<typeof validationSchema>;
const { register, handleSubmit, getValues } = useForm<ResetPasswordFormProps>(
{
reValidateMode: 'onSubmit',
defaultValues: {
email: '',
},
const StyledInput = styled(Input)({
backgroundColor: 'transparent',
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent !important',
},
});
export default function ResetPasswordPage() {
const { resetPassword, error, isSent } = useResetPassword();
const form = useForm<ResetPasswordFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
email: '',
},
);
resolver: yupResolver(validationSchema),
});
const onSubmit = async (data: ResetPasswordFormProps) => {
const { email } = data;
const { register, formState, getValues } = form;
await resetPassword(email);
};
useEffect(() => {
if (!error) {
return;
}
toast.error(
error?.message || 'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
}, [error]);
async function handleSubmit({ email }: ResetPasswordFormValues) {
try {
await resetPassword(email);
} catch {
toast.error(
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}
}
if (isSent) {
return (
@@ -44,102 +73,54 @@ function ResetPasswordForm() {
}
return (
<div className="flex max-w-2xl flex-col items-center">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex w-full flex-col space-y-3"
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
<Input
{...register('email')}
autoFocus
id="email"
label="Email"
placeholder="Email"
required
fullWidth
inputProps={{ min: 2, max: 128 }}
spellCheck="false"
autoCapitalize="none"
type="email"
/>
Reset Password
</Text>
<div className="flex flex-col">
<Button type="submit" disabled={isLoading} loading={isLoading}>
Send Reset Instructions
</Button>
</div>
</form>
{isError && (
<div className="my-3">
<Text className="font-medium" color="error">
Error: {error.message}
</Text>
</div>
)}
</div>
);
}
export default function ResetPasswordPage() {
const theme = useTheme();
return (
<div className="flex h-screen items-center justify-center">
<div className="flex max-w-3xl flex-col">
<div className="z-30 mb-8 flex justify-center">
<a href="https://nhost.io" tabIndex={-1}>
<Image
src={
theme.palette.mode === 'dark'
? '/assets/brands/light/nhost-with-text.svg'
: '/assets/brands/nhost-with-text.svg'
}
alt="Nhost Logo"
width={185}
height={64}
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="grid grid-flow-row gap-4 bg-transparent"
>
<StyledInput
{...register('email')}
type="email"
id="email"
label="Email"
placeholder="Email"
fullWidth
autoFocus
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
/>
</a>
</div>
<div className="flex items-center justify-center">
<div className="z-30">
<Box
className="rounded-lg border px-12 py-4"
style={{ width: '480px' }}
<Button
className="!bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
size="large"
type="submit"
disabled={formState.isSubmitting}
loading={formState.isSubmitting}
>
<div className="my-4">
<div className="mb-4 flex justify-center text-lg font-semibold">
Reset your password
</div>
<ResetPasswordForm />
</div>
</Box>
Send Reset Instructions
</Button>
</Form>
</FormProvider>
</Box>
<div className="mt-3 flex justify-center">
<div className="grid grid-flow-col items-center justify-center gap-1">
<Text className="text-sm">Is your password okay?</Text>
<NavLink href="/signin" passHref>
<Link href="signin" underline="hover">
Sign in
</Link>
</NavLink>
</div>
</div>
</div>
<div className="absolute z-0 w-full max-w-[887px]">
<Image
src="/assets/signup/bg-gradient.svg"
alt="Gradient background"
width={887}
height={620}
layout="responsive"
/>
</div>
</div>
</div>
</div>
<Text color="secondary" className="text-center text-base lg:text-lg">
Is your password okay?{' '}
<NavLink href="/signin/email" color="white" className="font-medium">
Sign In
</NavLink>
</Text>
</>
);
}

View File

@@ -1,238 +1,100 @@
import Form from '@/components/common/Form';
import NavLink from '@/components/common/NavLink';
import GithubIcon from '@/components/icons/GithubIcon';
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Link from '@/ui/v2/Link';
import Divider from '@/ui/v2/Divider';
import Text from '@/ui/v2/Text';
import { nhost } from '@/utils/nhost';
import { useTheme } from '@mui/material';
import { useSignInEmailPassword } from '@nhost/nextjs';
import Image from 'next/image';
import NavLink from 'next/link';
import { useRouter } from 'next/router';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import type { ReactElement } from 'react';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
function SignInWithGithub({ setSignInMethod }: any) {
const [isLoading, setIsLoading] = useState(false);
export default function SignUpPage() {
const [loading, setLoading] = useState(false);
return (
<div className="flex flex-col items-center">
<button
type="button"
className="flex flex-row items-center space-x-4 rounded-sm+ bg-github px-6 py-3 font-display font-medium text-white transition-all duration-200 ease-in-out disabled:opacity-40"
disabled={isLoading}
onClick={() => {
setIsLoading(true);
nhost.auth.signIn({ provider: 'github' });
}}
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
<GithubIcon className="h-6 w-6" />
<div>Continue with GitHub</div>
</button>
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
<span>or</span>
It&apos;s time to build
</Text>
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<Button
variant="borderless"
type="button"
size="small"
onClick={() => setSignInMethod('email')}
className="hover:bg-transparent hover:underline"
>
sign in with email
</Button>
</div>
</div>
);
}
className="!bg-white !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
startIcon={<GithubIcon />}
size="large"
disabled={loading}
loading={loading}
onClick={async () => {
setLoading(true);
type SignInFormProps = {
email: string;
password: string;
};
function SignInWithEmail({ setSignInMethod }: any) {
const { signInEmailPassword, isLoading, isSuccess, isError, error } =
useSignInEmailPassword();
const form = useForm<SignInFormProps>({
reValidateMode: 'onSubmit',
defaultValues: {
email: '',
password: '',
},
});
const { register } = form;
const router = useRouter();
async function onSubmit({ email, password }: SignInFormProps) {
await signInEmailPassword(email, password);
}
if (isSuccess) {
router.push('/');
}
return (
<Box className="flex max-w-2xl flex-col items-center">
<FormProvider register={register} {...form}>
<Form onSubmit={onSubmit} className="grid w-full grid-flow-row gap-3">
<Input
{...register('email')}
autoFocus
id="email"
label="Email"
placeholder="Email"
required
fullWidth
inputProps={{
min: 2,
max: 128,
}}
spellCheck="false"
autoCapitalize="none"
type="email"
hideEmptyHelperText
/>
<Input
{...register('password')}
id="password"
placeholder="Password"
required
fullWidth
label={
<span className="grid grid-flow-col justify-between">
<span>Password</span>
<NavLink href="/reset-password" passHref>
<Link
href="reset-password"
tabIndex={-1}
className="text-xs"
underline="hover"
>
Forgot your password?
</Link>
</NavLink>
</span>
try {
await nhost.auth.signIn({ provider: 'github' });
} catch {
toast.error(
`An error occurred while trying to sign in using GitHub. Please try again later.`,
getToastStyleProps(),
);
} finally {
setLoading(false);
}
inputProps={{ min: 2, max: 128 }}
spellCheck="false"
autoCapitalize="none"
type="password"
hideEmptyHelperText
/>
}}
>
Continue with GitHub
</Button>
<div className="flex flex-col">
<Button type="submit" disabled={isLoading} loading={isLoading}>
Sign In
</Button>
</div>
</Form>
</FormProvider>
{isError && (
<Text className="my-3 font-medium" color="error">
Error: {error.message}
</Text>
)}
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
<span>or</span>
<Button
variant="borderless"
type="button"
size="small"
onClick={() => setSignInMethod('github')}
className="hover:bg-transparent hover:underline"
className="!text-white hover:!bg-white hover:!bg-opacity-10 focus:!bg-white focus:!bg-opacity-10"
size="large"
href="/signin/email"
LinkComponent={NavLink}
>
sign in with GitHub
Continue with Email
</Button>
</div>
</Box>
<Divider className="!my-2" />
<Text color="secondary" className="text-center text-sm">
By clicking continue, you agree to our{' '}
<NavLink
href="https://nhost.io/legal/terms-of-service"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
>
Terms of Service
</NavLink>{' '}
and{' '}
<NavLink
href="https://nhost.io/legal/privacy-policy"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
>
Privacy Policy
</NavLink>
</Text>
</Box>
<Text color="secondary" className="text-center lg:text-lg">
Don&apos;t have an account?{' '}
<NavLink href="/signup" color="white" className="font-medium">
Sign Up
</NavLink>
</Text>
</>
);
}
function SignInController() {
const [signInMethod, setSignInMethod] = useState('github');
if (signInMethod === 'github') {
return <SignInWithGithub setSignInMethod={setSignInMethod} />;
}
if (signInMethod === 'email') {
return <SignInWithEmail setSignInMethod={setSignInMethod} />;
}
return null;
}
export default function SignInPage() {
const theme = useTheme();
return (
<div className="flex h-screen items-center justify-center">
<div className="flex max-w-3xl flex-col">
<div className="z-30 mb-8 flex justify-center">
<a href="https://nhost.io" tabIndex={-1}>
<Image
src={
theme.palette.mode === 'dark'
? '/assets/brands/light/nhost-with-text.svg'
: '/assets/brands/nhost-with-text.svg'
}
alt="Nhost Logo"
width={185}
height={64}
/>
</a>
</div>
<div className="flex items-center justify-center">
<div className="z-10">
<Box
className="grid grid-flow-row gap-4 rounded-lg border px-12 py-8"
style={{ width: '480px' }}
>
<Text variant="h1" className="text-center text-lg font-semibold">
Sign in to Nhost
</Text>
<SignInController />
</Box>
<div className="mt-3 flex justify-center">
<div className="grid grid-flow-col items-center justify-center gap-1">
<Text className="text-sm">Don&apos;t have an account?</Text>
<NavLink href="/signup" passHref>
<Link href="signup" underline="hover">
Sign up
</Link>
</NavLink>
</div>
</div>
</div>
<div className="absolute z-0 w-full max-w-[887px]">
<Image
src="/assets/signup/bg-gradient.svg"
alt="Gradient background"
width={887}
height={620}
layout="responsive"
/>
</div>
</div>
</div>
</div>
);
}
SignInPage.getLayout = function getLayout(page: ReactElement) {
SignUpPage.getLayout = function getLayout(page: ReactElement) {
return <UnauthenticatedLayout title="Sign In">{page}</UnauthenticatedLayout>;
};

View File

@@ -0,0 +1,155 @@
import Form from '@/components/common/Form';
import NavLink from '@/components/common/NavLink';
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Input, { inputClasses } from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material';
import { useSignInEmailPassword } from '@nhost/nextjs';
import type { ReactElement } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
password: Yup.string().label('Password').required(),
});
export type EmailSignUpFormValues = Yup.InferType<typeof validationSchema>;
const StyledInput = styled(Input)({
backgroundColor: 'transparent',
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent !important',
},
});
export default function EmailSignUpPage() {
const { signInEmailPassword, error } = useSignInEmailPassword();
const form = useForm<EmailSignUpFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
email: '',
password: '',
},
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
useEffect(() => {
if (!error) {
return;
}
toast.error(
error?.message || 'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
}, [error]);
async function handleSubmit({ email, password }: EmailSignUpFormValues) {
try {
await signInEmailPassword(email, password);
} catch {
toast.error(
'An error occurred while signing in. Please try again.',
getToastStyleProps(),
);
}
}
return (
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
Sign In
</Text>
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="grid grid-flow-row gap-4 bg-transparent"
>
<StyledInput
{...register('email')}
id="email"
placeholder="Email"
inputProps={{ min: 2, max: 128 }}
spellCheck="false"
autoCapitalize="none"
type="email"
label="Email"
hideEmptyHelperText
fullWidth
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
autoFocus
/>
<StyledInput
{...register('password')}
id="password"
placeholder="Password"
inputProps={{ min: 2, max: 128 }}
spellCheck="false"
autoCapitalize="none"
type="password"
label="Password"
hideEmptyHelperText
fullWidth
error={!!formState.errors.password}
helperText={formState.errors.password?.message}
/>
<NavLink
href="/reset-password"
color="white"
className="justify-self-start font-semibold"
>
Forgot password?
</NavLink>
<Button
className="!bg-white !text-black disabled:!text-black disabled:!text-opacity-60"
size="large"
disabled={formState.isSubmitting}
loading={formState.isSubmitting}
type="submit"
>
Sign In
</Button>
<Text color="secondary" className="text-center">
or{' '}
<NavLink color="white" className="font-semibold" href="/signin">
sign in with GitHub
</NavLink>
</Text>
</Form>
</FormProvider>
</Box>
<Text color="secondary" className="text-center text-base lg:text-lg">
Don&apos;t have an account?{' '}
<NavLink href="/signup" color="white">
Sign Up
</NavLink>
</Text>
</>
);
}
EmailSignUpPage.getLayout = function getLayout(page: ReactElement) {
return <UnauthenticatedLayout title="Sign In">{page}</UnauthenticatedLayout>;
};

View File

@@ -1,342 +1,214 @@
import Form from '@/components/common/Form';
import NavLink from '@/components/common/NavLink';
import GithubIcon from '@/components/icons/GithubIcon';
import UnauthenticatedLayout from '@/components/layout/UnauthenticatedLayout';
import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input';
import Link from '@/ui/v2/Link';
import Divider from '@/ui/v2/Divider';
import Input, { inputClasses } from '@/ui/v2/Input';
import Text from '@/ui/v2/Text';
import { nhost } from '@/utils/nhost';
import { useTheme } from '@mui/material';
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { styled } from '@mui/material';
import { useSignUpEmailPassword } from '@nhost/nextjs';
import kebabCase from 'just-kebab-case';
import Image from 'next/image';
import NavLink from 'next/link';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import * as Yup from 'yup';
const sellingPoints = [
'Awesome Free Plan',
'Postgres Database',
'Instant GraphQL API with Hasura',
'Authentication and Storage',
'TypeScript Client',
'Serverless Functions',
'Push code to deploy with our GitHub Integration',
];
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
password: Yup.string().label('Password').required(),
displayName: Yup.string().label('Name').required(),
});
const companies = [
'Revtron',
'HyperLab',
'Orthopy',
'Celsia',
'ServeHub',
'Teamtailor',
];
export type SignUpFormValues = Yup.InferType<typeof validationSchema>;
function SignUpWithGithub({ setSignUpMethod }: any) {
const [isLoading, setIsLoading] = useState(false);
const StyledInput = styled(Input)({
backgroundColor: 'transparent',
[`& .${inputClasses.input}`]: {
backgroundColor: 'transparent !important',
},
});
return (
<div className="flex flex-col items-center">
<button
type="button"
className="flex flex-row items-center space-x-6 rounded-sm+ bg-github px-6 py-3 font-display font-medium text-white transition-all duration-200 ease-in-out disabled:opacity-40"
disabled={isLoading}
onClick={() => {
setIsLoading(true);
nhost.auth.signIn({ provider: 'github' });
}}
>
<GithubIcon className="h-6 w-6 text-white" />
<div>Sign Up with GitHub</div>
</button>
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
<span>or</span>
<Button
variant="borderless"
type="button"
size="small"
onClick={() => setSignUpMethod('email')}
className="hover:bg-transparent hover:underline"
>
sign up with email
</Button>
</div>
</div>
);
}
export default function SignUpPage() {
const { signUpEmailPassword, error } = useSignUpEmailPassword();
const [loading, setLoading] = useState(false);
type SignUpFormProps = {
displayName: string;
email: string;
password: string;
};
function SignUpWithEmail({ setSignUpMethod }: any) {
const { signUpEmailPassword, isLoading, isSuccess, isError, error } =
useSignUpEmailPassword();
const form = useForm<SignUpFormProps>({
const form = useForm<SignUpFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
displayName: '',
email: '',
password: '',
},
resolver: yupResolver(validationSchema),
});
const { register } = form;
const { register, formState } = form;
const router = useRouter();
useEffect(() => {
if (!error) {
return;
}
async function onSubmit({ email, password, displayName }: SignUpFormProps) {
await signUpEmailPassword(email, password, {
displayName,
});
}
toast.error(
error?.message || 'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}, [error]);
if (isSuccess) {
router.push('/');
async function handleSubmit({
email,
password,
displayName,
}: SignUpFormValues) {
try {
await signUpEmailPassword(email, password, { displayName });
} catch {
toast.error(
'An error occurred while signing up. Please try again.',
getToastStyleProps(),
);
}
}
return (
<div className="grid grid-flow-row items-center justify-items-center gap-2">
<Text variant="h1" className="text-lg font-semibold">
Sign Up with Email
<>
<Text
variant="h2"
component="h1"
className="text-center text-3.5xl font-semibold lg:text-4.5xl"
>
Sign Up
</Text>
<FormProvider register={register} {...form}>
<Form onSubmit={onSubmit} className="grid w-full grid-flow-row gap-3">
<Input
{...register('displayName')}
id="displayName"
placeholder="Name"
required
inputProps={{
min: 2,
max: 128,
}}
spellCheck="false"
autoCapitalize="none"
type="text"
autoFocus
label="Name"
hideEmptyHelperText
fullWidth
autoComplete="off"
/>
<Input
{...register('email')}
id="email"
placeholder="Email"
required
inputProps={{
min: 2,
max: 128,
}}
spellCheck="false"
autoCapitalize="none"
type="email"
label="Email"
hideEmptyHelperText
fullWidth
/>
<Input
{...register('password')}
id="password"
placeholder="Password"
required
inputProps={{
min: 2,
max: 128,
}}
spellCheck="false"
autoCapitalize="none"
type="password"
label="Password"
hideEmptyHelperText
fullWidth
/>
<div className="flex flex-col">
<Button type="submit" disabled={isLoading} loading={isLoading}>
Sign Up
</Button>
</div>
</Form>
</FormProvider>
{isError && (
<Text className="font-medium" color="error">
Error: {error.message}
</Text>
)}
<div className="mt-2 grid grid-flow-col items-center justify-center gap-px">
<span>or</span>
<Box className="grid grid-flow-row gap-4 rounded-md border bg-transparent p-6 lg:p-12">
<Button
variant="borderless"
type="button"
size="small"
onClick={() => setSignUpMethod('github')}
className="hover:bg-transparent hover:underline"
className="!bg-white !text-black hover:ring-2 hover:ring-white hover:ring-opacity-50 disabled:!text-black disabled:!text-opacity-60"
startIcon={<GithubIcon />}
disabled={loading}
loading={loading}
size="large"
onClick={async () => {
setLoading(true);
try {
await nhost.auth.signIn({ provider: 'github' });
} catch {
toast.error(
`An error occurred while trying to sign up using GitHub. Please try again.`,
getToastStyleProps(),
);
} finally {
setLoading(false);
}
}}
>
sign up with GitHub
Sign Up with GitHub
</Button>
</div>
</div>
);
}
function SignUpController() {
const [signUpMethod, setSignUpMethod] = useState('github');
<div className="relative py-2">
<Text
className="absolute left-0 right-0 top-1/2 mx-auto w-12 -translate-y-1/2 bg-black px-2 text-center text-sm"
color="disabled"
>
OR
</Text>
if (signUpMethod === 'github') {
return <SignUpWithGithub setSignUpMethod={setSignUpMethod} />;
}
if (signUpMethod === 'email') {
return <SignUpWithEmail setSignUpMethod={setSignUpMethod} />;
}
<Divider />
</div>
return null;
}
export default function SignUpPage() {
const theme = useTheme();
return (
<div className="flex h-screen items-center justify-center">
<div className="grid max-w-6xl grid-cols-1 gap-x-20 gap-y-14 px-5 md:grid-cols-2">
<div className="flex items-center justify-center md:order-1">
<div className="z-30">
<div className="block md:hidden">
<div className="mb-5">
<Image
src={
theme.palette.mode === 'dark'
? '/assets/brands/light/nhost-with-text.svg'
: '/assets/brands/nhost-with-text.svg'
}
alt="Nhost Logo"
width={185}
height={64}
/>
</div>
<div className="mb-4 text-3xl font-semibold">
Build the App of Your Dreams
</div>
</div>
<Box className="rounded-lg border px-12 py-4">
<div className="my-4">
<SignUpController />
</div>
<Text className="mt-4 text-center text-xs" color="secondary">
By signing up, you agree to our{' '}
<Link
href="https://nhost.io/legal/terms-of-service"
target="_blank"
rel="noreferrer"
underline="hover"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
href="https://nhost.io/legal/privacy-policy"
target="_blank"
rel="noreferrer"
underline="hover"
>
Privacy Policy
</Link>
.
</Text>
</Box>
<div className="mt-3 flex justify-center">
<div className="grid grid-flow-col items-center justify-center gap-1">
<Text className="text-sm">Already have an account?</Text>
<NavLink href="/signin" passHref>
<Link href="signin" underline="hover">
Sign in
</Link>
</NavLink>
</div>
</div>
</div>
<div className="absolute w-full max-w-[887px]">
<Image
src="/assets/signup/bg-gradient.svg"
alt="Gradient background"
width={887}
height={620}
layout="responsive"
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="grid grid-flow-row gap-4 bg-transparent"
>
<StyledInput
{...register('displayName')}
id="displayName"
label="Name"
placeholder="Name"
fullWidth
autoFocus
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
/>
</div>
</div>
<div className="">
<div className="hidden md:block">
<div className="mb-10">
<Image
src={
theme.palette.mode === 'dark'
? '/assets/brands/light/nhost-with-text.svg'
: '/assets/brands/nhost-with-text.svg'
}
alt="Nhost Logo"
width={185}
height={64}
/>
</div>
<div className="mb-4 text-2xl font-semibold">
Build the App of Your Dreams
</div>
</div>
<div className="my-4 flex flex-col space-y-3">
{sellingPoints.map((sellingPoint) => (
<div key={sellingPoint} className="flex items-center space-x-2">
<Image
src="/assets/signup/CircleWavyCheck.svg"
alt="Check"
width={24}
height={24}
/>
<Text className="!text-xl" color="secondary">
{sellingPoint}
</Text>
</div>
))}
</div>
<Box
className="my-14 h-2 opacity-20"
sx={{ backgroundColor: 'primary.main' }}
/>
<div className="my-4 grid grid-cols-3 items-center gap-x-6 gap-y-6 opacity-40">
{companies.map((company) => (
<div key={company} className="h-[25px] w-[150px]">
<Image
src={
theme.palette.mode === 'dark'
? `/assets/brands/light/${kebabCase(company)}.svg`
: `/assets/brands/${kebabCase(company)}.svg`
}
alt={`Logo of ${company}`}
width={150}
height={25}
className="object-cover"
/>
</div>
))}
</div>
</div>
</div>
</div>
<StyledInput
{...register('email')}
type="email"
id="email"
label="Email"
placeholder="Email"
fullWidth
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.email}
helperText={formState.errors.email?.message}
/>
<StyledInput
{...register('password')}
type="password"
id="password"
label="Password"
placeholder="Password"
fullWidth
inputProps={{ min: 2, max: 128 }}
error={!!formState.errors.password}
helperText={formState.errors.password?.message}
/>
<Button
variant="outlined"
color="secondary"
className="hover:!bg-white hover:!bg-opacity-10 focus:ring-0"
size="large"
type="submit"
disabled={formState.isSubmitting}
loading={formState.isSubmitting}
>
Sign Up
</Button>
</Form>
</FormProvider>
<Divider className="!my-2" />
<Text color="secondary" className="text-center text-sm">
By signing up, you agree to our{' '}
<NavLink
href="https://nhost.io/legal/terms-of-service"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
>
Terms of Service
</NavLink>{' '}
and{' '}
<NavLink
href="https://nhost.io/legal/privacy-policy"
target="_blank"
rel="noopener noreferrer"
className="font-semibold"
color="white"
>
Privacy Policy
</NavLink>
</Text>
</Box>
<Text color="secondary" className="text-center text-base lg:text-lg">
Already have an account?{' '}
<NavLink href="/signin" color="white" className="font-medium">
Sign In
</NavLink>
</Text>
</>
);
}

View File

@@ -1,18 +1,6 @@
@tailwind base;
@tailwind components;
@media (prefers-color-scheme: dark) {
#__next {
background-color: #151a22;
}
}
@media (prefers-color-scheme: light) {
#__next {
background-color: #fafafa;
}
}
.sb-show-main {
overflow: auto;
}

View File

@@ -62,6 +62,7 @@ export interface HasuraMetadataPermission {
allow_aggregations: boolean;
query_root_fields: string[];
subscription_root_fields: string[];
computed_fields: string[];
set: Record<string, any>;
backend_only: boolean;
}>;

View File

@@ -4,7 +4,11 @@ import slugify from 'slugify';
import { LOCAL_BACKEND_URL } from './env';
import type { DeploymentRowFragment } from './__generated__/graphql';
export function getLastLiveDeployment(deployments: DeploymentRowFragment[]) {
export function getLastLiveDeployment(deployments?: DeploymentRowFragment[]) {
if (!deployments) {
return '';
}
return (
deployments.find((deployment) => deployment.deploymentStatus === 'DEPLOYED')
?.id || ''

View File

@@ -5,6 +5,7 @@ import { ManagedUIContext } from '@/context/UIContext';
import { WorkspaceProvider } from '@/context/workspace-context';
import { UserDataProvider } from '@/context/workspace1-context';
import createTheme from '@/ui/v2/createTheme';
import { createHttpLink } from '@apollo/client';
import { CacheProvider } from '@emotion/react';
import { ThemeProvider } from '@mui/material/styles';
import { NhostProvider } from '@nhost/nextjs';
@@ -33,7 +34,7 @@ const queryClient = new QueryClient({
global.fetch = fetch;
const mockRouter: NextRouter = {
export const mockRouter: NextRouter = {
basePath: '',
pathname: '/',
route: '/',
@@ -65,7 +66,12 @@ function Providers({ children }: PropsWithChildren<{}>) {
<QueryClientProvider client={queryClient}>
<CacheProvider value={emotionCache}>
<NhostProvider nhost={nhost}>
<NhostApolloProvider nhost={nhost}>
<NhostApolloProvider
nhost={nhost}
link={createHttpLink({
uri: 'http://localhost:1337/v1/graphql',
})}
>
<WorkspaceProvider>
<UserDataProvider>
<ManagedUIContext>

View File

@@ -111,10 +111,10 @@ module.exports = {
},
fontSize: {
'xs-': ['0.6875rem', '0.875rem'],
'sm-': '13px',
'sm-': ['0.8125rem', '1rem'],
'sm+': ['0.9375rem', '1.25rem'],
'2.5xl': '26px',
'5.5xl': '56px',
'3.5xl': ['2rem', '2.5rem'],
'4.5xl': ['2.5rem', '2.5rem'],
},
maxWidth: {
2: '0.5rem',

View File

@@ -1,5 +1,12 @@
# @nhost/docs
## 0.0.13
### Patch Changes
- bf1e4071: chore(deps): bump `react` to v18
- 8be094be: fix(deps): update docusaurus monorepo to v2.3.1
## 0.0.12
### Patch Changes

View File

@@ -5,6 +5,9 @@ sidebar_label: 'Overview'
image: /img/og/graphql.png
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
A GraphQL API is automatically and instantly available based on the tables and columns in your [database](/database).
The GraphQL API has support for inserting, selecting, updating, and deleting data, which usually accounts for 80% of all API operations you need.
@@ -21,11 +24,11 @@ Building your GraphQL API is a lot of work, but with Nhost it's easy because eve
## Endpoint
The GraphQL API is available at `https://[subdomain].nhost.run/v1/graphql` When using the [CLI](/cli) the GraphQL API is available at `http://localhost:1337/v1/graphql`.
The GraphQL API is available at `https://[subdomain].graphql.[region].nhost.run/v1` When using the [CLI](/cli) the GraphQL API is available at `http://localhost:1337/v1/graphql`.
## GraphQL Clients for JavaScript
The Nhost JavaScript client comes with a simple [GraphQL client](/reference/javascript/nhost-js/graphql) that works well for the backend or simple applications.
The [Nhost JavaScript client](/reference/javascript) comes with a simple [GraphQL client](/reference/javascript/nhost-js/graphql) that works well for the backend or simple applications.
When building more complex frontend applications, we recommend using a more advanced GraphQL client such as:
@@ -38,7 +41,7 @@ When building more complex frontend applications, we recommend using a more adva
## Queries
A GraphQL query is used to fetch data from the database.
A query is used to fetch data from the GraphQL API.
:::tip
The [Queries documentation from Hasura](https://hasura.io/docs/latest/graphql/core/databases/postgres/queries/index/) is applicable since we're using Hasura's GraphQL Engine for your project.
@@ -46,7 +49,10 @@ The [Queries documentation from Hasura](https://hasura.io/docs/latest/graphql/co
**Example:** A GraphQL query to select `title`, `body`, and `isCompleted` for every row in the `todos` table.
```graphql title=GraphQL
<Tabs >
<TabItem value="request" label="Request" default>
```graphql
query GetTodos {
todos {
title
@@ -56,9 +62,10 @@ query GetTodos {
}
```
**Response:**
</TabItem>
<TabItem value="response" label="Response">
```json title=Response
```json
{
"data": {
"todos": [
@@ -72,13 +79,19 @@ query GetTodos {
}
```
</TabItem>
</Tabs>
#### Filtering and sorting
More complex queries utilize filters, limits, sorting, and aggregation.
Here's an example of a more complex GraphQL query that selects all items in the `todos` table that are not completed, with the total number of comments and the last five comments:
```graphql title=GraphQL
<Tabs >
<TabItem value="request" label="Request" default>
```graphql
query GetTodosWithLatestComments {
todos(where: { isCompleted: { _eq: false } }) {
title
@@ -97,7 +110,10 @@ query GetTodosWithLatestComments {
}
```
```json title=Response
</TabItem>
<TabItem value="response" label="Response">
```json
{
"data": {
"todos": [
@@ -127,6 +143,9 @@ query GetTodosWithLatestComments {
}
```
</TabItem>
</Tabs>
## Mutations
A GraphQL mutation is used to insert, upsert, update, or delete data.
@@ -139,7 +158,10 @@ The [Mutations documentation from Hasura](https://hasura.io/docs/latest/graphql/
**Example:** A GraphQL mutation to insert data:
```graphql title=GraphQL
<Tabs >
<TabItem value="request" label="Request" default>
```graphql
mutation InsertTodo {
insert_todos(
objects: [{ title: "Delete Firebase account", body: "Migrate to Nhost", isCompleted: false }]
@@ -154,7 +176,10 @@ mutation InsertTodo {
}
```
```json title=Reponse
</TabItem>
<TabItem value="response" label="Response">
```json
{
"data": {
"insert_todos": [
@@ -169,6 +194,9 @@ mutation InsertTodo {
}
```
</TabItem>
</Tabs>
#### Insert Multiple Rows
Use an array of objects to insert multiple rows at the same time.
@@ -303,9 +331,7 @@ mutation DeleteDoneTodos {
}
```
If you have set up foreign keys which will restrict a delete violation, you will get an error and will not be able to delete the data until all violations are solved. The simplest way to solve this is by set `On Delete Violation` to `CASCADE` when you set up a foreign Key.
---
If you have set up foreign keys which will restrict a delete violation, you will get an error and will not be able to delete the data until all violations are solved. The simplest way to solve this is by set `On Delete Violation` to `CASCADE` when you set up a foreign key.
## Subscriptions

View File

@@ -8,35 +8,19 @@ The GraphQL API is protected by a role-based permission system.
For each **role**, you create **rules** for the **select**, **insert**, **update**, and **delete** operations.
**Example:** Let's say you have a `posts` table, and you want users to only access their own posts. This is how you would do it:
**Example:** Let's say you have a `posts` table with `id` `title` and `user_id` columns, and you want users to only access their own posts. This is how you would do it:
```sql title="Posts Table"
CREATE TABLE "public"."posts" (
"id" serial NOT NULL PRIMARY KEY,
"title" text NOT NULL,
"user_id" uuid NOT NULL,
);
```
![Permission Variables](/img/graphql/permissions/permission-example.png)
```json title="Hasura Permission Rule"
{
"user_id": {
"_eq": "X-Hasura-User-Id"
}
}
```
The rule above make it so users can only select posts where the value of `user_id` is equal (`_eq`) to their user ID (`x-hasura-user-id`).
The permission above makes sure users can only select their own posts, because the value of `user_id` must be equal (`_eq`) to the authenticated user's ID (`x-hasura-user-id`).
## What is `x-hasura-user-id`?
`x-hasura-user-id` is a permission variable that is used to create permission rules in Hasura. The permission variable comes from the [access token](/authentication/tokens#access-token) that authenticated users have.
The `x-hasura-user-id` permission variable is always available for all authenticated users. You can add [permission variables](#permission-variables) to create more complex permission rules unique to your project.
`x-hasura-user-id` is a permission variable that is used to create permission rules. The permission variable comes from the [access token](/authentication/tokens#access-token) that all authenticated users have. You can add [custom permission variables](#permission-variables) to create more complex permission rules unique to your project.
## Permission Variables
You can add permission variables in the Nhost Dashboard under **Settings -> Roles and permissions**. These permission variables are automatically added to users' [access tokens](/authentication/tokens#access-token). This way, permission variables are available when creating permissions for your GraphQL API in the Hasura Console.
You can add permission variables in the Nhost Dashboard under **Settings -> Roles and permissions -> Permission Variables**. These permission variables are automatically added to users' [access tokens](/authentication/tokens#access-token). This way, permission variables are available when creating permissions for your GraphQL API.
![Permission Variables](/img/permission-variables-preview.png)
@@ -137,37 +121,28 @@ GraphQL requests from unauthenticated users resolve permissions using the `publi
Here is a popular approach for insert permission for authenticated users.
1. At the top of the page, click **"insert"** on the **"user"** role.
2. Select **"Without any checks"**.
3. Select the columns you want to allow users to insert.
In our example, we only mark `title`, because the other columns should not be inserted by the user.
We also want every new record's `user_id` value to be set to the ID of the user making the request. We can tell Hasura to do this using **Column presets**.
4. Under **Column presets**, set `user_id` to `x-hasura-user-id`.
1. Select **"Without any checks"**.
1. Select the columns you want to allow users to insert. In our example, we only mark `title`, because that's the only column that should be inserted by the user. The `id` is automatically generated by the database and `user_id` is set using a column preset.
1. Under **Column presets**, set `user_id` to `x-hasura-user-id`. This way, every new record's `user_id` value is set to the ID of the user making the request.
Now, authenticated users are allowed to insert posts. Users are allowed to add a title when inserting a post. The post's `id` is automatically generated by the database and the `user_id` is automatically set to the user's id using the `user_id = x-hasura-user-id` column preset.
## Select, Update and Delete Permissions
Select, update, and delete permissions usually follows the same pattern. Here's an example of how to add select permissions:
Select, update, and delete permissions usually follow the same pattern. Here's an example of how to add select permissions:
![Select permissions](/img/permission-select.png)
![Select permissions](/img/graphql/permissions/select-permissions.png)
One of the most common permission requirements is that authenticated users should only be able to read their own data. This is how to do that:
1. Go to **Hasura Console**
1. Select your table and open the **Permissions** tab
1. At the top of the page, click **"select"** on the **"user"** role.
1. Go to the **Database** section in the Nhost Dashboard.
1. In the context menu of the table you want to edit, click on **Edit Permissions**.
1. Click on the **role** and **operation** you want to set.
1. Select **"With custom check"** to create a new rule
1. Enter `user_id`, `_eq` and `x-hasura-user-id` into the rule form.
This means that in order for users to read data, the user ID value in the database row must be the same as the user ID in the access token.
To further refine this rule, do the following:
1. Limit the number of rows to 100 (or some other relevant number).
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. Enter `user_id`, `_eq` and `x-hasura-user-id` into the rule form. This means that in order for users to read data, the user ID value in the database row must be the same as the user ID in the access token.
1. **Limit the number of rows** to 100 (or some other relevant number).
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**.
## Next Steps

View File

@@ -9,10 +9,16 @@ Initialize a local Nhost project.
nhost init
```
If you already have a Nhost project in Nhost Cloud you can use that project as a starting point by appending `--remote` to the command.
If you have an existing Nhost project in Nhost Cloud that you want to use as a starting point for local development and for the [Git-based workflow](/platform/git), run `nhost init --remote`.
```
nhost init --remote
```
The `nhost init --remote` command does the following:
This will reset the local database migrations and Hasura metadata, and pull the database migrations and Hasura metadata from the Nhost Cloud project locally for you to use as a starting point. While doing `nhost init --remote` it's also recommended to setup [Git](/platform/git) to activate automated deplouments on `git push` for your Nhost project.
- Creates a new local Nhost project.
- Pulls the database migrations and Hasura metadata from the Nhost Cloud project.
- Resets the remote Nhost Cloud project's database migrations.
:::warning
The `nhost init --remote` command should only be run **once**. Running it multiple times will reset the remote Nhost Cloud project's database migrations which can cause migration conflict issues in your development team.
:::

View File

@@ -3,7 +3,7 @@ title: 'list'
sidebar_position: 7
---
List all your Nhost projects in Nhost Cloud.
List projects in Nhost Cloud.
```bash
nhost list

View File

@@ -3,7 +3,7 @@ title: 'logs'
sidebar_position: 9
---
Output logs of any service container
View logs of all services.
```bash
nhost logs

View File

@@ -3,10 +3,10 @@ title: 'up'
sidebar_position: 2
---
Launch the development environment for your project. Once the environment is up, the command will:
To launch the development environment for your project, use the command `nhost up`. Once the environment is running, this command will
- Apply database migrations.
- Apply the Hasura metadata.
- Apply Hasura metadata.
```bash
nhost up

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.0.12",
"version": "0.0.13",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
@@ -16,21 +16,21 @@
},
"dependencies": {
"@algolia/client-search": "^4.9.1",
"@docusaurus/core": "2.2.0",
"@docusaurus/plugin-sitemap": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/core": "2.3.1",
"@docusaurus/plugin-sitemap": "2.3.1",
"@docusaurus/preset-classic": "2.3.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",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"unist-util-visit": "^2.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0",
"@docusaurus/module-type-aliases": "2.3.1",
"@tsconfig/docusaurus": "^1.0.6",
"typescript": "^4.8.4"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 967 KiB

View File

@@ -1,5 +1,19 @@
# @nhost/apollo
## 4.13.4
### Patch Changes
- Updated dependencies [5013213b]
- @nhost/nhost-js@1.13.4
## 4.13.3
### Patch Changes
- c38f6074: chore(react-apollo): allow `link` in configuration options
- @nhost/nhost-js@1.13.3
## 4.13.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/apollo",
"version": "4.13.2",
"version": "4.13.4",
"description": "Nhost Apollo Client library",
"license": "MIT",
"keywords": [
@@ -68,6 +68,6 @@
},
"devDependencies": {
"@nhost/nhost-js": "workspace:*",
"@apollo/client": "^3.7.1"
"@apollo/client": "^3.7.3"
}
}

View File

@@ -25,6 +25,7 @@ export type NhostApolloClientOptions = {
connectToDevTools?: boolean
cache?: InMemoryCache
onError?: RequestHandler
link?: ApolloClient<any>['link']
}
export const createApolloClient = ({
@@ -35,7 +36,8 @@ export const createApolloClient = ({
fetchPolicy,
cache = new InMemoryCache(),
connectToDevTools = isBrowser && process.env.NODE_ENV === 'development',
onError
onError,
link: customLink
}: NhostApolloClientOptions): ApolloClient<any> => {
let backendUrl = graphqlUrl || nhost?.graphql.getUrl()
if (!backendUrl) {
@@ -122,7 +124,11 @@ export const createApolloClient = ({
}
// add link
apolloClientOptions.link = typeof onError === 'function' ? from([onError, link]) : from([link])
if (customLink) {
apolloClientOptions.link = from([customLink])
} else {
apolloClientOptions.link = typeof onError === 'function' ? from([onError, link]) : from([link])
}
const client = new ApolloClient(apolloClientOptions)

View File

@@ -1,5 +1,22 @@
# @nhost/react-apollo
## 4.13.5
### Patch Changes
- Updated dependencies [5013213b]
- Updated dependencies [bf1e4071]
- @nhost/react@1.13.5
- @nhost/apollo@4.13.4
## 4.13.4
### Patch Changes
- Updated dependencies [c38f6074]
- @nhost/apollo@4.13.3
- @nhost/react@1.13.4
## 4.13.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-apollo",
"version": "4.13.3",
"version": "4.13.5",
"description": "Nhost React Apollo client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,19 @@
# @nhost/react-urql
## 1.0.5
### Patch Changes
- Updated dependencies [5013213b]
- Updated dependencies [bf1e4071]
- @nhost/react@1.13.5
## 1.0.4
### Patch Changes
- @nhost/react@1.13.4
## 1.0.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react-urql",
"version": "1.0.3",
"version": "1.0.5",
"description": "Nhost React URQL client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/docgen
## 0.1.6
### Patch Changes
- 5013213b: fix(deps): update dependency commander to v10
## 0.1.5
### Patch Changes

View File

@@ -2,7 +2,7 @@
"name": "@nhost/docgen",
"description": "Documentation generator for classes and functions",
"private": true,
"version": "0.1.5",
"version": "0.1.6",
"license": "MIT",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts",
@@ -40,7 +40,7 @@
},
"dependencies": {
"chalk": "4",
"commander": "^9.4.0",
"commander": "^10.0.0",
"just-kebab-case": "^4.1.1",
"prettier": "^2.7.1",
"valtio": "^1.6.4"

View File

@@ -1,5 +1,17 @@
# @nhost/hasura-auth-js
## 1.12.4
### Patch Changes
- 5013213b: chore(deps): update dependency @nhost/docgen to 0.1.6
## 1.12.3
### Patch Changes
- 5880f0cd: chore(hasura-auth-js): bump `msw` version to `1.0.1`
## 1.12.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/hasura-auth-js",
"version": "1.12.2",
"version": "1.12.4",
"description": "Hasura-auth client",
"license": "MIT",
"keywords": [
@@ -74,7 +74,7 @@
"@types/js-cookie": "^3.0.2",
"cheerio": "1.0.0-rc.12",
"mailhog": "^4.16.0",
"msw": "^0.47.4",
"msw": "^1.0.1",
"start-server-and-test": "^1.15.2"
}
}

View File

@@ -1,5 +1,11 @@
# @nhost/hasura-storage-js
## 1.13.2
### Patch Changes
- 5013213b: chore(deps): update dependency @nhost/docgen to 0.1.6
## 1.13.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/hasura-storage-js",
"version": "1.13.1",
"version": "1.13.2",
"description": "Hasura-storage client",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,21 @@
# @nhost/nextjs
## 1.13.5
### Patch Changes
- bf1e4071: chore(deps): bump `react` to v18
- 5013213b: chore(deps): update dependency @nhost/docgen to 0.1.6
- Updated dependencies [5013213b]
- Updated dependencies [bf1e4071]
- @nhost/react@1.13.5
## 1.13.4
### Patch Changes
- @nhost/react@1.13.4
## 1.13.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nextjs",
"version": "1.13.3",
"version": "1.13.5",
"description": "Nhost NextJS library",
"license": "MIT",
"keywords": [
@@ -77,7 +77,7 @@
"@nhost/docgen": "workspace:*",
"@types/js-cookie": "^3.0.2",
"next": "12.3.4",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

View File

@@ -1,5 +1,21 @@
# @nhost/nhost-js
## 1.13.4
### Patch Changes
- 5013213b: chore(deps): update dependency @nhost/docgen to 0.1.6
- Updated dependencies [5013213b]
- @nhost/hasura-auth-js@1.12.4
- @nhost/hasura-storage-js@1.13.2
## 1.13.3
### Patch Changes
- Updated dependencies [5880f0cd]
- @nhost/hasura-auth-js@1.12.3
## 1.13.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/nhost-js",
"version": "1.13.2",
"version": "1.13.4",
"description": "Nhost JavaScript SDK",
"license": "MIT",
"keywords": [

View File

@@ -1,5 +1,20 @@
# @nhost/react
## 1.13.5
### Patch Changes
- 5013213b: chore(deps): update dependency @nhost/docgen to 0.1.6
- bf1e4071: chore(react): fix exports for `@nhost/nextjs`
- Updated dependencies [5013213b]
- @nhost/nhost-js@1.13.4
## 1.13.4
### Patch Changes
- @nhost/nhost-js@1.13.3
## 1.13.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/react",
"version": "1.13.3",
"version": "1.13.5",
"description": "Nhost React library",
"license": "MIT",
"keywords": [

View File

@@ -1,16 +1,10 @@
import {
AuthMachine,
BackendUrl,
NhostAuthConstructorParams,
NhostClient as _VanillaNhostClient,
NHOST_REFRESH_TOKEN_KEY,
Subdomain
} from '@nhost/nhost-js'
// * Required for @nhost/nextjs
export type { NhostAuthConstructorParams, AuthMachine }
export { NHOST_REFRESH_TOKEN_KEY }
/** @internal */
export const VanillaNhostClient = _VanillaNhostClient
@@ -24,3 +18,7 @@ export class NhostClient extends VanillaNhostClient {
super({ ...params, start: false })
}
}
// * Required for @nhost/nextjs
export { NHOST_REFRESH_TOKEN_KEY } from '@nhost/nhost-js'
export type { AuthMachine, NhostAuthConstructorParams } from '@nhost/nhost-js'

View File

@@ -33,7 +33,7 @@ export interface MultipleFilesHookResult extends MultipleFilesUploadState {
}
/**
* Use the hook `useFileUpload` to upload multiple files.
* Use the hook `useMultipleFilesUpload` to upload multiple files.
*
* @example
* ```tsx

View File

@@ -1,5 +1,19 @@
# @nhost/vue
## 1.13.5
### Patch Changes
- 5013213b: chore(deps): update dependency @nhost/docgen to 0.1.6
- Updated dependencies [5013213b]
- @nhost/nhost-js@1.13.4
## 1.13.4
### Patch Changes
- @nhost/nhost-js@1.13.3
## 1.13.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/vue",
"version": "1.13.3",
"version": "1.13.5",
"description": "Nhost Vue library",
"license": "MIT",
"keywords": [

2792
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff