Compare commits
10 Commits
@nhost/das
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b087257e4 | ||
|
|
f1b2117c37 | ||
|
|
c1514eb098 | ||
|
|
21bddeed6a | ||
|
|
84dd864186 | ||
|
|
e2c7741468 | ||
|
|
89fd97cbf0 | ||
|
|
f830a9d5f2 | ||
|
|
9a7e431323 | ||
|
|
1f2b0dced4 |
@@ -2,7 +2,7 @@
|
||||
name: "gen: update depenendencies"
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 1 * *'
|
||||
- cron: '0 2 1 2,5,8,11 *'
|
||||
|
||||
jobs:
|
||||
run:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "2.1.3",
|
||||
"version": "2.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -42,6 +42,7 @@
|
||||
"@mui/x-date-pickers": "^5.0.20",
|
||||
"@nhost/nextjs": "workspace:*",
|
||||
"@nhost/react-apollo": "workspace:*",
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.1.2",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
|
||||
@@ -69,6 +69,7 @@ const projectSettingsPages = [
|
||||
},
|
||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||
].map((item) => ({
|
||||
label: item.name,
|
||||
value: item.slug,
|
||||
|
||||
@@ -152,6 +152,7 @@ const projectSettingsPages = [
|
||||
},
|
||||
{ name: 'AI', slug: 'ai', route: 'ai' },
|
||||
{ name: 'Configuration Editor', slug: 'editor', route: 'editor' },
|
||||
{ name: 'Observability', slug: 'metrics', route: 'metrics' },
|
||||
];
|
||||
|
||||
const createOrganization = (org: Org, isPlatform: boolean) => {
|
||||
|
||||
@@ -17,18 +17,18 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex w-full flex-auto flex-col overflow-y-auto overflow-x-hidden"
|
||||
className="flex flex-col flex-auto w-full overflow-x-hidden overflow-y-auto"
|
||||
>
|
||||
<Box
|
||||
sx={{ backgroundColor: 'background.default' }}
|
||||
className="flex h-full flex-col"
|
||||
className="flex flex-col h-full"
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{hasGitRepo && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="grid grid-flow-row place-content-center gap-2"
|
||||
className="grid grid-flow-row gap-2 place-content-center"
|
||||
>
|
||||
<Text color="warning" className="text-sm">
|
||||
As you have a connected repository, make sure to synchronize
|
||||
@@ -52,9 +52,9 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://docs.nhost.io/cli/overlays"
|
||||
href="https://docs.nhost.io/guides/cli/configuration-overlays#configuration-overlays"
|
||||
>
|
||||
docs.nhost.io/cli/overlays
|
||||
Configuration Overlays
|
||||
</a>{' '}
|
||||
for guidance.
|
||||
</Text>
|
||||
|
||||
58
dashboard/src/components/ui/v3/accordion.tsx
Normal file
58
dashboard/src/components/ui/v3/accordion.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn('border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = 'AccordionItem';
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm transition-all"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pb-4 pt-0', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
@@ -3,20 +3,30 @@ import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'prefix'> {
|
||||
prefix?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
({ className, type, prefix, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
||||
className,
|
||||
<div className="relative flex items-center">
|
||||
{prefix && (
|
||||
<span className="pointer-events-none absolute left-3 flex items-center text-muted-foreground">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
||||
prefix && 'pl-6',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { BillingCycle } from './components/BillingCycle';
|
||||
import { BillingDetails } from './components/BillingDetails';
|
||||
import { Estimate } from './components/Estimate';
|
||||
import { SpendingNotifications } from './components/SpendingNotifications';
|
||||
|
||||
export default function BillingEstimate() {
|
||||
return (
|
||||
<div className="">
|
||||
<div className="flex w-full flex-col rounded-md border bg-background">
|
||||
<div className="flex w-full flex-col gap-1 p-4">
|
||||
<span className="text-xl font-medium">Billing Estimate</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Divider />
|
||||
<BillingCycle />
|
||||
<Divider />
|
||||
<Estimate />
|
||||
<Divider />
|
||||
<SpendingNotifications />
|
||||
<Divider />
|
||||
<BillingDetails />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Progress } from '@/components/ui/v3/progress';
|
||||
import { getBillingCycleInfo } from '@/features/orgs/components/billing/utils/getBillingCycle';
|
||||
|
||||
export default function BillingCycle() {
|
||||
const { progress, billingCycleStart, billingCycleEnd, daysLeft } =
|
||||
getBillingCycleInfo();
|
||||
|
||||
const daysText = daysLeft === 1 ? 'day' : 'days';
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col justify-between gap-4 p-4 md:flex-row md:gap-8">
|
||||
<div className="flex basis-1/2 flex-col">
|
||||
<span className="font-medium">
|
||||
Current billing cycle ({daysLeft} {daysText} left)
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2 pb-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">{billingCycleStart}</span>
|
||||
<span className="text-muted-foreground">{billingCycleEnd}</span>
|
||||
</div>
|
||||
<Progress value={progress} className="h-3" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BillingCycle } from './BillingCycle';
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/v3/accordion';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/v3/table';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useBillingGetNextInvoiceQuery } from '@/utils/__generated__/graphql';
|
||||
|
||||
export default function BillingDetails() {
|
||||
const { org } = useCurrentOrg();
|
||||
const { data, loading } = useBillingGetNextInvoiceQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: {
|
||||
organizationID: org?.id,
|
||||
},
|
||||
skip: !org,
|
||||
});
|
||||
|
||||
const billingItems = data?.billingGetNextInvoice?.items ?? [];
|
||||
const amountDue = data?.billingGetNextInvoice?.AmountDue ?? null;
|
||||
|
||||
if (!data || loading) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex h-32 place-content-center">
|
||||
<ActivityIndicator
|
||||
label="Loading billing details..."
|
||||
className="justify-center text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="details" className="border-none">
|
||||
<AccordionTrigger className="p-4">Details</AccordionTrigger>
|
||||
<AccordionContent className="border-t-1 pb-0">
|
||||
<div className="rounded-md">
|
||||
<Table>
|
||||
<TableHeader className="w-full bg-accent">
|
||||
<TableRow>
|
||||
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
||||
Item
|
||||
</TableHead>
|
||||
<TableHead className="rounded-tr-md text-right">
|
||||
Amount
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{billingItems.map((billingItem) => (
|
||||
<TableRow key={billingItem.Description}>
|
||||
<TableCell colSpan={3}>{billingItem.Description}</TableCell>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
${billingItem.Amount}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter className="bg-accent">
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="rounded-bl-md">
|
||||
Total
|
||||
</TableCell>
|
||||
<TableCell className="rounded-br-md text-right">
|
||||
${amountDue}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BillingDetails } from './BillingDetails';
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useBillingGetNextInvoiceQuery } from '@/utils/__generated__/graphql';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export default function Estimate() {
|
||||
const { org } = useCurrentOrg();
|
||||
const { data, loading } = useBillingGetNextInvoiceQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: {
|
||||
organizationID: org?.id,
|
||||
},
|
||||
skip: !org,
|
||||
});
|
||||
|
||||
const amountDue = useMemo(() => {
|
||||
const amount = data?.billingGetNextInvoice?.AmountDue;
|
||||
if (!amount) {
|
||||
return 'N/A';
|
||||
}
|
||||
if (typeof amount !== 'number') {
|
||||
return 'N/A';
|
||||
}
|
||||
return amount.toLocaleString('en', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col justify-between gap-2 p-4 md:flex-row md:gap-8">
|
||||
<div className="flex basis-1/2 flex-col">
|
||||
<span className="font-medium">Estimate</span>
|
||||
<span className="text-xl font-semibold">${amountDue}</span>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<p className="max-w-prose">
|
||||
This estimate reflects your estimated next invoice based on current
|
||||
usage. Please note that usage data may have a processing delay of a
|
||||
few hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Estimate } from './Estimate';
|
||||
@@ -0,0 +1,320 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Switch } from '@/components/ui/v2/Switch';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/v3/form';
|
||||
import { Input } from '@/components/ui/v3/input';
|
||||
import { Progress } from '@/components/ui/v3/progress';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/v3/tooltip';
|
||||
import { useIsOrgAdmin } from '@/features/orgs/hooks/useIsOrgAdmin';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetOrganizationSpendingNotificationDocument,
|
||||
useBillingGetNextInvoiceQuery,
|
||||
useGetOrganizationSpendingNotificationQuery,
|
||||
useUpdateOrganizationSpendingNotificationMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useMemo, type ChangeEvent } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
enabled: Yup.boolean().required(),
|
||||
threshold: Yup.number().test(
|
||||
'is-valid-threshold',
|
||||
`Threshold must be greater than 110% of your plan's price`,
|
||||
(value: number, { options }) => {
|
||||
const planPrice = options?.context?.planPrice || 0;
|
||||
if (value === 0) {
|
||||
return true;
|
||||
}
|
||||
if (typeof value === 'number' && value > 1.1 * planPrice) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
});
|
||||
|
||||
type SpendingNotificationsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function SpendingNotifications() {
|
||||
const { org } = useCurrentOrg();
|
||||
|
||||
const isAdmin = useIsOrgAdmin();
|
||||
|
||||
const { data, loading } = useGetOrganizationSpendingNotificationQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: { orgId: org?.id },
|
||||
skip: !org,
|
||||
});
|
||||
|
||||
const { data: nextInvoiceData, loading: loadingInvoice } =
|
||||
useBillingGetNextInvoiceQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: {
|
||||
organizationID: org?.id,
|
||||
},
|
||||
skip: !org,
|
||||
});
|
||||
|
||||
const amountDue = nextInvoiceData?.billingGetNextInvoice?.AmountDue ?? null;
|
||||
|
||||
const [updateConfig] = useUpdateOrganizationSpendingNotificationMutation({
|
||||
refetchQueries: [GetOrganizationSpendingNotificationDocument],
|
||||
});
|
||||
|
||||
const { threshold } = data?.organizations[0] ?? {};
|
||||
|
||||
const form = useForm<SpendingNotificationsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: false,
|
||||
threshold: threshold ?? 0,
|
||||
},
|
||||
resolver: yupResolver(validationSchema),
|
||||
context: {
|
||||
planPrice: org?.plan?.price ?? 0,
|
||||
},
|
||||
});
|
||||
|
||||
const { watch, setValue } = form;
|
||||
|
||||
const currentThreshold = watch('threshold');
|
||||
|
||||
const handleEnabledChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { checked } = event.target;
|
||||
setValue('enabled', checked, { shouldDirty: true });
|
||||
if (!checked) {
|
||||
setValue('threshold', 0, { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
const enabled = watch('enabled');
|
||||
|
||||
const progress = useMemo(() => {
|
||||
if (!enabled || threshold <= 0 || !amountDue) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const percent = (amountDue / threshold) * 100;
|
||||
return Math.min(Math.max(percent, 0), 100);
|
||||
}, [amountDue, enabled, threshold]);
|
||||
|
||||
const handleThresholdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (event.target.value === '') {
|
||||
setValue('threshold', undefined, { shouldDirty: true });
|
||||
} else {
|
||||
setValue('threshold', Number(event.target.value), { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
enabled: !!threshold,
|
||||
threshold,
|
||||
});
|
||||
}
|
||||
}, [loading, threshold, form]);
|
||||
|
||||
const onSubmit = async (values: SpendingNotificationsFormValues) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
id: org?.id,
|
||||
threshold: values.threshold,
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset({
|
||||
enabled: !!values.threshold,
|
||||
threshold: values.threshold,
|
||||
});
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Spending notifications are being updated...',
|
||||
successMessage:
|
||||
'Spending notifications have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update spending notifications.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const getNotificationPercentageAmount = (factor: number) => {
|
||||
if (!threshold || threshold <= 0) {
|
||||
return '\u00A0';
|
||||
}
|
||||
const amount = threshold * factor;
|
||||
return `$${Math.round(amount)}`;
|
||||
};
|
||||
|
||||
const inputMin = useMemo(
|
||||
() => Math.ceil(1.1 * (amountDue ?? 0)),
|
||||
[amountDue],
|
||||
);
|
||||
|
||||
if (loading || loadingInvoice) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<div className="flex h-32 place-content-center">
|
||||
<ActivityIndicator
|
||||
label="Loading spending notifications..."
|
||||
className="justify-center text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-4 p-4"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<div className="flex flex-1 flex-row items-end justify-between gap-8">
|
||||
<span className="font-medium">Spending Notifications</span>
|
||||
<Switch
|
||||
className="self-end"
|
||||
id="enabled"
|
||||
checked={enabled}
|
||||
onChange={handleEnabledChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-full flex-col justify-between gap-8 md:flex-row">
|
||||
<div className="flex basis-1/2 flex-col gap-2">
|
||||
<p className="max-w-prose">
|
||||
Specify a spending threshold to receive email notifications when
|
||||
your usage approaches the designated amount.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4">
|
||||
{enabled && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="threshold"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-1 flex-col">
|
||||
<FormLabel className="flex flex-1 flex-row items-center gap-2">
|
||||
<span>Amount</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{isAdmin ? (
|
||||
<Input
|
||||
prefix="$"
|
||||
type="number"
|
||||
min={inputMin}
|
||||
placeholder="0"
|
||||
disabled={!enabled}
|
||||
{...field}
|
||||
onChange={handleThresholdChange}
|
||||
value={currentThreshold}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<Input
|
||||
prefix="$"
|
||||
type="number"
|
||||
min="0"
|
||||
placeholder="0"
|
||||
disabled
|
||||
{...field}
|
||||
onChange={handleThresholdChange}
|
||||
value={currentThreshold}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Only an organization admin can change this value.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-1">
|
||||
<div className="basis-3/4" />
|
||||
<div className="flex flex-1 justify-between gap-2">
|
||||
<div className="flex basis-2/3 text-muted-foreground">
|
||||
<span className="w-13 text-center">75%</span>
|
||||
</div>
|
||||
<div className="flex basis-1/3 text-muted-foreground">
|
||||
<span className="w-13 text-center">90%</span>
|
||||
</div>
|
||||
<div className="flex basis-1/3 text-muted-foreground">
|
||||
<span className="w-13 text-center">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Progress value={progress} className="h-3" />
|
||||
<div className="flex flex-1">
|
||||
<div className="basis-3/4" />
|
||||
<div className="flex flex-1 justify-between gap-2">
|
||||
<div className="flex basis-2/3 text-muted-foreground">
|
||||
<span className="w-13 overflow-hidden text-ellipsis text-center">
|
||||
{getNotificationPercentageAmount(0.75) || '\u00A0'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex basis-1/3 text-muted-foreground">
|
||||
<span className="w-13 overflow-hidden text-ellipsis text-center">
|
||||
{getNotificationPercentageAmount(0.9) || '\u00A0'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex basis-1/3 text-muted-foreground">
|
||||
<span className="w-13 overflow-hidden text-ellipsis text-center">
|
||||
{getNotificationPercentageAmount(1) || '\u00A0'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="max-w-prose">
|
||||
You'll receive email alerts when your usage reaches 75%,
|
||||
90%, and 100% of your configured value. These are
|
||||
notifications only - your service will continue running
|
||||
normally.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-fit self-end"
|
||||
disabled={!form.formState.isDirty || !isAdmin}
|
||||
>
|
||||
{form.formState.isSubmitting ? (
|
||||
<ActivityIndicator className="text-sm" />
|
||||
) : (
|
||||
'Save'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SpendingNotifications } from './SpendingNotifications';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as BillingEstimate } from './BillingEstimate';
|
||||
@@ -41,8 +41,8 @@ const changeOrgPlanForm = z.object({
|
||||
|
||||
export default function SubscriptionPlan() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { org, refetch: refetchOrg } = useCurrentOrg();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [changeOrgPlan] = useBillingChangeOrganizationPlanMutation();
|
||||
const { data: { plans = [] } = {} } = useGetOrganizationPlansQuery();
|
||||
const [fetchOrganizationCustomePortalLink, { loading }] =
|
||||
@@ -102,7 +102,6 @@ export default function SubscriptionPlan() {
|
||||
|
||||
if (billingOrganizationCustomePortal) {
|
||||
const newWindow = window.open(billingOrganizationCustomePortal);
|
||||
|
||||
if (!newWindow) {
|
||||
window.location.href = billingOrganizationCustomePortal;
|
||||
}
|
||||
@@ -126,30 +125,34 @@ export default function SubscriptionPlan() {
|
||||
<div className="flex w-full flex-col gap-1 border-b p-4">
|
||||
<h4 className="font-medium">Subscription plan</h4>
|
||||
</div>
|
||||
<div className="flex flex-col border-b md:flex-row">
|
||||
<div className="flex w-full flex-col gap-4 p-4">
|
||||
<div className="flex w-full flex-col justify-between gap-8 border-b p-4 md:flex-row">
|
||||
<div className="flex basis-1/2 flex-col gap-4">
|
||||
<span className="font-medium">Organization name</span>
|
||||
<span className="font-medium">{org?.name}</span>
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-2 p-4">
|
||||
<span className="font-medium">Current plan</span>
|
||||
<span className="text-xl font-bold text-primary">
|
||||
{org?.plan?.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex w-full flex-col items-start justify-start gap-4 p-4 md:items-end md:justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl font-semibold">
|
||||
${org?.plan?.price}
|
||||
<div className="flex flex-1 flex-col gap-8 md:flex-row">
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<span className="font-medium">Current plan</span>
|
||||
<span className="text-xl font-bold text-primary">
|
||||
{org?.plan?.name}
|
||||
</span>
|
||||
<Slash
|
||||
className="h-5 w-5 text-muted-foreground/40"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span className="text-xl font-semibold">month</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-start justify-start md:items-end md:justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl font-semibold">
|
||||
${org?.plan?.price}
|
||||
</span>
|
||||
<Slash
|
||||
className="h-5 w-5 text-muted-foreground/40"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<span className="text-xl font-semibold">month</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col-reverse items-end justify-between gap-2 p-4 md:flex-row md:items-center md:gap-0">
|
||||
<div>
|
||||
<span>For a complete list of features, visit our </span>
|
||||
@@ -164,14 +167,18 @@ export default function SubscriptionPlan() {
|
||||
<ArrowSquareOutIcon className="mb-[2px] ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-end gap-2">
|
||||
<div className="flex w-full flex-row items-center justify-end gap-2">
|
||||
<Button
|
||||
className="h-fit"
|
||||
className="h-fit truncate"
|
||||
variant="secondary"
|
||||
onClick={handleUpdatePaymentDetails}
|
||||
disabled={org?.plan?.isFree || maintenanceActive || loading}
|
||||
>
|
||||
{loading ? <ActivityIndicator /> : 'Stripe Customer Portal'}
|
||||
{loading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<span className="truncate">Stripe Customer Portal</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={org?.plan?.isFree || maintenanceActive}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Progress } from '@/components/ui/v3/progress';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/v3/table';
|
||||
import { getBillingCycleInfo } from '@/features/orgs/components/billing/utils/getBillingCycle';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useBillingGetNextInvoiceQuery } from '@/utils/__generated__/graphql';
|
||||
|
||||
export default function Usage() {
|
||||
const { org } = useCurrentOrg();
|
||||
const { billingCycleRange, progress } = getBillingCycleInfo();
|
||||
const { data, loading } = useBillingGetNextInvoiceQuery({
|
||||
fetchPolicy: 'cache-first',
|
||||
variables: {
|
||||
organizationID: org?.id,
|
||||
},
|
||||
skip: !org,
|
||||
});
|
||||
|
||||
const billingItems = data?.billingGetNextInvoice?.items ?? [];
|
||||
const amountDue = data?.billingGetNextInvoice?.AmountDue ?? null;
|
||||
|
||||
return (
|
||||
<div className="font-medium">
|
||||
<div className="flex flex-col w-full border rounded-md bg-background">
|
||||
<div className="flex flex-col w-full gap-1 p-4">
|
||||
<span>Usage</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center justify-between w-full p-4 border-t border-b">
|
||||
<span>Billing cycle ({billingCycleRange})</span>
|
||||
<Progress value={progress} className="h-2 max-w-xl" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
{loading && (
|
||||
<div className="flex h-32 place-content-center">
|
||||
<ActivityIndicator
|
||||
label="Loading usage stats..."
|
||||
className="justify-center text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && data && (
|
||||
<>
|
||||
<span>Breakdown</span>
|
||||
<div className="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader className="w-full bg-accent">
|
||||
<TableRow>
|
||||
<TableHead colSpan={3} className="w-full rounded-tl-md">
|
||||
Item
|
||||
</TableHead>
|
||||
<TableHead className="text-right rounded-tr-md">
|
||||
Amount
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{billingItems.map((billingItem) => (
|
||||
<TableRow key={billingItem.Description}>
|
||||
<TableCell colSpan={3}>
|
||||
{billingItem.Description}
|
||||
</TableCell>
|
||||
<TableCell colSpan={3} className="text-right">
|
||||
${billingItem.Amount}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
<TableFooter className="bg-accent">
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="rounded-bl-md">
|
||||
Total
|
||||
</TableCell>
|
||||
<TableCell className="text-right rounded-br-md">
|
||||
${amountDue}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Usage } from './Usage';
|
||||
@@ -18,9 +18,13 @@ export const getBillingCycleInfo = () => {
|
||||
(now.getTime() - startOfMonth.getTime()) / (1000 * 60 * 60 * 24) + 1;
|
||||
|
||||
const progress = (daysPassed / totalDays) * 100;
|
||||
const daysLeft = Math.max(Math.ceil(totalDays - daysPassed), 0);
|
||||
|
||||
return {
|
||||
billingCycleStart,
|
||||
billingCycleEnd,
|
||||
billingCycleRange: `${billingCycleStart} - ${billingCycleEnd}`,
|
||||
progress: Math.min(Math.max(progress, 0), 100), // Ensure the value is between 0 and 100
|
||||
daysLeft,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,9 +52,9 @@ export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
href="https://docs.nhost.io/cli/overlays"
|
||||
href="https://docs.nhost.io/guides/cli/configuration-overlays#configuration-overlays"
|
||||
>
|
||||
docs.nhost.io/cli/overlays
|
||||
Configuration Overlays
|
||||
</a>{' '}
|
||||
for guidance.
|
||||
</Text>
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { useIsPlatform } from '@/features/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/utils/execPromiseWithErrorToast';
|
||||
@@ -26,11 +26,12 @@ export default function DisableAIServiceConfirmationDialog({
|
||||
onCancel,
|
||||
onServiceDisabled,
|
||||
}: DisableAIServiceConfirmationDialogProps) {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { currentProject } = useCurrentWorkspaceAndProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
@@ -42,7 +43,7 @@ export default function DisableAIServiceConfirmationDialog({
|
||||
async () => {
|
||||
await updateConfig({
|
||||
variables: {
|
||||
appId: currentProject.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
ai: null,
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -32,13 +31,13 @@ export type AllowedEmailSettingsFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function AllowedEmailDomainsSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -29,13 +28,13 @@ export type AllowedRedirectURLFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function AllowedRedirectURLsSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -25,13 +24,13 @@ const validationSchema = Yup.object({
|
||||
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AnonymousSignInSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -58,13 +57,13 @@ export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AppleProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -269,7 +268,7 @@ export default function AppleProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
Software_Type_Enum,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useGetSoftwareVersionsQuery,
|
||||
@@ -34,13 +33,13 @@ export type AuthServiceVersionFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function AuthServiceVersionSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -154,7 +153,7 @@ export default function AuthServiceVersionSettings() {
|
||||
}}
|
||||
docsLink="https://github.com/nhost/hasura-auth/releases"
|
||||
docsTitle="the latest releases"
|
||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="version"
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -51,13 +50,13 @@ const validationSchema = Yup.object({
|
||||
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AzureADProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -214,7 +213,7 @@ export default function AzureADProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -30,13 +29,13 @@ const validationSchema = Yup.object({
|
||||
export type BlockedEmailFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function BlockedEmailSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -27,13 +26,13 @@ const validationSchema = Yup.object({
|
||||
export type ClientURLFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function ClientURLSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -27,13 +26,13 @@ export type ToggleConcealErrorsFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function ConcealErrorsSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useState } from 'react';
|
||||
@@ -52,16 +52,24 @@ function ConfirmDeleteSMTPSettingsModal({
|
||||
}
|
||||
|
||||
export default function DeleteSMTPSettings() {
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { project } = useProject();
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const smtpSettings = data?.config?.provider?.smtp ?? {};
|
||||
|
||||
const isSMTPConfigured = Boolean(Object.keys(smtpSettings).length);
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -111,7 +119,10 @@ export default function DeleteSMTPSettings() {
|
||||
component: (
|
||||
<ConfirmDeleteSMTPSettingsModal
|
||||
close={closeDialog}
|
||||
onDelete={deleteSMTPSettings}
|
||||
onDelete={async () => {
|
||||
await deleteSMTPSettings();
|
||||
await refetch();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -132,7 +143,7 @@ export default function DeleteSMTPSettings() {
|
||||
color="error"
|
||||
className="mx-4 mt-4 justify-self-end"
|
||||
onClick={confirmDeleteSMTPSettings}
|
||||
disabled={loading || maintenanceActive}
|
||||
disabled={loading || maintenanceActive || !isSMTPConfigured}
|
||||
loading={loading}
|
||||
>
|
||||
Delete
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -25,13 +24,13 @@ const validationSchema = Yup.object({
|
||||
export type DisableNewUsersFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function DisableNewUsersSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -30,13 +29,13 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function DiscordProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -184,7 +183,7 @@ export default function DiscordProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -35,13 +34,13 @@ const validationSchema = Yup.object({
|
||||
export type EmailAndPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function EmailAndPasswordSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function FacebookProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function FacebookProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -33,13 +32,13 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
||||
|
||||
export default function GitHubProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -191,7 +190,7 @@ export default function GitHubProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function GoogleProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function GoogleProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -35,13 +34,13 @@ const validationSchema = Yup.object({
|
||||
export type GravatarFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function GravatarSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function LinkedInProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function LinkedInProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -29,18 +28,19 @@ const validationSchema = Yup.object({
|
||||
export type MFASettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function MFASettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetAuthenticationSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -26,13 +25,13 @@ const validationSchema = Yup.object({
|
||||
export type MagicLinkFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function MagicLinkSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -28,21 +27,21 @@ const validationSchema = yup
|
||||
export type PostmarkFormValues = yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function PostmarkSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data } = useGetSmtpSettingsQuery({
|
||||
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { sender, password } = data?.config?.provider?.smtp || {};
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -81,6 +80,8 @@ export default function PostmarkSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset({ ...values });
|
||||
await refetch();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -50,17 +49,17 @@ const validationSchema = Yup.object({
|
||||
export type SMSSettingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function SMSSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, error, loading } = useGetSignInMethodsQuery({
|
||||
const { data, loading, error } = useGetSignInMethodsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetSmtpSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -38,14 +37,15 @@ const smtpValidationSchema = yup
|
||||
export type SmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
||||
|
||||
export default function SMTPSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data } = useGetSmtpSettingsQuery({
|
||||
const { data, refetch } = useGetSmtpSettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -83,7 +83,6 @@ export default function SMTPSettings() {
|
||||
} = form;
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSmtpSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -104,6 +103,8 @@ export default function SMTPSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset({ ...values });
|
||||
await refetch();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetAuthenticationSettingsDocument,
|
||||
useGetAuthenticationSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -34,13 +33,13 @@ const validationSchema = Yup.object({
|
||||
export type SessionFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function SessionSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { openDialog } = useDialog();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetAuthenticationSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function SpotifyProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -185,7 +184,7 @@ export default function SpotifyProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -33,13 +32,13 @@ import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWith
|
||||
|
||||
export default function TwitchProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -191,7 +190,7 @@ export default function TwitchProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { CopyIcon } from '@/components/ui/v2/icons/CopyIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -45,13 +44,13 @@ const validationSchema = Yup.object({
|
||||
export type TwitterProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function TwitterProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -217,7 +216,7 @@ export default function TwitterProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -26,13 +25,13 @@ const validationSchema = Yup.object({
|
||||
export type WebAuthnFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function WebAuthnSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
baseProviderValidationSchema,
|
||||
} from '@/features/orgs/projects/authentication/settings/components/BaseProviderSettings';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,13 +30,13 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function WindowsLiveProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -183,7 +182,7 @@ export default function WindowsLiveProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetSignInMethodsDocument,
|
||||
useGetSignInMethodsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -57,13 +56,13 @@ const validationSchema = Yup.object({
|
||||
export type WorkOsProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function WorkOsProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -244,7 +243,7 @@ export default function WorkOsProviderSettings() {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="w-4 h-4" />
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
baseEnvironmentVariableFormValidationSchema,
|
||||
} from '@/features/orgs/projects/environmentVariables/settings/components/BaseEnvironmentVariableForm';
|
||||
import {
|
||||
GetEnvironmentVariablesDocument,
|
||||
useGetEnvironmentVariablesQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -51,13 +50,13 @@ export default function CreateEnvironmentVariableForm({
|
||||
|
||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const availableEnvironmentVariables = data?.config?.global?.environment || [];
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from '@/features/orgs/projects/environmentVariables/settings/components/BaseEnvironmentVariableForm';
|
||||
import type { EnvironmentVariable } from '@/types/application';
|
||||
import {
|
||||
GetEnvironmentVariablesDocument,
|
||||
useGetEnvironmentVariablesQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -58,13 +57,13 @@ export default function EditEnvironmentVariableForm({
|
||||
|
||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const availableEnvironmentVariables = data?.config?.global?.environment || [];
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettings
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
@@ -17,7 +16,6 @@ import { CreateEnvironmentVariableForm } from '@/features/orgs/projects/environm
|
||||
import { EditEnvironmentVariableForm } from '@/features/orgs/projects/environmentVariables/settings/components/EditEnvironmentVariableForm';
|
||||
import type { EnvironmentVariable } from '@/types/application';
|
||||
import {
|
||||
GetEnvironmentVariablesDocument,
|
||||
useGetEnvironmentVariablesQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -43,8 +41,9 @@ export default function EnvironmentVariableSettings() {
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading, error, refetch } = useGetEnvironmentVariablesQuery({
|
||||
const { data, error, refetch } = useGetEnvironmentVariablesQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -63,19 +62,9 @@ export default function EnvironmentVariableSettings() {
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetEnvironmentVariablesDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading environment variables..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -100,6 +89,7 @@ export default function EnvironmentVariableSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
await refetch();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
|
||||
@@ -41,6 +41,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
|
||||
const { data, loading, error } = useGetEnvironmentVariablesQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -36,7 +35,6 @@ export default function HasuraAllowListSettings() {
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -32,8 +31,8 @@ export default function HasuraConsoleSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Input } from '@/components/ui/v2/Input';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -40,8 +39,8 @@ export default function HasuraCorsDomainSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -32,8 +31,8 @@ export default function HasuraDevModeSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -42,8 +41,8 @@ export default function HasuraEnabledAPISettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -144,7 +143,7 @@ export default function HasuraEnabledAPISettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-6"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-6"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="enabledAPIs"
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -33,8 +32,8 @@ export default function HasuraInferFunctionPermissionsSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { HighlightedText } from '@/components/presentational/HighlightedText';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -38,11 +37,11 @@ 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 localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -161,7 +160,7 @@ export default function HasuraLogLevelSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="logLevel"
|
||||
|
||||
@@ -6,7 +6,6 @@ import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -36,8 +35,8 @@ export default function HasuraPoolSizeSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -124,7 +123,7 @@ export default function HasuraPoolSizeSettings() {
|
||||
loading: formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<Input
|
||||
{...register('httpPoolSize')}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetHasuraSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -33,8 +32,8 @@ export default function HasuraRemoteSchemaPermissionsSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimi
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
Software_Type_Enum,
|
||||
useGetHasuraSettingsQuery,
|
||||
useGetSoftwareVersionsQuery,
|
||||
@@ -40,8 +39,8 @@ export default function HasuraServiceVersionSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -158,7 +157,7 @@ export default function HasuraServiceVersionSettings() {
|
||||
}}
|
||||
docsLink="https://hub.docker.com/r/nhost/graphql-engine/tags"
|
||||
docsTitle="the latest releases"
|
||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="version"
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
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 {
|
||||
GetObservabilitySettingsDocument,
|
||||
useGetObservabilitySettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
type ConfigConfigUpdateInput,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { DiscordFormSection } from '@/features/orgs/projects/metrics/settings/components/DiscordFormSection';
|
||||
import { EmailsFormSection } from '@/features/orgs/projects/metrics/settings/components/EmailsFormSection';
|
||||
import { PagerdutyFormSection } from '@/features/orgs/projects/metrics/settings/components/PagerdutyFormSection';
|
||||
import type { EventSeverity } from '@/features/orgs/projects/metrics/settings/components/PagerdutyFormSection/PagerdutyFormSectionTypes';
|
||||
import { SlackFormSection } from '@/features/orgs/projects/metrics/settings/components/SlackFormSection';
|
||||
import { WebhookFormSection } from '@/features/orgs/projects/metrics/settings/components/WebhookFormSection';
|
||||
import type { HttpMethod } from '@/features/orgs/projects/metrics/settings/components/WebhookFormSection/WebhookFormSectionTypes';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import type { ContactPointsFormValues } from './ContactPointsSettingsTypes';
|
||||
import { validationSchema } from './ContactPointsSettingsTypes';
|
||||
|
||||
export default function ContactPointsSettings() {
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
|
||||
const { data, loading, error } = useGetObservabilitySettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { emails, pagerduty, discord, slack, webhook } =
|
||||
data?.config?.observability?.grafana?.contacts || {};
|
||||
|
||||
const form = useForm<ContactPointsFormValues>({
|
||||
defaultValues: {
|
||||
emails: [],
|
||||
discord: [],
|
||||
pagerduty: [],
|
||||
slack: [],
|
||||
webhook: [],
|
||||
},
|
||||
values: {
|
||||
emails: emails?.map((email) => ({ email })) || [],
|
||||
discord: discord || [],
|
||||
pagerduty:
|
||||
pagerduty?.map((elem) => ({
|
||||
...elem,
|
||||
severity: elem.severity as EventSeverity,
|
||||
})) || [],
|
||||
slack:
|
||||
slack?.map((elem) => ({
|
||||
...elem,
|
||||
mentionUsers: elem.mentionUsers.join(','),
|
||||
mentionGroups: elem.mentionGroups.join(','),
|
||||
})) || [],
|
||||
webhook:
|
||||
webhook?.map((elem) => ({
|
||||
...elem,
|
||||
httpMethod: elem.httpMethod as HttpMethod,
|
||||
})) || [],
|
||||
},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetObservabilitySettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const getFormattedConfig = (values: ContactPointsFormValues) => {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as ContactPointsFormValues;
|
||||
const newEmails =
|
||||
sanitizedValues.emails?.map((email) => email.email) ?? null;
|
||||
const newPagerduty =
|
||||
sanitizedValues.pagerduty?.length > 0 ? sanitizedValues.pagerduty : null;
|
||||
const newDiscord =
|
||||
sanitizedValues.discord?.length > 0 ? sanitizedValues.discord : null;
|
||||
|
||||
const newSlack =
|
||||
sanitizedValues.slack?.length > 0
|
||||
? sanitizedValues.slack.map((elem) => ({
|
||||
...elem,
|
||||
mentionUsers: elem.mentionUsers.split(','),
|
||||
mentionGroups: elem.mentionGroups.split(','),
|
||||
}))
|
||||
: null;
|
||||
|
||||
const newWebhook =
|
||||
sanitizedValues.webhook?.length > 0 ? sanitizedValues.webhook : null;
|
||||
|
||||
const config: ConfigConfigUpdateInput = {
|
||||
observability: {
|
||||
grafana: {
|
||||
contacts: {
|
||||
emails: newEmails,
|
||||
pagerduty: newPagerduty,
|
||||
discord: newDiscord,
|
||||
slack: newSlack,
|
||||
webhook: newWebhook,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={1000} label="Loading contact points..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const handleSubmit = async (formValues: ContactPointsFormValues) => {
|
||||
const config = getFormattedConfig(formValues);
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
config,
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
form.reset(formValues);
|
||||
await refetchProject();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Contact points are being updated...',
|
||||
successMessage: 'Contact points have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update contact points.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Contact Points"
|
||||
description="Define the contact points where your notifications will be sent."
|
||||
docsLink="https://docs.nhost.io/platform/metrics#configure-contact-points"
|
||||
rootClassName="gap-0"
|
||||
className={twMerge('my-2 px-0')}
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !form.formState.isDirty || maintenanceActive,
|
||||
loading: form.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Divider />
|
||||
<EmailsFormSection />
|
||||
<Divider />
|
||||
<PagerdutyFormSection />
|
||||
<Divider />
|
||||
<DiscordFormSection />
|
||||
<Divider />
|
||||
<SlackFormSection />
|
||||
<Divider />
|
||||
<WebhookFormSection />
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { EventSeverity } from '@/features/orgs/projects/metrics/settings/components/PagerdutyFormSection/PagerdutyFormSectionTypes';
|
||||
import { HttpMethod } from '@/features/orgs/projects/metrics/settings/components/WebhookFormSection/WebhookFormSectionTypes';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const validationSchema = Yup.object({
|
||||
emails: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
email: Yup.string().email('Invalid email address').required(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
discord: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
url: Yup.string()
|
||||
.url('Invalid Discord URL')
|
||||
.required('Discord webhook URL is required'),
|
||||
avatarUrl: Yup.string().url('Invalid avatar URL'),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
pagerduty: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
integrationKey: Yup.string().required(
|
||||
'PagerDuty integration key is required',
|
||||
),
|
||||
severity: Yup.string()
|
||||
.oneOf(Object.values(EventSeverity))
|
||||
.required('PagerDuty severity is required'),
|
||||
class: Yup.string(),
|
||||
component: Yup.string(),
|
||||
group: Yup.string(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
slack: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
recipient: Yup.string(),
|
||||
token: Yup.string(),
|
||||
username: Yup.string(),
|
||||
iconEmoji: Yup.string(),
|
||||
iconURL: Yup.string().url('Invalid icon URL'),
|
||||
mentionUsers: Yup.string(),
|
||||
mentionGroups: Yup.string(),
|
||||
mentionChannel: Yup.string(),
|
||||
url: Yup.string().url('Invalid Slack webhook URL'),
|
||||
endpointURL: Yup.string().url('Invalid endpoint URL'),
|
||||
}),
|
||||
)
|
||||
.test(
|
||||
'either-url-or-recipient-token',
|
||||
'Either URL or both recipient and token must be provided',
|
||||
(value) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
const result = value.every(
|
||||
(item) => item.url || (item.recipient && item.token),
|
||||
);
|
||||
if (result) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
)
|
||||
.nullable(),
|
||||
webhook: Yup.array()
|
||||
.of(
|
||||
Yup.object({
|
||||
url: Yup.string()
|
||||
.url('Invalid webhook URL')
|
||||
.required('URL is required'),
|
||||
httpMethod: Yup.string()
|
||||
.oneOf(Object.values(HttpMethod), 'Invalid HTTP method')
|
||||
.required('HTTP method is required'),
|
||||
username: Yup.string(),
|
||||
password: Yup.string(),
|
||||
authorizationScheme: Yup.string(),
|
||||
authorizationCredentials: Yup.string(),
|
||||
maxAlerts: Yup.number()
|
||||
.min(0, 'Max alerts must be greater than 0')
|
||||
.integer('Max alerts must be an integer'),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
});
|
||||
|
||||
export type ContactPointsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ContactPointsSettings } from './ContactPointsSettings';
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import type { ContactPointsFormValues } from '@/features/orgs/projects/metrics/settings/components/ContactPointsSettings/ContactPointsSettingsTypes';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
export default function DiscordFormSection() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
control,
|
||||
} = useFormContext<ContactPointsFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'discord',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Box className="flex flex-row items-center justify-between">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Discord
|
||||
</Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Receive alert notifications in your Discord channels when your
|
||||
Grafana alert rules are triggered and resolved.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() => append({ url: '', avatarUrl: '' })}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
{fields?.length > 0 ? (
|
||||
<Box className="flex flex-col gap-12">
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex w-full items-center gap-2">
|
||||
<Box className="flex flex-1 flex-col gap-2">
|
||||
<Input
|
||||
{...register(`discord.${index}.url`)}
|
||||
id={`${field.id}-discord`}
|
||||
label="Discord URL"
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.discord?.[index]?.url}
|
||||
helperText={errors?.discord?.[index]?.url?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`discord.${index}.avatarUrl`)}
|
||||
id={`${field.id}-discord-avatar`}
|
||||
label="Avatar URL"
|
||||
placeholder="https://discord.com/api/avatar/..."
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.discord?.[index]?.avatarUrl}
|
||||
helperText={errors?.discord?.[index]?.avatarUrl?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
className=""
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-6 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as DiscordFormSection } from './DiscordFormSection';
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import type { ContactPointsFormValues } from '@/features/orgs/projects/metrics/settings/components/ContactPointsSettings/ContactPointsSettingsTypes';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
export default function EmailsFormSection() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
control,
|
||||
} = useFormContext<ContactPointsFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'emails',
|
||||
});
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Box className="flex flex-row items-center justify-between">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Email
|
||||
</Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Select your preferred emails for receiving notifications when
|
||||
your alert rules are firing.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button variant="borderless" onClick={() => append({ email: '' })}>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{fields?.length > 0 ? (
|
||||
<Box className="flex flex-col gap-6">
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex w-full items-center gap-2">
|
||||
<Input
|
||||
{...register(`emails.${index}.email`)}
|
||||
id={`${field.id}-email`}
|
||||
placeholder="Enter email address"
|
||||
className="w-full"
|
||||
label={`Email #${index + 1}`}
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.emails?.[index]?.email}
|
||||
helperText={errors?.emails?.[index]?.email?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
variant="borderless"
|
||||
className="h-10 self-end"
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-6 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as EmailsFormSection } from './EmailsFormSection';
|
||||
@@ -0,0 +1,206 @@
|
||||
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 {
|
||||
GetSmtpSettingsDocument,
|
||||
useGetObservabilitySettingsQuery,
|
||||
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';
|
||||
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
const smtpValidationSchema = yup
|
||||
.object({
|
||||
host: yup.string().label('SMTP Host').required(),
|
||||
port: yup
|
||||
.number()
|
||||
.typeError('The SMTP port should contain only numbers.')
|
||||
.required(),
|
||||
user: yup.string().label('Username').required(),
|
||||
password: yup.string().label('Password'),
|
||||
sender: yup.string().label('SMTP Sender').email().required(),
|
||||
})
|
||||
.required();
|
||||
|
||||
export type MetricsSmtpFormValues = yup.InferType<typeof smtpValidationSchema>;
|
||||
|
||||
export default function MetricsSMTPSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data } = useGetObservabilitySettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { host, port, user, sender, password } =
|
||||
data?.config?.observability?.grafana?.smtp || {};
|
||||
|
||||
const form = useForm<Optional<MetricsSmtpFormValues, 'password'>>({
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(smtpValidationSchema),
|
||||
defaultValues: {
|
||||
host: '',
|
||||
port: undefined,
|
||||
user: '',
|
||||
password: '',
|
||||
sender: '',
|
||||
},
|
||||
values: {
|
||||
host: host || '',
|
||||
port,
|
||||
user: user || '',
|
||||
password: password || '',
|
||||
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: MetricsSmtpFormValues) => {
|
||||
const { password: newPassword, ...valuesWithoutPassword } = values;
|
||||
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.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: 'Metrics SMTP settings are being updated...',
|
||||
successMessage: 'Metrics SMTP settings have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update the Metrics SMTP settings.',
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form onSubmit={handleEditSMTPSettings}>
|
||||
<SettingsContainer
|
||||
title="SMTP Settings"
|
||||
description="Configure your SMTP settings to send emails as part of your alerting."
|
||||
docsLink="https://docs.nhost.io/platform/metrics#smtp"
|
||||
submitButtonText="Save"
|
||||
className="grid gap-4 lg:grid-cols-9"
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !isDirty || maintenanceActive,
|
||||
loading: isSubmitting,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
{...registerSmtp('sender')}
|
||||
id="sender"
|
||||
name="sender"
|
||||
label="From Email"
|
||||
placeholder="admin@localhost"
|
||||
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="localhost"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.host)}
|
||||
helperText={errors.host?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('port')}
|
||||
id="port"
|
||||
name="port"
|
||||
label="Port"
|
||||
type="number"
|
||||
placeholder="25"
|
||||
className="lg:col-span-1"
|
||||
hideEmptyHelperText
|
||||
fullWidth
|
||||
error={Boolean(errors.port)}
|
||||
helperText={errors.port?.message}
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...registerSmtp('user')}
|
||||
id="user"
|
||||
label="SMTP Username"
|
||||
placeholder="Enter 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}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as MetricsSMTPSettings } from './MetricsSMTPSettings';
|
||||
@@ -0,0 +1,155 @@
|
||||
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 { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { ContactPointsSettings } from '@/features/orgs/projects/metrics/settings/components/ContactPointsSettings';
|
||||
import { MetricsSMTPSettings } from '@/features/orgs/projects/metrics/settings/components/MetricsSMTPSettings';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
GetObservabilitySettingsDocument,
|
||||
useGetObservabilitySettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
const metricsAlertingValidationSchema = Yup.object({
|
||||
enabled: Yup.boolean(),
|
||||
});
|
||||
|
||||
export type MetricsAlertingFormValues = Yup.InferType<
|
||||
typeof metricsAlertingValidationSchema
|
||||
>;
|
||||
|
||||
export default function MetricsSettings() {
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetObservabilitySettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { data, loading, error } = useGetObservabilitySettingsQuery({
|
||||
variables: { appId: project?.id },
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { enabled: alertingEnabled } =
|
||||
data?.config?.observability.grafana.alerting || {};
|
||||
|
||||
const alertingForm = useForm<MetricsAlertingFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: alertingEnabled,
|
||||
},
|
||||
resolver: yupResolver(metricsAlertingValidationSchema),
|
||||
});
|
||||
|
||||
const { watch } = alertingForm;
|
||||
const alerting = watch('enabled');
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
alertingForm.reset({
|
||||
enabled: alertingEnabled,
|
||||
});
|
||||
}
|
||||
}, [loading, alertingEnabled, alertingForm]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
label="Loading Alerting settings..."
|
||||
className="justify-center"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function handleSubmit(formValues: MetricsAlertingFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
config: {
|
||||
observability: {
|
||||
grafana: {
|
||||
alerting: {
|
||||
enabled: formValues.enabled,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
alertingForm.reset(formValues);
|
||||
await refetchProject();
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
title: 'Apply your changes',
|
||||
component: <ApplyLocalSettingsDialog />,
|
||||
props: {
|
||||
PaperProps: {
|
||||
className: 'max-w-2xl',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Alerting settings are being updated...',
|
||||
successMessage: 'Alerting settings have been updated successfully.',
|
||||
errorMessage:
|
||||
'An error occurred while trying to update alerting settings.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid max-w-5xl grid-flow-row bg-transparent gap-y-6">
|
||||
<FormProvider {...alertingForm}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<SettingsContainer
|
||||
title="Alerting"
|
||||
description="Enable or disable Alerting."
|
||||
slotProps={{
|
||||
submitButton: {
|
||||
disabled: !alertingForm.formState.isDirty || maintenanceActive,
|
||||
loading: alertingForm.formState.isSubmitting,
|
||||
},
|
||||
}}
|
||||
switchId="enabled"
|
||||
docsTitle="enabling or disabling Alerting"
|
||||
docsLink="https://docs.nhost.io/platform/metrics#alerting"
|
||||
showSwitch
|
||||
className="hidden"
|
||||
/>
|
||||
</Form>
|
||||
</FormProvider>
|
||||
{alerting ? (
|
||||
<>
|
||||
<MetricsSMTPSettings />
|
||||
<ContactPointsSettings />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as MetricsSettings } from './MetricsSettings';
|
||||
@@ -0,0 +1,161 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import type { ContactPointsFormValues } from '@/features/orgs/projects/metrics/settings/components/ContactPointsSettings/ContactPointsSettingsTypes';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import { EventSeverity } from './PagerdutyFormSectionTypes';
|
||||
|
||||
export default function PagerdutyFormSection() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
control,
|
||||
} = useFormContext<ContactPointsFormValues>();
|
||||
const formValues = useWatch<ContactPointsFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'pagerduty',
|
||||
});
|
||||
|
||||
const onChangeSeverity = (value: string | undefined, index: number) =>
|
||||
setValue(`pagerduty.${index}.severity`, value as EventSeverity);
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Box className="flex flex-row items-center justify-between">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
PagerDuty
|
||||
</Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Receive notifications in PagerDuty when your alert rules are
|
||||
firing.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
append({
|
||||
class: '',
|
||||
component: '',
|
||||
group: '',
|
||||
severity: EventSeverity.CRITICAL,
|
||||
integrationKey: '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{fields?.length > 0 ? (
|
||||
<Box className="flex flex-col gap-12">
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex w-full items-center gap-2">
|
||||
<Box className="grid flex-grow gap-4 lg:grid-cols-9">
|
||||
<Input
|
||||
{...register(`pagerduty.${index}.integrationKey`)}
|
||||
id={`${field.id}-integrationKey`}
|
||||
placeholder="Enter PagerDuty Integration Key"
|
||||
className="w-full lg:col-span-7"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.pagerduty?.[index]?.integrationKey}
|
||||
helperText={
|
||||
errors?.pagerduty?.[index]?.integrationKey?.message
|
||||
}
|
||||
fullWidth
|
||||
label="Integration Key"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Select
|
||||
fullWidth
|
||||
value={formValues.pagerduty.at(index)?.severity || ''}
|
||||
className="lg:col-span-2"
|
||||
label="Severity"
|
||||
onChange={(_event, inputValue) =>
|
||||
onChangeSeverity(inputValue as string, index)
|
||||
}
|
||||
placeholder="Select severity"
|
||||
slotProps={{
|
||||
listbox: { className: 'min-w-0 w-full' },
|
||||
popper: {
|
||||
disablePortal: false,
|
||||
className: 'z-[10000] w-[270px]',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(EventSeverity).map((severity) => (
|
||||
<Option key={severity} value={severity}>
|
||||
{severity}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
{...register(`pagerduty.${index}.class`)}
|
||||
id={`${field.id}-class`}
|
||||
placeholder="Enter type of the event"
|
||||
label="Class"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.pagerduty?.[index]?.class}
|
||||
helperText={errors?.pagerduty?.[index]?.class?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`pagerduty.${index}.component`)}
|
||||
id={`${field.id}-component`}
|
||||
placeholder="Enter component of the event"
|
||||
label="Component"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.pagerduty?.[index]?.component}
|
||||
helperText={errors?.pagerduty?.[index]?.component?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`pagerduty.${index}.group`)}
|
||||
id={`${field.id}-group`}
|
||||
placeholder="Enter logical group of components"
|
||||
label="Group"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.pagerduty?.[index]?.group}
|
||||
helperText={errors?.pagerduty?.[index]?.group?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-6 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum EventSeverity {
|
||||
CRITICAL = 'critical',
|
||||
ERROR = 'error',
|
||||
WARNING = 'warning',
|
||||
INFO = 'info',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as PagerdutyFormSection } from './PagerdutyFormSection';
|
||||
@@ -0,0 +1,222 @@
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import type { ContactPointsFormValues } from '@/features/orgs/projects/metrics/settings/components/ContactPointsSettings/ContactPointsSettingsTypes';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
export default function SlackFormSection() {
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
formState: { errors },
|
||||
trigger: triggerValidation,
|
||||
} = useFormContext<ContactPointsFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'slack',
|
||||
});
|
||||
|
||||
const handleRemove = (index: number) => {
|
||||
remove(index);
|
||||
if (fields?.length === 1) {
|
||||
triggerValidation();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Box className="flex flex-row items-center justify-between">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Slack
|
||||
</Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Select your preferred Slack channels for receiving notifications
|
||||
when your alert rules are firing.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
append({
|
||||
recipient: '',
|
||||
token: '',
|
||||
username: '',
|
||||
iconEmoji: '',
|
||||
iconURL: '',
|
||||
mentionGroups: '',
|
||||
mentionUsers: '',
|
||||
mentionChannel: '',
|
||||
url: '',
|
||||
endpointURL: '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
{fields?.length > 0 ? (
|
||||
<Box className="flex flex-col gap-12">
|
||||
{!!errors?.slack?.root?.message && (
|
||||
<Alert severity="error" className="w-full">
|
||||
{errors?.slack?.root?.message}
|
||||
</Alert>
|
||||
)}
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex w-full items-center gap-2">
|
||||
<Box className="grid flex-grow gap-4 lg:grid-cols-9">
|
||||
<Input
|
||||
{...register(`slack.${index}.recipient`)}
|
||||
id={`${field.id}-recipient`}
|
||||
placeholder="Enter recipient"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.recipient}
|
||||
helperText={errors?.slack?.[index]?.recipient?.message}
|
||||
fullWidth
|
||||
label="Recipient"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.token`)}
|
||||
id={`${field.id}-token`}
|
||||
placeholder="Enter Slack API token"
|
||||
label="Token"
|
||||
className="w-full lg:col-span-6"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.token}
|
||||
helperText={errors?.slack?.[index]?.token?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.username`)}
|
||||
id={`${field.id}-username`}
|
||||
placeholder="Enter bot's username"
|
||||
label="Bot Username"
|
||||
className="w-full lg:col-span-5"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.username}
|
||||
helperText={errors?.slack?.[index]?.username?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`slack.${index}.mentionChannel`)}
|
||||
id={`${field.id}-mentionChannel`}
|
||||
placeholder="Enter channel to mention"
|
||||
label="Mention Channel"
|
||||
className="w-full lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.mentionChannel}
|
||||
helperText={errors?.slack?.[index]?.mentionChannel?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.mentionUsers`)}
|
||||
id={`${field.id}-mentionUsers`}
|
||||
placeholder="Enter users to mention (separated by commas)"
|
||||
label="Mention Users"
|
||||
className="w-full lg:col-span-9"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.mentionUsers}
|
||||
helperText={errors?.slack?.[index]?.mentionUsers?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.mentionGroups`)}
|
||||
id={`${field.id}-mentionGroups`}
|
||||
placeholder="Enter groups to mention (separated by commas)"
|
||||
label="Mention Groups"
|
||||
className="w-full lg:col-span-9"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.mentionGroups}
|
||||
helperText={errors?.slack?.[index]?.mentionGroups?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.iconEmoji`)}
|
||||
id={`${field.id}-iconEmoji`}
|
||||
placeholder="Enter emoji icon"
|
||||
label="Emoji Icon"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.iconEmoji}
|
||||
helperText={errors?.slack?.[index]?.iconEmoji?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`slack.${index}.iconURL`)}
|
||||
id={`${field.id}-iconURL`}
|
||||
placeholder="Enter emoji icon URL"
|
||||
label="Emoji Icon URL"
|
||||
className="w-full lg:col-span-6"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.iconURL}
|
||||
helperText={errors?.slack?.[index]?.iconURL?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.url`)}
|
||||
id={`${field.id}-url`}
|
||||
placeholder="Enter Slack Webhook URL"
|
||||
label="Slack Webhook URL"
|
||||
className="w-full lg:col-span-9"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.url}
|
||||
helperText={errors?.slack?.[index]?.url?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`slack.${index}.endpointURL`)}
|
||||
id={`${field.id}-endpointURL`}
|
||||
placeholder="Enter endpoint URL"
|
||||
label="Endpoint URL"
|
||||
className="w-full lg:col-span-9"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.slack?.[index]?.endpointURL}
|
||||
helperText={errors?.slack?.[index]?.endpointURL?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
className=""
|
||||
color="error"
|
||||
onClick={() => handleRemove(index)}
|
||||
>
|
||||
<TrashIcon className="h-6 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as SlackFormSection } from './SlackFormSection';
|
||||
@@ -0,0 +1,218 @@
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { IconButton } from '@/components/ui/v2/IconButton';
|
||||
import { EyeIcon } from '@/components/ui/v2/icons/EyeIcon';
|
||||
import { EyeOffIcon } from '@/components/ui/v2/icons/EyeOffIcon';
|
||||
import { InfoIcon } from '@/components/ui/v2/icons/InfoIcon';
|
||||
import { PlusIcon } from '@/components/ui/v2/icons/PlusIcon';
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { Input } from '@/components/ui/v2/Input';
|
||||
import { InputAdornment } from '@/components/ui/v2/InputAdornment';
|
||||
import { Option } from '@/components/ui/v2/Option';
|
||||
import { Select } from '@/components/ui/v2/Select';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import type { ContactPointsFormValues } from '@/features/orgs/projects/metrics/settings/components/ContactPointsSettings/ContactPointsSettingsTypes';
|
||||
import { useState } from 'react';
|
||||
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
import { HttpMethod } from './WebhookFormSectionTypes';
|
||||
|
||||
export default function WebhookFormSection() {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
control,
|
||||
} = useFormContext<ContactPointsFormValues>();
|
||||
const formValues = useWatch<ContactPointsFormValues>();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: 'webhook',
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const onChangeHttpMethod = (value: string | undefined, index: number) =>
|
||||
setValue(`webhook.${index}.httpMethod`, value as HttpMethod);
|
||||
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-4">
|
||||
<Box className="flex flex-row items-center justify-between">
|
||||
<Box className="flex flex-row items-center space-x-2">
|
||||
<Text variant="h4" className="font-semibold">
|
||||
Webhook
|
||||
</Text>
|
||||
<Tooltip
|
||||
title={
|
||||
<span>
|
||||
Send information about a state change to an external service
|
||||
over HTTP.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<InfoIcon aria-label="Info" className="h-4 w-4" color="primary" />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant="borderless"
|
||||
onClick={() =>
|
||||
append({
|
||||
url: '',
|
||||
httpMethod: HttpMethod.POST,
|
||||
username: '',
|
||||
password: '',
|
||||
authorizationScheme: '',
|
||||
authorizationCredentials: '',
|
||||
maxAlerts: 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{fields?.length > 0 ? (
|
||||
<Box className="flex flex-col gap-12">
|
||||
{fields.map((field, index) => (
|
||||
<Box key={field.id} className="flex w-full items-center space-x-2">
|
||||
<Box className="grid flex-grow gap-4 lg:grid-cols-9">
|
||||
<Input
|
||||
{...register(`webhook.${index}.url`)}
|
||||
id={`${field.id}-url`}
|
||||
placeholder="Enter URL"
|
||||
className="w-full lg:col-span-7"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhook?.[index]?.url}
|
||||
helperText={errors?.webhook?.[index]?.url?.message}
|
||||
fullWidth
|
||||
label="URL"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Select
|
||||
fullWidth
|
||||
value={formValues.webhook.at(index)?.httpMethod || ''}
|
||||
className="lg:col-span-2"
|
||||
label="HTTP Method"
|
||||
onChange={(_event, inputValue) =>
|
||||
onChangeHttpMethod(inputValue as string, index)
|
||||
}
|
||||
placeholder="Select HTTP Method"
|
||||
slotProps={{
|
||||
listbox: { className: 'min-w-0 w-full' },
|
||||
popper: {
|
||||
disablePortal: false,
|
||||
className: 'z-[10000] w-[270px]',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(HttpMethod).map((httpMethod) => (
|
||||
<Option key={httpMethod} value={httpMethod}>
|
||||
{httpMethod}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
{...register(`webhook.${index}.username`)}
|
||||
id={`${field.id}-username`}
|
||||
placeholder="Enter username"
|
||||
label="Username"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhook?.[index]?.username}
|
||||
helperText={errors?.webhook?.[index]?.username?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`webhook.${index}.password`)}
|
||||
id={`${field.id}-password`}
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter password"
|
||||
label="Password"
|
||||
className="w-full lg:col-span-4"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhook?.[index]?.password}
|
||||
helperText={errors?.webhook?.[index]?.password?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
endAdornment={
|
||||
<InputAdornment className="px-2" position="end">
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
aria-label={
|
||||
showPassword ? 'Hide Password' : 'Show Password'
|
||||
}
|
||||
onClick={() => setShowPassword((show) => !show)}
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOffIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<EyeIcon className="h-5 w-5" />
|
||||
)}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
{...register(`webhook.${index}.maxAlerts`)}
|
||||
id={`${field.id}-maxAlerts`}
|
||||
placeholder="Enter max alerts"
|
||||
label="Max Alerts (0 means no limit)"
|
||||
className="w-full lg:col-span-2"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhook?.[index]?.maxAlerts}
|
||||
helperText={errors?.webhook?.[index]?.maxAlerts?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Input
|
||||
{...register(`webhook.${index}.authorizationScheme`)}
|
||||
id={`${field.id}-authorizationScheme`}
|
||||
placeholder="Enter authorization scheme"
|
||||
label="Authorization Scheme"
|
||||
className="w-full lg:col-span-3"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhook?.[index]?.authorizationScheme}
|
||||
helperText={
|
||||
errors?.webhook?.[index]?.authorizationScheme?.message
|
||||
}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
{...register(`webhook.${index}.authorizationCredentials`)}
|
||||
id={`${field.id}-authorizationCredentials`}
|
||||
placeholder="Enter authorization credentials"
|
||||
label="Authorization Credentials"
|
||||
className="w-full lg:col-span-6"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhook?.[index]?.authorizationCredentials}
|
||||
helperText={
|
||||
errors?.webhook?.[index]?.authorizationCredentials?.message
|
||||
}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
variant="borderless"
|
||||
className=""
|
||||
color="error"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<TrashIcon className="h-6 w-4" />
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum HttpMethod {
|
||||
POST = 'POST',
|
||||
PUT = 'PUT',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as WebhookFormSection } from './WebhookFormSection';
|
||||
@@ -0,0 +1,55 @@
|
||||
query GetObservabilitySettings($appId: uuid!) {
|
||||
config(appID: $appId, resolve: false) {
|
||||
id: __typename
|
||||
__typename
|
||||
observability {
|
||||
grafana {
|
||||
alerting {
|
||||
enabled
|
||||
}
|
||||
smtp {
|
||||
host
|
||||
password
|
||||
port
|
||||
sender
|
||||
user
|
||||
}
|
||||
contacts {
|
||||
emails
|
||||
discord {
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
pagerduty {
|
||||
integrationKey
|
||||
severity
|
||||
class
|
||||
component
|
||||
group
|
||||
}
|
||||
slack {
|
||||
recipient
|
||||
token
|
||||
username
|
||||
iconEmoji
|
||||
iconURL
|
||||
mentionUsers
|
||||
mentionGroups
|
||||
mentionChannel
|
||||
url
|
||||
endpointURL
|
||||
}
|
||||
webhook {
|
||||
url
|
||||
httpMethod
|
||||
username
|
||||
password
|
||||
authorizationScheme
|
||||
authorizationCredentials
|
||||
maxAlerts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from '@/features/orgs/projects/permissions/settings/components/BasePermissionVariableForm';
|
||||
import { getAllPermissionVariables } from '@/features/orgs/projects/permissions/settings/utils/getAllPermissionVariables';
|
||||
import {
|
||||
GetRolesPermissionsDocument,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -35,12 +34,14 @@ export default function CreatePermissionVariableForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: CreatePermissionVariableFormProps) {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data, error, loading } = useGetRolesPermissionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -57,7 +58,6 @@ export default function CreatePermissionVariableForm({
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetRolesPermissionsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
import { getAllPermissionVariables } from '@/features/orgs/projects/permissions/settings/utils/getAllPermissionVariables';
|
||||
import type { PermissionVariable } from '@/types/application';
|
||||
import {
|
||||
GetRolesPermissionsDocument,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -41,13 +40,14 @@ export default function EditPermissionVariableForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: EditPermissionVariableFormProps) {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data, error, loading } = useGetRolesPermissionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -64,7 +64,6 @@ export default function EditPermissionVariableForm({
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetRolesPermissionsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettings
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Divider } from '@/components/ui/v2/Divider';
|
||||
@@ -22,7 +21,6 @@ import { getAllPermissionVariables } from '@/features/orgs/projects/permissions/
|
||||
import type { PermissionVariable } from '@/types/application';
|
||||
|
||||
import {
|
||||
GetRolesPermissionsDocument,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -35,14 +33,15 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function PermissionVariableSettings() {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading, error, refetch } = useGetRolesPermissionsQuery({
|
||||
const { data, error, refetch } = useGetRolesPermissionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -50,16 +49,9 @@ export default function PermissionVariableSettings() {
|
||||
data?.config?.auth?.session?.accessToken || {};
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetRolesPermissionsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ActivityIndicator delay={1000} label="Loading permission variables..." />
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -102,6 +94,7 @@ export default function PermissionVariableSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
await refetch();
|
||||
showApplyChangesDialog();
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from '@/utils/constants/common';
|
||||
import type { GetResourcesQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
GetResourcesDocument,
|
||||
useGetResourcesQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -49,10 +48,10 @@ function getInitialServiceResources(
|
||||
}
|
||||
|
||||
export default function ResourcesForm() {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
const { project } = useProject();
|
||||
|
||||
const {
|
||||
data,
|
||||
@@ -62,6 +61,7 @@ export default function ResourcesForm() {
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -72,7 +72,6 @@ export default function ResourcesForm() {
|
||||
} = useProPlan();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetResourcesDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -254,6 +253,8 @@ export default function ResourcesForm() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
// await refetch();
|
||||
form.reset({ ...formValues });
|
||||
|
||||
if (!isPlatform) {
|
||||
openDialog({
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
BaseRoleFormValues,
|
||||
} from '@/features/projects/roles/settings/components/BaseRoleForm';
|
||||
import {
|
||||
GetRolesPermissionsDocument,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -35,14 +34,17 @@ export default function CreateRoleForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: CreateRoleFormProps) {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { allowed: allowedRoles } = data?.config?.auth?.user?.roles || {};
|
||||
|
||||
const form = useForm<BaseRoleFormValues>({
|
||||
@@ -52,7 +54,6 @@ export default function CreateRoleForm({
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetRolesPermissionsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import type {
|
||||
} from '@/features/projects/roles/settings/components/BaseRoleForm';
|
||||
import type { Role } from '@/types/application';
|
||||
import {
|
||||
GetRolesPermissionsDocument,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -41,12 +40,14 @@ export default function EditRoleForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: EditRoleFormProps) {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const { data, loading, error } = useGetRolesPermissionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -62,7 +63,6 @@ export default function EditRoleForm({
|
||||
});
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetRolesPermissionsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettings
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { Button } from '@/components/ui/v2/Button';
|
||||
import { Chip } from '@/components/ui/v2/Chip';
|
||||
@@ -22,7 +21,6 @@ import { getUserRoles } from '@/features/orgs/projects/roles/settings/utils/getU
|
||||
|
||||
import type { Role } from '@/types/application';
|
||||
import {
|
||||
GetRolesPermissionsDocument,
|
||||
useGetRolesPermissionsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -46,14 +44,15 @@ export interface RoleSettingsFormValues {
|
||||
}
|
||||
|
||||
export default function RoleSettings() {
|
||||
const { project } = useProject();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
const { openDialog, openAlertDialog } = useDialog();
|
||||
|
||||
const { data, loading, error, refetch } = useGetRolesPermissionsQuery({
|
||||
const { data, error, refetch } = useGetRolesPermissionsQuery({
|
||||
variables: { appId: project?.id },
|
||||
fetchPolicy: 'cache-and-network',
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -61,14 +60,9 @@ export default function RoleSettings() {
|
||||
data?.config?.auth?.user?.roles || {};
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetRolesPermissionsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <ActivityIndicator delay={1000} label="Loading user roles..." />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -107,6 +101,7 @@ export default function RoleSettings() {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
await refetch();
|
||||
showApplyChangesDialog();
|
||||
},
|
||||
{
|
||||
@@ -119,25 +114,25 @@ export default function RoleSettings() {
|
||||
}
|
||||
|
||||
async function handleDeleteRole({ name }: Role) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
user: {
|
||||
roles: {
|
||||
allowed: allowedRoles.filter((role) => role !== name),
|
||||
default: name === defaultRole ? 'user' : defaultRole,
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfig({
|
||||
variables: {
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
user: {
|
||||
roles: {
|
||||
allowed: allowedRoles.filter((role) => role !== name),
|
||||
default: name === defaultRole ? 'user' : defaultRole,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
await updateConfigPromise;
|
||||
await refetch();
|
||||
showApplyChangesDialog();
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,10 +8,7 @@ import {
|
||||
BaseSecretForm,
|
||||
baseSecretFormValidationSchema,
|
||||
} from '@/features/orgs/projects/secrets/settings/components/BaseSecretForm';
|
||||
import {
|
||||
GetSecretsDocument,
|
||||
useInsertSecretMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { useInsertSecretMutation } from '@/utils/__generated__/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
@@ -32,6 +29,7 @@ export default function CreateSecretForm({
|
||||
onSubmit,
|
||||
...props
|
||||
}: CreateSecretFormProps) {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
@@ -45,9 +43,7 @@ export default function CreateSecretForm({
|
||||
resolver: yupResolver(baseSecretFormValidationSchema),
|
||||
});
|
||||
|
||||
const { project } = useProject();
|
||||
const [insertSecret] = useInsertSecretMutation({
|
||||
refetchQueries: [GetSecretsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetHasuraSettingsDocument,
|
||||
useGetStorageSettingsQuery,
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
@@ -31,8 +30,8 @@ export default function HasuraStorageAVSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project, refetch: refetchProject } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetHasuraSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
import {
|
||||
GetStorageSettingsDocument,
|
||||
Software_Type_Enum,
|
||||
useGetSoftwareVersionsQuery,
|
||||
useGetStorageSettingsQuery,
|
||||
@@ -36,13 +35,13 @@ export type StorageServiceVersionFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function StorageServiceVersionSettings() {
|
||||
const { project } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
const localMimirClient = useLocalMimirClient();
|
||||
const { project } = useProject();
|
||||
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetStorageSettingsDocument],
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
@@ -158,7 +157,7 @@ export default function StorageServiceVersionSettings() {
|
||||
}}
|
||||
docsLink="https://github.com/nhost/hasura-storage/releases"
|
||||
docsTitle="the latest releases"
|
||||
className="grid grid-flow-row px-4 gap-x-4 gap-y-2 lg:grid-cols-5"
|
||||
className="grid grid-flow-row gap-x-4 gap-y-2 px-4 lg:grid-cols-5"
|
||||
>
|
||||
<ControlledAutocomplete
|
||||
id="version"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
query getOrganizationSpendingNotification($orgId: uuid!) {
|
||||
organizations(where: { id: { _eq: $orgId } }) {
|
||||
threshold
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mutation updateOrganizationSpendingNotification($id: uuid!, $threshold: Int!) {
|
||||
updateOrganization(pk_columns: { id: $id }, _set: { threshold: $threshold }) {
|
||||
threshold
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BillingEstimate } from '@/features/orgs/components/billing/components/BillingEstimate';
|
||||
import { SubscriptionPlan } from '@/features/orgs/components/billing/components/SubscriptionPlan';
|
||||
import { Usage } from '@/features/orgs/components/billing/components/Usage';
|
||||
import { ProjectLayout } from '@/features/orgs/layout/ProjectLayout';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -9,7 +9,7 @@ export default function OrgBilling() {
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-4 overflow-auto bg-accent p-4">
|
||||
<SubscriptionPlan />
|
||||
{!isFree && <Usage />}
|
||||
{!isFree && <BillingEstimate />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user