Compare commits
67 Commits
@nhost/rea
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32a2fceae | ||
|
|
d690eb86bb | ||
|
|
d91271cce1 | ||
|
|
1e74a2da85 | ||
|
|
bc8837b961 | ||
|
|
78fdad8404 | ||
|
|
2e8a72d445 | ||
|
|
ce1ae32772 | ||
|
|
b51455d324 | ||
|
|
28a305d9be | ||
|
|
e23bf4500d | ||
|
|
d0457fe5c3 | ||
|
|
766d1e1c5a | ||
|
|
44d460cd01 | ||
|
|
adf934c871 | ||
|
|
0a963486e2 | ||
|
|
227d1704f2 | ||
|
|
2baef92988 | ||
|
|
2a10da128d | ||
|
|
62a51c9fc7 | ||
|
|
58977b173b | ||
|
|
b5e5dcf6de | ||
|
|
157e1b74b8 | ||
|
|
b3a475c60f | ||
|
|
3d62871db1 | ||
|
|
4f0368b95f | ||
|
|
0385093111 | ||
|
|
463cb50c27 | ||
|
|
a50174a0a1 | ||
|
|
21cbe7487e | ||
|
|
6e4b34126e | ||
|
|
fd3a1a44ef | ||
|
|
66e6021dc0 | ||
|
|
57fdba70e0 | ||
|
|
676c11f814 | ||
|
|
d8442a290b | ||
|
|
0db333353b | ||
|
|
7ea8120723 | ||
|
|
64a8f41d03 | ||
|
|
8e12ded94b | ||
|
|
564ce1ac2d | ||
|
|
b024817eb5 | ||
|
|
24f98630fd | ||
|
|
c1b024cf53 | ||
|
|
dbacbf140b | ||
|
|
eda9e57583 | ||
|
|
0a9af5075c | ||
|
|
f92d9d1fd2 | ||
|
|
15168539d8 | ||
|
|
0d74217a4c | ||
|
|
9721527324 | ||
|
|
fd4d024bfc | ||
|
|
c994c8f05b | ||
|
|
4c00a796eb | ||
|
|
2d3a77af76 | ||
|
|
ef05d69889 | ||
|
|
9b1d0f7a5b | ||
|
|
07abea4c16 | ||
|
|
8733961026 | ||
|
|
3dc97f17ae | ||
|
|
6d2963ffa7 | ||
|
|
d1ec8c0781 | ||
|
|
8b205e9c08 | ||
|
|
e2792cd453 | ||
|
|
8871267b91 | ||
|
|
e3001ba4a5 | ||
|
|
1133b76a7e |
@@ -14,7 +14,7 @@ runs:
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.6.0
|
||||
version: 8.5.1
|
||||
run_install: false
|
||||
- name: Get pnpm cache directory
|
||||
id: pnpm-cache-dir
|
||||
@@ -29,7 +29,7 @@ runs:
|
||||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- shell: bash
|
||||
name: Install packages
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.17.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- d0457fe5c: feat(settings): improve the dashboard and config parity
|
||||
- @nhost/react-apollo@5.0.26
|
||||
- @nhost/nextjs@1.13.28
|
||||
|
||||
## 0.17.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4f0368b95: fix(account): don't break account settings page
|
||||
|
||||
## 0.17.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 64a8f41d0: chore(resources): lower the maximum allowed resources per service
|
||||
|
||||
## 0.17.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.25
|
||||
- @nhost/nextjs@1.13.27
|
||||
|
||||
## 0.17.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9b1d0f7a5: fix(deployments): use correct timestamp for deployment details
|
||||
- 6d2963ffa: chore(deps): bump `@types/react` to `v18.2.8`
|
||||
- 8871267b9: chore(deps): downgrade `pnpm` to `v8.5.1` because of no Turborepo support
|
||||
|
||||
## 0.17.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -29,7 +29,7 @@ ENV NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL __NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL_
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL __NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__
|
||||
ENV NEXT_PUBLIC_NHOST_HASURA_API_URL __NEXT_PUBLIC_NHOST_HASURA_API_URL__
|
||||
|
||||
RUN yarn global add pnpm@8.6.0
|
||||
RUN yarn global add pnpm@8.5.1
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=pruner /app/out/json/ .
|
||||
COPY --from=pruner /app/out/pnpm-*.yaml .
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.17.3",
|
||||
"version": "0.17.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -104,7 +104,7 @@
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/react": "18.2.7",
|
||||
"@types/react": "18.2.8",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/react-table": "^7.7.12",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
|
||||
1
dashboard/public/assets/brands/bitbucket.svg
Normal file
1
dashboard/public/assets/brands/bitbucket.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#2684FF" fill-rule="evenodd" d="M3.41 4.393a.563.563 0 0 1 .434-.195l16.416.003a.562.562 0 0 1 .563.652l-2.388 14.66a.562.562 0 0 1-.563.472H6.417a.765.765 0 0 1-.748-.639L3.281 4.851a.562.562 0 0 1 .13-.458Zm6.832 10.282h3.656l.886-5.173H9.252l.99 5.173Z" clip-rule="evenodd"/><path fill="url(#a)" d="M20.063 9.502h-5.279l-.886 5.173h-3.656l-4.317 5.124a.762.762 0 0 0 .492.186h11.458a.562.562 0 0 0 .563-.473l1.625-10.01Z"/><defs><linearGradient id="a" x1="16.692" x2="10.594" y1="7.717" y2="16.375" gradientUnits="userSpaceOnUse"><stop offset=".18" stop-color="#0052CC"/><stop offset="1" stop-color="#2684FF"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 730 B |
1
dashboard/public/assets/brands/gitlab.svg
Normal file
1
dashboard/public/assets/brands/gitlab.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#E24329" fill-rule="evenodd" d="m12 19.996 3.223-9.917H8.777L12 19.995Z" clip-rule="evenodd"/><path fill="#FC6D26" fill-rule="evenodd" d="m12 19.996-3.223-9.917H4.261L12 19.996Z" clip-rule="evenodd"/><path fill="#FCA326" fill-rule="evenodd" d="m4.262 10.079-.98 3.013a.667.667 0 0 0 .243.746L12 19.996l-7.738-9.917Z" clip-rule="evenodd"/><path fill="#E24329" fill-rule="evenodd" d="M4.261 10.079h4.517L6.837 4.106a.333.333 0 0 0-.635 0l-1.94 5.973Z" clip-rule="evenodd"/><path fill="#FC6D26" fill-rule="evenodd" d="m12 19.996 3.222-9.917h4.516L12 19.996Z" clip-rule="evenodd"/><path fill="#FCA326" fill-rule="evenodd" d="m19.738 10.079.98 3.013a.667.667 0 0 1-.243.746L12 19.996l7.738-9.917Z" clip-rule="evenodd"/><path fill="#E24329" fill-rule="evenodd" d="M19.739 10.079h-4.517l1.941-5.973a.334.334 0 0 1 .635 0l1.94 5.973Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 941 B |
1
dashboard/public/assets/brands/strava.svg
Normal file
1
dashboard/public/assets/brands/strava.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#FE7203" d="M4.125 5.25c0-.621.504-1.125 1.125-1.125h13.5c.621 0 1.125.504 1.125 1.125v13.5c0 .621-.504 1.125-1.125 1.125H5.25a1.125 1.125 0 0 1-1.125-1.125V5.25Z"/><path fill="url(#a)" d="M4.125 5.25c0-.621.504-1.125 1.125-1.125h13.5c.621 0 1.125.504 1.125 1.125v13.5c0 .621-.504 1.125-1.125 1.125H5.25a1.125 1.125 0 0 1-1.125-1.125V5.25Z"/><path fill="#fff" fill-rule="evenodd" d="m10.917 12.787 2.461 4.43 2.363-4.43h-1.477l-.886 1.674-.984-1.674h-1.477Z" clip-rule="evenodd" opacity=".6"/><path fill="#fff" fill-rule="evenodd" d="m11.213 6.586 3.051 6.201H8.063l3.15-6.201Zm0 3.74 1.18 2.461h-2.46l1.28-2.46Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="12" x2="12" y1="4.125" y2="19.875" gradientUnits="userSpaceOnUse"><stop stop-color="#FB2F01" stop-opacity="0"/><stop offset="1" stop-color="#FB2F01"/></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 935 B |
@@ -1,25 +1 @@
|
||||
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H64C68.4183 0 72 3.58172 72 8V64C72 68.4183 68.4183 72 64 72H8C3.58172 72 0 68.4183 0 64V8Z" fill="#1EB4D4"/>
|
||||
<path d="M0 8C0 3.58172 3.58172 0 8 0H64C68.4183 0 72 3.58172 72 8V64C72 68.4183 68.4183 72 64 72H8C3.58172 72 0 68.4183 0 64V8Z" fill="url(#paint0_linear_1_85)" fill-opacity="0.2"/>
|
||||
<g filter="url(#filter0_d_1_85)">
|
||||
<circle cx="36" cy="39" r="16" fill="#35BCD8"/>
|
||||
</g>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.3994 17.2212C52.831 21.3475 52.831 26.737 51.7362 29.3896C51.1889 30.6948 51.1467 32.2106 51.5678 33.558C52.1152 35.2001 52.4099 36.9264 52.4099 38.7791C52.4099 48.0001 44.9573 55.3685 35.7783 55.2001C27.0625 55.0738 19.6941 47.6633 19.4836 38.9896C19.4415 37.0948 19.7362 35.2001 20.3257 33.5159C20.7889 32.1685 20.7889 30.6948 20.2415 29.3896C19.1889 26.7791 19.1467 21.3896 20.5783 17.1791C20.9152 16.4212 22.0941 16.6738 22.0941 17.4738V17.7685C22.3467 21.7685 23.8625 24.1685 26.052 24.9685C26.3889 25.137 26.8099 25.0948 27.1467 24.8843C29.7152 23.3264 32.7046 22.358 35.9467 22.358C39.1889 22.358 42.2204 23.2843 44.7467 24.8843C45.1257 25.137 45.6731 25.137 46.0099 24.9685C48.1573 23.9159 49.5889 21.7685 49.8415 17.8106V17.5159C49.8836 16.7159 51.0204 16.4633 51.3994 17.2212ZM36.1994 26.2738C29.1257 26.1054 23.3152 31.9159 23.4836 39.0317C23.5678 45.7264 29.0836 51.158 35.7362 51.3264C42.852 51.4106 48.6204 45.6422 48.4941 38.5685C48.3678 31.8738 42.8941 26.358 36.1994 26.2738ZM34.9362 32.8844L37.8836 37.4318L40.7468 41.9792C40.9152 42.2318 40.9994 42.5265 40.9994 42.8213C40.9994 43.3686 40.7047 43.8739 40.2415 44.1686C39.6941 44.5055 38.9783 44.5055 38.431 44.1265C38.2204 44.0002 38.052 43.8739 37.9678 43.6634L36.3257 41.3055C36.1994 41.0528 35.9047 41.0528 35.6941 41.2634L33.3783 43.9581C33.0836 44.2528 32.7047 44.4634 32.2415 44.4634C31.8626 44.4634 31.4415 44.3371 31.1468 44.0844C30.5152 43.495 30.4731 42.4844 31.0626 41.8528L34.1783 38.4423C34.3047 38.2318 34.3889 37.9371 34.2204 37.6844L32.2415 34.5686C32.0731 34.316 31.9889 34.0213 31.9889 33.7265C31.9889 33.1792 32.2836 32.6739 32.7468 32.3792C33.5047 31.916 34.4731 32.1265 34.9362 32.8844Z" fill="white"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_1_85" x="17" y="23" width="38" height="41" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="4" operator="erode" in="SourceAlpha" result="effect1_dropShadow_1_85"/>
|
||||
<feOffset dy="6"/>
|
||||
<feGaussianBlur stdDeviation="3.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.231373 0 0 0 0 0.278431 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1_85"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1_85" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1_85" x1="0" y1="0" x2="72" y2="72" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" fill="none"><path fill="#1EB4D4" d="M0 8a8 8 0 0 1 8-8h56a8 8 0 0 1 8 8v56a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8Z"/><path fill="url(#a)" fill-opacity=".2" d="M0 8a8 8 0 0 1 8-8h56a8 8 0 0 1 8 8v56a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8Z"/><g filter="url(#b)"><circle cx="36" cy="39" r="16" fill="#35BCD8"/></g><path fill="#fff" fill-rule="evenodd" d="M51.4 17.221c1.431 4.127 1.431 9.516.336 12.169-.547 1.305-.59 2.82-.168 4.168.547 1.642.842 3.368.842 5.221 0 9.221-7.453 16.59-16.632 16.421-8.715-.126-16.084-7.537-16.294-16.21-.043-1.895.252-3.79.842-5.474.463-1.347.463-2.821-.085-4.126-1.052-2.61-1.094-8 .337-12.21.337-.759 1.516-.506 1.516.294v.294c.253 4 1.768 6.4 3.958 7.2.337.169.758.127 1.095-.084 2.568-1.558 5.558-2.526 8.8-2.526 3.242 0 6.273.926 8.8 2.526.379.253.926.253 1.263.084 2.147-1.052 3.579-3.2 3.832-7.157v-.295c.042-.8 1.178-1.053 1.557-.295Zm-15.2 9.053c-7.074-.169-12.885 5.642-12.716 12.758.084 6.694 5.6 12.126 12.252 12.294 7.116.085 12.884-5.684 12.758-12.758-.126-6.694-5.6-12.21-12.295-12.294Zm-1.264 6.61 2.948 4.548 2.863 4.547c.168.253.252.547.252.842 0 .548-.294 1.053-.758 1.348a1.662 1.662 0 0 1-1.81-.042c-.21-.127-.379-.253-.463-.464l-1.642-2.357c-.127-.253-.421-.253-.632-.043l-2.316 2.695c-.294.295-.673.505-1.136.505-.38 0-.8-.126-1.095-.379a1.59 1.59 0 0 1-.084-2.231l3.115-3.41c.127-.211.21-.506.042-.759l-1.978-3.115a1.518 1.518 0 0 1-.253-.843c0-.547.295-1.052.758-1.347.758-.463 1.726-.252 2.19.505Z" clip-rule="evenodd"/><defs><linearGradient id="a" x1="0" x2="72" y1="0" y2="72" gradientUnits="userSpaceOnUse"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient><filter id="b" width="38" height="41" x="17" y="23" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feMorphology in="SourceAlpha" radius="4" result="effect1_dropShadow_1_85"/><feOffset dy="6"/><feGaussianBlur stdDeviation="3.5"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix values="0 0 0 0 0 0 0 0 0 0.231373 0 0 0 0 0.278431 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" result="effect1_dropShadow_1_85"/><feBlend in="SourceGraphic" in2="effect1_dropShadow_1_85" result="shape"/></filter></defs></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -1,5 +1,6 @@
|
||||
import { Chip } from '@/components/ui/v2/Chip';
|
||||
import type { FormControlProps } from '@/components/ui/v2/FormControl';
|
||||
import { CheckIcon } from '@/components/ui/v2/icons/CheckIcon';
|
||||
import { ChevronDownIcon } from '@/components/ui/v2/icons/ChevronDownIcon';
|
||||
import { XIcon } from '@/components/ui/v2/icons/XIcon';
|
||||
import type { InputProps } from '@/components/ui/v2/Input';
|
||||
@@ -134,6 +135,13 @@ const StyledAutocomplete = styled(MaterialAutocomplete)(({ theme }) => ({
|
||||
MaterialAutocompleteProps<AutocompleteOption, boolean, boolean, boolean>
|
||||
>;
|
||||
|
||||
const StyledOptionBase = styled(OptionBase)(({ theme }) => ({
|
||||
display: 'grid !important',
|
||||
gridAutoFlow: 'column',
|
||||
justifyContent: 'space-between !important',
|
||||
gap: theme.spacing(0.5),
|
||||
}));
|
||||
|
||||
export const AutocompletePopper = styled(PopperUnstyled)(({ theme }) => ({
|
||||
zIndex: theme.zIndex.modal + 1,
|
||||
boxShadow: 'none',
|
||||
@@ -326,6 +334,7 @@ function Autocomplete(
|
||||
<StyledTag
|
||||
deleteIcon={<XIcon />}
|
||||
size="small"
|
||||
sx={{ fontSize: (theme) => theme.typography.pxToRem(12) }}
|
||||
label={
|
||||
typeof option !== 'object' ? option.toString() : option.value
|
||||
}
|
||||
@@ -349,17 +358,32 @@ function Autocomplete(
|
||||
optionProps,
|
||||
option: string | number | AutocompleteOption<string>,
|
||||
) => {
|
||||
const selected = optionProps['aria-selected'];
|
||||
|
||||
if (typeof option !== 'object') {
|
||||
return <OptionBase {...optionProps}>{option.toString()}</OptionBase>;
|
||||
return (
|
||||
<StyledOptionBase {...optionProps} key={option.toString()}>
|
||||
{option.toString()}
|
||||
{selected && props.multiple && (
|
||||
<CheckIcon sx={{ width: 16, height: 16 }} />
|
||||
)}
|
||||
</StyledOptionBase>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OptionBase
|
||||
<StyledOptionBase
|
||||
{...optionProps}
|
||||
key={option.dropdownLabel || option.label}
|
||||
>
|
||||
{option.dropdownLabel || option.label}
|
||||
</OptionBase>
|
||||
<>
|
||||
<span>{option.dropdownLabel || option.label}</span>
|
||||
|
||||
{selected && props.multiple && (
|
||||
<CheckIcon key="asd" sx={{ width: 16, height: 16 }} />
|
||||
)}
|
||||
</>
|
||||
</StyledOptionBase>
|
||||
);
|
||||
}}
|
||||
filterOptions={
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
query GetPersonalAccessTokens {
|
||||
personalAccessTokens: authRefreshTokens(
|
||||
where: { type: { _eq: "pat" } }
|
||||
where: { type: { _eq: pat } }
|
||||
order_by: { expiresAt: asc }
|
||||
) {
|
||||
id
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function AppleProviderSettings() {
|
||||
const { register, formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (values: AppleProviderFormValues) => {
|
||||
async function handleSubmit(formValues: AppleProviderFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -107,7 +107,7 @@ export default function AppleProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
apple: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -130,15 +130,15 @@ export default function AppleProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Apple"
|
||||
description="Allow users to sign in with Apple."
|
||||
@@ -214,7 +214,7 @@ export default function AppleProviderSettings() {
|
||||
/>
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="apple-redirectUrl"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function AzureADProviderSettings() {
|
||||
const { register, formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (values: AzureADProviderFormValues) => {
|
||||
async function handleSubmit(formValues: AzureADProviderFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -98,7 +98,7 @@ export default function AzureADProviderSettings() {
|
||||
auth: {
|
||||
method: {
|
||||
oauth: {
|
||||
azuread: values,
|
||||
azuread: formValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -119,15 +119,15 @@ export default function AzureADProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Azure AD"
|
||||
description="Allow users to sign in with Azure AD."
|
||||
@@ -160,7 +160,7 @@ export default function AzureADProviderSettings() {
|
||||
/>
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="azuerad-redirectUrl"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import type { BaseProviderSettingsFormValues } from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
BaseProviderSettings,
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function BitbucketProviderSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { clientId, clientSecret, enabled } =
|
||||
data?.config?.auth?.method?.oauth?.bitbucket || {};
|
||||
|
||||
const form = useForm<BaseProviderSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
clientId: clientId || '',
|
||||
clientSecret: clientSecret || '',
|
||||
enabled: enabled || false,
|
||||
},
|
||||
resolver: yupResolver(baseProviderValidationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading settings for Bitbucket..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
oauth: {
|
||||
bitbucket: formValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Bitbucket settings are being updated...`,
|
||||
success: `Bitbucket settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update the project's Bitbucket settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Bitbucket"
|
||||
description="Allow users to sign in with Bitbucket."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
icon="/assets/brands/bitbucket.svg"
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
className={twMerge(
|
||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||
!authEnabled && 'hidden',
|
||||
)}
|
||||
>
|
||||
<BaseProviderSettings providerName="bitbucket" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="bitbucket-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
'auth',
|
||||
)}/signin/provider/bitbucket/callback`}
|
||||
disabled
|
||||
endAdornment={
|
||||
<InputAdornment position="end" className="absolute right-2">
|
||||
<IconButton
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
color="secondary"
|
||||
variant="borderless"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
'auth',
|
||||
)}/signin/provider/bitbucket/callback`,
|
||||
'Redirect URL',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BitbucketProviderSettings } from './BitbucketProviderSettings';
|
||||
@@ -68,9 +68,7 @@ export default function DiscordProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject?.id,
|
||||
@@ -79,7 +77,7 @@ export default function DiscordProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
discord: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -102,15 +100,15 @@ export default function DiscordProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Discord"
|
||||
description="Allow users to sign in with Discord."
|
||||
@@ -133,7 +131,7 @@ export default function DiscordProviderSettings() {
|
||||
<BaseProviderSettings providerName="discord" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="discord-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
@@ -20,6 +21,11 @@ import * as Yup from 'yup';
|
||||
const validationSchema = Yup.object({
|
||||
emailVerificationRequired: Yup.boolean(),
|
||||
hibpEnabled: Yup.boolean(),
|
||||
passwordMinLength: Yup.number()
|
||||
.label('Minimum password length')
|
||||
.min(3)
|
||||
.typeError('Minimum password length must be a number')
|
||||
.required(),
|
||||
});
|
||||
|
||||
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
@@ -36,7 +42,7 @@ export default function EmailAndPasswordSettings() {
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { hibpEnabled, emailVerificationRequired } =
|
||||
const { hibpEnabled, emailVerificationRequired, passwordMinLength } =
|
||||
data?.config?.auth?.method?.emailPassword || {};
|
||||
|
||||
const form = useForm<EmailAndPasswordFormValues>({
|
||||
@@ -44,6 +50,7 @@ export default function EmailAndPasswordSettings() {
|
||||
defaultValues: {
|
||||
hibpEnabled: hibpEnabled || false,
|
||||
emailVerificationRequired: emailVerificationRequired || false,
|
||||
passwordMinLength: passwordMinLength || 9,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -62,18 +69,16 @@ export default function EmailAndPasswordSettings() {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState } = form;
|
||||
const { formState, register } = form;
|
||||
|
||||
const handleEmailAndPasswordSettingsChange = async (
|
||||
values: EmailAndPasswordFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: EmailAndPasswordFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
emailPassword: values,
|
||||
emailPassword: formValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -93,15 +98,15 @@ export default function EmailAndPasswordSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleEmailAndPasswordSettingsChange}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Email and Password"
|
||||
description="Allow users to sign in with email and password."
|
||||
@@ -118,6 +123,19 @@ export default function EmailAndPasswordSettings() {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
{...register('passwordMinLength')}
|
||||
id="passwordMinLength"
|
||||
name="passwordMinLength"
|
||||
type="number"
|
||||
label="Minimum required password length"
|
||||
fullWidth
|
||||
className="lg:max-w-[50%]"
|
||||
error={Boolean(formState.errors.passwordMinLength?.message)}
|
||||
helperText={formState.errors.passwordMinLength?.message}
|
||||
slotProps={{ inputRoot: { min: 3 } }}
|
||||
/>
|
||||
|
||||
<ControlledCheckbox
|
||||
name="emailVerificationRequired"
|
||||
id="emailVerificationRequired"
|
||||
|
||||
@@ -68,9 +68,7 @@ export default function FacebookProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -79,7 +77,7 @@ export default function FacebookProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
facebook: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -102,15 +100,15 @@ export default function FacebookProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Facebook"
|
||||
description="Allow users to sign in with Facebook."
|
||||
@@ -133,7 +131,7 @@ export default function FacebookProviderSettings() {
|
||||
<BaseProviderSettings providerName="facebook" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="facebook-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -70,9 +70,7 @@ export default function GitHubProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -81,7 +79,7 @@ export default function GitHubProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
github: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -104,15 +102,15 @@ export default function GitHubProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="GitHub"
|
||||
description="Allow users to sign in with GitHub."
|
||||
@@ -139,7 +137,7 @@ export default function GitHubProviderSettings() {
|
||||
<BaseProviderSettings providerName="github" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="github-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import type { BaseProviderSettingsFormValues } from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
BaseProviderSettings,
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function GitLabProviderSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { clientId, clientSecret, enabled } =
|
||||
data?.config?.auth?.method?.oauth?.gitlab || {};
|
||||
|
||||
const form = useForm<BaseProviderSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
clientId: clientId || '',
|
||||
clientSecret: clientSecret || '',
|
||||
enabled: enabled || false,
|
||||
},
|
||||
resolver: yupResolver(baseProviderValidationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading settings for GitLab..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
async function handleSubmit(values: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
oauth: {
|
||||
gitlab: {
|
||||
...values,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `GitLab settings are being updated...`,
|
||||
success: `GitLab settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update the project's GitLab settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="GitLab"
|
||||
description="Allow users to sign in with GitLab."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
icon="/assets/brands/gitlab.svg"
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
className={twMerge(
|
||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||
!authEnabled && 'hidden',
|
||||
)}
|
||||
>
|
||||
<BaseProviderSettings providerName="gitlab" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="gitlab-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
'auth',
|
||||
)}/signin/provider/gitlab/callback`}
|
||||
disabled
|
||||
endAdornment={
|
||||
<InputAdornment position="end" className="absolute right-2">
|
||||
<IconButton
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
color="secondary"
|
||||
variant="borderless"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
'auth',
|
||||
)}/signin/provider/gitlab/callback`,
|
||||
'Redirect URL',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as GitLabProviderSettings } from './GitLabProviderSettings';
|
||||
@@ -68,9 +68,7 @@ export default function GoogleProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -79,7 +77,7 @@ export default function GoogleProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
google: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -102,15 +100,15 @@ export default function GoogleProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Google"
|
||||
description="Allow users to sign in with Google."
|
||||
@@ -133,7 +131,7 @@ export default function GoogleProviderSettings() {
|
||||
<BaseProviderSettings providerName="google" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="google-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -68,9 +68,7 @@ export default function LinkedInProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -79,7 +77,7 @@ export default function LinkedInProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
linkedin: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -102,15 +100,15 @@ export default function LinkedInProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="LinkedIn"
|
||||
description="Allow users to sign in with LinkedIn."
|
||||
@@ -133,7 +131,7 @@ export default function LinkedInProviderSettings() {
|
||||
<BaseProviderSettings providerName="linkedin" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="linkedin-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
accessTokenExpiresIn: Yup.number()
|
||||
.label('Access token expiration')
|
||||
.typeError('Access token expiration must be a number')
|
||||
.required(),
|
||||
refreshTokenExpiresIn: Yup.number()
|
||||
.label('Refresh token expiration')
|
||||
.typeError('Refresh token expiration must be a number')
|
||||
.required(),
|
||||
});
|
||||
|
||||
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function SessionSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { accessToken, refreshToken } = data?.config?.auth?.session || {};
|
||||
|
||||
const form = useForm<SessionFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
accessTokenExpiresIn: accessToken?.expiresIn || 900,
|
||||
refreshTokenExpiresIn: refreshToken?.expiresIn || 43200,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading session settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { register, formState } = form;
|
||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||
|
||||
const handleSessionSettingsChange = async (formValues: SessionFormValues) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
auth: {
|
||||
session: {
|
||||
accessToken: { expiresIn: formValues.accessTokenExpiresIn },
|
||||
refreshToken: { expiresIn: formValues.refreshTokenExpiresIn },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Session settings are being updated...`,
|
||||
success: `Session settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update the project's session settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSessionSettingsChange}>
|
||||
<SettingsContainer
|
||||
title="Session"
|
||||
description="Change the expiration time of the access and refresh tokens."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-cols-5 grid-rows-2 gap-y-6"
|
||||
>
|
||||
<Input
|
||||
{...register('accessTokenExpiresIn')}
|
||||
id="accessTokenExpiresIn"
|
||||
type="number"
|
||||
label="Access Token Expires In (Seconds)"
|
||||
fullWidth
|
||||
className="col-span-5 lg:col-span-2"
|
||||
error={Boolean(formState.errors.accessTokenExpiresIn?.message)}
|
||||
helperText={formState.errors.accessTokenExpiresIn?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('refreshTokenExpiresIn')}
|
||||
id="refreshTokenExpiresIn"
|
||||
type="number"
|
||||
label="Refresh Token Expires In (Seconds)"
|
||||
fullWidth
|
||||
className="col-span-5 row-start-2 lg:col-span-2"
|
||||
error={Boolean(formState.errors.refreshTokenExpiresIn?.message)}
|
||||
helperText={formState.errors.refreshTokenExpiresIn?.message}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './SessionSettings';
|
||||
export { default as SessionSettings } from './SessionSettings';
|
||||
@@ -68,9 +68,7 @@ export default function SpotifyProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -79,7 +77,7 @@ export default function SpotifyProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
spotify: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -102,15 +100,15 @@ export default function SpotifyProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Spotify"
|
||||
description="Allow users to sign in with Spotify."
|
||||
@@ -133,7 +131,7 @@ export default function SpotifyProviderSettings() {
|
||||
<BaseProviderSettings providerName="spotify" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="spotify-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import type { BaseProviderSettingsFormValues } from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
BaseProviderSettings,
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function StravaProviderSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { clientId, clientSecret, enabled } =
|
||||
data?.config?.auth?.method?.oauth?.strava || {};
|
||||
|
||||
const form = useForm<BaseProviderSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
clientId: clientId || '',
|
||||
clientSecret: clientSecret || '',
|
||||
enabled: enabled || false,
|
||||
},
|
||||
resolver: yupResolver(baseProviderValidationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading settings for Strava..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
async function handleSubmit(values: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
oauth: {
|
||||
strava: {
|
||||
...values,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Strava settings are being updated...`,
|
||||
success: `Strava settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update the project's Strava settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Strava"
|
||||
description="Allow users to sign in with Strava."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !formState.isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
icon="/assets/brands/strava.svg"
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
className={twMerge(
|
||||
'grid-flow-rows grid grid-cols-2 grid-rows-2 gap-y-4 gap-x-3 px-4 py-2',
|
||||
!authEnabled && 'hidden',
|
||||
)}
|
||||
>
|
||||
<BaseProviderSettings providerName="strava" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="strava-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
'auth',
|
||||
)}/signin/provider/strava/callback`}
|
||||
disabled
|
||||
endAdornment={
|
||||
<InputAdornment position="end" className="absolute right-2">
|
||||
<IconButton
|
||||
sx={{ minWidth: 0, padding: 0 }}
|
||||
color="secondary"
|
||||
variant="borderless"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
'auth',
|
||||
)}/signin/provider/strava/callback`,
|
||||
'Redirect URL',
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as StravaProviderSettings } from './StravaProviderSettings';
|
||||
@@ -70,9 +70,7 @@ export default function TwitchProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -81,7 +79,7 @@ export default function TwitchProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
twitch: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -104,15 +102,15 @@ export default function TwitchProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Twitch"
|
||||
description="Allow users to sign in with Twitch."
|
||||
@@ -139,7 +137,7 @@ export default function TwitchProviderSettings() {
|
||||
<BaseProviderSettings providerName="twitch" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="twitch-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function TwitterProviderSettings() {
|
||||
const { register, formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (values: TwitterProviderFormValues) => {
|
||||
async function handleSubmit(formValues: TwitterProviderFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -90,7 +90,7 @@ export default function TwitterProviderSettings() {
|
||||
auth: {
|
||||
method: {
|
||||
oauth: {
|
||||
twitter: values,
|
||||
twitter: formValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -111,15 +111,15 @@ export default function TwitterProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Twitter"
|
||||
description="Allow users to sign in with Twitter."
|
||||
@@ -164,7 +164,7 @@ export default function TwitterProviderSettings() {
|
||||
/>
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="twitter-redirectUrl"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
|
||||
@@ -68,9 +68,7 @@ export default function WindowsLiveProviderSettings() {
|
||||
const { formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (
|
||||
values: BaseProviderSettingsFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -79,7 +77,7 @@ export default function WindowsLiveProviderSettings() {
|
||||
method: {
|
||||
oauth: {
|
||||
windowslive: {
|
||||
...values,
|
||||
...formValues,
|
||||
scope: [],
|
||||
},
|
||||
},
|
||||
@@ -102,15 +100,15 @@ export default function WindowsLiveProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Windows Live"
|
||||
description="Allow users to sign in with Windows Live."
|
||||
@@ -132,7 +130,7 @@ export default function WindowsLiveProviderSettings() {
|
||||
<BaseProviderSettings providerName="windowslive" />
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="windowslive-redirectUrl"
|
||||
className="col-span-2"
|
||||
fullWidth
|
||||
hideEmptyHelperText
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function WorkOsProviderSettings() {
|
||||
const { register, formState, watch } = form;
|
||||
const authEnabled = watch('enabled');
|
||||
|
||||
const handleProviderUpdate = async (values: WorkOsProviderFormValues) => {
|
||||
async function handleSubmit(formValues: WorkOsProviderFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -105,7 +105,7 @@ export default function WorkOsProviderSettings() {
|
||||
auth: {
|
||||
method: {
|
||||
oauth: {
|
||||
workos: values,
|
||||
workos: formValues,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -126,15 +126,15 @@ export default function WorkOsProviderSettings() {
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(values);
|
||||
form.reset(formValues);
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleProviderUpdate}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="WorkOS"
|
||||
description="Allow users to sign in with WorkOS."
|
||||
@@ -181,7 +181,7 @@ export default function WorkOsProviderSettings() {
|
||||
/>
|
||||
<Input
|
||||
name="redirectUrl"
|
||||
id="redirectUrl"
|
||||
id="workos-redirectUrl"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
currentProject.subdomain,
|
||||
currentProject.region,
|
||||
|
||||
@@ -16,6 +16,14 @@ query GetAuthenticationSettings($appId: uuid!) {
|
||||
signUp {
|
||||
enabled
|
||||
}
|
||||
session {
|
||||
accessToken {
|
||||
expiresIn
|
||||
}
|
||||
refreshToken {
|
||||
expiresIn
|
||||
}
|
||||
}
|
||||
user {
|
||||
email {
|
||||
allowed
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
generateAppServiceUrl,
|
||||
} from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { getHasuraConsoleServiceUrl } from '@/utils/env';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface HasuraConnectionInfoProps {
|
||||
close?: () => void;
|
||||
}
|
||||
|
||||
export default function HasuraConnectionInfo({
|
||||
close,
|
||||
}: HasuraConnectionInfoProps) {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const projectAdminSecret = currentProject?.config?.hasura.adminSecret;
|
||||
|
||||
if (!currentProject?.subdomain || !projectAdminSecret) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${getHasuraConsoleServiceUrl()}`
|
||||
: generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'hasura',
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
src="/assets/hasuramodal.svg"
|
||||
width={72}
|
||||
height={72}
|
||||
alt="Hasura"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text variant="h3" component="h1" className="text-center">
|
||||
Open Hasura
|
||||
</Text>
|
||||
|
||||
<Text className="text-center">
|
||||
Hasura is the dashboard you'll use to edit your schema and
|
||||
permissions as well as browse data. Copy the admin secret to your
|
||||
clipboard and enter it in the next screen.
|
||||
</Text>
|
||||
|
||||
<Box className="mt-6 border-y-1">
|
||||
<div className="grid w-full grid-cols-1 place-content-between items-center py-2 sm:grid-cols-3">
|
||||
<Text className="col-span-1 text-center font-medium sm:justify-start sm:text-left">
|
||||
Admin Secret
|
||||
</Text>
|
||||
|
||||
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
||||
<Text className="font-medium" variant="subtitle2">
|
||||
{Array(projectAdminSecret.length).fill('•').join('')}
|
||||
</Text>
|
||||
|
||||
<IconButton
|
||||
onClick={() => copy(projectAdminSecret, 'Hasura admin secret')}
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="min-w-0 p-1"
|
||||
aria-label="Copy admin secret"
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button
|
||||
href={hasuraUrl}
|
||||
// Both `target` and `rel` are available when `href` is set. This is
|
||||
// a limitation of MUI.
|
||||
// @ts-ignore
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
||||
>
|
||||
Open Hasura
|
||||
</Button>
|
||||
|
||||
{close && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
className="text-sm+ font-normal"
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as HasuraConnectionInfo } from './HasuraConnectionInfo';
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type HasuraAllowListFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraAllowListSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { enableAllowList } = data?.config?.hasura.settings || {};
|
||||
|
||||
const form = useForm<HasuraAllowListFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: enableAllowList,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading allow list settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: HasuraAllowListFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
settings: {
|
||||
enableAllowList: formValues.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Allow list settings are being updated...`,
|
||||
success: `Allow list settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update allow list settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Allow List"
|
||||
description="Safely allow a limited number of GraphQL queries, mutations and subscriptions for your project."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling Allow Lists"
|
||||
docsLink="https://hasura.io/learn/graphql/hasura-advanced/security/3-allow-list/"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraAllowListSettings';
|
||||
export { default as HasuraAllowListSettings } from './HasuraAllowListSettings';
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type HasuraConsoleFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraConsoleSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { enableConsole } = data?.config?.hasura.settings || {};
|
||||
|
||||
const form = useForm<HasuraConsoleFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: enableConsole,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading Hasura Console settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: HasuraConsoleFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
settings: {
|
||||
enableConsole: formValues.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Hasura Console settings are being updated...`,
|
||||
success: `Hasura Console settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update Hasura Console settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Hasura Console"
|
||||
description="Enable or disable the Hasura Console. This will enable or disable the Hasura Console on the dashboard as well."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling the Hasura Console"
|
||||
docsLink="https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/#enable-console"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraConsoleSettings';
|
||||
export { default as HasuraConsoleSettings } from './HasuraConsoleSettings';
|
||||
@@ -0,0 +1,77 @@
|
||||
import { render, screen, waitFor } from '@/tests/testUtils';
|
||||
import { graphql } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { beforeAll, expect, test } from 'vitest';
|
||||
import HasuraCorsDomainSettings from './HasuraCorsDomainSettings';
|
||||
|
||||
const server = setupServer(
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
hasura: {
|
||||
version: 'v2.25.1-ce',
|
||||
settings: {
|
||||
corsDomain: ['*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
test('should not enable switch by default when CORS domain is set to *', async () => {
|
||||
render(<HasuraCorsDomainSettings />);
|
||||
|
||||
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole('checkbox')).not.toBeChecked();
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should enable switch by default when CORS domain is set to one or more domains', async () => {
|
||||
server.use(
|
||||
graphql.query('GetHasuraSettings', (_req, res, ctx) =>
|
||||
res(
|
||||
ctx.data({
|
||||
config: {
|
||||
id: 'HasuraSettings',
|
||||
__typename: 'HasuraSettings',
|
||||
hasura: {
|
||||
version: 'v2.25.1-ce',
|
||||
settings: {
|
||||
corsDomain: ['https://example.com', 'https://*.example.com'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
render(<HasuraCorsDomainSettings />);
|
||||
|
||||
expect(await screen.findByText(/configure cors/i)).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(screen.getByRole('checkbox')).toBeChecked());
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toHaveValue(
|
||||
'https://example.com, https://*.example.com',
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean().label('Enabled'),
|
||||
corsDomain: Yup.string()
|
||||
.label('Allowed CORS domains')
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
then: (schema) => schema.required(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type HasuraCorsDomainFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraCorsDomainSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { corsDomain } = data?.config?.hasura.settings || {};
|
||||
|
||||
const form = useForm<HasuraCorsDomainFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
values: {
|
||||
enabled:
|
||||
corsDomain && corsDomain.length === 1
|
||||
? corsDomain[0] !== '*'
|
||||
: !!corsDomain?.length,
|
||||
corsDomain:
|
||||
corsDomain && corsDomain.length === 1 && corsDomain[0] !== '*'
|
||||
? corsDomain[0]
|
||||
: corsDomain?.join(', ') || '',
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const { register, formState, watch } = form;
|
||||
const enabled = watch('enabled');
|
||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading CORS domain settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: HasuraCorsDomainFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
settings: {
|
||||
corsDomain: formValues.enabled
|
||||
? formValues.corsDomain
|
||||
.split(',')
|
||||
.map((domain) => domain.trim())
|
||||
: ['*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `CORS domain settings are being updated...`,
|
||||
success: `CORS domain settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update the project's CORS domain settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Configure CORS"
|
||||
description="Allow requests from specific domains to access your GraphQL API. Disable this setting to allow requests from all domains."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
showSwitch
|
||||
docsTitle="CORS configuration"
|
||||
docsLink="https://hasura.io/docs/latest/deployment/graphql-engine-flags/config-examples/#configure-cors"
|
||||
className={twMerge(
|
||||
'grid grid-cols-5 gap-4 px-4',
|
||||
!enabled && 'hidden',
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
{...register('corsDomain')}
|
||||
label="Allowed CORS domains"
|
||||
placeholder="https://example.com, https://*.example.com"
|
||||
id="corsDomain"
|
||||
fullWidth
|
||||
className="col-span-5 lg:col-span-2"
|
||||
error={Boolean(formState.errors.corsDomain)}
|
||||
aria-hidden={!enabled}
|
||||
helperText={formState.errors.corsDomain?.message}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraCorsDomainSettings';
|
||||
export { default as HasuraCorsDomainSettings } from './HasuraCorsDomainSettings';
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type HasuraDevModeFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraDevModeSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { devMode } = data?.config?.hasura.settings || {};
|
||||
|
||||
const form = useForm<HasuraDevModeFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: devMode,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading Dev Mode settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: HasuraDevModeFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
settings: {
|
||||
enableConsole: formValues.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Dev Mode settings are being updated...`,
|
||||
success: `Dev Mode settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update Dev Mode settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Dev Mode"
|
||||
description="Enable or disable Dev Mode."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling Dev Mode"
|
||||
docsLink="https://hasura.io/learn/graphql/hasura-advanced/debugging/1-dev-mode/"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraDevModeSettings';
|
||||
export { default as HasuraDevModeSettings } from './HasuraDevModeSettings';
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabledAPIs: Yup.array(
|
||||
Yup.object({
|
||||
label: Yup.string().required(),
|
||||
value: Yup.string().required(),
|
||||
}),
|
||||
)
|
||||
.label('Enabled Hasura APIs')
|
||||
.required(),
|
||||
});
|
||||
|
||||
export type HasuraEnabledAPIFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
const AVAILABLE_HASURA_APIS = ['metadata', 'graphql', 'pgdump', 'config'];
|
||||
|
||||
export default function HasuraEnabledAPISettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-only',
|
||||
});
|
||||
|
||||
const { enabledAPIs } = data?.config?.hasura.settings || {};
|
||||
|
||||
const form = useForm<HasuraEnabledAPIFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabledAPIs: enabledAPIs.map((api) => ({
|
||||
label: api,
|
||||
value: api,
|
||||
})),
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading enabled APIs..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState } = form;
|
||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||
|
||||
const availableAPIs = AVAILABLE_HASURA_APIS.map((api) => ({
|
||||
label: api,
|
||||
value: api,
|
||||
}));
|
||||
|
||||
async function handleSubmit(formValues: HasuraEnabledAPIFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
settings: {
|
||||
enabledAPIs: formValues.enabledAPIs.map((api) => api.value),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Enabled APIs are being updated...`,
|
||||
success: `Enabled APIs have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update enabled APIs.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Enabled APIs"
|
||||
description="Enable or disable APIs for your Hasura instance."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-6"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="enabledAPIs"
|
||||
name="enabledAPIs"
|
||||
fullWidth
|
||||
multiple
|
||||
className="lg:col-span-3"
|
||||
aria-label="Enabled APIs"
|
||||
options={availableAPIs}
|
||||
error={!!formState.errors?.enabledAPIs?.message}
|
||||
helperText={formState.errors?.enabledAPIs?.message}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraEnabledAPISettings';
|
||||
export { default as HasuraEnabledAPISettings } from './HasuraEnabledAPISettings';
|
||||
@@ -0,0 +1,155 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { HighlightedText } from '@/components/presentational/HighlightedText';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
logLevel: Yup.object({
|
||||
label: Yup.string().required(),
|
||||
value: Yup.string().required(),
|
||||
})
|
||||
.label('Log level')
|
||||
.required(),
|
||||
});
|
||||
|
||||
export type HasuraLogLevelFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
const AVAILABLE_HASURA_LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
|
||||
|
||||
export default function HasuraLogLevelSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { level } = data?.config?.hasura.logs || {};
|
||||
|
||||
const form = useForm<HasuraLogLevelFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
logLevel: level
|
||||
? {
|
||||
label: level,
|
||||
value: level,
|
||||
}
|
||||
: { label: 'warn', value: 'warn' },
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading log level settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState } = form;
|
||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||
|
||||
const availableLogLevels = AVAILABLE_HASURA_LOG_LEVELS.map((api) => ({
|
||||
label: api,
|
||||
value: api,
|
||||
}));
|
||||
|
||||
async function handleSubmit(formValues: HasuraLogLevelFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
logs: {
|
||||
level: formValues.logLevel?.value || 'warn',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Log level is being updated...`,
|
||||
success: `Log level has been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update log level.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Log Level"
|
||||
description={
|
||||
<>
|
||||
Setting a log-level will print all logs of priority greater than
|
||||
the set level. The log-level hierarchy is:{' '}
|
||||
<HighlightedText>
|
||||
debug → info → warn → error
|
||||
</HighlightedText>
|
||||
</>
|
||||
}
|
||||
docsLink="https://hasura.io/docs/latest/deployment/logging/#logging-levels"
|
||||
docsTitle="Log Levels"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="logLevel"
|
||||
name="logLevel"
|
||||
fullWidth
|
||||
className="lg:col-span-2"
|
||||
aria-label="Hasura Log Level"
|
||||
options={availableLogLevels}
|
||||
error={!!formState.errors?.logLevel?.message}
|
||||
helperText={formState.errors?.logLevel?.message}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraLogLevelSettings';
|
||||
export { default as HasuraLogLevelSettings } from './HasuraLogLevelSettings';
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
httpPoolSize: Yup.number()
|
||||
.label('HTTP Pool Size')
|
||||
.min(1)
|
||||
.max(100)
|
||||
.typeError('HTTP Pool Size must be a number')
|
||||
.required(),
|
||||
});
|
||||
|
||||
export type HasuraPoolSizeFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function HasuraPoolSizeSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { httpPoolSize } = data?.config?.hasura.events || {};
|
||||
|
||||
const form = useForm<HasuraPoolSizeFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: { httpPoolSize: httpPoolSize || 100 },
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading pool size settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { formState, register } = form;
|
||||
const isDirty = Object.keys(formState.dirtyFields).length > 0;
|
||||
|
||||
async function handleSubmit(formValues: HasuraPoolSizeFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
events: {
|
||||
httpPoolSize: formValues.httpPoolSize,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Pool size is being updated...`,
|
||||
success: `Pool size has been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update the pool size.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="HTTP Pool Size"
|
||||
description="Set the maximum number of concurrent HTTP workers for event delivery."
|
||||
docsLink="https://hasura.io/docs/latest/deployment/graphql-engine-flags/reference/#events-http-pool-size"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-flow-row gap-y-2 gap-x-4 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<Input
|
||||
{...register('httpPoolSize')}
|
||||
id="httpPoolSize"
|
||||
name="httpPoolSize"
|
||||
type="number"
|
||||
label="HTTP Pool Size"
|
||||
fullWidth
|
||||
className="lg:col-span-2"
|
||||
error={Boolean(formState.errors.httpPoolSize?.message)}
|
||||
helperText={formState.errors.httpPoolSize?.message}
|
||||
slotProps={{ inputRoot: { min: 1, max: 100 } }}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as HasuraPoolSizeSettings } from './HasuraPoolSizeSettings';
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { getServerError } from '@/utils/getServerError';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type HasuraRemoteSchemaPermissionsFormValues = Yup.InferType<
|
||||
typeof validationSchema
|
||||
>;
|
||||
|
||||
export default function HasuraRemoteSchemaPermissionsSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
|
||||
const { enableRemoteSchemaPermissions } = data?.config?.hasura.settings || {};
|
||||
|
||||
const form = useForm<HasuraRemoteSchemaPermissionsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: enableRemoteSchemaPermissions,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading remote schema permission settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(
|
||||
formValues: HasuraRemoteSchemaPermissionsFormValues,
|
||||
) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
hasura: {
|
||||
settings: {
|
||||
enableConsole: formValues.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
updateConfigPromise,
|
||||
{
|
||||
loading: `Remote schema permission settings are being updated...`,
|
||||
success: `Remote schema permission settings have been updated successfully.`,
|
||||
error: getServerError(
|
||||
`An error occurred while trying to update remote schema permission settings.`,
|
||||
),
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Remote Schema Permissions"
|
||||
description="Enable or disable remote schema permissions."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling Remote Schema Permissions"
|
||||
docsLink="https://hasura.io/docs/latest/remote-schemas/auth/remote-schema-permissions/"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './HasuraRemoteSchemaPermissionsSettings';
|
||||
export { default as HasuraRemoteSchemaPermissionsSettings } from './HasuraRemoteSchemaPermissionsSettings';
|
||||
@@ -39,7 +39,8 @@ const AVAILABLE_HASURA_VERSIONS = [
|
||||
|
||||
export default function HasuraServiceVersionSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
const { currentProject, refetch: refetchWorkspaceAndProject } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
});
|
||||
@@ -82,9 +83,7 @@ export default function HasuraServiceVersionSettings() {
|
||||
|
||||
const { formState } = form;
|
||||
|
||||
const handleHasuraServiceVersionsChange = async (
|
||||
formValues: HasuraServiceVersionFormValues,
|
||||
) => {
|
||||
async function handleSubmit(formValues: HasuraServiceVersionFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
@@ -110,14 +109,15 @@ export default function HasuraServiceVersionSettings() {
|
||||
);
|
||||
|
||||
form.reset(formValues);
|
||||
await refetchWorkspaceAndProject();
|
||||
} catch {
|
||||
// Note: The toast will handle the error.
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleHasuraServiceVersionsChange}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Hasura GraphQL Engine Version"
|
||||
description="The version of the Hasura GraphQL Engine to use."
|
||||
@@ -143,6 +143,7 @@ export default function HasuraServiceVersionSettings() {
|
||||
}}
|
||||
fullWidth
|
||||
className="lg:col-span-2"
|
||||
aria-label="Hasura Service Version"
|
||||
options={availableVersions}
|
||||
error={!!formState.errors?.version?.message}
|
||||
helperText={formState.errors?.version?.message}
|
||||
|
||||
@@ -4,6 +4,20 @@ query GetHasuraSettings($appId: uuid!) {
|
||||
__typename
|
||||
hasura {
|
||||
version
|
||||
settings {
|
||||
enableAllowList
|
||||
enableRemoteSchemaPermissions
|
||||
enableConsole
|
||||
devMode
|
||||
corsDomain
|
||||
enabledAPIs
|
||||
}
|
||||
logs {
|
||||
level
|
||||
}
|
||||
events {
|
||||
httpPoolSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RocketIcon } from '@/components/ui/v2/icons/RocketIcon';
|
||||
import { StorageIcon } from '@/components/ui/v2/icons/StorageIcon';
|
||||
import type { SvgIconProps } from '@/components/ui/v2/icons/SvgIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
@@ -55,6 +56,8 @@ export interface ProjectRoute {
|
||||
export default function useProjectRoutes() {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { currentProject, loading: currentProjectLoading } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
|
||||
const nhostRoutes: ProjectRoute[] = [
|
||||
{
|
||||
@@ -119,6 +122,7 @@ export default function useProjectRoutes() {
|
||||
exact: true,
|
||||
label: 'Hasura',
|
||||
icon: <HasuraIcon />,
|
||||
disabled: !currentProject?.config?.hasura.settings?.enableConsole,
|
||||
},
|
||||
{
|
||||
relativePath: '/users',
|
||||
@@ -135,5 +139,9 @@ export default function useProjectRoutes() {
|
||||
...nhostRoutes,
|
||||
];
|
||||
|
||||
return { nhostRoutes, allRoutes };
|
||||
return {
|
||||
nhostRoutes,
|
||||
allRoutes,
|
||||
loading: currentProjectLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
|
||||
import { ResourcesConfirmationDialog } from '@/features/projects/resources/settings/components/ResourcesConfirmationDialog';
|
||||
@@ -348,6 +349,13 @@ export default function ResourcesForm() {
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box className="px-4 pb-4">
|
||||
<Alert severity="info">
|
||||
In case you need more resources, please reach out to us at{' '}
|
||||
<Link href="mailto:support@nhost.io">support@nhost.io</Link>.
|
||||
</Alert>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Box className={twMerge('px-4', 'pb-4')}>
|
||||
|
||||
@@ -21,7 +21,7 @@ export const MIN_TOTAL_MEMORY =
|
||||
/**
|
||||
* The maximum total CPU that can be allocated.
|
||||
*/
|
||||
export const MAX_TOTAL_VCPU = 60 * RESOURCE_VCPU_MULTIPLIER;
|
||||
export const MAX_TOTAL_VCPU = 28 * RESOURCE_VCPU_MULTIPLIER;
|
||||
|
||||
/**
|
||||
* The maximum amount of memory that can be allocated in total.
|
||||
@@ -46,7 +46,7 @@ export const MIN_SERVICE_VCPU = 0.25 * RESOURCE_VCPU_MULTIPLIER;
|
||||
/**
|
||||
* The maximum amount of CPU that can be allocated per service.
|
||||
*/
|
||||
export const MAX_SERVICE_VCPU = 15 * RESOURCE_VCPU_MULTIPLIER;
|
||||
export const MAX_SERVICE_VCPU = 7 * RESOURCE_VCPU_MULTIPLIER;
|
||||
|
||||
/**
|
||||
* The minimum amount of memory that has to be allocated per service.
|
||||
|
||||
@@ -277,7 +277,16 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||
throw new Error(fileError.message);
|
||||
}
|
||||
|
||||
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
|
||||
if (!fileMetadata) {
|
||||
throw new Error('File metadata is missing.');
|
||||
}
|
||||
|
||||
const fileId =
|
||||
'processedFiles' in fileMetadata
|
||||
? fileMetadata.processedFiles[0]?.id
|
||||
: fileMetadata.id;
|
||||
|
||||
triggerToast(`File has been uploaded successfully (${fileId})`);
|
||||
|
||||
await refetchFilesAndAggregate();
|
||||
} catch (uploadError) {
|
||||
|
||||
@@ -19,6 +19,7 @@ query GetSignInMethods($appId: uuid!) {
|
||||
emailPassword {
|
||||
emailVerificationRequired
|
||||
hibpEnabled
|
||||
passwordMinLength
|
||||
}
|
||||
emailPasswordless {
|
||||
enabled
|
||||
@@ -40,6 +41,23 @@ query GetSignInMethods($appId: uuid!) {
|
||||
teamId
|
||||
privateKey
|
||||
}
|
||||
bitbucket {
|
||||
enabled
|
||||
clientId
|
||||
clientSecret
|
||||
}
|
||||
gitlab {
|
||||
enabled
|
||||
clientId
|
||||
clientSecret
|
||||
scope
|
||||
}
|
||||
strava {
|
||||
enabled
|
||||
clientId
|
||||
clientSecret
|
||||
scope
|
||||
}
|
||||
discord {
|
||||
enabled
|
||||
clientId
|
||||
|
||||
@@ -17,6 +17,9 @@ fragment Project on apps {
|
||||
}
|
||||
hasura {
|
||||
adminSecret
|
||||
settings {
|
||||
enableConsole
|
||||
}
|
||||
}
|
||||
}
|
||||
featureFlags {
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function DeploymentDetailsPage() {
|
||||
{deployment.deploymentLogs.map((log) => (
|
||||
<div key={log.id} className="flex font-mono">
|
||||
<div className=" mr-2 flex-shrink-0">
|
||||
{format(parseISO(log.createdAt), 'KK:mm:ss')}:
|
||||
{format(parseISO(log.createdAt), 'HH:mm:ss')}:
|
||||
</div>
|
||||
<div className="break-all">{log.message}</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,114 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { ProjectLayout } from '@/components/layout/ProjectLayout';
|
||||
import { HasuraConnectionInfo } from '@/features/hasura/overview/components/HasuraConnectionInfo';
|
||||
import { LoadingScreen } from '@/components/presentational/LoadingScreen';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { ArrowSquareOutIcon } from '@/components/ui/v2/icons/ArrowSquareOutIcon';
|
||||
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
defaultLocalBackendSlugs,
|
||||
defaultRemoteBackendSlugs,
|
||||
generateAppServiceUrl,
|
||||
} from '@/features/projects/common/utils/generateAppServiceUrl';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { getHasuraConsoleServiceUrl } from '@/utils/env';
|
||||
import Image from 'next/image';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
export default function HasuraPage() {
|
||||
const { currentProject, loading: currentProjectLoading } =
|
||||
useCurrentWorkspaceAndProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
|
||||
const { adminSecret: projectAdminSecret, settings } =
|
||||
currentProject?.config?.hasura || {};
|
||||
|
||||
if (currentProjectLoading || !projectAdminSecret) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
const hasuraUrl =
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev' || !isPlatform
|
||||
? `${getHasuraConsoleServiceUrl()}`
|
||||
: generateAppServiceUrl(
|
||||
currentProject?.subdomain,
|
||||
currentProject?.region,
|
||||
'hasura',
|
||||
defaultLocalBackendSlugs,
|
||||
{ ...defaultRemoteBackendSlugs, hasura: '/console' },
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HasuraConnectionInfo />
|
||||
<div className="mx-auto w-full max-w-md px-6 py-4 text-left">
|
||||
<div className="grid grid-flow-row gap-1">
|
||||
<div className="mx-auto">
|
||||
<Image
|
||||
src="/assets/hasuramodal.svg"
|
||||
width={72}
|
||||
height={72}
|
||||
alt="Hasura"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text variant="h3" component="h1" className="text-center">
|
||||
Open Hasura
|
||||
</Text>
|
||||
|
||||
<Text className="text-center">
|
||||
Hasura is the dashboard you'll use to edit your schema and
|
||||
permissions as well as browse data. Copy the admin secret to your
|
||||
clipboard and enter it in the next screen.
|
||||
</Text>
|
||||
|
||||
<Box className="mt-6 border-y-1">
|
||||
<div className="grid w-full grid-cols-1 place-content-between items-center py-2 sm:grid-cols-3">
|
||||
<Text className="col-span-1 text-center font-medium sm:justify-start sm:text-left">
|
||||
Admin Secret
|
||||
</Text>
|
||||
|
||||
<div className="col-span-1 grid grid-flow-col items-center justify-center gap-2 sm:col-span-2 sm:justify-end">
|
||||
<Text className="font-medium" variant="subtitle2">
|
||||
{Array(projectAdminSecret.length).fill('•').join('')}
|
||||
</Text>
|
||||
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
copy(projectAdminSecret, 'Hasura admin secret')
|
||||
}
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
className="min-w-0 p-1"
|
||||
aria-label="Copy admin secret"
|
||||
>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<div className="mt-6 grid grid-flow-row gap-2">
|
||||
<Button
|
||||
href={hasuraUrl}
|
||||
// Both `target` and `rel` are available when `href` is set. This is
|
||||
// a limitation of MUI.
|
||||
// @ts-ignore
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
endIcon={<ArrowSquareOutIcon className="h-4 w-4" />}
|
||||
disabled={!settings?.enableConsole}
|
||||
variant={settings?.enableConsole ? 'contained' : 'outlined'}
|
||||
color={settings?.enableConsole ? 'primary' : 'secondary'}
|
||||
>
|
||||
Open Hasura
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ClientURLSettings } from '@/features/authentication/settings/components
|
||||
import { DisableNewUsersSettings } from '@/features/authentication/settings/components/DisableNewUsersSettings';
|
||||
import { GravatarSettings } from '@/features/authentication/settings/components/GravatarSettings';
|
||||
import { MFASettings } from '@/features/authentication/settings/components/MFASettings';
|
||||
import { SessionSettings } from '@/features/authentication/settings/components/SessionSettings';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useGetAuthenticationSettingsQuery } from '@/utils/__generated__/graphql';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -16,16 +17,16 @@ import type { ReactElement } from 'react';
|
||||
export default function SettingsAuthenticationPage() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { loading, error } = useGetAuthenticationSettingsQuery({
|
||||
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
if (!data && loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading Authentication settings..."
|
||||
label="Loading authentication settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
@@ -46,6 +47,7 @@ export default function SettingsAuthenticationPage() {
|
||||
<AllowedEmailSettings />
|
||||
<BlockedEmailSettings />
|
||||
<MFASettings />
|
||||
<SessionSettings />
|
||||
<GravatarSettings />
|
||||
<DisableNewUsersSettings />
|
||||
</Container>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { SettingsLayout } from '@/components/layout/SettingsLayout';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { HasuraAllowListSettings } from '@/features/hasura/settings/components/HasuraAllowListSettings';
|
||||
import { HasuraConsoleSettings } from '@/features/hasura/settings/components/HasuraConsoleSettings';
|
||||
import { HasuraCorsDomainSettings } from '@/features/hasura/settings/components/HasuraCorsDomainSettings';
|
||||
import { HasuraDevModeSettings } from '@/features/hasura/settings/components/HasuraDevModeSettings';
|
||||
import { HasuraEnabledAPISettings } from '@/features/hasura/settings/components/HasuraEnabledAPISettings';
|
||||
import { HasuraLogLevelSettings } from '@/features/hasura/settings/components/HasuraLogLevelSettings';
|
||||
import { HasuraPoolSizeSettings } from '@/features/hasura/settings/components/HasuraPoolSizeSettings';
|
||||
import { HasuraRemoteSchemaPermissionsSettings } from '@/features/hasura/settings/components/HasuraRemoteSchemaPermissionsSettings';
|
||||
import { HasuraServiceVersionSettings } from '@/features/hasura/settings/components/HasuraServiceVersionSettings';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useGetHasuraSettingsQuery } from '@/utils/__generated__/graphql';
|
||||
@@ -9,12 +17,12 @@ import type { ReactElement } from 'react';
|
||||
export default function HasuraSettingsPage() {
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { loading, error } = useGetHasuraSettingsQuery({
|
||||
const { data, loading, error } = useGetHasuraSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
skip: !currentProject,
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
if (!data && loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -34,6 +42,14 @@ export default function HasuraSettingsPage() {
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<HasuraServiceVersionSettings />
|
||||
<HasuraLogLevelSettings />
|
||||
<HasuraEnabledAPISettings />
|
||||
<HasuraPoolSizeSettings />
|
||||
<HasuraCorsDomainSettings />
|
||||
<HasuraConsoleSettings />
|
||||
<HasuraDevModeSettings />
|
||||
<HasuraAllowListSettings />
|
||||
<HasuraRemoteSchemaPermissionsSettings />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { RetryableErrorBoundary } from '@/components/presentational/RetryableErrorBoundary';
|
||||
import { AccountSettingsLayout } from '@/features/account/settings/components/AccountSettingsLayout';
|
||||
import { PasswordSettings } from '@/features/account/settings/components/PasswordSettings';
|
||||
import { PATSettings } from '@/features/account/settings/components/PATSettings';
|
||||
@@ -10,8 +11,13 @@ export default function AccountSettingsPage() {
|
||||
className="grid max-w-5xl grid-flow-row gap-8 bg-transparent"
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<PasswordSettings />
|
||||
<PATSettings />
|
||||
<RetryableErrorBoundary>
|
||||
<PasswordSettings />
|
||||
</RetryableErrorBoundary>
|
||||
|
||||
<RetryableErrorBoundary>
|
||||
<PATSettings />
|
||||
</RetryableErrorBoundary>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
703
dashboard/src/utils/__generated__/graphql.ts
generated
703
dashboard/src/utils/__generated__/graphql.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -63,8 +63,8 @@ export const AUTH_GRAVATAR_DEFAULT = [
|
||||
label: 'monsterid',
|
||||
},
|
||||
{
|
||||
value: 'waatar',
|
||||
label: 'waatar',
|
||||
value: 'wavatar',
|
||||
label: 'wavatar',
|
||||
},
|
||||
{
|
||||
value: 'retro',
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 227d1704f: docs: use correct sample URLs for custom client
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 564ce1ac2: docs: add docs about using custom URLs for the Nhost SDK
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
108
docs/docs/cli/patching-local-environment.mdx
Normal file
108
docs/docs/cli/patching-local-environment.mdx
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: 'Patching Local Environment'
|
||||
sidebar_label: 'Patching Local Environment'
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
In some cases you may need to modify your configuration file to accommodate minor deviations from your production environment. For instance, imagine your `nhost.toml` file looks like:
|
||||
|
||||
```
|
||||
[global]
|
||||
[[global.environment]]
|
||||
name = 'ENVIRONMENT'
|
||||
value = 'production'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[hasura]
|
||||
version = 'v2.24.1-ce'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[hasura.logs]
|
||||
level = 'warn'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[auth.redirections]
|
||||
clientUrl = 'https://my.app.com'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[auth.method.oauth]
|
||||
[auth.method.oauth.apple]
|
||||
enabled = true
|
||||
clientId = '{{ secrets.APPLE_CLIENT_ID }}'
|
||||
keyId = '{{ secrets.APPLE_KEY_ID }}'
|
||||
teamId = '{{ secrets.APPLE_TEAM_ID }}'
|
||||
privateKey = '{{ secrets.APPLE_PRIVATE_KEY }}'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
```
|
||||
|
||||
While this may work in production you may want to do minor tweaks in your local development. To do so we rely on [JSON patches RFC6902](https://datatracker.ietf.org/doc/html/rfc6902). To make use of it just drop a file under `nhost/overlays/local.yaml`. For instance, a file with the following contents would apply minor modifications to your local environment without affecting production:
|
||||
|
||||
```
|
||||
- op: replace
|
||||
path: /hasura/version # override hasura version
|
||||
value: "v2.25.1-ce"
|
||||
|
||||
- op: replace
|
||||
path: /global/environment/0 # replace first environment variables
|
||||
value:
|
||||
name: ENVIRONMENT
|
||||
value: development
|
||||
|
||||
- op: add # add a new env var
|
||||
path: /global/environment/-
|
||||
value:
|
||||
name: FUNCTION_LOG_LEVEL
|
||||
value: debug
|
||||
|
||||
- op: replace # change the client url to local
|
||||
path: /auth/redirections/clientUrl
|
||||
value: http://localhost:3000
|
||||
|
||||
- op: remove # remove apple authentication
|
||||
path: /auth/method/oauth/apple
|
||||
```
|
||||
|
||||
To verify that the file is being manipulated as we desire we can use the `nhost` cli:
|
||||
|
||||
```
|
||||
$ nhost config show
|
||||
[global]
|
||||
[[global.environment]]
|
||||
name = 'ENVIRONMENT'
|
||||
value = 'development'
|
||||
|
||||
[[global.environment]]
|
||||
name = 'FUNCTION_LOG_LEVEL'
|
||||
value = 'debug'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[hasura]
|
||||
version = 'v2.25.1-ce'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[auth.redirections]
|
||||
clientUrl = 'http://localhost:3000'
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
[auth.method.oauth]
|
||||
[auth.method.oauth.apple]
|
||||
enabled = false
|
||||
|
||||
... (omitted for brevity)
|
||||
|
||||
```
|
||||
|
||||
Once you have finished making changes, don't forget to restart your development environment by running the command `nhost up` after modifying your configuration.
|
||||
|
||||
:::info
|
||||
While it may be convenient to modify your local environment for development, the further it deviates from production, the harder it is to detect issues before release. Therefore, we recommend keeping changes strictly necessary
|
||||
:::
|
||||
84
docs/docs/cli/seeds.mdx
Normal file
84
docs/docs/cli/seeds.mdx
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: 'Seeds'
|
||||
sidebar_label: 'Seeds'
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
When developing locally, it is very useful to work with a known set of data as it can simplify testing and development, especially when working in larger teams with multiple developers.
|
||||
|
||||
With the CLI, it is easy to extract data from an existing environment and generate a "seed" that can be shared and used to pre-populate any development environment as it initializes.
|
||||
|
||||
As mentioned before, you can create a seed from any environment. In this guide, we will assume that we have already started a local development with a table called "animals". At this point, we can add some data ourselves as usual. Once we are satisfied and have the data we want, we can run the following command to create a seed:
|
||||
|
||||
```
|
||||
$ nhost dev hasura seed create some-initial-data \
|
||||
--endpoint https://local.hasura.nhost.run \
|
||||
--admin-secret nhost-admin-secret \
|
||||
--database-name default \
|
||||
--from-table animals
|
||||
|
||||
INFO created seed file successfully file=/app/seeds/default/1685692310174_some-initial-data.sql
|
||||
```
|
||||
|
||||
:::info
|
||||
In the previous command, we instructed the CLI to create a seed named `some-initial-data` while specifying the connection parameters for our local environment. You could also extract data from a cloud project by specifying the correct parameters. Finally, we are only extracting data from the `animals` table, but you could also extract data from any other table or even from all tables
|
||||
:::
|
||||
|
||||
We can now inspect the file and see its contents:
|
||||
|
||||
```
|
||||
$ cat nhost/seeds/default/1685692310174_some-initial-data.sql
|
||||
SET check_function_bodies = false;
|
||||
INSERT INTO public.animals (id, created_at, updated_at, name) VALUES ('d50ff2e8-ec2a-496b-a2e6-a50eecccdb16', '2023-05-16 14:01:59.072576+00', '2023-05-16 14:01:59.072576+00', 'dog');
|
||||
INSERT INTO public.animals (id, created_at, updated_at, name) VALUES ('8224ec02-6fed-48ff-8c06-6c36298d0fd0', '2023-05-16 14:02:06.300074+00', '2023-05-16 14:02:06.300074+00', 'cat');
|
||||
```
|
||||
|
||||
Now, when you start a new development environment you can pass the `--apply-seeds` argument to pre-populate your environment with the seeds:
|
||||
|
||||
|
||||
```
|
||||
$ nhost up --apply-seeds
|
||||
Setting up Nhost development environment...
|
||||
Starting Nhost development environment...
|
||||
|
||||
(...) omitted for brevity
|
||||
|
||||
Applying migrations...
|
||||
INFO migrations applied on database: default
|
||||
Applying metadata...
|
||||
INFO Metadata applied
|
||||
Applying seeds...
|
||||
INFO Seed data planted for database: default
|
||||
|
||||
(...) omitted for brevity
|
||||
```
|
||||
|
||||
Or you could also apply the seeds yourself after starting nhost:
|
||||
|
||||
```
|
||||
$ nhost up
|
||||
Setting up Nhost development environment...
|
||||
Starting Nhost development environment...
|
||||
|
||||
(...) omitted for brevity
|
||||
|
||||
Applying migrations...
|
||||
INFO migrations applied on database: default
|
||||
Applying metadata...
|
||||
INFO Metadata applied
|
||||
|
||||
(...) omitted for brevity
|
||||
|
||||
$ nhost dev hasura seed apply \
|
||||
--endpoint https://local.hasura.nhost.run \
|
||||
--admin-secret nhost-admin-secret \
|
||||
--database-name default
|
||||
INFO Help us improve Hasura! The cli collects anonymized usage stats which
|
||||
allow us to keep improving Hasura at warp speed. To opt-out or read more,
|
||||
visit https://hasura.io/docs/latest/graphql/core/guides/telemetry.html
|
||||
INFO Seeds planted
|
||||
```
|
||||
|
||||
:::info
|
||||
Seeds are different from migrations because seeds are not automatically applied. If there is data that you want to have in all of your environments, it is best to use a database migration.
|
||||
:::
|
||||
@@ -36,17 +36,35 @@ yarn add @nhost/nhost-js graphql
|
||||
|
||||
## Initializing
|
||||
|
||||
Initialize a single `nhost` instance using your Nhost backend URL:
|
||||
Initialize a single `nhost` instance using your Nhost `subdomain` and `region`:
|
||||
|
||||
```ts title=utils/nhost.ts
|
||||
```ts title=src/lib/nhost.ts
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
export const nhost = new NhostClient({
|
||||
subdomain: '<your-subdomain>',
|
||||
region: '<your-region>'
|
||||
})
|
||||
```
|
||||
|
||||
## Using custom URLs
|
||||
|
||||
There are cases where you might want to use a custom URL for one or more of the
|
||||
services (e.g: when you are self-hosting or you are running services on custom
|
||||
ports). You can do this by passing in the custom URLs when initializing the
|
||||
Nhost client:
|
||||
|
||||
```ts title=src/lib/nhost.ts
|
||||
import { NhostClient } from '@nhost/nhost-js'
|
||||
|
||||
export const nhost = new NhostClient({
|
||||
authUrl: 'https://auth.mydomain.com/v1',
|
||||
storageUrl: 'https://storage.mydomain.com/v1',
|
||||
graphqlUrl: 'https://graphql.mydomain.com/v1',
|
||||
functionsUrl: 'https://functions.mydomain.com/v1'
|
||||
})
|
||||
```
|
||||
|
||||
## GraphQL Support
|
||||
|
||||
The Nhost client has a small GraphQL client built-in which is great to use server-side or in very simple frontend apps. For more serious frontend apps, we recommend using a more complete GraphQL client such as:
|
||||
|
||||
@@ -31,12 +31,12 @@ yarn add @nhost/nextjs graphql
|
||||
|
||||
## Initializing
|
||||
|
||||
Initialize a single `nhost` instance and wrap your app with the `NhostProvider`.
|
||||
|
||||
```jsx title=pages/_app.js
|
||||
import type { AppProps } from 'next/app'
|
||||
Initialize a single `nhost` instance using your Nhost `subdomain` and `region`,
|
||||
and wrap your app with the `NhostProvider`:
|
||||
|
||||
```tsx title=pages/_app.tsx
|
||||
import { NhostClient, NhostProvider } from '@nhost/nextjs'
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
subdomain: '<your-subdomain>',
|
||||
@@ -60,6 +60,35 @@ The `nhost` instance created with the `NhostClient` above is the same as the [Ja
|
||||
|
||||
:::
|
||||
|
||||
## Using custom URLs
|
||||
|
||||
There are cases where you might want to use a custom URL for one or more of the
|
||||
services (e.g: when you are self-hosting or you are running services on custom
|
||||
ports). You can do this by passing in the custom URLs when initializing the
|
||||
Nhost client:
|
||||
|
||||
```tsx title=pages/_app.tsx
|
||||
import { NhostClient, NhostProvider } from '@nhost/nextjs'
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
authUrl: 'https://auth.mydomain.com/v1',
|
||||
storageUrl: 'https://storage.mydomain.com/v1',
|
||||
graphqlUrl: 'https://graphql.mydomain.com/v1',
|
||||
functionsUrl: 'https://functions.mydomain.com/v1'
|
||||
})
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NhostProvider nhost={nhost} initial={pageProps.nhostSession}>
|
||||
<Component {...pageProps} />
|
||||
</NhostProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
```
|
||||
|
||||
## Server-Side Rendering (SSR)
|
||||
|
||||
You need to load the session from the server first from `getServerSideProps`. Once it is done, the `_app` component will make sure to load or update the session through `pageProps`.
|
||||
|
||||
@@ -31,17 +31,33 @@ yarn add @nhost/react graphql
|
||||
|
||||
## Initializing
|
||||
|
||||
After installation, initialize a single Nhost Client (`nhost`) under `src/lib/nhost.js`.
|
||||
Initialize a single `nhost` instance using your Nhost `subdomain` and `region`:
|
||||
|
||||
```jsx title=src/lib/nhost.js
|
||||
import { NhostClient } from '@nhost/react'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
export const nhost = new NhostClient({
|
||||
subdomain: '<your-subdomain>',
|
||||
region: '<your-region>'
|
||||
})
|
||||
```
|
||||
|
||||
export { nhost }
|
||||
## Using custom URLs
|
||||
|
||||
There are cases where you might want to use a custom URL for one or more of the
|
||||
services (e.g: when you are self-hosting or you are running services on custom
|
||||
ports). You can do this by passing in the custom URLs when initializing the
|
||||
Nhost client:
|
||||
|
||||
```ts title=src/lib/nhost.ts
|
||||
import { NhostClient } from '@nhost/react'
|
||||
|
||||
export const nhost = new NhostClient({
|
||||
authUrl: 'https://auth.mydomain.com/v1',
|
||||
storageUrl: 'https://storage.mydomain.com/v1',
|
||||
graphqlUrl: 'https://graphql.mydomain.com/v1',
|
||||
functionsUrl: 'https://functions.mydomain.com/v1'
|
||||
})
|
||||
```
|
||||
|
||||
Import `nhost` and wrap your app with the `NhostProvider`.
|
||||
|
||||
@@ -31,12 +31,12 @@ yarn add @nhost/vue graphql
|
||||
|
||||
## Initializing
|
||||
|
||||
Initialize a single `nhost` instance, and install it as a plugin in your Vue app.
|
||||
Initialize a single `nhost` instance using your Nhost `subdomain` and `region`,
|
||||
and install it as a plugin in your Vue app:
|
||||
|
||||
```js title=src/main.js
|
||||
import { createApp } from 'vue'
|
||||
import { NhostClient } from '@nhost/vue'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
@@ -46,3 +46,25 @@ const nhost = new NhostClient({
|
||||
|
||||
createApp(App).use(nhost).mount('#app')
|
||||
```
|
||||
|
||||
## Using custom URLs
|
||||
|
||||
There are cases where you might want to use a custom URL for one or more of the
|
||||
services (e.g: when you are self-hosting or you are running services on custom
|
||||
ports). You can do this by passing in the custom URLs when initializing the
|
||||
Nhost client:
|
||||
|
||||
```js title=src/main.js
|
||||
import { NhostClient } from '@nhost/vue'
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
const nhost = new NhostClient({
|
||||
authUrl: 'https://auth.mydomain.com/v1',
|
||||
storageUrl: 'https://storage.mydomain.com/v1',
|
||||
graphqlUrl: 'https://graphql.mydomain.com/v1',
|
||||
functionsUrl: 'https://functions.mydomain.com/v1'
|
||||
})
|
||||
|
||||
createApp(App).use(nhost).mount('#app')
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
3
examples/node-storage/.env.example
Normal file
3
examples/node-storage/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
SUBDOMAIN="local"
|
||||
REGION=""
|
||||
ADMIN_SECRET="nhost-admin-secret"
|
||||
1
examples/node-storage/.gitignore
vendored
Normal file
1
examples/node-storage/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.secrets.nhost
|
||||
8
examples/node-storage/CHANGELOG.md
Normal file
8
examples/node-storage/CHANGELOG.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# @nhost-examples/node-storage
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4c00a796e: feat: add example for Storage + Node.js
|
||||
- @nhost/nhost-js@2.2.7
|
||||
30
examples/node-storage/README.md
Normal file
30
examples/node-storage/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Node.js Storage Example
|
||||
|
||||
This example demonstrates how to use the [Nhost Storage SDK](https://docs.nhost.io/reference/javascript/storage) in Node.js.
|
||||
|
||||
Make sure to install the dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Settting up the environment
|
||||
|
||||
Create a `.env` file in the root of the project with the following content:
|
||||
|
||||
```bash
|
||||
SUBDOMAIN=<your-subdomain>
|
||||
REGION=<your-region>
|
||||
ADMIN_SECRET=<your-admin-secret>
|
||||
```
|
||||
|
||||
You can use the `.env.example` file as a starting point.
|
||||
|
||||
## Running the example
|
||||
|
||||
```bash
|
||||
pnpm start
|
||||
```
|
||||
|
||||
The example will download a file from a public URL and upload it to your Nhost
|
||||
Storage bucket.
|
||||
1
examples/node-storage/nhost/config.yaml
Normal file
1
examples/node-storage/nhost/config.yaml
Normal file
@@ -0,0 +1 @@
|
||||
version: 3
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Потвърдете смяната на вашия имейл</h2>
|
||||
<p>Използвайте посочения линк, за да повърдите смяната на имейл:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Смени имейл
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Потвърждение за смяна на имейл
|
||||
18
examples/node-storage/nhost/emails/bg/email-verify/body.html
Normal file
18
examples/node-storage/nhost/emails/bg/email-verify/body.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Потвърдете вашия имейл</h2>
|
||||
<p>Използвайте посочения линк, за да потвърдите вашия имейл:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Потвърдете имейл
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Потвърждаване на имейл
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Смяна на парола</h2>
|
||||
<p>Използвайте посочения линк, за да смените вашата парола:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Смяна на парола
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Смяна на парола
|
||||
@@ -0,0 +1 @@
|
||||
Вашият код е ${code}.
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Магически линк за вход</h2>
|
||||
<p>Използвайте посочения линк за защитен и бърз вход:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Вход
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Магически линк за вход
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Potvrzení změny emailové adresy</h2>
|
||||
<p>Použijte tento odkaz k potvrzení změny emailové adresy:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Změnit email
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Změna vaší emailové adresy
|
||||
18
examples/node-storage/nhost/emails/cs/email-verify/body.html
Normal file
18
examples/node-storage/nhost/emails/cs/email-verify/body.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Ověření emailové adresy</h2>
|
||||
<p>Použijte tento odkaz k ověření vaší emailové adresy:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Ověřit emailovou adresu
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Ověření vaší emailové adresy
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Obnova hesla</h2>
|
||||
<p>Použijte tento odkaz k obnovení vašeho hesla:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Obnova hesla
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Obnova hesla
|
||||
@@ -0,0 +1 @@
|
||||
Váš kód je ${code}.
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Magický odkaz</h2>
|
||||
<p>Použijte tento odkaz k bezpečnému přihlášení:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Přihlášení
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Bezpečný odkaz k přihlášení
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Confirm Email Change</h2>
|
||||
<p>Use this link to confirm changing email:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Change email
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Change your email address
|
||||
18
examples/node-storage/nhost/emails/en/email-verify/body.html
Normal file
18
examples/node-storage/nhost/emails/en/email-verify/body.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Verify Email</h2>
|
||||
<p>Use this link to verify your email:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Verify Email
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user