Compare commits

...

23 Commits

Author SHA1 Message Date
github-actions[bot]
6cec04bd6f chore: update versions (#2680)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.14.0

### Minor Changes

-   a448d7d: feat: allow configuring postmark and delete SMTP settings

## @nhost/docs@2.10.2

### Patch Changes

-   9480489: fix: update docs performance info

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-25 13:13:32 +01:00
David Barroso
a448d7d182 feat (dashboard): allow configuring postmark and deleting SMTP settings (#2678)
Co-authored-by: Hassan Ben Jobrane <hsanbenjobrane@gmail.com>
2024-04-25 12:58:47 +01:00
David Barroso
948048940e fix (docs): update docs performance info (#2679)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-04-25 13:50:45 +02:00
github-actions[bot]
5e91221d5a chore: update versions (#2672)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.3

### Patch Changes

-   5924bc3: fix: include password in `GetSmtpSettings` query
- c5ad634: fix: resolved an issue where one-click install links were
broken on Safari
- 7278991: fix: update graphql auto-embeddings configuration to use
String type for model field

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-18 15:49:01 +01:00
Hassan Ben Jobrane
7278991a59 fix: dashboard: update graphql auto-embeddings configuration to use String type for model field (#2674) 2024-04-18 15:22:32 +01:00
Hassan Ben Jobrane
5924bc3248 fix: dashboard: include password in GetSmtpSettings query (#2673) 2024-04-18 15:14:02 +01:00
Hassan Ben Jobrane
c5ad634799 fix: dashboard: check for undefined router query on Safari when accessing base64config (#2671) 2024-04-18 10:04:18 +01:00
github-actions[bot]
426b93a19f chore: update versions (#2670)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.2

### Patch Changes

-   026f84f: fix: use configuration server URL from environment variable

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-17 14:15:04 +01:00
Hassan Ben Jobrane
026f84f466 fix: dashboard: use config server url from env variable (#2669)
related to https://github.com/nhost/cli/issues/862
2024-04-17 14:02:11 +01:00
github-actions[bot]
384fac00b1 chore: update versions (#2664)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.1

### Patch Changes

-   7e9a2ce: fix: resolve issue where run services form fails to open

## @nhost/docs@2.10.1

### Patch Changes

-   9525fd7: fix: update AI docs for 0.5.0
-   076fd4a: fix: update permissions page to indicate we use jsonpath

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-16 10:53:36 +01:00
Hassan Ben Jobrane
7e9a2ce136 fix: dashboard: fix isPlatform check when opening run services form (#2666) 2024-04-16 10:38:28 +01:00
David Barroso
076fd4a7c0 fix (docs): update permissions page to indicate we use jsonpath (#2663) 2024-04-15 14:50:21 +02:00
David Barroso
9525fd74b3 fix (docs): update AI docs for 0.5.0 (#2662) 2024-04-15 14:33:59 +02:00
github-actions[bot]
8a2bc98214 chore: update versions (#2648)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.13.0

### Minor Changes

-   dd5d262: feat: add model field to the auto-embeddings form
- 09962be: feat: enable settings and run services when running the
dashboard locally
- 9cdecb6: feat: enable users to update their email address from the
account settings page

## @nhost/docs@2.10.0

### Minor Changes

-   87ae23b: feat: added "advanced graphql" documentation

### Patch Changes

-   b2be364: feat: added postmark native integration

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-15 13:12:24 +01:00
Hassan Ben Jobrane
dd5d262062 feat: dashboard: add model field to auto-embeddings form (#2661)
fixes https://github.com/nhost/nhost/issues/2660
2024-04-15 12:51:57 +01:00
Hassan Ben Jobrane
09962bef37 feat: dashboard: enable local settings (#2647)
fixes https://github.com/nhost/projects/issues/66
2024-04-15 12:49:20 +01:00
Hassan Ben Jobrane
9cdecb6b23 feat: dashboard: add email field to account settings (#2612)
fixes https://github.com/nhost/nhost/issues/2561
2024-04-11 10:47:03 +01:00
David Barroso
e7eb90318e fix: observability: amend export of graphql dashboard (#2655) 2024-04-11 11:44:56 +02:00
Hassan Ben Jobrane
f67f22d321 chore: update @google-cloud/translate to 8.2.0 (#2654) 2024-04-11 10:00:13 +01:00
David Barroso
87ae23ba05 feat (docs/observability): added docs and observability dashboard for "advanced graphql" (#2653)
Co-authored-by: Nuno Pato <nunopato@gmail.com>
2024-04-11 10:22:54 +02:00
David Barroso
b2be3642aa feat (docs): added postmark native integration (#2636) 2024-04-09 09:36:48 +02:00
github-actions[bot]
1230081ce6 chore: update versions (#2640)
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @nhost/dashboard@1.12.2

### Patch Changes

-   c195c51: fix: send email upon signin for unverified users

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-05 12:48:55 +01:00
Hassan Ben Jobrane
c195c517de fix: send email upon signin for unverified users (#2639) 2024-04-05 11:48:14 +01:00
132 changed files with 4593 additions and 1783 deletions

View File

@@ -1,5 +1,45 @@
# @nhost/dashboard
## 1.14.0
### Minor Changes
- a448d7d: feat: allow configuring postmark and delete SMTP settings
## 1.13.3
### Patch Changes
- 5924bc3: fix: include password in `GetSmtpSettings` query
- c5ad634: fix: resolved an issue where one-click install links were broken on Safari
- 7278991: fix: update graphql auto-embeddings configuration to use String type for model field
## 1.13.2
### Patch Changes
- 026f84f: fix: use configuration server URL from environment variable
## 1.13.1
### Patch Changes
- 7e9a2ce: fix: resolve issue where run services form fails to open
## 1.13.0
### Minor Changes
- dd5d262: feat: add model field to the auto-embeddings form
- 09962be: feat: enable settings and run services when running the dashboard locally
- 9cdecb6: feat: enable users to update their email address from the account settings page
## 1.12.2
### Patch Changes
- c195c51: fix: send email upon signin for unverified users
## 1.12.1
### Patch Changes

View File

@@ -28,6 +28,7 @@ ENV NEXT_PUBLIC_NHOST_STORAGE_URL __NEXT_PUBLIC_NHOST_STORAGE_URL__
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__
ENV NEXT_PUBLIC_NHOST_CONFIGSERVER_URL __NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__
RUN yarn global add pnpm@8.10.5
COPY .gitignore .gitignore

View File

@@ -21,5 +21,6 @@ find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_STORAGE_URL__~${NEXT_
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL__~${NEXT_PUBLIC_NHOST_HASURA_CONSOLE_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_MIGRATIONS_API_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_HASURA_API_URL__~${NEXT_PUBLIC_NHOST_HASURA_API_URL}~g" {} +
find dashboard -type f -exec sed -i "s~__NEXT_PUBLIC_NHOST_CONFIGSERVER_URL__~${NEXT_PUBLIC_NHOST_CONFIGSERVER_URL}~g" {} +
exec "$@"

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "1.12.1",
"version": "1.14.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",

View File

@@ -0,0 +1,30 @@
import { useDialog } from '@/components/common/DialogProvider';
import { Button } from '@/components/ui/v2/Button';
import { Text } from '@/components/ui/v2/Text';
export default function ApplyLocalSettingsDialog() {
const { closeDialog } = useDialog();
return (
<div className="flex flex-col gap-4 px-6 pb-6">
<div className="flex flex-col gap-2">
<Text color="secondary">
Run{' '}
<code className="px-1 py-px mx-1 rounded-md bg-slate-500 text-slate-100">
$ nhost up
</code>{' '}
using the cli to apply your changes
</Text>
</div>
<Button
className="w-full"
color="primary"
onClick={() => closeDialog()}
autoFocus
>
OK
</Button>
</div>
);
}

View File

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

View File

@@ -11,7 +11,6 @@ import { useNotFoundRedirect } from '@/features/projects/common/hooks/useNotFoun
import { useProjectRoutes } from '@/features/projects/common/hooks/useProjectRoutes';
import { NextSeo } from 'next-seo';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { twMerge } from 'tailwind-merge';
export interface ProjectLayoutProps extends AuthenticatedLayoutProps {
@@ -48,15 +47,16 @@ function ProjectLayoutContent({
useNotFoundRedirect();
useEffect(() => {
if (isPlatform || !router.isReady) {
return;
}
// useEffect(() => {
// if (isPlatform || !router.isReady) {
// return;
// }
if (isRestrictedPath) {
router.push('/local/local');
}
}, [isPlatform, isRestrictedPath, router]);
// TODO // Double check what restricted path means here
// if (isRestrictedPath) {
// router.push('/local/local');
// }
// }, [isPlatform, isRestrictedPath, router]);
if (isRestrictedPath || loading) {
return <LoadingScreen />;

View File

@@ -7,6 +7,7 @@ import { List } from '@/components/ui/v2/List';
import type { ListItemButtonProps } from '@/components/ui/v2/ListItem';
import { ListItem } from '@/components/ui/v2/ListItem';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
@@ -61,6 +62,7 @@ export default function SettingsSidebar({
className,
...props
}: SettingsSidebarProps) {
const isPlatform = useIsPlatform();
const [expanded, setExpanded] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -95,7 +97,7 @@ export default function SettingsSidebar({
<>
<Backdrop
open={expanded}
className="absolute top-0 left-0 bottom-0 right-0 z-[34] md:hidden"
className="absolute bottom-0 left-0 right-0 top-0 z-[34] md:hidden"
role="button"
tabIndex={-1}
onClick={() => setExpanded(false)}
@@ -112,7 +114,7 @@ export default function SettingsSidebar({
<Box
component="aside"
className={twMerge(
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pt-2 pb-17 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
'absolute top-0 z-[35] h-full w-full overflow-auto border-r-1 px-2 pb-17 pt-2 motion-safe:transition-transform md:relative md:z-0 md:h-full md:py-2.5 md:transition-none',
expanded ? 'translate-x-0' : '-translate-x-full md:translate-x-0',
className,
)}
@@ -181,7 +183,12 @@ export default function SettingsSidebar({
SMTP
</SettingsNavLink>
<SettingsNavLink href="/git" exact={false} onClick={handleSelect}>
<SettingsNavLink
href="/git"
exact={false}
onClick={handleSelect}
disabled={!isPlatform}
>
Git
</SettingsNavLink>

View File

@@ -0,0 +1,80 @@
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { Input } from '@/components/ui/v2/Input';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useNhostClient, useUserData } from '@nhost/nextjs';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
email: Yup.string().label('Email').email().required(),
});
export type EmailSettingFormValues = Yup.InferType<typeof validationSchema>;
export default function EmailSetting() {
const nhost = useNhostClient();
const { email } = useUserData();
const form = useForm<EmailSettingFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { email },
resolver: yupResolver(validationSchema),
});
const { register, formState } = form;
const isDirty = Object.keys(formState.dirtyFields).length > 0;
async function handleSubmit(formValues: EmailSettingFormValues) {
await execPromiseWithErrorToast(
async () => {
await nhost.auth.changeEmail({
newEmail: formValues.email,
options: {
redirectTo: `${window.location.origin}/account`,
},
});
form.reset({ email: formValues.email });
},
{
loadingMessage: 'Updating your email...',
successMessage:
'Please check your inbox. Follow the link to finalize changing your email.',
errorMessage:
'An error occurred while trying to update your email. Please try again.',
},
);
}
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<SettingsContainer
title="Update your email"
slotProps={{
submitButton: {
disabled: !isDirty,
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row lg:grid-cols-5"
>
<Input
{...register('email')}
className="col-span-2"
id="email"
spellCheck="false"
autoCapitalize="none"
type="email"
label="Email"
hideEmptyHelperText
fullWidth
helperText={formState.errors.email?.message}
error={Boolean(formState.errors.email)}
/>
</SettingsContainer>
</Form>
</FormProvider>
);
}

View File

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

View File

@@ -1,4 +1,5 @@
import { useDialog } from '@/components/common/DialogProvider';
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Form } from '@/components/form/Form';
import { Box } from '@/components/ui/v2/Box';
import { Button } from '@/components/ui/v2/Button';
@@ -6,6 +7,7 @@ import { ArrowsClockwise } from '@/components/ui/v2/icons/ArrowsClockwise';
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { Input } from '@/components/ui/v2/Input';
import { Option } from '@/components/ui/v2/Option';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useAdminApolloClient } from '@/features/projects/common/hooks/useAdminApolloClient';
@@ -20,11 +22,18 @@ import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const AUTO_EMBEDDINGS_MODELS = [
'text-embedding-ada-002',
'text-embedding-3-small',
'text-embedding-3-large',
];
export const validationSchema = Yup.object({
name: Yup.string().required('The name is required.'),
schemaName: Yup.string().required('The schema is required'),
tableName: Yup.string().required('The table is required'),
columnName: Yup.string().required('The column is required'),
name: Yup.string().required('The name field is required.'),
model: Yup.string().oneOf(AUTO_EMBEDDINGS_MODELS),
schemaName: Yup.string().required('The schema field is required'),
tableName: Yup.string().required('The table field is required'),
columnName: Yup.string().required('The column field is required'),
query: Yup.string(),
mutation: Yup.string(),
});
@@ -40,7 +49,7 @@ export interface AutoEmbeddingsFormProps extends DialogFormProps {
/**
* if there is initialData then it's an update operation
*/
initialData?: AutoEmbeddingsFormValues;
initialData?: AutoEmbeddingsFormValues & { model: string };
/**
* Function to be called when the operation is cancelled.
@@ -74,7 +83,10 @@ export default function AutoEmbeddingsForm({
});
const form = useForm<AutoEmbeddingsFormValues>({
defaultValues: initialData,
defaultValues: {
...initialData,
model: initialData?.model ?? 'text-embedding-ada-002',
},
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
@@ -106,7 +118,9 @@ export default function AutoEmbeddingsForm({
}
await insertGraphiteAutoEmbeddingsConfiguration({
variables: values,
variables: {
...values,
},
});
};
@@ -129,9 +143,9 @@ export default function AutoEmbeddingsForm({
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex h-full flex-col gap-4 overflow-hidden"
className="flex flex-col h-full gap-4 overflow-hidden"
>
<div className="flex flex-1 flex-col space-y-6 overflow-auto px-6">
<div className="flex flex-col flex-1 px-6 space-y-6 overflow-auto">
<Input
{...register('name')}
id="name"
@@ -141,7 +155,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Name of the Auto-Embeddings">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -155,6 +169,36 @@ export default function AutoEmbeddingsForm({
autoComplete="off"
autoFocus
/>
<ControlledSelect
slotProps={{
popper: { disablePortal: false, className: 'z-[10000]' },
}}
id="model"
name="model"
label={
<Box className="flex flex-row items-center space-x-2">
<Text>Model</Text>
<Tooltip title="Auto-Embeddings Model">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
</Box>
}
fullWidth
error={!!errors?.model?.message}
helperText={errors?.model?.message}
>
{AUTO_EMBEDDINGS_MODELS.map((model) => (
<Option key={model} value={model}>
{model}
</Option>
))}
</ControlledSelect>
<Input
{...register('schemaName')}
id="schemaName"
@@ -164,7 +208,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title={<span>Schema where the table belongs to</span>}>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -186,7 +230,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Table Name">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -208,7 +252,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Column name">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -230,7 +274,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Query">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -254,7 +298,7 @@ export default function AutoEmbeddingsForm({
<Tooltip title="Mutation">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -271,7 +315,7 @@ export default function AutoEmbeddingsForm({
/>
</div>
<Box className="flex w-full flex-row justify-between rounded border-t px-6 py-4">
<Box className="flex flex-row justify-between w-full px-6 py-4 border-t rounded">
<Button variant="outlined" color="secondary" onClick={onCancel}>
Cancel
</Button>

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
@@ -12,6 +13,7 @@ import { Switch } from '@/components/ui/v2/Switch';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { ComputeFormSection } from '@/features/services/components/ServiceForm/components/ComputeFormSection';
import {
@@ -20,6 +22,7 @@ import {
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { getToastStyleProps } from '@/utils/constants/settings';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
@@ -50,9 +53,13 @@ const validationSchema = Yup.object({
export type AISettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function AISettings() {
const { maintenanceActive } = useUI();
const isPlatform = useIsPlatform();
const { openDialog } = useDialog();
const [updateConfig] = useUpdateConfigMutation();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { currentProject } = useCurrentWorkspaceAndProject();
const [aiServiceEnabled, setAIServiceEnabled] = useState(true);
@@ -67,6 +74,7 @@ export default function AISettings() {
variables: {
appId: currentProject.id,
},
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data: graphiteVersionsData, loading: loadingGraphiteVersionsData } =
@@ -74,6 +82,7 @@ export default function AISettings() {
variables: {
software: Software_Type_Enum.Graphite,
},
skip: !isPlatform,
});
const graphiteVersions = graphiteVersionsData?.softwareVersions || [];
@@ -98,8 +107,8 @@ export default function AISettings() {
reValidateMode: 'onSubmit',
defaultValues: {
version: {
label: ai?.version ?? availableVersions?.at(0)?.label,
value: ai?.version ?? availableVersions?.at(0)?.value,
label: ai?.version || availableVersions?.at(0)?.label || '',
value: ai?.version || availableVersions?.at(0)?.value || '',
},
webhookSecret: '',
organization: '',
@@ -225,6 +234,18 @@ export default function AISettings() {
});
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'AI settings are being updated...',
@@ -247,7 +268,7 @@ export default function AISettings() {
return (
<Box className="space-y-4" sx={{ backgroundColor: 'background.default' }}>
<Box className="flex flex-row items-center justify-between rounded-lg border-1 p-4">
<Box className="flex flex-row items-center justify-between p-4 rounded-lg border-1">
<Text className="text-lg font-semibold">Enable AI service</Text>
<Switch
checked={aiServiceEnabled}
@@ -270,54 +291,58 @@ export default function AISettings() {
className="flex flex-col"
>
<Box className="space-y-4">
{availableVersions.length > 0 && (
<Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2">
<Text className="text-lg font-semibold">Version</Text>
<Tooltip title="Version of the service to use.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
color="primary"
/>
</Tooltip>
</Box>
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
}
});
const result = [...matched, ...otherOptions];
return result;
}}
fullWidth
className="col-span-4"
options={availableVersions}
error={!!formState.errors?.version?.message}
helperText={formState.errors?.version?.message}
showCustomOption="auto"
customOptionLabel={(value) =>
`Use custom value: "${value}"`
}
/>
<Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2">
<Text className="text-lg font-semibold">Version</Text>
<Tooltip title="Version of the service to use.">
<InfoIcon
aria-label="Info"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
</Box>
)}
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();
const matched = [];
const otherOptions = [];
options.forEach((option) => {
const optionLabelLower = option.label.toLowerCase();
if (optionLabelLower.startsWith(inputValueLower)) {
matched.push(option);
} else {
otherOptions.push(option);
}
});
const result = [...matched, ...otherOptions];
return result;
}}
fullWidth
className="col-span-4"
options={availableVersions}
error={
!!formState.errors?.version?.message ||
!!formState.errors?.version?.value?.message
}
helperText={
formState.errors?.version?.message ||
formState.errors?.version?.value?.message
}
showCustomOption="auto"
customOptionLabel={(value) =>
`Use custom value: "${value}"`
}
/>
</Box>
<Box className="space-y-2">
<Box className="flex flex-row items-center space-x-2">
@@ -327,7 +352,7 @@ export default function AISettings() {
<Tooltip title="Used to validate requests between postgres and the AI service. The AI service will also include the header X-Graphite-Webhook-Secret with this value set when calling external webhooks so the source of the request can be validated.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -351,26 +376,28 @@ export default function AISettings() {
<Tooltip title="Dedicated resources allocated for the service.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
</Box>
<Alert
severity="info"
className="flex items-center justify-between space-x-2"
>
<span>{getAIResourcesCost()}</span>
<b>
$
{parseFloat(
(
aiSettingsFormValues.compute.cpu * COST_PER_VCPU
).toFixed(2),
)}
</b>
</Alert>
{isPlatform ? (
<Alert
severity="info"
className="flex items-center justify-between space-x-2"
>
<span>{getAIResourcesCost()}</span>
<b>
$
{parseFloat(
(
aiSettingsFormValues.compute.cpu * COST_PER_VCPU
).toFixed(2),
)}
</b>
</Alert>
) : null}
<ComputeFormSection />
</Box>
@@ -389,7 +416,7 @@ export default function AISettings() {
<Tooltip title="Key to use for authenticating API requests to OpenAI">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -412,7 +439,7 @@ export default function AISettings() {
<Tooltip title="Optional. OpenAI organization to use.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -440,7 +467,7 @@ export default function AISettings() {
<Tooltip title="How often to run the job that keeps embeddings up to date.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>

View File

@@ -1,8 +1,11 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
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 { useUpdateConfigMutation } from '@/utils/__generated__/graphql';
import { useState } from 'react';
@@ -23,11 +26,14 @@ export default function DisableAIServiceConfirmationDialog({
onCancel,
onServiceDisabled,
}: DisableAIServiceConfirmationDialogProps) {
const { closeDialog } = useDialog();
const isPlatform = useIsPlatform();
const { openDialog, closeDialog } = useDialog();
const localMimirClient = useLocalMimirClient();
const [loading, setLoading] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
async function handleClick() {
setLoading(true);
@@ -45,6 +51,18 @@ export default function DisableAIServiceConfirmationDialog({
onServiceDisabled();
closeDialog();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Disabling the AI service...',

View File

@@ -1,16 +1,21 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -26,15 +31,19 @@ export type AllowedEmailSettingsFormValues = Yup.InferType<
>;
export default function AllowedEmailDomainsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { email, emailDomains } = data?.config?.auth?.user || {};
@@ -54,6 +63,17 @@ export default function AllowedEmailDomainsSettings() {
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (!loading && email && emailDomains) {
form.reset({
enabled:
email?.allowed?.length > 0 || emailDomains?.allowed?.length > 0,
allowedEmails: email?.allowed?.join(', ') || '',
allowedEmailDomains: emailDomains?.allowed?.join(', ') || '',
});
}
}, [loading, form, email, emailDomains]);
if (loading) {
return (
<ActivityIndicator
@@ -105,6 +125,18 @@ export default function AllowedEmailDomainsSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Allowed email settings are being updated...',

View File

@@ -1,16 +1,21 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -23,15 +28,19 @@ export type AllowedRedirectURLFormValues = Yup.InferType<
>;
export default function AllowedRedirectURLsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { allowedUrls } = data?.config?.auth?.redirections || {};
@@ -44,6 +53,14 @@ export default function AllowedRedirectURLsSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && allowedUrls) {
form.reset({
allowedUrls: allowedUrls?.join(', ') || '',
});
}
}, [loading, allowedUrls, form]);
if (loading) {
return (
<ActivityIndicator
@@ -82,6 +99,18 @@ export default function AllowedRedirectURLsSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Allowed redirect URL settings are being updated...',

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,15 +25,19 @@ const validationSchema = Yup.object({
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
export default function AnonymousSignInSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enabled } = data?.config?.auth?.method?.anonymous || {};
@@ -41,6 +50,12 @@ export default function AnonymousSignInSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({ enabled });
}
}, [loading, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -75,6 +90,18 @@ export default function AnonymousSignInSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Anonymous sign-in settings are being updated...',

View File

@@ -1,3 +1,5 @@
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';
@@ -7,16 +9,19 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useTheme } from '@mui/material';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -53,15 +58,19 @@ export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function AppleProviderSettings() {
const theme = useTheme();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, enabled, keyId, privateKey, teamId } =
@@ -79,6 +88,18 @@ export default function AppleProviderSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
teamId: teamId || '',
keyId: keyId || '',
clientId: clientId || '',
privateKey: privateKey || '',
enabled: enabled || false,
});
}
}, [loading, teamId, keyId, clientId, privateKey, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -119,6 +140,18 @@ export default function AppleProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Apple settings are being updated...',
@@ -236,7 +269,7 @@ export default function AppleProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
Software_Type_Enum,
@@ -11,8 +14,10 @@ import {
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -28,21 +33,26 @@ export type AuthServiceVersionFormValues = Yup.InferType<
>;
export default function AuthServiceVersionSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data: authVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Auth,
},
skip: !isPlatform,
});
const { version } = data?.config?.auth || {};
@@ -59,10 +69,21 @@ export default function AuthServiceVersionSettings() {
const form = useForm<AuthServiceVersionFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } },
defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && version) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) {
return (
<ActivityIndicator
@@ -97,6 +118,18 @@ export default function AuthServiceVersionSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Auth version is being updated...',
@@ -120,12 +153,20 @@ export default function AuthServiceVersionSettings() {
}}
docsLink="https://github.com/nhost/hasura-auth/releases"
docsTitle="the latest releases"
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
>
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();

View File

@@ -1,3 +1,5 @@
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';
@@ -8,15 +10,18 @@ import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { BaseProviderSettings } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -46,15 +51,19 @@ const validationSchema = Yup.object({
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function AzureADProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, tenant, enabled } =
@@ -71,6 +80,17 @@ export default function AzureADProviderSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
tenant: tenant || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, tenant, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -108,6 +128,18 @@ export default function AzureADProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Azure AD settings are being updated...',
@@ -182,7 +214,7 @@ export default function AzureADProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,16 +1,21 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -24,15 +29,19 @@ const validationSchema = Yup.object({
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
export default function BlockedEmailSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { email, emailDomains } = data?.config?.auth?.user || {};
@@ -51,6 +60,17 @@ export default function BlockedEmailSettings() {
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (!loading && email && emailDomains) {
form.reset({
enabled:
email?.blocked?.length > 0 || emailDomains?.blocked?.length > 0,
blockedEmails: email?.blocked?.join(', ') || '',
blockedEmailDomains: emailDomains?.blocked?.join(', ') || '',
});
}
}, [loading, email, emailDomains, form]);
if (loading) {
return (
<ActivityIndicator
@@ -112,6 +132,18 @@ export default function BlockedEmailSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage:

View File

@@ -1,16 +1,21 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -21,15 +26,19 @@ const validationSchema = Yup.object({
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
export default function ClientURLSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientUrl, allowedUrls } = data?.config?.auth?.redirections || {};
@@ -42,6 +51,12 @@ export default function ClientURLSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && clientUrl) {
form.reset({ clientUrl });
}
}, [loading, clientUrl, form]);
if (loading) {
return (
<ActivityIndicator
@@ -77,6 +92,18 @@ export default function ClientURLSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Client URL is being updated...',

View File

@@ -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>
);
}

View File

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

View File

@@ -1,14 +1,19 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
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 {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/utils/__generated__/graphql';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -19,15 +24,19 @@ const validationSchema = Yup.object({
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
export default function DisableNewUsersSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<DisableNewUsersFormValues>({
@@ -37,6 +46,14 @@ export default function DisableNewUsersSettings() {
},
});
useEffect(() => {
if (!loading) {
form.reset({
disabled: !data?.config?.auth?.signUp?.enabled,
});
}
}, [loading, data, form]);
if (loading) {
return (
<ActivityIndicator
@@ -73,6 +90,18 @@ export default function DisableNewUsersSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Disabling new user sign ups...',

View File

@@ -1,3 +1,5 @@
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';
@@ -12,28 +14,35 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function DiscordProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -49,6 +58,16 @@ export default function DiscordProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -89,6 +108,18 @@ export default function DiscordProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Discord settings are being updated...',
@@ -153,7 +184,7 @@ export default function DiscordProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
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';
@@ -6,13 +8,16 @@ 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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -29,15 +34,19 @@ const validationSchema = Yup.object({
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
export default function EmailAndPasswordSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, error, loading } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { hibpEnabled, emailVerificationRequired, passwordMinLength } =
@@ -53,6 +62,22 @@ export default function EmailAndPasswordSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
hibpEnabled,
emailVerificationRequired,
passwordMinLength,
});
}
}, [
loading,
hibpEnabled,
emailVerificationRequired,
passwordMinLength,
form,
]);
if (loading) {
return (
<ActivityIndicator
@@ -87,6 +112,18 @@ export default function EmailAndPasswordSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: `Email and password sign-in settings are being updated...`,

View File

@@ -1,3 +1,5 @@
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';
@@ -12,28 +14,35 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function FacebookProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -49,6 +58,16 @@ export default function FacebookProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -89,6 +108,18 @@ export default function FacebookProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Facebook settings are being updated...',
@@ -153,7 +184,7 @@ export default function FacebookProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
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';
@@ -12,30 +14,37 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useTheme } from '@mui/material';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function GitHubProviderSettings() {
const theme = useTheme();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -51,6 +60,16 @@ export default function GitHubProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -91,6 +110,18 @@ export default function GitHubProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'GitHub settings are being updated...',
@@ -159,7 +190,7 @@ export default function GitHubProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
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';
@@ -12,28 +14,35 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function GoogleProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -49,6 +58,16 @@ export default function GoogleProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -89,6 +108,18 @@ export default function GoogleProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Google settings are being updated...',
@@ -153,7 +184,7 @@ export default function GoogleProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { ControlledSelect } from '@/components/form/ControlledSelect';
import { Form } from '@/components/form/Form';
@@ -5,17 +7,20 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Option } from '@/components/ui/v2/Option';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import {
AUTH_GRAVATAR_DEFAULT,
AUTH_GRAVATAR_RATING,
} from '@/utils/constants/settings';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -29,15 +34,19 @@ const validationSchema = Yup.object({
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
export default function GravatarSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const {
@@ -56,6 +65,16 @@ export default function GravatarSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
default: defaultGravatar || '',
rating: rating || '',
enabled: enabled || false,
});
}
}, [loading, defaultGravatar, rating, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -91,6 +110,18 @@ export default function GravatarSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Gravatar settings are being updated...',

View File

@@ -1,3 +1,5 @@
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';
@@ -12,28 +14,35 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function LinkedInProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -49,6 +58,16 @@ export default function LinkedInProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -89,6 +108,18 @@ export default function LinkedInProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'LinkedIn settings are being updated...',
@@ -153,7 +184,7 @@ export default function LinkedInProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,16 +1,21 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -23,15 +28,19 @@ const validationSchema = Yup.object({
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function MFASettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enabled, issuer } = data?.config?.auth?.totp || {};
@@ -45,6 +54,15 @@ export default function MFASettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && issuer && enabled) {
form.reset({
issuer,
enabled,
});
}
}, [loading, issuer, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -78,6 +96,18 @@ export default function MFASettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage:

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,15 +25,19 @@ const validationSchema = Yup.object({
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
export default function MagicLinkSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enabled } = data?.config?.auth?.method?.emailPasswordless || {};
@@ -41,6 +50,12 @@ export default function MagicLinkSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({ enabled });
}
}, [loading, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -75,6 +90,18 @@ export default function MagicLinkSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Magic Link settings are being updated...',

View File

@@ -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>
);
}

View File

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

View File

@@ -1,3 +1,5 @@
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';
@@ -7,14 +9,17 @@ import { Option } from '@/components/ui/v2/Option';
import { Select } from '@/components/ui/v2/Select';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import Image from 'next/image';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -44,15 +49,19 @@ const validationSchema = Yup.object({
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
export default function SMSSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, error, loading } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { accountSid, authToken, messagingServiceId } =
@@ -70,6 +79,17 @@ export default function SMSSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
accountSid: accountSid || '',
authToken: authToken || '',
messagingServiceId: messagingServiceId || '',
enabled: enabled || false,
});
}
}, [loading, accountSid, authToken, messagingServiceId, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -114,6 +134,18 @@ export default function SMSSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'SMS settings are being updated...',

View File

@@ -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>
);
}

View File

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

View File

@@ -1,16 +1,21 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetAuthenticationSettingsDocument,
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -28,15 +33,19 @@ const validationSchema = Yup.object({
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
export default function SessionSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetAuthenticationSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { accessToken, refreshToken } = data?.config?.auth?.session || {};
@@ -50,6 +59,15 @@ export default function SessionSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && accessToken && refreshToken) {
form.reset({
accessTokenExpiresIn: accessToken?.expiresIn || 900,
refreshTokenExpiresIn: refreshToken?.expiresIn || 43200,
});
}
}, [loading, accessToken, refreshToken, form]);
if (loading) {
return (
<ActivityIndicator
@@ -86,6 +104,18 @@ export default function SessionSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Session settings are being updated...',

View File

@@ -1,3 +1,5 @@
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';
@@ -12,28 +14,35 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function SpotifyProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -49,6 +58,16 @@ export default function SpotifyProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -89,6 +108,18 @@ export default function SpotifyProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Spotify settings are being updated...',
@@ -153,7 +184,7 @@ export default function SpotifyProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
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';
@@ -12,30 +14,37 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useTheme } from '@mui/material';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function TwitchProviderSettings() {
const theme = useTheme();
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -51,6 +60,16 @@ export default function TwitchProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -91,6 +110,18 @@ export default function TwitchProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Twitch settings are being updated...',
@@ -159,7 +190,7 @@ export default function TwitchProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
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';
@@ -7,15 +9,18 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -39,15 +44,19 @@ const validationSchema = Yup.object({
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function TwitterProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { consumerKey, consumerSecret, enabled } =
@@ -63,6 +72,16 @@ export default function TwitterProviderSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
consumerSecret: consumerSecret || '',
consumerKey: consumerKey || '',
enabled: enabled || false,
});
}
}, [loading, consumerKey, consumerSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -100,6 +119,18 @@ export default function TwitterProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Twitter settings are being updated...',
@@ -185,7 +216,7 @@ export default function TwitterProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,15 +25,19 @@ const validationSchema = Yup.object({
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
export default function WebAuthnSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enabled } = data?.config?.auth?.method?.webauthn || {};
@@ -41,6 +50,12 @@ export default function WebAuthnSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({ enabled });
}
}, [loading, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -80,6 +95,18 @@ export default function WebAuthnSettings() {
async () => {
await updateConfigPromise;
form.reset(values);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'WebAuthn settings are being updated...',

View File

@@ -1,3 +1,5 @@
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';
@@ -12,28 +14,35 @@ import {
baseProviderValidationSchema,
} from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
export default function WindowsLiveProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, enabled } =
@@ -49,6 +58,16 @@ export default function WindowsLiveProviderSettings() {
resolver: yupResolver(baseProviderValidationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
enabled: enabled || false,
});
}
}, [loading, clientId, clientSecret, enabled, form]);
if (loading) {
return (
<ActivityIndicator
@@ -89,6 +108,18 @@ export default function WindowsLiveProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Windows Live settings are being updated...',
@@ -151,7 +182,7 @@ export default function WindowsLiveProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,3 +1,5 @@
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';
@@ -8,15 +10,18 @@ import { Input } from '@/components/ui/v2/Input';
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
import { BaseProviderSettings } from '@/features/authentication/settings/components/BaseProviderSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/projects/common/utils/generateAppServiceUrl';
import {
GetSignInMethodsDocument,
useGetSignInMethodsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import * as Yup from 'yup';
@@ -52,15 +57,19 @@ const validationSchema = Yup.object({
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
export default function WorkOsProviderSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetSignInMethodsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetSignInMethodsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { clientId, clientSecret, organization, connection, enabled } =
@@ -78,6 +87,26 @@ export default function WorkOsProviderSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
clientId: clientId || '',
clientSecret: clientSecret || '',
organization: organization || '',
connection: connection || '',
enabled: enabled || false,
});
}
}, [
loading,
clientId,
clientSecret,
organization,
connection,
enabled,
form,
]);
if (loading) {
return (
<ActivityIndicator
@@ -115,6 +144,18 @@ export default function WorkOsProviderSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'WorkOS settings are being updated...',
@@ -203,7 +244,7 @@ export default function WorkOsProviderSettings() {
);
}}
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</InputAdornment>
}

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetPostgresSettingsDocument,
Software_Type_Enum,
@@ -11,8 +14,10 @@ import {
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -30,24 +35,30 @@ export type DatabaseServiceVersionFormValues = Yup.InferType<
>;
export default function DatabaseServiceVersionSettings() {
const isPlatform = useIsPlatform();
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetPostgresSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data: databaseVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.PostgreSql,
},
skip: !isPlatform,
});
const { version } = data?.config?.postgres || {};
const databaseVersions = databaseVersionsData?.softwareVersions || [];
const availableVersions = Array.from(
new Set(databaseVersions.map((el) => el.version)).add(version),
@@ -61,10 +72,21 @@ export default function DatabaseServiceVersionSettings() {
const form = useForm<DatabaseServiceVersionFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } },
defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && version) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) {
return (
<ActivityIndicator
@@ -99,6 +121,18 @@ export default function DatabaseServiceVersionSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Postgres version is being updated...',
@@ -123,12 +157,20 @@ export default function DatabaseServiceVersionSettings() {
}}
docsLink="https://hub.docker.com/r/nhost/postgres/tags"
docsTitle="the latest releases"
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
>
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();

View File

@@ -7,10 +7,12 @@ import { Box } from '@/components/ui/v2/Box';
import { Input } from '@/components/ui/v2/Input';
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 {
useGetPostgresSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
@@ -18,13 +20,15 @@ import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
const validationSchema = Yup.object({
capacity: Yup.number().required(),
capacity: Yup.number().required().min(10),
});
export type AuthDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthDomain() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const {
@@ -34,14 +38,17 @@ export default function AuthDomain() {
refetch: refetchPostgresSettings,
} = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const capacity =
data?.config?.postgres?.resources?.storage?.capacity ??
currentProject.plan.featureMaxDbSize;
(data?.config?.postgres?.resources?.storage?.capacity ??
currentProject?.plan?.featureMaxDbSize) ||
0;
const [updateConfig] = useUpdateConfigMutation();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<Yup.InferType<typeof validationSchema>>({
reValidateMode: 'onSubmit',
@@ -117,7 +124,7 @@ export default function AuthDomain() {
}}
className="flex flex-col"
>
{currentProject.plan.isFree && (
{currentProject.plan?.isFree && (
<UpgradeNotification message="Unlock by upgrading your project to the Pro plan." />
)}
<Box className="grid grid-flow-row lg:grid-cols-5">
@@ -127,7 +134,7 @@ export default function AuthDomain() {
name="capacity"
type="number"
fullWidth
disabled={currentProject.plan.isFree}
disabled={currentProject.plan?.isFree}
className="lg:col-span-2"
error={Boolean(formState.errors.capacity?.message)}
helperText={formState.errors.capacity?.message}
@@ -138,7 +145,7 @@ export default function AuthDomain() {
}}
/>
</Box>
{!currentProject.plan.isFree && (
{!currentProject.plan?.isFree && (
<Alert severity="info" className="col-span-6 text-left">
Note that volumes can only be increased (not decreased). Also, due
to an AWS limitation, the same volume can only be increased once

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraAllowListFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraAllowListSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enableAllowList } = data?.config?.hasura.settings || {};
@@ -42,6 +51,14 @@ export default function HasuraAllowListSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
enabled: enableAllowList,
});
}
}, [loading, enableAllowList, form]);
if (loading) {
return (
<ActivityIndicator
@@ -75,6 +92,18 @@ export default function HasuraAllowListSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Allow list settings are being updated...',

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraConsoleFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraConsoleSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enableConsole } = data?.config?.hasura.settings || {};
@@ -42,6 +51,14 @@ export default function HasuraConsoleSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
enabled: enableConsole,
});
}
}, [loading, enableConsole, form]);
if (loading) {
return (
<ActivityIndicator
@@ -75,6 +92,18 @@ export default function HasuraConsoleSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Hasura Console settings are being updated...',

View File

@@ -1,14 +1,18 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
@@ -28,16 +32,20 @@ const validationSchema = Yup.object({
export type HasuraCorsDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraCorsDomainSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { corsDomain } = data?.config?.hasura.settings || {};
@@ -98,6 +106,18 @@ export default function HasuraCorsDomainSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'CORS domain settings are being updated...',

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraDevModeFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraDevModeSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { devMode } = data?.config?.hasura.settings || {};
@@ -42,6 +51,14 @@ export default function HasuraDevModeSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
enabled: devMode,
});
}
}, [loading, devMode, form]);
if (loading) {
return (
<ActivityIndicator
@@ -63,7 +80,7 @@ export default function HasuraDevModeSettings() {
config: {
hasura: {
settings: {
enableConsole: formValues.enabled,
devMode: formValues.enabled,
},
},
},
@@ -75,6 +92,18 @@ export default function HasuraDevModeSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Dev Mode settings are being updated...',

View File

@@ -1,16 +1,21 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -30,31 +35,40 @@ export type HasuraEnabledAPIFormValues = Yup.InferType<typeof validationSchema>;
const AVAILABLE_HASURA_APIS = ['metadata', 'graphql', 'pgdump', 'config'];
export default function HasuraEnabledAPISettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enabledAPIs } = data?.config?.hasura.settings || {};
const { enabledAPIs = [] } = data?.config?.hasura?.settings || {};
const form = useForm<HasuraEnabledAPIFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabledAPIs: enabledAPIs.map((api) => ({
label: api,
value: api,
})),
enabledAPIs: [],
},
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (enabledAPIs && !loading) {
form.reset({
enabledAPIs: enabledAPIs.map((api) => ({ label: api, value: api })),
});
}
}, [form, enabledAPIs, loading]);
if (loading) {
return (
<ActivityIndicator
@@ -96,6 +110,18 @@ export default function HasuraEnabledAPISettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Enabled APIs are being updated...',
@@ -117,7 +143,7 @@ export default function HasuraEnabledAPISettings() {
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-6"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-6"
>
<ControlledAutocomplete
id="enabledAPIs"

View File

@@ -1,3 +1,5 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
import { Form } from '@/components/form/Form';
@@ -5,13 +7,16 @@ 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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -29,16 +34,20 @@ export type HasuraLogLevelFormValues = Yup.InferType<typeof validationSchema>;
const AVAILABLE_HASURA_LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
export default function HasuraLogLevelSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { maintenanceActive } = useUI();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { level } = data?.config?.hasura.logs || {};
@@ -56,6 +65,17 @@ export default function HasuraLogLevelSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && level) {
form.reset({
logLevel: {
label: level,
value: level,
},
});
}
}, [form, loading, level]);
if (loading) {
return (
<ActivityIndicator
@@ -97,6 +117,18 @@ export default function HasuraLogLevelSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Log level is being updated...',
@@ -128,7 +160,7 @@ export default function HasuraLogLevelSettings() {
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
>
<ControlledAutocomplete
id="logLevel"

View File

@@ -1,14 +1,18 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form';
@@ -26,16 +30,20 @@ const validationSchema = Yup.object({
export type HasuraPoolSizeFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraPoolSizeSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { httpPoolSize } = data?.config?.hasura.events || {};
@@ -82,6 +90,18 @@ export default function HasuraPoolSizeSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Pool size is being updated...',
@@ -104,7 +124,7 @@ export default function HasuraPoolSizeSettings() {
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
>
<Input
{...register('httpPoolSize')}

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -22,16 +27,20 @@ export type HasuraRemoteSchemaPermissionsFormValues = Yup.InferType<
>;
export default function HasuraRemoteSchemaPermissionsSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { enableRemoteSchemaPermissions } = data?.config?.hasura.settings || {};
@@ -44,6 +53,14 @@ export default function HasuraRemoteSchemaPermissionsSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
enabled: enableRemoteSchemaPermissions,
});
}
}, [loading, enableRemoteSchemaPermissions, form]);
if (loading) {
return (
<ActivityIndicator
@@ -67,7 +84,7 @@ export default function HasuraRemoteSchemaPermissionsSettings() {
config: {
hasura: {
settings: {
enableConsole: formValues.enabled,
enableRemoteSchemaPermissions: formValues.enabled,
},
},
},
@@ -79,6 +96,18 @@ export default function HasuraRemoteSchemaPermissionsSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage:

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
Software_Type_Enum,
@@ -11,8 +14,10 @@ import {
useGetSoftwareVersionsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -30,22 +35,27 @@ export type HasuraServiceVersionFormValues = Yup.InferType<
>;
export default function HasuraServiceVersionSettings() {
const isPlatform = useIsPlatform();
const { openDialog } = useDialog();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetHasuraSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data: hasuraVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Hasura,
},
skip: !isPlatform,
});
const { version } = data?.config?.hasura || {};
@@ -53,6 +63,7 @@ export default function HasuraServiceVersionSettings() {
const availableVersions = Array.from(
new Set(versions.map((el) => el.version)).add(version),
)
.filter((v) => !!v)
.sort()
.reverse()
.map((availableVersion) => ({
@@ -60,12 +71,25 @@ export default function HasuraServiceVersionSettings() {
value: availableVersion,
}));
// TODO make sure the network request is made in the parent component
// also this request should be made against cache only and the data should be available right away
const form = useForm<HasuraServiceVersionFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } },
defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading && version) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) {
return (
<ActivityIndicator
@@ -99,6 +123,18 @@ export default function HasuraServiceVersionSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Hasura version is being updated...',
@@ -123,11 +159,19 @@ export default function HasuraServiceVersionSettings() {
}}
docsLink="https://hub.docker.com/r/nhost/graphql-engine/tags"
docsTitle="the latest releases"
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
>
<ControlledAutocomplete
id="version"
name="version"
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
autoHighlight
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {

View File

@@ -3,12 +3,12 @@ import { Button } from '@/components/ui/v2/Button';
import { Checkbox } from '@/components/ui/v2/Checkbox';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { type RunService } from '@/hooks/useRunServices';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
useDeleteRunServiceConfigMutation,
useDeleteRunServiceMutation,
} from '@/utils/__generated__/graphql';
import { type RunService } from 'pages/[workspaceSlug]/[appSlug]/services';
import { useState } from 'react';
import { twMerge } from 'tailwind-merge';

View File

@@ -91,7 +91,7 @@ export default function useCurrentWorkspaceAndProject(): UseCurrentWorkspaceAndP
// Return a default project if working locally
if (!isPlatform) {
const localProject: Project = {
id: 'local',
id: '00000000-0000-0000-0000-000000000000',
slug: 'local',
name: 'local',
appStates: [

View File

@@ -1,9 +1,12 @@
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useGetPlansQuery } from '@/utils/__generated__/graphql';
/**
* Returns the Pro plan.
*/
export default function useProPlan() {
const isPlatform = useIsPlatform();
const { data, ...rest } = useGetPlansQuery({
variables: {
where: {
@@ -13,6 +16,7 @@ export default function useProPlan() {
},
},
fetchPolicy: 'cache-first',
skip: !isPlatform,
});
return { data: data?.plans?.at(0), ...rest };

View File

@@ -90,14 +90,6 @@ export default function useProjectRoutes() {
icon: <GaugeIcon />,
disabled: !isPlatform,
},
{
relativeMainPath: '/settings',
relativePath: '/settings/general',
exact: false,
label: 'Settings',
icon: <CogIcon />,
disabled: !isPlatform || maintenanceActive,
},
];
const allRoutes: ProjectRoute[] = [
@@ -143,7 +135,6 @@ export default function useProjectRoutes() {
exact: false,
label: 'Run',
icon: <ServicesIcon />,
disabled: !isPlatform,
},
{
relativeMainPath: '/ai',
@@ -153,6 +144,14 @@ export default function useProjectRoutes() {
icon: <AIIcon />,
},
...nhostRoutes,
{
relativeMainPath: '/settings',
relativePath: '/settings/general',
exact: false,
label: 'Settings',
icon: <CogIcon />,
disabled: maintenanceActive,
},
];
return {

View File

@@ -1,15 +1,19 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import {
useGetAuthenticationSettingsQuery,
useUpdateConfigMutation,
type ConfigIngressUpdateInput,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
@@ -23,12 +27,18 @@ const validationSchema = Yup.object({
export type AuthDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function AuthDomain() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const [isVerified, setIsVerified] = useState(false);
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<Yup.InferType<typeof validationSchema>>({
reValidateMode: 'onSubmit',
@@ -40,6 +50,7 @@ export default function AuthDomain() {
variables: {
appId: currentProject.id,
},
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { networking } = data?.config?.auth?.resources || {};
@@ -94,6 +105,18 @@ export default function AuthDomain() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Auth domain is being updated...',
@@ -104,6 +127,14 @@ export default function AuthDomain() {
);
}
const isDisabled = () => {
if (!isPlatform) {
return !isDirty || maintenanceActive;
}
return !isDirty || maintenanceActive || (!isVerified && !initialValue);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
@@ -112,12 +143,11 @@ export default function AuthDomain() {
description="Enter below your custom domain for the authentication service."
slotProps={{
submitButton: {
disabled:
!isDirty || maintenanceActive || (!isVerified && !initialValue),
disabled: isDisabled(),
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-x-4 gap-y-4 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-4 lg:grid-cols-5"
>
<Input
{...register('auth_fqdn')}

View File

@@ -1,15 +1,19 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import {
useGetHasuraSettingsQuery,
useUpdateConfigMutation,
type ConfigIngressUpdateInput,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
@@ -23,12 +27,18 @@ const validationSchema = Yup.object({
export type HasuraDomainFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraDomain() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const [isVerified, setIsVerified] = useState(false);
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<Yup.InferType<typeof validationSchema>>({
reValidateMode: 'onSubmit',
@@ -40,6 +50,7 @@ export default function HasuraDomain() {
variables: {
appId: currentProject.id,
},
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { networking } = data?.config?.hasura?.resources || {};
@@ -96,6 +107,18 @@ export default function HasuraDomain() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Hasura domain is being updated...',
@@ -106,6 +129,14 @@ export default function HasuraDomain() {
);
}
const isDisabled = () => {
if (!isPlatform) {
return !isDirty || maintenanceActive;
}
return !isDirty || maintenanceActive || (!isVerified && !initialValue);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
@@ -114,12 +145,11 @@ export default function HasuraDomain() {
description="Enter below your custom domain for the Hasura/GraphQL service."
slotProps={{
submitButton: {
disabled:
!isDirty || maintenanceActive || (!isVerified && !initialValue),
disabled: isDisabled(),
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-x-4 gap-y-4 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-4 lg:grid-cols-5"
>
<Input
{...register('hasura_fqdn')}

View File

@@ -5,29 +5,12 @@ import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { RunServicePortDomain } from '@/features/projects/custom-domains/settings/components/RunServicePortDomain';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useMemo } from 'react';
import { useRunServices } from '@/hooks/useRunServices';
export default function RunServiceDomains() {
const { currentProject, currentWorkspace } = useCurrentWorkspaceAndProject();
const {
data,
loading,
// refetch: refetchServices, // TODO refetch after update
} = useGetRunServicesQuery({
variables: {
appID: currentProject.id,
resolve: false,
limit: 1000, // TODO consider pagination
offset: 0,
},
});
const services = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
const { services, loading } = useRunServices();
if (loading) {
return (
@@ -45,7 +28,7 @@ export default function RunServiceDomains() {
.filter((service) => service.config?.ports?.length > 0)
.map((service) => (
<SettingsContainer
key={service.id}
key={service.id ?? service.serviceID}
title={
<div className="flex flex-row items-center">
<Text className="text-lg font-semibold">
@@ -58,7 +41,7 @@ export default function RunServiceDomains() {
underline="hover"
className="font-medium"
>
<ArrowSquareOutIcon className="mb-1 ml-1 h-4 w-4" />
<ArrowSquareOutIcon className="w-4 h-4 mb-1 ml-1" />
</Link>
</div>
}
@@ -72,7 +55,7 @@ export default function RunServiceDomains() {
className: 'hidden',
},
}}
className="grid gap-y-4 gap-x-4 px-4"
className="grid px-4 gap-x-4 gap-y-4"
>
{service.config?.ports?.map((port) => (
<RunServicePortDomain

View File

@@ -1,14 +1,18 @@
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 { Button } from '@/components/ui/v2/Button';
import { Input } from '@/components/ui/v2/Input';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import { useUpdateRunServiceConfigMutation } from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { type RunService } from '@/hooks/useRunServices';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { type RunService } from 'pages/[workspaceSlug]/[appSlug]/services';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -28,12 +32,17 @@ export default function RunServicePortDomain({
service,
port,
}: RunServicePortProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const [loading, setLoading] = useState(false);
const [isVerified, setIsVerified] = useState(false);
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateRunServiceConfig] = useUpdateRunServiceConfigMutation();
const [updateRunServiceConfig] = useUpdateRunServiceConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const runServicePort = service.config.ports.find((p) => p.port === port);
const initialValue = runServicePort?.ingresses?.[0]?.fqdn?.[0];
@@ -59,7 +68,7 @@ export default function RunServicePortDomain({
await updateRunServiceConfig({
variables: {
appID: currentProject.id,
serviceID: service.id,
serviceID: service.id ?? service.serviceID,
config: {
ports: service?.config?.ports?.map((p) => {
// exclude the `__typename` because the mutation will fail otherwise
@@ -78,7 +87,7 @@ export default function RunServicePortDomain({
return {
...rest,
// exclude the `__typename` because the mutation will fail otherwise
ingresses: rest.ingresses.map((item) => ({
ingresses: rest?.ingresses?.map((item) => ({
fqdn: item.fqdn,
})),
};
@@ -88,6 +97,18 @@ export default function RunServicePortDomain({
});
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: `Port ${port} is being updated...`,
@@ -99,6 +120,16 @@ export default function RunServicePortDomain({
setLoading(false);
}
const isDisabled = () => {
if (!isPlatform) {
return loading || !isDirty || maintenanceActive;
}
return (
loading || !isDirty || maintenanceActive || (!isVerified && !initialValue)
);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
@@ -121,16 +152,7 @@ export default function RunServicePortDomain({
inputRoot: { min: 1, max: 100 },
}}
/>
<Button
variant="outlined"
type="submit"
disabled={
loading ||
!isDirty ||
maintenanceActive ||
(!isVerified && !initialValue)
}
>
<Button variant="outlined" type="submit" disabled={isDisabled()}>
Save
</Button>
</div>

View File

@@ -1,15 +1,19 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Input } from '@/components/ui/v2/Input';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { VerifyDomain } from '@/features/projects/custom-domains/settings/components/VerifyDomain';
import {
useGetServerlessFunctionsSettingsQuery,
useUpdateConfigMutation,
type ConfigIngressUpdateInput,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useState } from 'react';
@@ -25,12 +29,17 @@ export type ServerlessFunctionsDomainFormValues = Yup.InferType<
>;
export default function ServerlessFunctionsDomain() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const [isVerified, setIsVerified] = useState(false);
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation();
const [updateConfig] = useUpdateConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const form = useForm<ServerlessFunctionsDomainFormValues>({
reValidateMode: 'onSubmit',
@@ -42,6 +51,7 @@ export default function ServerlessFunctionsDomain() {
variables: {
appId: currentProject.id,
},
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { networking } = data?.config?.functions?.resources || {};
@@ -98,6 +108,18 @@ export default function ServerlessFunctionsDomain() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Serverless Functions domain is being updated...',
@@ -109,6 +131,14 @@ export default function ServerlessFunctionsDomain() {
);
}
const isDisabled = () => {
if (!isPlatform) {
return !isDirty || maintenanceActive;
}
return !isDirty || maintenanceActive || (!isVerified && !initialValue);
};
return (
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
@@ -117,12 +147,11 @@ export default function ServerlessFunctionsDomain() {
description="Enter below your custom domain for Serverless Functions."
slotProps={{
submitButton: {
disabled:
!isDirty || maintenanceActive || (!isVerified && !initialValue),
disabled: isDisabled(),
loading: formState.isSubmitting,
},
}}
className="grid grid-flow-row gap-x-4 gap-y-4 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-4 lg:grid-cols-5"
>
<Input
{...register('functions_fqdn')}

View File

@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/v2/Button';
import { IconButton } from '@/components/ui/v2/IconButton';
import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
import { Text } from '@/components/ui/v2/Text';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { useDnsLookupCnameLazyQuery } from '@/utils/__generated__/graphql';
@@ -21,6 +22,7 @@ export default function VerifyDomain({
value,
onHostNameVerified,
}: VerifyDomainProps) {
const isPlatform = useIsPlatform();
const [verificationFailed, setVerificationFailed] = useState(false);
const [verificationSucceeded, setVerificationSucceeded] = useState(false);
@@ -72,8 +74,11 @@ export default function VerifyDomain({
backgroundColor: 'success.light',
color: 'success.dark',
},
!isPlatform && {
backgroundColor: 'grey.300',
},
]}
className="flex flex-col space-y-4 rounded-md p-4"
className="flex flex-col p-4 space-y-4 rounded-md"
>
<div className="flex flex-row items-center justify-between">
{!verificationFailed && !verificationSucceeded && (
@@ -110,23 +115,29 @@ export default function VerifyDomain({
</div>
<div className="flex flex-row space-x-2">
<Text>Value:</Text>
<Text className="font-bold">{value}</Text>
<IconButton
aria-label="Copy Personal Access Token"
variant="borderless"
color="secondary"
onClick={() => copy(value, 'CNAME Value')}
>
<CopyIcon className="h-4 w-4" />
</IconButton>
{isPlatform ? (
<>
<Text className="font-bold">{value}</Text>
<IconButton
aria-label="Copy Personal Access Token"
variant="borderless"
color="secondary"
onClick={() => copy(value, 'CNAME Value')}
>
<CopyIcon className="w-4 h-4" />
</IconButton>
</>
) : null}
</div>
<Button
disabled={loading || !hostname}
onClick={handleVerifyDomain}
className="mt-4 sm:absolute sm:bottom-0 sm:right-0 sm:mt-0"
>
Verify
</Button>
{isPlatform ? (
<Button
disabled={loading || !hostname || isPlatform}
onClick={handleVerifyDomain}
className="mt-4 sm:absolute sm:bottom-0 sm:right-0 sm:mt-0"
>
Verify
</Button>
) : null}
</div>
</Box>
);

View File

@@ -1,5 +1,8 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BaseEnvironmentVariableFormProps,
BaseEnvironmentVariableFormValues,
@@ -8,6 +11,7 @@ import {
BaseEnvironmentVariableForm,
baseEnvironmentVariableFormValidationSchema,
} from '@/features/projects/environmentVariables/settings/components/BaseEnvironmentVariableForm';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetEnvironmentVariablesDocument,
@@ -22,13 +26,17 @@ export interface CreateEnvironmentVariableFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => Promise<any>;
}
export default function CreateEnvironmentVariableForm({
onSubmit,
...props
}: CreateEnvironmentVariableFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const form = useForm<BaseEnvironmentVariableFormValues>({
defaultValues: {
name: '',
@@ -42,13 +50,14 @@ export default function CreateEnvironmentVariableForm({
const { data, loading, error } = useGetEnvironmentVariablesQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const availableEnvironmentVariables = data?.config?.global?.environment || [];
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetEnvironmentVariablesDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -103,7 +112,19 @@ export default function CreateEnvironmentVariableForm({
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
onSubmit?.();
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Creating environment variable...',

View File

@@ -1,5 +1,8 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BaseEnvironmentVariableFormProps,
BaseEnvironmentVariableFormValues,
@@ -8,6 +11,7 @@ import {
BaseEnvironmentVariableForm,
baseEnvironmentVariableFormValidationSchema,
} from '@/features/projects/environmentVariables/settings/components/BaseEnvironmentVariableForm';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { EnvironmentVariable } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -27,7 +31,7 @@ export interface EditEnvironmentVariableFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => Promise<any>;
}
export default function EditEnvironmentVariableForm({
@@ -35,6 +39,10 @@ export default function EditEnvironmentVariableForm({
onSubmit,
...props
}: EditEnvironmentVariableFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const form = useForm<BaseEnvironmentVariableFormValues>({
defaultValues: {
id: originalEnvironmentVariable.id || '',
@@ -49,13 +57,14 @@ export default function EditEnvironmentVariableForm({
const { data, loading, error } = useGetEnvironmentVariablesQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const availableEnvironmentVariables = data?.config?.global?.environment || [];
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetEnvironmentVariablesDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -117,7 +126,19 @@ export default function EditEnvironmentVariableForm({
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
onSubmit?.();
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating environment variable...',

View File

@@ -3,6 +3,8 @@ import { Form } from '@/components/form/Form';
import { Button } from '@/components/ui/v2/Button';
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 type { DialogFormProps } from '@/types/common';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -63,9 +65,12 @@ export default function EditJwtSecretForm({
submitButtonText = 'Save',
location,
}: EditJwtSecretFormProps) {
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetEnvironmentVariablesDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { onDirtyStateChange } = useDialog();
@@ -118,9 +123,9 @@ export default function EditJwtSecretForm({
<FormProvider {...form}>
<Form
onSubmit={handleSubmit}
className="flex flex-auto flex-col content-between overflow-hidden pb-4"
className="flex flex-col content-between flex-auto pb-4 overflow-hidden"
>
<div className="flex-auto overflow-y-auto px-6">
<div className="flex-auto px-6 overflow-y-auto">
<Input
{...register('jwtSecret')}
error={Boolean(errors.jwtSecret?.message)}

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -13,8 +14,10 @@ import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { CreateEnvironmentVariableForm } from '@/features/projects/environmentVariables/settings/components/CreateEnvironmentVariableForm';
import { EditEnvironmentVariableForm } from '@/features/projects/environmentVariables/settings/components/EditEnvironmentVariableForm';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { EnvironmentVariable } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -33,12 +36,14 @@ export interface EnvironmentVariableSettingsFormValues {
}
export default function EnvironmentVariableSettings() {
const { openDialog, openAlertDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { openDialog, openAlertDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetEnvironmentVariablesQuery({
const { data, loading, error, refetch } = useGetEnvironmentVariablesQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const availableEnvironmentVariables = [
@@ -57,6 +62,7 @@ export default function EnvironmentVariableSettings() {
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetEnvironmentVariablesDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -92,6 +98,18 @@ export default function EnvironmentVariableSettings() {
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Deleting environment variable...',
@@ -105,7 +123,7 @@ export default function EnvironmentVariableSettings() {
function handleOpenCreator() {
openDialog({
title: 'Create Environment Variable',
component: <CreateEnvironmentVariableForm />,
component: <CreateEnvironmentVariableForm onSubmit={refetch} />,
props: {
titleProps: { className: '!pb-0' },
PaperProps: { className: 'gap-2 max-w-sm' },
@@ -119,6 +137,7 @@ export default function EnvironmentVariableSettings() {
component: (
<EditEnvironmentVariableForm
originalEnvironmentVariable={originalVariable}
onSubmit={refetch}
/>
),
props: {
@@ -159,7 +178,7 @@ export default function EnvironmentVariableSettings() {
)}
slotProps={{ submitButton: { className: 'hidden' } }}
>
<Box className="grid grid-cols-2 gap-2 border-b-1 px-4 py-3 lg:grid-cols-3">
<Box className="grid grid-cols-2 gap-2 px-4 py-3 border-b-1 lg:grid-cols-3">
<Text className="font-medium">Variable Name</Text>
</Box>
@@ -175,7 +194,7 @@ export default function EnvironmentVariableSettings() {
<Dropdown.Trigger
asChild
hideChevron
className="absolute right-4 top-1/2 -translate-y-1/2"
className="absolute -translate-y-1/2 right-4 top-1/2"
>
<IconButton
variant="borderless"

View File

@@ -21,11 +21,15 @@ import {
} from '@/features/projects/common/utils/generateAppServiceUrl';
import { EditJwtSecretForm } from '@/features/projects/environmentVariables/settings/components/EditJwtSecretForm';
import { getJwtSecretsWithoutFalsyValues } from '@/features/projects/environmentVariables/settings/utils/getJwtSecretsWithoutFalsyValues';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { getHasuraConsoleServiceUrl } from '@/utils/env';
import { useGetEnvironmentVariablesQuery } from '@/utils/__generated__/graphql';
import { Fragment, useState } from 'react';
export default function SystemEnvironmentVariableSettings() {
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const [showAdminSecret, setShowAdminSecret] = useState(false);
const [showWebhookSecret, setShowWebhookSecret] = useState(false);
@@ -34,7 +38,7 @@ export default function SystemEnvironmentVariableSettings() {
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetEnvironmentVariablesQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { jwtSecrets, webhookSecret, adminSecret } = data?.config?.hasura || {};
@@ -46,8 +50,6 @@ export default function SystemEnvironmentVariableSettings() {
? JSON.stringify(jwtSecretsWithoutFalsyValues[0], null, 2)
: JSON.stringify(jwtSecretsWithoutFalsyValues, null, 2);
const isPlatform = useIsPlatform();
const appClient = useAppClient();
if (loading) {
@@ -124,10 +126,10 @@ export default function SystemEnvironmentVariableSettings() {
description="System environment variables are automatically generated from the configuration file and your project's subdomain and region."
docsLink="https://docs.nhost.io/platform/environment-variables#system-environment-variables"
rootClassName="gap-0"
className="mt-2 mb-2.5 px-0"
className="mb-2.5 mt-2 px-0"
slotProps={{ submitButton: { className: 'hidden' } }}
>
<Box className="grid grid-cols-3 gap-2 border-b-1 px-4 py-3">
<Box className="grid grid-cols-3 gap-2 px-4 py-3 border-b-1">
<Text className="font-medium">Variable Name</Text>
<Text className="font-medium lg:col-span-2">Value</Text>
</Box>
@@ -136,7 +138,7 @@ export default function SystemEnvironmentVariableSettings() {
<ListItem.Root className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3">
<ListItem.Text>NHOST_ADMIN_SECRET</ListItem.Text>
<div className="grid grid-flow-col items-center justify-start gap-2 lg:col-span-2">
<div className="grid items-center justify-start grid-flow-col gap-2 lg:col-span-2">
<Text className="truncate" color="secondary">
{showAdminSecret ? (
<InlineCode className="!text-sm font-medium">
@@ -156,9 +158,9 @@ export default function SystemEnvironmentVariableSettings() {
onClick={() => setShowAdminSecret((show) => !show)}
>
{showAdminSecret ? (
<EyeOffIcon className="h-5 w-5" />
<EyeOffIcon className="w-5 h-5" />
) : (
<EyeIcon className="h-5 w-5" />
<EyeIcon className="w-5 h-5" />
)}
</IconButton>
</div>
@@ -169,7 +171,7 @@ export default function SystemEnvironmentVariableSettings() {
<ListItem.Root className="grid grid-cols-2 gap-2 px-4 lg:grid-cols-3">
<ListItem.Text>NHOST_WEBHOOK_SECRET</ListItem.Text>
<div className="grid grid-flow-col items-center justify-start gap-2 lg:col-span-2">
<div className="grid items-center justify-start grid-flow-col gap-2 lg:col-span-2">
<Text className="truncate" color="secondary">
{showWebhookSecret ? (
<InlineCode className="!text-sm font-medium">
@@ -191,9 +193,9 @@ export default function SystemEnvironmentVariableSettings() {
onClick={() => setShowWebhookSecret((show) => !show)}
>
{showWebhookSecret ? (
<EyeOffIcon className="h-5 w-5" />
<EyeOffIcon className="w-5 h-5" />
) : (
<EyeIcon className="h-5 w-5" />
<EyeIcon className="w-5 h-5" />
)}
</IconButton>
</div>
@@ -217,9 +219,9 @@ export default function SystemEnvironmentVariableSettings() {
</Fragment>
))}
<Divider component="li" className="!mt-4 !mb-2.5" />
<Divider component="li" className="!mb-2.5 !mt-4" />
<ListItem.Root className="grid grid-cols-2 justify-start px-4 lg:grid-cols-3">
<ListItem.Root className="grid justify-start grid-cols-2 px-4 lg:grid-cols-3">
<ListItem.Text>NHOST_JWT_SECRET</ListItem.Text>
<div className="grid grid-flow-row items-center justify-center gap-1.5 text-center md:grid-flow-col lg:col-span-2 lg:justify-start lg:text-left">

View File

@@ -1,5 +1,8 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BasePermissionVariableFormProps,
BasePermissionVariableFormValues,
@@ -9,6 +12,7 @@ import {
basePermissionVariableValidationSchema,
} from '@/features/projects/permissions/settings/components/BasePermissionVariableForm';
import { getAllPermissionVariables } from '@/features/projects/permissions/settings/utils/getAllPermissionVariables';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetRolesPermissionsDocument,
@@ -23,18 +27,20 @@ export interface CreatePermissionVariableFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => Promise<any>;
}
export default function CreatePermissionVariableForm({
onSubmit,
...props
}: CreatePermissionVariableFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, error, loading } = useGetRolesPermissionsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { customClaims: permissionVariables } =
@@ -51,6 +57,7 @@ export default function CreatePermissionVariableForm({
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetRolesPermissionsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -106,6 +113,18 @@ export default function CreatePermissionVariableForm({
async () => {
await updateConfigPromise;
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Creating permission variable...',

View File

@@ -1,5 +1,8 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BasePermissionVariableFormProps,
BasePermissionVariableFormValues,
@@ -9,6 +12,7 @@ import {
basePermissionVariableValidationSchema,
} from '@/features/projects/permissions/settings/components/BasePermissionVariableForm';
import { getAllPermissionVariables } from '@/features/projects/permissions/settings/utils/getAllPermissionVariables';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { PermissionVariable } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -28,7 +32,7 @@ export interface EditPermissionVariableFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => any;
}
export default function EditPermissionVariableForm({
@@ -36,11 +40,14 @@ export default function EditPermissionVariableForm({
onSubmit,
...props
}: EditPermissionVariableFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, error, loading } = useGetRolesPermissionsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { customClaims: permissionVariables } =
@@ -57,6 +64,7 @@ export default function EditPermissionVariableForm({
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetRolesPermissionsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -132,6 +140,19 @@ export default function EditPermissionVariableForm({
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating permission variable...',
@@ -140,8 +161,6 @@ export default function EditPermissionVariableForm({
'An error occurred while trying to update the permission variable.',
},
);
await onSubmit?.();
}
return (

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -15,9 +16,11 @@ import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { CreatePermissionVariableForm } from '@/features/projects/permissions/settings/components/CreatePermissionVariableForm';
import { EditPermissionVariableForm } from '@/features/projects/permissions/settings/components/EditPermissionVariableForm';
import { getAllPermissionVariables } from '@/features/projects/permissions/settings/utils/getAllPermissionVariables';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { PermissionVariable } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -29,13 +32,15 @@ import { Fragment } from 'react';
import { twMerge } from 'tailwind-merge';
export default function PermissionVariableSettings() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { openDialog, openAlertDialog } = useDialog();
const { data, loading, error } = useGetRolesPermissionsQuery({
const { data, loading, error, refetch } = useGetRolesPermissionsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { customClaims: permissionVariables } =
@@ -43,6 +48,7 @@ export default function PermissionVariableSettings() {
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetRolesPermissionsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -55,6 +61,20 @@ export default function PermissionVariableSettings() {
throw error;
}
function showApplyChangesDialog() {
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
}
async function handleDeleteVariable({ id }: PermissionVariable) {
const updateConfigPromise = updateConfig({
variables: {
@@ -79,6 +99,7 @@ export default function PermissionVariableSettings() {
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
showApplyChangesDialog();
},
{
loadingMessage: 'Deleting permission variable...',
@@ -92,7 +113,7 @@ export default function PermissionVariableSettings() {
function handleOpenCreator() {
openDialog({
title: 'Create Permission Variable',
component: <CreatePermissionVariableForm />,
component: <CreatePermissionVariableForm onSubmit={refetch} />,
props: {
titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' },
@@ -104,7 +125,10 @@ export default function PermissionVariableSettings() {
openDialog({
title: 'Edit Permission Variable',
component: (
<EditPermissionVariableForm originalVariable={originalVariable} />
<EditPermissionVariableForm
originalVariable={originalVariable}
onSubmit={refetch}
/>
),
props: {
titleProps: { className: '!pb-0' },
@@ -140,10 +164,10 @@ export default function PermissionVariableSettings() {
description="Permission variables are used to define permission rules in the GraphQL API."
docsLink="https://docs.nhost.io/guides/api/permissions#permission-variables"
rootClassName="gap-0"
className="my-2 px-0"
className="px-0 my-2"
slotProps={{ submitButton: { className: 'hidden' } }}
>
<Box className="grid grid-cols-2 border-b-1 px-4 py-3">
<Box className="grid grid-cols-2 px-4 py-3 border-b-1">
<Text className="font-medium">Field name</Text>
<Text className="font-medium">Path</Text>
</Box>
@@ -167,7 +191,7 @@ export default function PermissionVariableSettings() {
!permissionVariable.isSystemVariable
}
hasDisabledChildren={permissionVariable.isSystemVariable}
className="absolute right-4 top-1/2 -translate-y-1/2"
className="absolute -translate-y-1/2 right-4 top-1/2"
>
<Dropdown.Trigger asChild hideChevron>
<IconButton
@@ -224,7 +248,7 @@ export default function PermissionVariableSettings() {
<>
X-Hasura-{permissionVariable.key}{' '}
{permissionVariable.isSystemVariable && (
<LockIcon className="h-4 w-4" />
<LockIcon className="w-4 h-4" />
)}
</>
}

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { Form } from '@/components/form/Form';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -7,6 +8,7 @@ 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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
import { ResourcesConfirmationDialog } from '@/features/projects/resources/settings/components/ResourcesConfirmationDialog';
import { ServiceResourcesFormFragment } from '@/features/projects/resources/settings/components/ServiceResourcesFormFragment';
@@ -14,6 +16,7 @@ import { TotalResourcesFormFragment } from '@/features/projects/resources/settin
import { calculateBillableResources } from '@/features/projects/resources/settings/utils/calculateBillableResources';
import type { ResourceSettingsFormValues } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { resourceSettingsValidationSchema } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import {
RESOURCE_VCPU_MULTIPLIER,
RESOURCE_VCPU_PRICE,
@@ -44,6 +47,8 @@ function getInitialServiceResources(
}
export default function ResourcesForm() {
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { openDialog, closeDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
@@ -55,6 +60,7 @@ export default function ResourcesForm() {
variables: {
appId: currentProject?.id,
},
...(!isPlatform ? { client: localMimirClient } : {}),
});
const {
@@ -65,6 +71,7 @@ export default function ResourcesForm() {
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetResourcesDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const initialDatabaseResources = getInitialServiceResources(data, 'postgres');
@@ -113,7 +120,7 @@ export default function ResourcesForm() {
resolver: yupResolver(resourceSettingsValidationSchema),
});
if (!proPlan && !proPlanLoading) {
if (isPlatform && !proPlan && !proPlanLoading) {
return (
<Alert severity="error">
Couldn&apos;t load the plan for this project. Please try again.
@@ -121,7 +128,7 @@ export default function ResourcesForm() {
);
}
if (loading || proPlanLoading) {
if (isPlatform && (loading || proPlanLoading)) {
return (
<ActivityIndicator
label="Loading resource settings..."
@@ -156,9 +163,10 @@ export default function ResourcesForm() {
},
);
const initialPrice =
proPlan.price +
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE;
const initialPrice = isPlatform
? proPlan.price +
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) * RESOURCE_VCPU_PRICE
: 0;
async function handleSubmit(formValues: ResourceSettingsFormValues) {
const updateConfigPromise = updateConfig({
@@ -217,6 +225,18 @@ export default function ResourcesForm() {
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating resources...',
@@ -260,7 +280,12 @@ export default function ResourcesForm() {
}
}
function handleConfirm(formValues: ResourceSettingsFormValues) {
async function handleConfirm(formValues: ResourceSettingsFormValues) {
if (!isPlatform) {
await handleSubmit(formValues);
return;
}
openDialog({
title: formValues.enabled
? 'Confirm Dedicated Resources'

View File

@@ -6,6 +6,7 @@ import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
import { Link } from '@/components/ui/v2/Link';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
import { calculateBillableResources } from '@/features/projects/resources/settings/utils/calculateBillableResources';
import type { ResourceSettingsFormValues } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
@@ -16,6 +17,8 @@ import {
import { useFormState, useWatch } from 'react-hook-form';
export default function ResourcesFormFooter() {
const isPlatform = useIsPlatform();
const {
data: proPlan,
loading: proPlanLoading,
@@ -63,17 +66,27 @@ export default function ResourcesFormFooter() {
},
);
const updatedPrice = enabled
? Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE,
) + proPlan.price
: proPlan.price;
const computeUpdatedPrice = () => {
if (!isPlatform) {
return 0;
}
if (enabled) {
return (
Math.max(
priceForTotalAvailableVCPU,
(billableResources.vcpu / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE,
) + proPlan.price
);
}
return proPlan.price;
};
return (
<Box
className="grid items-center gap-4 border-t px-4 pt-4 lg:grid-flow-col lg:justify-between lg:gap-2"
className="grid items-center gap-4 px-4 pt-4 border-t lg:grid-flow-col lg:justify-between lg:gap-2"
component="footer"
>
<Text>
@@ -86,20 +99,22 @@ export default function ResourcesFormFooter() {
className="font-medium"
>
Compute Resources
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
</Link>
</Text>
{(enabled || isDirty) && (
<Box className="grid grid-flow-col items-center justify-between gap-4">
<Box className="grid items-center justify-between grid-flow-col gap-4">
<Box className="grid grid-flow-col items-center gap-1.5">
<Text>
Approximate cost:{' '}
<span className="font-medium">${updatedPrice.toFixed(2)}/mo</span>
<span className="font-medium">
${computeUpdatedPrice().toFixed(2)}/mo
</span>
</Text>
<Tooltip title="$0.0012/minute for every 1 vCPU and 2 GiB of RAM">
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
<InfoIcon aria-label="Info" className="w-4 h-4" color="primary" />
</Tooltip>
</Box>

View File

@@ -3,6 +3,7 @@ import { Box } from '@/components/ui/v2/Box';
import { ArrowRightIcon } from '@/components/ui/v2/icons/ArrowRightIcon';
import { Slider, sliderClasses } from '@/components/ui/v2/Slider';
import { Text } from '@/components/ui/v2/Text';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useProPlan } from '@/features/projects/common/hooks/useProPlan';
import { getAllocatedResources } from '@/features/projects/resources/settings/utils/getAllocatedResources';
import { prettifyMemory } from '@/features/projects/resources/settings/utils/prettifyMemory';
@@ -38,6 +39,8 @@ const StyledAvailableCpuSlider = styled(Slider)(({ theme }) => ({
export default function TotalResourcesFormFragment({
initialPrice,
}: TotalResourcesFormFragmentProps) {
const isPlatform = useIsPlatform();
const {
data: proPlan,
error: proPlanError,
@@ -46,7 +49,7 @@ export default function TotalResourcesFormFragment({
const { setValue } = useFormContext<ResourceSettingsFormValues>();
const formValues = useWatch<ResourceSettingsFormValues>();
if (!proPlan && !proPlanLoading) {
if (isPlatform && !proPlan && !proPlanLoading) {
return (
<Alert severity="error">
Couldn&apos;t load the plan for this projectee. Please try again.
@@ -62,7 +65,9 @@ export default function TotalResourcesFormFragment({
(formValues.totalAvailableVCPU / RESOURCE_VCPU_MULTIPLIER) *
RESOURCE_VCPU_PRICE;
const updatedPrice = priceForTotalAvailableVCPU + proPlan.price;
const updatedPrice = isPlatform
? priceForTotalAvailableVCPU + proPlan.price
: 0;
const { vcpu: allocatedVCPU, memory: allocatedMemory } =
getAllocatedResources(formValues);
@@ -102,8 +107,8 @@ export default function TotalResourcesFormFragment({
return (
<Box className="px-4 pb-4">
<Box className="rounded-md border">
<Box className="flex flex-col gap-4 bg-transparent p-4">
<Box className="border rounded-md">
<Box className="flex flex-col gap-4 p-4 bg-transparent">
<Box className="flex flex-row items-center justify-between gap-4">
<Text color="secondary">
Total available compute for your project:
@@ -151,7 +156,7 @@ export default function TotalResourcesFormFragment({
severity={
hasUnusedResources || hasOverallocatedResources ? 'warning' : 'info'
}
className="grid grid-flow-row gap-2 rounded-t-none rounded-b-[5px] text-left"
className="grid grid-flow-row gap-2 rounded-b-[5px] rounded-t-none text-left"
>
{hasUnusedResources && !hasOverallocatedResources && (
<>

View File

@@ -1,5 +1,8 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BaseRoleFormProps,
BaseRoleFormValues,
@@ -9,6 +12,7 @@ import {
baseRoleFormValidationSchema,
} from '@/features/projects/roles/settings/components/BaseRoleForm';
import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetRolesPermissionsDocument,
@@ -23,17 +27,20 @@ export interface CreateRoleFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => Promise<any>;
}
export default function CreateRoleForm({
onSubmit,
...props
}: CreateRoleFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetRolesPermissionsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { allowed: allowedRoles } = data?.config?.auth?.user?.roles || {};
@@ -45,6 +52,7 @@ export default function CreateRoleForm({
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetRolesPermissionsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -85,7 +93,19 @@ export default function CreateRoleForm({
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
onSubmit?.();
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Creating role...',

View File

@@ -1,5 +1,8 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BaseRoleFormProps,
BaseRoleFormValues,
@@ -9,6 +12,7 @@ import {
baseRoleFormValidationSchema,
} from '@/features/projects/roles/settings/components/BaseRoleForm';
import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { Role } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -28,7 +32,7 @@ export interface EditRoleFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => Promise<any>;
}
export default function EditRoleForm({
@@ -36,10 +40,13 @@ export default function EditRoleForm({
onSubmit,
...props
}: EditRoleFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { data, loading, error } = useGetRolesPermissionsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { allowed: allowedRoles, default: defaultRole } =
@@ -55,6 +62,7 @@ export default function EditRoleForm({
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetRolesPermissionsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -114,7 +122,19 @@ export default function EditRoleForm({
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
onSubmit?.();
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Updating role...',

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useUI } from '@/components/common/UIProvider';
import { SettingsContainer } from '@/components/layout/SettingsContainer';
@@ -15,9 +16,11 @@ import { List } from '@/components/ui/v2/List';
import { ListItem } from '@/components/ui/v2/ListItem';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { CreateRoleForm } from '@/features/projects/roles/settings/components/CreateRoleForm';
import { EditRoleForm } from '@/features/projects/roles/settings/components/EditRoleForm';
import { getUserRoles } from '@/features/projects/roles/settings/utils/getUserRoles';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { Role } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -40,13 +43,15 @@ export interface RoleSettingsFormValues {
}
export default function RoleSettings() {
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { openDialog, openAlertDialog } = useDialog();
const { data, loading, error } = useGetRolesPermissionsQuery({
const { data, loading, error, refetch } = useGetRolesPermissionsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { allowed: allowedRoles, default: defaultRole } =
@@ -54,6 +59,7 @@ export default function RoleSettings() {
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetRolesPermissionsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -64,6 +70,20 @@ export default function RoleSettings() {
throw error;
}
async function showApplyChangesDialog() {
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
}
async function handleSetAsDefault({ name }: Role) {
const updateConfigPromise = updateConfig({
variables: {
@@ -84,6 +104,7 @@ export default function RoleSettings() {
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
showApplyChangesDialog();
},
{
loadingMessage: 'Updating default role...',
@@ -114,6 +135,7 @@ export default function RoleSettings() {
await execPromiseWithErrorToast(
async () => {
await updateConfigPromise;
showApplyChangesDialog();
},
{
loadingMessage: 'Deleting allowed role...',
@@ -127,7 +149,7 @@ export default function RoleSettings() {
function handleOpenCreator() {
openDialog({
title: 'Create Allowed Role',
component: <CreateRoleForm />,
component: <CreateRoleForm onSubmit={refetch} />,
props: {
titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' },
@@ -138,7 +160,9 @@ export default function RoleSettings() {
function handleOpenEditor(originalRole: Role) {
openDialog({
title: 'Edit Allowed Role',
component: <EditRoleForm originalRole={originalRole} />,
component: (
<EditRoleForm originalRole={originalRole} onSubmit={refetch} />
),
props: {
titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' },
@@ -177,7 +201,7 @@ export default function RoleSettings() {
)}
slotProps={{ submitButton: { className: 'hidden' } }}
>
<Box className="border-b-1 px-4 py-3">
<Box className="px-4 py-3 border-b-1">
<Text className="font-medium">Name</Text>
</Box>
@@ -193,7 +217,7 @@ export default function RoleSettings() {
<Dropdown.Trigger
asChild
hideChevron
className="absolute right-4 top-1/2 -translate-y-1/2"
className="absolute -translate-y-1/2 right-4 top-1/2"
>
<IconButton
variant="borderless"
@@ -252,7 +276,7 @@ export default function RoleSettings() {
<>
{role.name}
{role.isSystemRole && <LockIcon className="h-4 w-4" />}
{role.isSystemRole && <LockIcon className="w-4 h-4" />}
{defaultRole === role.name && (
<Chip

View File

@@ -1,4 +1,7 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BaseSecretFormProps,
BaseSecretFormValues,
@@ -7,6 +10,7 @@ import {
BaseSecretForm,
baseSecretFormValidationSchema,
} from '@/features/projects/secrets/settings/components/BaseSecretForm';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
GetSecretsDocument,
@@ -20,13 +24,17 @@ export interface CreateSecretFormProps
/**
* Function to be called when the form is submitted.
*/
onSubmit?: () => Promise<void>;
onSubmit?: () => Promise<any>;
}
export default function CreateSecretForm({
onSubmit,
...props
}: CreateSecretFormProps) {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const form = useForm<BaseSecretFormValues>({
defaultValues: {
name: '',
@@ -39,6 +47,7 @@ export default function CreateSecretForm({
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertSecret] = useInsertSecretMutation({
refetchQueries: [GetSecretsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
async function handleSubmit({ name, value }: BaseSecretFormValues) {
@@ -56,7 +65,19 @@ export default function CreateSecretForm({
await execPromiseWithErrorToast(
async () => {
await insertSecretPromise;
onSubmit?.();
await onSubmit?.();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Creating secret...',

View File

@@ -1,4 +1,5 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import type {
BaseSecretFormProps,
BaseSecretFormValues,
@@ -7,6 +8,7 @@ import {
BaseSecretForm,
baseSecretFormValidationSchema,
} from '@/features/projects/secrets/settings/components/BaseSecretForm';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import type { Secret } from '@/types/application';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import {
@@ -33,6 +35,9 @@ export default function EditSecretForm({
onSubmit,
...props
}: EditSecretFormProps) {
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const form = useForm<BaseSecretFormValues>({
defaultValues: {
name: originalSecret.name,
@@ -45,6 +50,7 @@ export default function EditSecretForm({
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateSecret] = useUpdateSecretMutation({
refetchQueries: [GetSecretsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
async function handleSubmit({ name, value }: BaseSecretFormValues) {

View File

@@ -1,3 +1,4 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
import { Form } from '@/components/form/Form';
import { Alert } from '@/components/ui/v2/Alert';
@@ -12,6 +13,7 @@ import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useHostName } from '@/features/projects/common/hooks/useHostName';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { InfoCard } from '@/features/projects/overview/components/InfoCard';
import { COST_PER_VCPU } from '@/features/projects/resources/settings/utils/resourceSettingsValidationSchema';
import { ComputeFormSection } from '@/features/services/components/ServiceForm/components/ComputeFormSection';
@@ -25,6 +27,7 @@ import {
type ServiceFormProps,
type ServiceFormValues,
} from '@/features/services/components/ServiceForm/ServiceFormTypes';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { copy } from '@/utils/copy';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
@@ -50,11 +53,15 @@ export default function ServiceForm({
location,
}: ServiceFormProps) {
const hostName = useHostName();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { onDirtyStateChange, openDialog, closeDialog } = useDialog();
const [insertRunService] = useInsertRunServiceMutation();
const { currentProject } = useCurrentWorkspaceAndProject();
const [insertRunServiceConfig] = useInsertRunServiceConfigMutation();
const [replaceRunServiceConfig] = useReplaceRunServiceConfigMutation();
const [replaceRunServiceConfig] = useReplaceRunServiceConfigMutation({
...(!isPlatform ? { client: localMimirClient } : {}),
});
const [detailsServiceId, setDetailsServiceId] = useState('');
const [detailsServiceSubdomain, setDetailsServiceSubdomain] = useState(
initialData?.subdomain,
@@ -145,6 +152,18 @@ export default function ServiceForm({
});
setDetailsServiceId(serviceID);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
} else {
// Insert service config
const {
@@ -197,7 +216,12 @@ export default function ServiceForm({
);
};
const handleConfirm = (values: ServiceFormValues) => {
const handleConfirm = async (values: ServiceFormValues) => {
if (!isPlatform) {
await handleSubmit(formValues);
return;
}
openDialog({
title: 'Confirm Resources',
component: (
@@ -213,26 +237,34 @@ export default function ServiceForm({
};
useEffect(() => {
(async () => {
if (detailsServiceId) {
openDialog({
title: 'Service Details',
component: (
<ServiceDetailsDialog
serviceID={detailsServiceId}
subdomain={detailsServiceSubdomain}
ports={formValues.ports}
/>
),
props: {
PaperProps: {
className: 'max-w-2xl',
},
if (!isPlatform) {
return;
}
if (detailsServiceId) {
openDialog({
title: 'Service Details',
component: (
<ServiceDetailsDialog
serviceID={detailsServiceId}
subdomain={detailsServiceSubdomain}
ports={formValues.ports}
/>
),
props: {
PaperProps: {
className: 'max-w-2xl',
},
});
}
})();
}, [detailsServiceId, detailsServiceSubdomain, formValues, openDialog]);
},
});
}
}, [
detailsServiceId,
detailsServiceSubdomain,
formValues,
openDialog,
isPlatform,
]);
const pricingExplanation = () => {
const vCPUs = `${formValues.compute.cpu / RESOURCE_VCPU_MULTIPLIER} vCPUs`;
@@ -271,7 +303,7 @@ export default function ServiceForm({
<Tooltip title="Name of the service, must be unique per project.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -311,7 +343,7 @@ export default function ServiceForm({
>
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -325,8 +357,8 @@ export default function ServiceForm({
autoComplete="off"
/>
{/* This shows only when trying to edit a service */}
{serviceID && serviceImage && (
{/* This shows only when trying to edit a service and when running against the nhost platform */}
{isPlatform && serviceID && serviceImage && (
<InfoCard
title="Private registry"
value={`registry.${currentProject.region.awsName}.${currentProject.region.domain}/${serviceID}`}
@@ -342,7 +374,7 @@ export default function ServiceForm({
<Tooltip title="Command to run when to start the service. This is optional as the image may already have a baked-in command.">
<InfoIcon
aria-label="Info"
className="h-4 w-4"
className="w-4 h-4"
color="primary"
/>
</Tooltip>
@@ -356,22 +388,24 @@ export default function ServiceForm({
autoComplete="off"
/>
<Alert
severity="info"
className="flex items-center justify-between space-x-2"
>
<span>{pricingExplanation()}</span>
<b>
$
{parseFloat(
(
formValues.compute.cpu *
formValues.replicas *
COST_PER_VCPU
).toFixed(2),
)}
</b>
</Alert>
{isPlatform ? (
<Alert
severity="info"
className="flex items-center justify-between space-x-2"
>
<span>{pricingExplanation()}</span>
<b>
$
{parseFloat(
(
formValues.compute.cpu *
formValues.replicas *
COST_PER_VCPU
).toFixed(2),
)}
</b>
</Alert>
) : null}
<ComputeFormSection showTooltip />
@@ -388,7 +422,7 @@ export default function ServiceForm({
{createServiceFormError && (
<Alert
severity="error"
className="grid grid-flow-col items-center justify-between px-4 py-3"
className="grid items-center justify-between grid-flow-col px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {createServiceFormError.message}

View File

@@ -11,11 +11,12 @@ import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
import { Text } from '@/components/ui/v2/Text';
import { Tooltip } from '@/components/ui/v2/Tooltip';
import { DeleteServiceModal } from '@/features/projects/common/components/DeleteServiceModal';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { ServiceForm } from '@/features/services/components/ServiceForm';
import { type PortTypes } from '@/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import { type RunService } from '@/hooks/useRunServices';
import { copy } from '@/utils/copy';
import { formatDistanceToNow } from 'date-fns';
import type { RunService } from 'pages/[workspaceSlug]/[appSlug]/services';
interface ServicesListProps {
/**
@@ -42,19 +43,20 @@ export default function ServicesList({
onCreateOrUpdate,
onDelete,
}: ServicesListProps) {
const isPlatform = useIsPlatform();
const { openDrawer, openDialog, closeDialog } = useDialog();
const viewService = async (service: RunService) => {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<CubeIcon className="w-5 h-5" />
<Text>Edit {service.config?.name ?? 'unset'}</Text>
</Box>
),
component: (
<ServiceForm
serviceID={service.id}
serviceID={service.id ?? service.serviceID}
initialData={{
...service.config,
image: service.config?.image?.image,
@@ -94,50 +96,52 @@ export default function ServicesList({
<Box className="flex flex-col">
{services.map((service) => (
<Box
key={service.id}
key={service.id ?? service.serviceID}
className="flex h-[64px] w-full cursor-pointer items-center justify-between space-x-4 border-b-1 px-4 py-2 transition-colors"
sx={{
[`&:hover`]: {
backgroundColor: 'action.hover',
},
}}
onClick={() => viewService(service)}
>
<Box
onClick={() => viewService(service)}
className="flex w-full flex-row justify-between"
className="flex flex-row justify-between w-full"
sx={{
backgroundColor: 'transparent',
}}
>
<div className="flex flex-1 flex-row items-center space-x-4">
<CubeIcon className="h-5 w-5" />
<div className="flex flex-row items-center flex-1 space-x-4">
<CubeIcon className="w-5 h-5" />
<div className="flex flex-col">
<Text variant="h4" className="font-semibold">
{service.config?.name ?? 'unset'}
</Text>
<Tooltip title={service.updatedAt}>
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
Deployed {formatDistanceToNow(new Date(service.updatedAt))}{' '}
ago
</span>
</Tooltip>
{isPlatform ? (
<Tooltip title={service.updatedAt}>
<span className="hidden cursor-pointer text-sm text-slate-500 xs+:flex">
Deployed{' '}
{formatDistanceToNow(new Date(service.updatedAt))} ago
</span>
</Tooltip>
) : null}
</div>
</div>
<div className="hidden flex-row items-center space-x-2 md:flex">
<div className="flex-row items-center hidden space-x-2 md:flex">
<Text variant="subtitle1" className="font-mono text-xs">
{service.id}
{service.id ?? service.serviceID}
</Text>
<IconButton
variant="borderless"
color="secondary"
onClick={(event) => {
copy(service.id, 'Service Id');
copy(service.id ?? service.serviceID, 'Service Id');
event.stopPropagation();
}}
aria-label="Service Id"
>
<CopyIcon className="h-4 w-4" />
<CopyIcon className="w-4 h-4" />
</IconButton>
</div>
</Box>
@@ -167,7 +171,7 @@ export default function ServicesList({
onClick={() => viewService(service)}
className="z-50 grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
>
<UserIcon className="h-4 w-4" />
<UserIcon className="w-4 h-4" />
<Text className="font-medium">View Service</Text>
</Dropdown.Item>
<Divider component="li" />
@@ -175,8 +179,9 @@ export default function ServicesList({
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
sx={{ color: 'error.main' }}
onClick={() => deleteService(service)}
disabled={!isPlatform}
>
<TrashIcon className="h-4 w-4" />
<TrashIcon className="w-4 h-4" />
<Text className="font-medium" color="error">
Delete Service
</Text>

View File

@@ -1,9 +1,12 @@
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
import { useDialog } from '@/components/common/DialogProvider';
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetStorageSettingsDocument,
Software_Type_Enum,
@@ -11,8 +14,10 @@ import {
useGetStorageSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -30,21 +35,26 @@ export type StorageServiceVersionFormValues = Yup.InferType<
>;
export default function StorageServiceVersionSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetStorageSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetStorageSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-only',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data: storageVersionsData } = useGetSoftwareVersionsQuery({
variables: {
software: Software_Type_Enum.Storage,
},
skip: !isPlatform,
});
const { version } = data?.config?.storage || {};
@@ -52,6 +62,7 @@ export default function StorageServiceVersionSettings() {
const availableVersions = Array.from(
new Set(versions.map((el) => el.version)).add(version),
)
.filter((v) => !!v)
.sort()
.reverse()
.map((availableVersion) => ({
@@ -61,10 +72,21 @@ export default function StorageServiceVersionSettings() {
const form = useForm<StorageServiceVersionFormValues>({
reValidateMode: 'onSubmit',
defaultValues: { version: { label: version, value: version } },
defaultValues: { version: { label: '', value: '' } },
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
version: {
label: version,
value: version,
},
});
}
}, [loading, version, form]);
if (loading) {
return (
<ActivityIndicator
@@ -99,6 +121,18 @@ export default function StorageServiceVersionSettings() {
async () => {
await updateConfigPromise;
form.reset(formValues);
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Storage version is being updated...',
@@ -123,12 +157,20 @@ export default function StorageServiceVersionSettings() {
}}
docsLink="https://github.com/nhost/hasura-storage/releases"
docsTitle="the latest releases"
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
>
<ControlledAutocomplete
id="version"
name="version"
autoHighlight
freeSolo
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option || '';
}
return option.value;
}}
isOptionEqualToValue={() => false}
filterOptions={(options, { inputValue }) => {
const inputValueLower = inputValue.toLowerCase();

View File

@@ -1,15 +1,20 @@
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 { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import {
GetHasuraSettingsDocument,
useGetStorageSettingsQuery,
useUpdateConfigMutation,
} from '@/generated/graphql';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import * as Yup from 'yup';
@@ -20,16 +25,20 @@ const validationSchema = Yup.object({
export type HasuraStorageAVFormValues = Yup.InferType<typeof validationSchema>;
export default function HasuraStorageAVSettings() {
const { openDialog } = useDialog();
const isPlatform = useIsPlatform();
const { maintenanceActive } = useUI();
const localMimirClient = useLocalMimirClient();
const { currentProject, refetch: refetchWorkspaceAndProject } =
useCurrentWorkspaceAndProject();
const [updateConfig] = useUpdateConfigMutation({
refetchQueries: [GetHasuraSettingsDocument],
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { data, loading, error } = useGetStorageSettingsQuery({
variables: { appId: currentProject?.id },
fetchPolicy: 'cache-first',
...(!isPlatform ? { client: localMimirClient } : {}),
});
const { server } = data?.config?.storage?.antivirus || {};
@@ -42,6 +51,14 @@ export default function HasuraStorageAVSettings() {
resolver: yupResolver(validationSchema),
});
useEffect(() => {
if (!loading) {
form.reset({
enabled: !!server,
});
}
}, [loading, server, form]);
if (loading) {
return (
<ActivityIndicator
@@ -81,6 +98,18 @@ export default function HasuraStorageAVSettings() {
await updateConfigPromise;
form.reset(formValues);
await refetchWorkspaceAndProject();
if (!isPlatform) {
openDialog({
title: 'Apply your changes',
component: <ApplyLocalSettingsDialog />,
props: {
PaperProps: {
className: 'max-w-2xl',
},
},
});
}
},
{
loadingMessage: 'Antivirus settings are being updated...',

View File

@@ -12,6 +12,7 @@ query GetSmtpSettings($appId: uuid!) {
secure
sender
user
password
}
}
}

View File

@@ -2,6 +2,7 @@ query getGraphiteAutoEmbeddingsConfigurations($limit: Int!, $offset: Int!) {
graphiteAutoEmbeddingsConfigurations(limit: $limit, offset: $offset) {
id
name
model
schemaName
tableName
columnName

View File

@@ -1,5 +1,6 @@
mutation insertGraphiteAutoEmbeddingsConfiguration(
$name: String
$model: String
$schemaName: String
$tableName: String
$columnName: String
@@ -9,6 +10,7 @@ mutation insertGraphiteAutoEmbeddingsConfiguration(
insertGraphiteAutoEmbeddingsConfiguration(
object: {
name: $name
model: $model
schemaName: $schemaName
tableName: $tableName
columnName: $columnName

View File

@@ -1,6 +1,7 @@
mutation updateGraphiteAutoEmbeddingsConfiguration(
$id: uuid!
$name: String
$model: String
$schemaName: String
$tableName: String
$columnName: String
@@ -11,6 +12,7 @@ mutation updateGraphiteAutoEmbeddingsConfiguration(
pk_columns: { id: $id }
_set: {
name: $name
model: $model
schemaName: $schemaName
tableName: $tableName
columnName: $columnName
@@ -20,6 +22,7 @@ mutation updateGraphiteAutoEmbeddingsConfiguration(
) {
id
name
model
schemaName
tableName
columnName

View File

@@ -1,3 +1,40 @@
fragment RunServiceConfig on ConfigRunServiceConfig {
name
image {
image
}
command
resources {
compute {
cpu
memory
}
storage {
name
path
capacity
}
replicas
}
environment {
name
value
}
ports {
port
type
publish
ingresses {
fqdn
}
}
healthCheck {
port
initialDelaySeconds
probePeriodSeconds
}
}
query getRunServices(
$appID: uuid!
$resolve: Boolean!
@@ -11,40 +48,7 @@ query getRunServices(
updatedAt
subdomain
config(resolve: $resolve) {
name
image {
image
}
command
resources {
compute {
cpu
memory
}
storage {
name
path
capacity
}
replicas
}
environment {
name
value
}
ports {
port
type
publish
ingresses {
fqdn
}
}
healthCheck {
port
initialDelaySeconds
probePeriodSeconds
}
...RunServiceConfig
}
}
runServices_aggregate {
@@ -54,3 +58,12 @@ query getRunServices(
}
}
}
query getLocalRunServiceConfigs($appID: uuid!, $resolve: Boolean!) {
runServiceConfigs(appID: $appID, resolve: $resolve) {
serviceID
config {
...RunServiceConfig
}
}
}

View File

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

View File

@@ -0,0 +1,20 @@
import { getConfigServerUrl } from '@/utils/env';
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
import { useMemo } from 'react';
/**
* It creates a new Apollo Client instance that connects to the local mimir when running the a local nhost project
* @returns A function that returns a new ApolloClient instance.
*/
export default function useLocalMimirClient() {
return useMemo(
() =>
new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: getConfigServerUrl(),
}),
}),
[],
);
}

View File

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

View File

@@ -0,0 +1,109 @@
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import {
useGetLocalRunServiceConfigsQuery,
useGetRunServicesQuery,
type GetRunServicesQuery,
} from '@/utils/__generated__/graphql';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useRef, useState } from 'react';
export type RunService = Pick<
GetRunServicesQuery['app']['runServices'][0],
'config'
> & {
id?: string;
serviceID?: string;
createdAt?: string;
updatedAt?: string;
subdomain?: string;
};
export type RunServiceConfig = Omit<
GetRunServicesQuery['app']['runServices'][0]['config'],
'__typename'
>;
export default function useRunServices() {
const limit = useRef(25);
const router = useRouter();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const [nrOfPages, setNrOfPages] = useState(0);
const [totalServicesCount, setTotalServicesCount] = useState(0);
const [currentPage, setCurrentPage] = useState(
parseInt(router.query.page as string, 10) || 1,
);
const offset = useMemo(() => currentPage - 1, [currentPage]);
const {
data,
loading: loadingPlatformServices,
refetch: refetchPlatformServices,
} = useGetRunServicesQuery({
variables: {
appID: currentProject.id,
resolve: false,
limit: limit.current,
offset,
},
skip: !isPlatform,
});
const {
loading: loadingLocalServices,
data: localServicesData,
refetch: refetchLocalServices,
} = useGetLocalRunServiceConfigsQuery({
variables: { appID: currentProject.id as any, resolve: false },
skip: isPlatform,
client: localMimirClient,
});
const platformServices = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
const localServices = useMemo(
() => localServicesData?.runServiceConfigs.map((service) => service) ?? [],
[localServicesData],
);
const services: RunService[] = isPlatform ? platformServices : localServices;
const loading = isPlatform ? loadingPlatformServices : loadingLocalServices;
const refetch = isPlatform ? refetchPlatformServices : refetchLocalServices;
useEffect(() => {
if (!isPlatform) {
return;
}
if (loading) {
return;
}
const userCount = data?.app?.runServices_aggregate.aggregate.count ?? 0;
setTotalServicesCount(
data?.app?.runServices_aggregate.aggregate.count ?? 0,
);
setNrOfPages(Math.ceil(userCount / limit.current));
}, [data, loading, isPlatform]);
return {
services,
loading,
refetch,
limit,
totalServicesCount,
nrOfPages,
currentPage,
setCurrentPage,
};
}

View File

@@ -9,75 +9,34 @@ import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
import { ServicesIcon } from '@/components/ui/v2/icons/ServicesIcon';
import { Text } from '@/components/ui/v2/Text';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import type { GetRunServicesQuery } from '@/utils/__generated__/graphql';
import { useGetRunServicesQuery } from '@/utils/__generated__/graphql';
import { UpgradeNotification } from '@/features/projects/common/components/UpgradeNotification';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { ServiceForm } from '@/features/services/components/ServiceForm';
import { type PortTypes } from '@/features/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import ServicesList from '@/features/services/components/ServicesList/ServicesList';
import { useRunServices, type RunServiceConfig } from '@/hooks/useRunServices';
import { useRouter } from 'next/router';
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type ReactElement,
} from 'react';
export type RunService = Omit<
GetRunServicesQuery['app']['runServices'][0],
'__typename'
>;
export type RunServiceConfig = Omit<
GetRunServicesQuery['app']['runServices'][0]['config'],
'__typename'
>;
import { useCallback, useEffect, type ReactElement } from 'react';
export default function ServicesPage() {
const limit = useRef(25);
const router = useRouter();
const isPlatform = useIsPlatform();
const { openDrawer, openAlertDialog } = useDialog();
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlanFree = currentProject?.plan?.isFree;
const [currentPage, setCurrentPage] = useState(
parseInt(router.query.page as string, 10) || 1,
);
const [nrOfPages, setNrOfPages] = useState(0);
const offset = useMemo(() => currentPage - 1, [currentPage]);
const {
data,
loading,
refetch: refetchServices,
} = useGetRunServicesQuery({
variables: {
appID: currentProject.id,
resolve: false,
limit: limit.current,
offset,
},
});
services,
totalServicesCount,
limit,
nrOfPages,
currentPage,
setCurrentPage,
refetch,
} = useRunServices();
useEffect(() => {
if (loading) {
return;
}
const userCount = data?.app?.runServices_aggregate.aggregate.count ?? 0;
setNrOfPages(Math.ceil(userCount / limit.current));
}, [data, loading]);
const services = useMemo(
() => data?.app?.runServices.map((service) => service) ?? [],
[data],
);
const isPlanFree = currentProject?.plan?.isFree;
const checkConfigFromQuery = useCallback(
(base64Config: string) => {
@@ -89,7 +48,7 @@ export default function ServicesPage() {
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<CubeIcon className="w-5 h-5" />
<Text>Create a new run service</Text>
</Box>
),
@@ -111,7 +70,7 @@ export default function ServicesPage() {
replicas: parsedConfig?.resources?.replicas,
storage: parsedConfig?.resources?.storage,
}}
onSubmit={refetchServices}
onSubmit={refetch}
/>
),
});
@@ -127,7 +86,7 @@ export default function ServicesPage() {
}
}
},
[router.query.config, openDrawer, refetchServices, openAlertDialog],
[router.query.config, openDrawer, refetch, openAlertDialog],
);
useEffect(() => {
@@ -137,18 +96,23 @@ export default function ServicesPage() {
}, [checkConfigFromQuery, router.query]);
const openCreateServiceDialog = () => {
// creating services using the local dashboard is not supported
if (!isPlatform) {
return;
}
openDrawer({
title: (
<Box className="flex flex-row items-center space-x-2">
<CubeIcon className="h-5 w-5" />
<CubeIcon className="w-5 h-5" />
<Text>Create a new service</Text>
</Box>
),
component: <ServiceForm onSubmit={refetchServices} />,
component: <ServiceForm onSubmit={refetch} />,
});
};
if (isPlanFree) {
if (isPlatform && isPlanFree) {
return (
<Container>
<UpgradeNotification
@@ -159,41 +123,44 @@ export default function ServicesPage() {
);
}
if (data?.app.runServices.length === 0 && !loading) {
if (services.length === 0 && !loading) {
return (
<Container className="mx-auto max-w-9xl space-y-5 overflow-x-hidden">
<Container className="mx-auto space-y-5 overflow-x-hidden max-w-9xl">
<div className="flex flex-row place-content-end">
<Button
variant="contained"
color="primary"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="h-4 w-4" />}
startIcon={<PlusIcon className="w-4 h-4" />}
disabled={!isPlatform}
>
Add service
</Button>
</div>
<Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
<ServicesIcon className="h-10 w-10" />
<Box className="flex flex-col items-center justify-center px-48 py-12 space-y-5 border rounded-lg shadow-sm">
<ServicesIcon className="w-10 h-10" />
<div className="flex flex-col space-y-1">
<Text className="text-center font-medium" variant="h3">
<Text className="font-medium text-center" variant="h3">
No custom services are available
</Text>
<Text variant="subtitle1" className="text-center">
All your projects custom services will be listed here.
All your project&apos;s custom services will be listed here.
</Text>
</div>
<div className="flex flex-row place-content-between rounded-lg ">
<Button
variant="contained"
color="primary"
className="w-full"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Add service
</Button>
</div>
{isPlatform ? (
<div className="flex flex-row rounded-lg place-content-between ">
<Button
variant="contained"
color="primary"
className="w-full"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="w-4 h-4" />}
>
Add service
</Button>
</div>
) : null}
</Box>
</Container>
);
@@ -201,12 +168,13 @@ export default function ServicesPage() {
return (
<div className="flex flex-col">
<Box className="flex flex-row place-content-end border-b-1 p-4">
<Box className="flex flex-row p-4 place-content-end border-b-1">
<Button
variant="contained"
color="primary"
onClick={openCreateServiceDialog}
startIcon={<PlusIcon className="h-4 w-4" />}
startIcon={<PlusIcon className="w-4 h-4" />}
disabled={!isPlatform}
>
Add service
</Button>
@@ -214,42 +182,42 @@ export default function ServicesPage() {
<Box className="space-y-4">
<ServicesList
services={services}
onDelete={() => refetchServices()}
onCreateOrUpdate={() => refetchServices()}
onDelete={() => refetch()}
onCreateOrUpdate={() => refetch()}
/>
<Pagination
className="px-2"
totalNrOfPages={nrOfPages}
currentPageNumber={currentPage}
totalNrOfElements={
data?.app?.runServices_aggregate.aggregate.count ?? 0
}
itemsLabel="services"
elementsPerPage={limit.current}
onPrevPageClick={async () => {
setCurrentPage((page) => page - 1);
if (currentPage - 1 !== 1) {
{isPlatform ? (
<Pagination
className="px-2"
totalNrOfPages={nrOfPages}
currentPageNumber={currentPage}
totalNrOfElements={totalServicesCount}
itemsLabel="services"
elementsPerPage={limit.current}
onPrevPageClick={async () => {
setCurrentPage((page) => page - 1);
if (currentPage - 1 !== 1) {
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage - 1 },
});
}
}}
onNextPageClick={async () => {
setCurrentPage((page) => page + 1);
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage - 1 },
query: { ...router.query, page: currentPage + 1 },
});
}
}}
onNextPageClick={async () => {
setCurrentPage((page) => page + 1);
await router.push({
pathname: router.pathname,
query: { ...router.query, page: currentPage + 1 },
});
}}
onPageChange={async (page) => {
setCurrentPage(page);
await router.push({
pathname: router.pathname,
query: { ...router.query, page },
});
}}
/>
}}
onPageChange={async (page) => {
setCurrentPage(page);
await router.push({
pathname: router.pathname,
query: { ...router.query, page },
});
}}
/>
) : null}
</Box>
</div>
);

View File

@@ -11,7 +11,7 @@ import type { ReactElement } from 'react';
export default function StorageSettingsPage() {
const { currentProject, loading, error } = useCurrentWorkspaceAndProject();
if (currentProject.plan.isFree) {
if (currentProject?.plan?.isFree) {
return (
<Box className="p-4" sx={{ backgroundColor: 'background.default' }}>
<UpgradeToProBanner
@@ -43,7 +43,7 @@ export default function StorageSettingsPage() {
return (
<Container
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
className="grid max-w-5xl grid-flow-row bg-transparent gap-y-6"
rootClassName="bg-transparent"
>
<AISettings />

View File

@@ -11,15 +11,20 @@ import { GravatarSettings } from '@/features/authentication/settings/components/
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 { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
import { useGetAuthenticationSettingsQuery } from '@/utils/__generated__/graphql';
import type { ReactElement } from 'react';
export default function SettingsAuthenticationPage() {
const { currentProject } = useCurrentWorkspaceAndProject();
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { data, loading, error } = useGetAuthenticationSettingsQuery({
variables: { appId: currentProject?.id },
skip: !currentProject,
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (!data && loading) {
@@ -38,7 +43,7 @@ export default function SettingsAuthenticationPage() {
return (
<Container
className="grid max-w-5xl grid-flow-row gap-y-6 bg-transparent"
className="grid max-w-5xl grid-flow-row bg-transparent gap-y-6"
rootClassName="bg-transparent"
>
<AuthServiceVersionSettings />

View File

@@ -16,7 +16,7 @@ import { type ReactElement } from 'react';
export default function CustomDomains() {
const { currentProject } = useCurrentWorkspaceAndProject();
if (currentProject.plan.isFree) {
if (currentProject?.plan?.isFree) {
return (
<Container
className="grid grid-flow-row gap-6 bg-transparent"
@@ -35,7 +35,7 @@ export default function CustomDomains() {
className="grid max-w-5xl grid-flow-row gap-6 bg-transparent"
rootClassName="bg-transparent"
>
<Box className="flex flex-row items-center gap-4 overflow-hidden rounded-lg border-1 p-4">
<Box className="flex flex-row items-center gap-4 p-4 overflow-hidden rounded-lg border-1">
<div className="flex flex-col space-y-2">
<Text className="text-lg font-semibold">Custom Domains</Text>
@@ -50,7 +50,7 @@ export default function CustomDomains() {
className="ml-1 font-medium"
>
Custom Domains
<ArrowSquareOutIcon className="ml-1 h-4 w-4" />
<ArrowSquareOutIcon className="w-4 h-4 ml-1" />
</Link>
</Text>
</div>

View File

@@ -6,15 +6,20 @@ import { DatabaseServiceVersionSettings } from '@/features/database/settings/com
import { DatabaseStorageCapacity } from '@/features/database/settings/components/DatabaseStorageCapacity';
import { ResetDatabasePasswordSettings } from '@/features/database/settings/components/ResetDatabasePasswordSettings';
import { useCurrentWorkspaceAndProject } from '@/features/projects/common/hooks/useCurrentWorkspaceAndProject';
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
import { useGetPostgresSettingsQuery } from '@/generated/graphql';
import useLocalMimirClient from '@/hooks/useLocalMimirClient/useLocalMimirClient';
import type { ReactElement } from 'react';
export default function DatabaseSettingsPage() {
const isPlatform = useIsPlatform();
const localMimirClient = useLocalMimirClient();
const { currentProject } = useCurrentWorkspaceAndProject();
const { loading, error } = useGetPostgresSettingsQuery({
variables: { appId: currentProject?.id },
skip: !currentProject,
...(!isPlatform ? { client: localMimirClient } : {}),
});
if (loading) {
@@ -38,8 +43,13 @@ export default function DatabaseSettingsPage() {
>
<DatabaseServiceVersionSettings />
<DatabaseStorageCapacity />
<DatabaseConnectionInfo />
<ResetDatabasePasswordSettings />
{isPlatform && (
<>
<DatabaseConnectionInfo />
<ResetDatabasePasswordSettings />
</>
)}
</Container>
);
}

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