Compare commits
11 Commits
@nhost/das
...
@nhost/rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e503b8fe8b | ||
|
|
304065ae22 | ||
|
|
68e0622eb0 | ||
|
|
70c6834636 | ||
|
|
a7bde37bba | ||
|
|
1bc615beca | ||
|
|
a58c5cfc96 | ||
|
|
c61228e45d | ||
|
|
6cec04bd6f | ||
|
|
a448d7d182 | ||
|
|
948048940e |
@@ -1,5 +1,23 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 1.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a7bde37: feat: send metadata in the edit form
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1bc615b: feat: improve error message handling in `ErrorToast` component
|
||||
- @nhost/react-apollo@11.0.2
|
||||
- @nhost/nextjs@2.1.11
|
||||
|
||||
## 1.14.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- a448d7d: feat: allow configuring postmark and delete SMTP settings
|
||||
|
||||
## 1.13.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
2
dashboard/e2e/e2e-tests-project/.gitignore
vendored
Normal file
2
dashboard/e2e/e2e-tests-project/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.secrets
|
||||
.nhost
|
||||
1
dashboard/e2e/e2e-tests-project/nhost/config.yaml
Normal file
1
dashboard/e2e/e2e-tests-project/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 @@
|
||||
Потвърждение за смяна на имейл
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
Verify your email
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Reset Password</h2>
|
||||
<p>Use this link to reset your password:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Reset password
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Reset your password
|
||||
@@ -0,0 +1 @@
|
||||
Your code is ${code}.
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Magic Link</h2>
|
||||
<p>Use this link to securely sign in:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Sign In
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Secure sign-in link
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Confirmar cambio de correo electrónico</h2>
|
||||
<p>Utiliza el siguiente enlace para confirmar el cambio de correo:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Cambiar correo electrónico
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Cambiar dirección de correo electrónico
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Verificar correo electrónico</h2>
|
||||
<p>Utilza el siguiente enlace para verificar tu correo:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Verificar correo electrónico
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Verifica tu correo electrónico
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Recuperar contraseña</h2>
|
||||
<p>Utiliza el siguiente enlace para recuperar tu contraseña:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Recuperar contraseña
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Recuperar contraseña
|
||||
@@ -0,0 +1 @@
|
||||
Tu código es ${code}.
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Enlace mágico</h2>
|
||||
<p>Utiliza este enlace para iniciar sesión de forma segura:</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Iniciar sesión
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Enlace de acceso seguro
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Confirmer changement de courriel</h2>
|
||||
<p>Utilisez ce lien pour confirmer le changement de courriel :</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Changer courriel
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Changez votre adresse courriel
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Vérifiez votre courriel</h2>
|
||||
<p>Utilisez ce lien pour vérifier votre courriel :</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Vérifier courriel
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Vérifier votre courriel
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Réinitialiser votre mot de passe</h2>
|
||||
<p>Utilisez ce lien pour réinitialiser votre mot de passe :</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Réinitialiser mot de passe
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Réinitialiser votre mot de passe
|
||||
@@ -0,0 +1 @@
|
||||
Votre code est ${code}.
|
||||
@@ -0,0 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Lien magique</h2>
|
||||
<p>Utilisez ce lien pour vous connecter de façon sécurisée :</p>
|
||||
<p>
|
||||
<a href="${link}">
|
||||
Connexion
|
||||
</a>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
Lien de connexion sécurisé
|
||||
@@ -0,0 +1,8 @@
|
||||
${link},
|
||||
${displayName},
|
||||
${email},
|
||||
${ticket},
|
||||
${redirectTo},
|
||||
${serverUrl},
|
||||
${clientUrl},
|
||||
${locale},
|
||||
@@ -0,0 +1 @@
|
||||
${link}, ${displayName}, ${email}, ${ticket}, ${redirectTo}, ${serverUrl}, ${clientUrl}, ${locale}
|
||||
151
dashboard/e2e/e2e-tests-project/nhost/nhost.toml
Normal file
151
dashboard/e2e/e2e-tests-project/nhost/nhost.toml
Normal file
@@ -0,0 +1,151 @@
|
||||
[global]
|
||||
|
||||
[hasura]
|
||||
version = 'v2.33.4-ce'
|
||||
adminSecret = '{{ secrets.HASURA_GRAPHQL_ADMIN_SECRET }}'
|
||||
webhookSecret = '{{ secrets.NHOST_WEBHOOK_SECRET }}'
|
||||
|
||||
[[hasura.jwtSecrets]]
|
||||
type = 'HS256'
|
||||
key = '{{ secrets.HASURA_GRAPHQL_JWT_SECRET }}'
|
||||
|
||||
[hasura.settings]
|
||||
corsDomain = ['*']
|
||||
devMode = true
|
||||
enableAllowList = false
|
||||
enableConsole = true
|
||||
enableRemoteSchemaPermissions = false
|
||||
enabledAPIs = ['metadata', 'graphql', 'pgdump', 'config']
|
||||
liveQueriesMultiplexedRefetchInterval = 1000
|
||||
stringifyNumericTypes = false
|
||||
|
||||
[hasura.logs]
|
||||
level = 'warn'
|
||||
|
||||
[hasura.events]
|
||||
httpPoolSize = 100
|
||||
|
||||
[functions]
|
||||
[functions.node]
|
||||
version = 18
|
||||
|
||||
[auth]
|
||||
version = '0.24.1'
|
||||
|
||||
[auth.elevatedPrivileges]
|
||||
mode = 'disabled'
|
||||
|
||||
[auth.redirections]
|
||||
clientUrl = 'http://localhost:3000'
|
||||
|
||||
[auth.signUp]
|
||||
enabled = true
|
||||
disableNewUsers = false
|
||||
|
||||
[auth.user]
|
||||
[auth.user.roles]
|
||||
default = 'user'
|
||||
allowed = ['user', 'me']
|
||||
|
||||
[auth.user.locale]
|
||||
default = 'en'
|
||||
allowed = ['en']
|
||||
|
||||
[auth.user.gravatar]
|
||||
enabled = true
|
||||
default = 'blank'
|
||||
rating = 'g'
|
||||
|
||||
[auth.user.email]
|
||||
|
||||
[auth.user.emailDomains]
|
||||
|
||||
[auth.session]
|
||||
[auth.session.accessToken]
|
||||
expiresIn = 900
|
||||
|
||||
[auth.session.refreshToken]
|
||||
expiresIn = 2592000
|
||||
|
||||
[auth.method]
|
||||
[auth.method.anonymous]
|
||||
enabled = false
|
||||
|
||||
[auth.method.emailPasswordless]
|
||||
enabled = false
|
||||
|
||||
[auth.method.emailPassword]
|
||||
hibpEnabled = false
|
||||
emailVerificationRequired = true
|
||||
passwordMinLength = 9
|
||||
|
||||
[auth.method.smsPasswordless]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth]
|
||||
[auth.method.oauth.apple]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.azuread]
|
||||
tenant = 'common'
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.bitbucket]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.discord]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.facebook]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.github]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.gitlab]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.google]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.linkedin]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.spotify]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.strava]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.twitch]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.twitter]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.windowslive]
|
||||
enabled = false
|
||||
|
||||
[auth.method.oauth.workos]
|
||||
enabled = false
|
||||
|
||||
[auth.method.webauthn]
|
||||
enabled = false
|
||||
|
||||
[auth.method.webauthn.attestation]
|
||||
timeout = 60000
|
||||
|
||||
[auth.totp]
|
||||
enabled = false
|
||||
|
||||
[postgres]
|
||||
version = '14.6-20240129-1'
|
||||
|
||||
[provider]
|
||||
|
||||
[storage]
|
||||
version = '0.6.0'
|
||||
|
||||
[observability]
|
||||
[observability.grafana]
|
||||
adminPassword = '{{ secrets.GRAFANA_ADMIN_PASSWORD }}'
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "1.13.3",
|
||||
"version": "1.15.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
|
||||
@@ -29,10 +29,10 @@ const getInternalErrorMessage = (
|
||||
|
||||
if (error.name === 'ApolloError') {
|
||||
// @ts-ignore
|
||||
const internalError = error.graphQLErrors?.[0]?.extensions?.internal as {
|
||||
error: { message: string };
|
||||
};
|
||||
return internalError?.error?.message || null;
|
||||
const graphqlError = error.graphQLErrors?.[0];
|
||||
const graphqlExtensionsError = graphqlError?.extensions?.internal
|
||||
?.error as { message: string };
|
||||
return graphqlError.message || graphqlExtensionsError?.message || null;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
function ConfirmDeleteSMTPSettingsModal({
|
||||
close,
|
||||
onDelete,
|
||||
}: {
|
||||
onDelete?: () => Promise<any>;
|
||||
close: () => void;
|
||||
}) {
|
||||
const onClickDelete = async () => {
|
||||
await onDelete();
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className={twMerge('w-full rounded-lg p-6 text-left')}>
|
||||
<div className="grid grid-flow-row gap-4">
|
||||
<Text variant="h3" component="h2">
|
||||
Delete SMTP Settings?
|
||||
</Text>
|
||||
|
||||
<Text>This will reset all your SMTP and Postmark settings.</Text>
|
||||
|
||||
<div className="grid grid-flow-row gap-2">
|
||||
<Button color="error" onClick={onClickDelete}>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<Button variant="outlined" color="secondary" onClick={close}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function DeleteSMTPSettings() {
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const deleteSMTPSettings = async () => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
provider: {
|
||||
smtp: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'SMTP settings are being deleted...',
|
||||
successMessage: 'SMTP settings have been deleted successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to delete the SMTP settings.',
|
||||
},
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const confirmDeleteSMTPSettings = async () => {
|
||||
openDialog({
|
||||
component: (
|
||||
<ConfirmDeleteSMTPSettingsModal
|
||||
close={closeDialog}
|
||||
onDelete={deleteSMTPSettings}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer
|
||||
title="Delete SMTP Settings"
|
||||
description="Delete SMTP settings and revert to default values"
|
||||
className="px-0"
|
||||
slotProps={{
|
||||
submitButton: { className: 'hidden' },
|
||||
footer: { className: 'hidden' },
|
||||
}}
|
||||
>
|
||||
<Box className="grid grid-flow-row border-t-1">
|
||||
<Button
|
||||
color="error"
|
||||
className="mx-4 mt-4 justify-self-end"
|
||||
onClick={confirmDeleteSMTPSettings}
|
||||
disabled={loading || maintenanceActive}
|
||||
loading={loading}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DeleteSTMPSettings } from './DeleteSMTPSettings';
|
||||
@@ -0,0 +1,149 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as yup from 'yup';
|
||||
|
||||
const validationSchema = yup
|
||||
.object({
|
||||
sender: yup.string().label('SMTP Sender').email().required(),
|
||||
password: yup.string().label('Password').required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export type PostmarkFormValues = yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function PostmarkSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { data } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { sender, password } = data?.config?.provider?.smtp || {};
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const form = useForm<PostmarkFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
defaultValues: {
|
||||
password: '',
|
||||
sender: '',
|
||||
},
|
||||
values: {
|
||||
password: password || '',
|
||||
sender: sender || '',
|
||||
},
|
||||
mode: 'onSubmit',
|
||||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isDirty, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const handleEditPostmarkSettings = async (values: PostmarkFormValues) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
provider: {
|
||||
smtp: { method: 'LOGIN', host: 'postmark', ...values },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Postmark settings are being updated...',
|
||||
successMessage: 'Postmark settings have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update your Postmark settings.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleEditPostmarkSettings}>
|
||||
<SettingsContainer
|
||||
title="Postmark Settings"
|
||||
description="Configure postmark's native integration to send emails from your email domain."
|
||||
submitButtonText="Save"
|
||||
className="grid grid-cols-9 gap-4"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: isSubmitting,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
{...register('sender')}
|
||||
id="sender"
|
||||
name="sender"
|
||||
label="From Email"
|
||||
placeholder="noreply@nhost.app"
|
||||
className="lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.sender)}
|
||||
helperText={errors.sender?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('password')}
|
||||
id="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
className="lg:col-span-5"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.password)}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './PostmarkSettings';
|
||||
export { default as PostmarkSettings } from './PostmarkSettings';
|
||||
@@ -0,0 +1,238 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { type Optional } from 'utility-types';
|
||||
import * as yup from 'yup';
|
||||
|
||||
const smtpValidationSchema = yup
|
||||
.object({
|
||||
secure: yup.bool().label('SMTP Secure'),
|
||||
host: yup
|
||||
.string()
|
||||
.label('SMTP Host')
|
||||
.matches(
|
||||
/((https?):\/\/)?(www\.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#[a-zA-Z0-9#]+?)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/,
|
||||
'SMTP Host must be a valid URL',
|
||||
)
|
||||
.required(),
|
||||
port: yup
|
||||
.number()
|
||||
.typeError('The SMTP port should contain only numbers.')
|
||||
.required(),
|
||||
user: yup.string().label('Username').required(),
|
||||
password: yup.string().label('Password'),
|
||||
method: yup.string().required(),
|
||||
sender: yup.string().label('SMTP Sender').email().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
||||
|
||||
export default function SMTPSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const { data } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { secure, host, port, user, method, sender, password } =
|
||||
data?.config?.provider?.smtp || {};
|
||||
|
||||
const form = useForm<Optional<SmtpFormValues, 'password'>>({
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(smtpValidationSchema),
|
||||
defaultValues: {
|
||||
secure: false,
|
||||
host: '',
|
||||
port: undefined,
|
||||
user: '',
|
||||
password: '',
|
||||
method: '',
|
||||
sender: '',
|
||||
},
|
||||
values: {
|
||||
secure: secure || false,
|
||||
host: host || '',
|
||||
port,
|
||||
user: user || '',
|
||||
password: password || '',
|
||||
method: method || '',
|
||||
sender: sender || '',
|
||||
},
|
||||
mode: 'onSubmit',
|
||||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
const {
|
||||
register: registerSmtp,
|
||||
formState: { errors, isDirty, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const handleEditSMTPSettings = async (values: SmtpFormValues) => {
|
||||
const { password: newPassword, ...valuesWithoutPassword } = values;
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
provider: {
|
||||
smtp: newPassword ? values : valuesWithoutPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'SMTP settings are being updated...',
|
||||
successMessage: 'SMTP settings have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update the SMTP settings.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleEditSMTPSettings}>
|
||||
<SettingsContainer
|
||||
title="SMTP Settings"
|
||||
description="Configure your SMTP settings to send emails from your email domain."
|
||||
submitButtonText="Save"
|
||||
className="grid grid-cols-9 gap-4"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: isSubmitting,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
{...registerSmtp('sender')}
|
||||
id="sender"
|
||||
name="sender"
|
||||
label="From Email"
|
||||
placeholder="noreply@nhost.app"
|
||||
className="lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.sender)}
|
||||
helperText={errors.sender?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('host')}
|
||||
id="host"
|
||||
name="host"
|
||||
label="SMTP Host"
|
||||
className="lg:col-span-4"
|
||||
placeholder="e.g. smtp.sendgrid.net"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.host)}
|
||||
helperText={errors.host?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('port')}
|
||||
id="port"
|
||||
name="port"
|
||||
label="Port"
|
||||
type="number"
|
||||
placeholder="587"
|
||||
className="lg:col-span-1"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.port)}
|
||||
helperText={errors.port?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('user')}
|
||||
id="user"
|
||||
label="SMTP Username"
|
||||
placeholder="SMTP Username"
|
||||
className="lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.user)}
|
||||
helperText={errors.user?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('password')}
|
||||
id="password"
|
||||
label="SMTP Password"
|
||||
type="password"
|
||||
placeholder="Enter SMTP password"
|
||||
className="lg:col-span-5"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.password)}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('method')}
|
||||
id="method"
|
||||
name="method"
|
||||
label="SMTP Auth Method"
|
||||
placeholder="LOGIN"
|
||||
hideEmptyHelperText
|
||||
className="lg:col-span-4"
|
||||
fullWidth
|
||||
error={Boolean(errors.method)}
|
||||
helperText={errors.method?.message}
|
||||
/>
|
||||
|
||||
<ControlledCheckbox
|
||||
name="secure"
|
||||
id="secure"
|
||||
label="Use SSL"
|
||||
className="lg:col-span-9"
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './SMTPSettings';
|
||||
export { default as SMTPSettings } from './SMTPSettings';
|
||||
@@ -31,9 +31,10 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { format } from 'date-fns';
|
||||
import kebabCase from 'just-kebab-case';
|
||||
import debounce from 'lodash.debounce';
|
||||
import Image from 'next/image';
|
||||
import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
@@ -76,6 +77,21 @@ export const EditUserFormValidationSchema = Yup.object({
|
||||
locale: Yup.string(),
|
||||
defaultRole: Yup.string(),
|
||||
roles: Yup.array().of(Yup.boolean()),
|
||||
metadata: Yup.string().test(
|
||||
'is-valid-json',
|
||||
'Metadata must be valid JSON or empty',
|
||||
(value) => {
|
||||
if (value === '') {
|
||||
return true;
|
||||
} // Allow empty string as valid input
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
export type EditUserFormValues = Yup.InferType<
|
||||
@@ -116,14 +132,55 @@ export default function EditUserForm({
|
||||
locale: user.locale,
|
||||
defaultRole: user.defaultRole,
|
||||
roles: roles.map((role) => Object.values(role)[0]),
|
||||
metadata: user?.metadata ? JSON.stringify(user.metadata, null, 2) : '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
setError,
|
||||
clearErrors,
|
||||
formState: { errors, dirtyFields, isSubmitting, isValidating },
|
||||
} = form;
|
||||
|
||||
const handleMetadataError = useMemo(() => {
|
||||
const debouncedSetError = debounce((value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
// Only set an error if JSON parsing fails
|
||||
} catch (error) {
|
||||
setError('metadata', {
|
||||
type: 'manual',
|
||||
message: 'Invalid JSON format',
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return {
|
||||
call: debouncedSetError,
|
||||
cancel: debouncedSetError.cancel, // lodash debounce provides a cancel method to stop the delayed function
|
||||
};
|
||||
}, [setError]);
|
||||
|
||||
const handleMetadataChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
if (value === '') {
|
||||
clearErrors('metadata'); // Clear errors when the input is explicitly cleared
|
||||
handleMetadataError.cancel(); // Cancel any debounced error checks
|
||||
} else {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
clearErrors('metadata'); // Clear errors when valid JSON is entered
|
||||
handleMetadataError.cancel(); // Cancel pending debounced error checks
|
||||
} catch (error) {
|
||||
handleMetadataError.call(value); // Call the debounced error setter
|
||||
}
|
||||
}
|
||||
},
|
||||
[clearErrors, handleMetadataError],
|
||||
);
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -467,6 +524,28 @@ export default function EditUserForm({
|
||||
</div>
|
||||
</Box>
|
||||
)}
|
||||
<Box component="section" className="grid grid-flow-row gap-8 p-6">
|
||||
<Input
|
||||
{...register('metadata', { onChange: handleMetadataChange })}
|
||||
id="metadata"
|
||||
label="Metadata"
|
||||
variant="inline"
|
||||
hideEmptyHelperText
|
||||
error={!!errors.metadata}
|
||||
fullWidth
|
||||
multiline
|
||||
inputProps={{
|
||||
className: 'resize-y min-h-[130px]',
|
||||
}}
|
||||
helperText={
|
||||
errors.metadata
|
||||
? errors.metadata.message
|
||||
: 'Enter valid JSON. This can be a number, boolean, array, or object.'
|
||||
}
|
||||
maxRows={100}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box className="grid w-full flex-shrink-0 snap-end grid-flow-col justify-between gap-3 place-self-end border-t-1 p-2">
|
||||
|
||||
@@ -113,6 +113,9 @@ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
|
||||
phoneNumber: values.phoneNumber,
|
||||
phoneNumberVerified: values.phoneNumberVerified,
|
||||
locale: values.locale,
|
||||
...(values?.metadata !== undefined && values.metadata !== ''
|
||||
? { metadata: JSON.parse(values.metadata) }
|
||||
: { metadata: null }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ fragment RemoteAppGetUsers on users {
|
||||
defaultRole
|
||||
lastSeen
|
||||
locale
|
||||
metadata
|
||||
roles {
|
||||
id
|
||||
role
|
||||
|
||||
@@ -1,102 +1,35 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ControlledCheckbox } from '@/components/form/ControlledCheckbox';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { Container } from '@/components/layout/Container';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { SettingsLayout } from '@/components/layout/SettingsLayout';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import DeleteSMTPSettings from '@/features/authentication/settings/components/DeleteSMTPSettings/DeleteSMTPSettings';
|
||||
import { PostmarkSettings } from '@/features/authentication/settings/components/PostmarkSettings';
|
||||
import { SMTPSettings } from '@/features/authentication/settings/components/SMTPSettings';
|
||||
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
|
||||
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import type { ReactElement } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import type { Optional } from 'utility-types';
|
||||
import * as yup from 'yup';
|
||||
|
||||
const smtpValidationSchema = yup
|
||||
.object({
|
||||
secure: yup.bool().label('SMTP Secure'),
|
||||
host: yup
|
||||
.string()
|
||||
.label('SMTP Host')
|
||||
.matches(
|
||||
/((https?):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/,
|
||||
'SMTP Host must be a valid URL',
|
||||
)
|
||||
.required(),
|
||||
port: yup
|
||||
.number()
|
||||
.typeError('The SMTP port should contain only numbers.')
|
||||
.required(),
|
||||
user: yup.string().label('Username').required(),
|
||||
password: yup.string().label('Password'),
|
||||
method: yup.string().required(),
|
||||
sender: yup.string().label('SMTP Sender').email().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
||||
import { useGetSmtpSettingsQuery } from '@/utils/__generated__/graphql';
|
||||
import { useEffect, useState, type ReactElement } from 'react';
|
||||
|
||||
export default function SMTPSettingsPage() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [mode, setMode] = useState('postmark');
|
||||
|
||||
const { data, loading, error } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: currentProject?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { secure, host, port, user, method, sender, password } =
|
||||
data?.config?.provider?.smtp || {};
|
||||
const { host } = data?.config?.provider?.smtp || {};
|
||||
|
||||
const form = useForm<Optional<SmtpFormValues, 'password'>>({
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(smtpValidationSchema),
|
||||
defaultValues: {
|
||||
secure: false,
|
||||
host: '',
|
||||
port: undefined,
|
||||
user: '',
|
||||
password: '',
|
||||
method: '',
|
||||
sender: '',
|
||||
},
|
||||
values: {
|
||||
secure: secure || false,
|
||||
host: host || '',
|
||||
port,
|
||||
user: user || '',
|
||||
password: password || '',
|
||||
method: method || '',
|
||||
sender: sender || '',
|
||||
},
|
||||
mode: 'onSubmit',
|
||||
criteriaMode: 'all',
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isDirty, isSubmitting },
|
||||
} = form;
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
useEffect(() => {
|
||||
setMode(host !== 'postmark' ? 'smtp' : 'postmark');
|
||||
}, [host]);
|
||||
|
||||
if (isPlatform && currentProject?.plan?.isFree) {
|
||||
return (
|
||||
@@ -123,151 +56,29 @@ export default function SMTPSettingsPage() {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const handleEditSMTPSettings = async (values: SmtpFormValues) => {
|
||||
const { password: newPassword, ...valuesWithoutPassword } = values;
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
config: {
|
||||
provider: {
|
||||
smtp: newPassword ? values : valuesWithoutPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'SMTP settings are being updated...',
|
||||
successMessage: 'SMTP settings have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update the SMTP settings.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container
|
||||
className="grid max-w-5xl grid-flow-row gap-4 bg-transparent"
|
||||
rootClassName="bg-transparent"
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleEditSMTPSettings}>
|
||||
<SettingsContainer
|
||||
title="SMTP Settings"
|
||||
description="Configure your SMTP settings to send emails from your email domain."
|
||||
submitButtonText="Save"
|
||||
className="grid grid-cols-9 gap-4"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: isSubmitting,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
{...register('sender')}
|
||||
id="sender"
|
||||
name="sender"
|
||||
label="From Email"
|
||||
placeholder="noreply@nhost.app"
|
||||
className="lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.sender)}
|
||||
helperText={errors.sender?.message}
|
||||
/>
|
||||
<Select
|
||||
slotProps={{
|
||||
popper: { disablePortal: false, className: 'z-[10000]' },
|
||||
}}
|
||||
value={mode}
|
||||
onChange={(_, value) => setMode(value as string)}
|
||||
fullWidth
|
||||
>
|
||||
<Option key="smtp" value="smtp">
|
||||
SMTP
|
||||
</Option>
|
||||
<Option key="postmark" value="postmark">
|
||||
Postmark
|
||||
</Option>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
{...register('host')}
|
||||
id="host"
|
||||
name="host"
|
||||
label="SMTP Host"
|
||||
className="lg:col-span-4"
|
||||
placeholder="e.g. smtp.sendgrid.net"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.host)}
|
||||
helperText={errors.host?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('port')}
|
||||
id="port"
|
||||
name="port"
|
||||
label="Port"
|
||||
type="number"
|
||||
placeholder="587"
|
||||
className="lg:col-span-1"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.port)}
|
||||
helperText={errors.port?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('user')}
|
||||
id="user"
|
||||
label="SMTP Username"
|
||||
placeholder="SMTP Username"
|
||||
className="lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.user)}
|
||||
helperText={errors.user?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('password')}
|
||||
id="password"
|
||||
label="SMTP Password"
|
||||
type="password"
|
||||
placeholder="Enter SMTP password"
|
||||
className="lg:col-span-5"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.password)}
|
||||
helperText={errors.password?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register('method')}
|
||||
id="method"
|
||||
name="method"
|
||||
label="SMTP Auth Method"
|
||||
placeholder="LOGIN"
|
||||
hideEmptyHelperText
|
||||
className="lg:col-span-4"
|
||||
fullWidth
|
||||
error={Boolean(errors.method)}
|
||||
helperText={errors.method?.message}
|
||||
/>
|
||||
|
||||
<ControlledCheckbox
|
||||
name="secure"
|
||||
id="secure"
|
||||
label="Use SSL"
|
||||
className="lg:col-span-9"
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
{mode === 'postmark' ? <PostmarkSettings /> : <SMTPSettings />}
|
||||
<DeleteSMTPSettings />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
35
dashboard/src/utils/__generated__/graphql.ts
generated
35
dashboard/src/utils/__generated__/graphql.ts
generated
@@ -2524,6 +2524,7 @@ export type ConfigSystemConfigPostgres = {
|
||||
__typename?: 'ConfigSystemConfigPostgres';
|
||||
connectionString: ConfigSystemConfigPostgresConnectionString;
|
||||
database: Scalars['String'];
|
||||
disk?: Maybe<ConfigSystemConfigPostgresDisk>;
|
||||
enabled?: Maybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
@@ -2533,6 +2534,7 @@ export type ConfigSystemConfigPostgresComparisonExp = {
|
||||
_or?: InputMaybe<Array<ConfigSystemConfigPostgresComparisonExp>>;
|
||||
connectionString?: InputMaybe<ConfigSystemConfigPostgresConnectionStringComparisonExp>;
|
||||
database?: InputMaybe<ConfigStringComparisonExp>;
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
|
||||
enabled?: InputMaybe<ConfigBooleanComparisonExp>;
|
||||
};
|
||||
|
||||
@@ -2568,15 +2570,41 @@ export type ConfigSystemConfigPostgresConnectionStringUpdateInput = {
|
||||
storage?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresDisk = {
|
||||
__typename?: 'ConfigSystemConfigPostgresDisk';
|
||||
iops?: Maybe<Scalars['ConfigUint32']>;
|
||||
tput?: Maybe<Scalars['ConfigUint32']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresDiskComparisonExp = {
|
||||
_and?: InputMaybe<Array<ConfigSystemConfigPostgresDiskComparisonExp>>;
|
||||
_not?: InputMaybe<ConfigSystemConfigPostgresDiskComparisonExp>;
|
||||
_or?: InputMaybe<Array<ConfigSystemConfigPostgresDiskComparisonExp>>;
|
||||
iops?: InputMaybe<ConfigUint32ComparisonExp>;
|
||||
tput?: InputMaybe<ConfigUint32ComparisonExp>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresDiskInsertInput = {
|
||||
iops?: InputMaybe<Scalars['ConfigUint32']>;
|
||||
tput?: InputMaybe<Scalars['ConfigUint32']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresDiskUpdateInput = {
|
||||
iops?: InputMaybe<Scalars['ConfigUint32']>;
|
||||
tput?: InputMaybe<Scalars['ConfigUint32']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresInsertInput = {
|
||||
connectionString: ConfigSystemConfigPostgresConnectionStringInsertInput;
|
||||
database: Scalars['String'];
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskInsertInput>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ConfigSystemConfigPostgresUpdateInput = {
|
||||
connectionString?: InputMaybe<ConfigSystemConfigPostgresConnectionStringUpdateInput>;
|
||||
database?: InputMaybe<Scalars['String']>;
|
||||
disk?: InputMaybe<ConfigSystemConfigPostgresDiskUpdateInput>;
|
||||
enabled?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
@@ -23098,7 +23126,7 @@ export type GetRemoteAppMetricsQueryVariables = Exact<{ [key: string]: never; }>
|
||||
|
||||
export type GetRemoteAppMetricsQuery = { __typename?: 'query_root', filesAggregate: { __typename?: 'files_aggregate', aggregate?: { __typename?: 'files_aggregate_fields', count: number, sum?: { __typename?: 'files_sum_fields', size?: number | null } | null } | null }, usersAggregate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null } };
|
||||
|
||||
export type RemoteAppGetUsersFragment = { __typename?: 'users', id: any, createdAt: any, displayName: string, avatarUrl: string, email?: any | null, emailVerified: boolean, phoneNumber?: string | null, phoneNumberVerified: boolean, disabled: boolean, defaultRole: string, lastSeen?: any | null, locale: string, roles: Array<{ __typename?: 'authUserRoles', id: any, role: string }>, userProviders: Array<{ __typename?: 'authUserProviders', id: any, providerId: string }> };
|
||||
export type RemoteAppGetUsersFragment = { __typename?: 'users', id: any, createdAt: any, displayName: string, avatarUrl: string, email?: any | null, emailVerified: boolean, phoneNumber?: string | null, phoneNumberVerified: boolean, disabled: boolean, defaultRole: string, lastSeen?: any | null, locale: string, metadata?: any | null, roles: Array<{ __typename?: 'authUserRoles', id: any, role: string }>, userProviders: Array<{ __typename?: 'authUserProviders', id: any, providerId: string }> };
|
||||
|
||||
export type RemoteAppGetUsersQueryVariables = Exact<{
|
||||
where: Users_Bool_Exp;
|
||||
@@ -23107,7 +23135,7 @@ export type RemoteAppGetUsersQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type RemoteAppGetUsersQuery = { __typename?: 'query_root', users: Array<{ __typename?: 'users', id: any, createdAt: any, displayName: string, avatarUrl: string, email?: any | null, emailVerified: boolean, phoneNumber?: string | null, phoneNumberVerified: boolean, disabled: boolean, defaultRole: string, lastSeen?: any | null, locale: string, roles: Array<{ __typename?: 'authUserRoles', id: any, role: string }>, userProviders: Array<{ __typename?: 'authUserProviders', id: any, providerId: string }> }>, filteredUsersAggreggate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, usersAggregate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null } };
|
||||
export type RemoteAppGetUsersQuery = { __typename?: 'query_root', users: Array<{ __typename?: 'users', id: any, createdAt: any, displayName: string, avatarUrl: string, email?: any | null, emailVerified: boolean, phoneNumber?: string | null, phoneNumberVerified: boolean, disabled: boolean, defaultRole: string, lastSeen?: any | null, locale: string, metadata?: any | null, roles: Array<{ __typename?: 'authUserRoles', id: any, role: string }>, userProviders: Array<{ __typename?: 'authUserProviders', id: any, providerId: string }> }>, filteredUsersAggreggate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null }, usersAggregate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null } };
|
||||
|
||||
export type RemoteAppGetUsersCustomQueryVariables = Exact<{
|
||||
where: Users_Bool_Exp;
|
||||
@@ -23124,7 +23152,7 @@ export type RemoteAppGetUsersWholeQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type RemoteAppGetUsersWholeQuery = { __typename?: 'query_root', users: Array<{ __typename?: 'users', id: any, createdAt: any, displayName: string, avatarUrl: string, email?: any | null, emailVerified: boolean, phoneNumber?: string | null, phoneNumberVerified: boolean, disabled: boolean, defaultRole: string, lastSeen?: any | null, locale: string, roles: Array<{ __typename?: 'authUserRoles', id: any, role: string }>, userProviders: Array<{ __typename?: 'authUserProviders', id: any, providerId: string }> }>, usersAggregate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null } };
|
||||
export type RemoteAppGetUsersWholeQuery = { __typename?: 'query_root', users: Array<{ __typename?: 'users', id: any, createdAt: any, displayName: string, avatarUrl: string, email?: any | null, emailVerified: boolean, phoneNumber?: string | null, phoneNumberVerified: boolean, disabled: boolean, defaultRole: string, lastSeen?: any | null, locale: string, metadata?: any | null, roles: Array<{ __typename?: 'authUserRoles', id: any, role: string }>, userProviders: Array<{ __typename?: 'authUserProviders', id: any, providerId: string }> }>, usersAggregate: { __typename?: 'users_aggregate', aggregate?: { __typename?: 'users_aggregate_fields', count: number } | null } };
|
||||
|
||||
export type TotalUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -23623,6 +23651,7 @@ export const RemoteAppGetUsersFragmentDoc = gql`
|
||||
defaultRole
|
||||
lastSeen
|
||||
locale
|
||||
metadata
|
||||
roles {
|
||||
id
|
||||
role
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @nhost/docs
|
||||
|
||||
## 2.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- a58c5cf: fix: broken link
|
||||
|
||||
## 2.10.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9480489: fix: update docs performance info
|
||||
|
||||
## 2.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/docs",
|
||||
"version": "2.10.1",
|
||||
"version": "2.10.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "mintlify dev"
|
||||
|
||||
@@ -74,19 +74,4 @@ To setup dedicated resources for your project, you can either use the Dashboard
|
||||
|
||||
## Disk Performance
|
||||
|
||||
Services may require a disk provisioned to store data. For instance, [postgres](/guides/database/configuring-postgres#configuration-example) comes with a disk provisioned by default and [Nhost Run](/product/run) may [too](/guides/run/resources#storage). For these cases we provisioned SSD disks with the following performance:
|
||||
|
||||
- Baseline: 3000 IOPS
|
||||
- Baseline: 125Mbps of thoughput
|
||||
- Every 50GB: +350 IOPS and +15Mbps of throughput
|
||||
|
||||
For example, the following disk sizes will have the following performance:
|
||||
|
||||
| Size | IOPS | Throughput |
|
||||
| ---- | ---- | ---------- |
|
||||
| 1 | 3000| 125 |
|
||||
| 10 | 3000| 125 |
|
||||
| 49 | 3000| 125 |
|
||||
| 50 | 3350| 140 |
|
||||
| 100 | 3700 | 155 |
|
||||
| 300 | 5100 | 215 |
|
||||
By default disks are provisioned with a capacity for 3000 IOPS and 125 Mbps of throughput. If you need higher performance don't hesitate to contact us.
|
||||
|
||||
@@ -50,7 +50,7 @@ In addition, thanks to [pgvector](https://github.com/pgvector/pgvector) you can
|
||||
<CardGroup cols={4}>
|
||||
<Card title="Enabling Service" icon="square-1" href="../guides/ai/enabling-service">
|
||||
</Card>
|
||||
<Card title="Local Development" icon="square-2" href="../guides/ai/local_development">
|
||||
<Card title="Local Development" icon="square-2" href="../guides/ai/local-development">
|
||||
</Card>
|
||||
<Card title="Auto-Embeddings" icon="square-3" href="../guides/ai/auto-embeddings">
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/cli
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [304065a]
|
||||
- @nhost/nhost-js@3.1.0
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/cli",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"main": "src/index.mjs",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/codegen-react-apollo
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
- @nhost/react-apollo@11.0.2
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-apollo",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost-examples/codegen-react-query
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-query",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/react-urql
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
- @nhost/react-urql@8.0.2
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-urql",
|
||||
"private": true,
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -23,7 +23,7 @@ The following endpoints are now exposed:
|
||||
- `http://localhost:3030`: Nhost Dashboard
|
||||
- `http://localhost:1337`: Hasura Console
|
||||
- `http://localhost:8025`: Mailhog SMTP testing dashboard
|
||||
- `http://localhost:9090`: Traefik dashboad
|
||||
- `http://localhost:9090`: Traefik dashboard
|
||||
|
||||
## Running the Nhost dashboard locally
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/multi-tenant-one-to-many
|
||||
|
||||
## 2.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [304065a]
|
||||
- @nhost/nhost-js@3.1.0
|
||||
|
||||
## 2.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/multi-tenant-one-to-many",
|
||||
"private": true,
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @nhost-examples/nextjs
|
||||
|
||||
## 0.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
- @nhost/react-apollo@11.0.2
|
||||
- @nhost/nextjs@2.1.11
|
||||
|
||||
## 0.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/node-storage
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [304065a]
|
||||
- @nhost/nhost-js@3.1.0
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/node-storage",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"description": "This is an example of how to use the Storage with Node.js",
|
||||
"main": "src/index.mjs",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/nextjs-server-components
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [304065a]
|
||||
- @nhost/nhost-js@3.1.0
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs-server-components",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/react-apollo
|
||||
|
||||
## 0.8.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
- @nhost/react-apollo@11.0.2
|
||||
|
||||
## 0.8.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.9.9",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost-examples/react-gqty
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-gqty",
|
||||
"private": true,
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# @nhost-examples/vue-apollo
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [304065a]
|
||||
- @nhost/nhost-js@3.1.0
|
||||
- @nhost/apollo@7.0.0
|
||||
- @nhost/vue@2.5.2
|
||||
|
||||
## 0.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-apollo",
|
||||
"private": true,
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost-examples/vue-quickstart
|
||||
|
||||
## 0.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@7.0.0
|
||||
- @nhost/vue@2.5.2
|
||||
|
||||
## 0.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-quickstart",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 7.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [304065a]
|
||||
- @nhost/nhost-js@3.1.0
|
||||
|
||||
## 6.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "6.2.1",
|
||||
"version": "7.0.0",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @nhost/react-apollo
|
||||
|
||||
## 11.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@7.0.0
|
||||
- @nhost/react@3.4.2
|
||||
|
||||
## 11.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-apollo",
|
||||
"version": "11.0.1",
|
||||
"version": "11.0.2",
|
||||
"description": "Nhost React Apollo client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/react-urql
|
||||
|
||||
## 8.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@3.4.2
|
||||
|
||||
## 8.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-urql",
|
||||
"version": "8.0.1",
|
||||
"version": "8.0.2",
|
||||
"description": "Nhost React URQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/graphql-js
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 304065a: feat: add `setHeaders` method enabling global configuration of storage, graphql, and functions client headers, alongside added support for passing specific headers with individual calls
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user