mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-20 11:12:31 +02:00
Merge pull request #3541 from fosrl/feat/test-alert-button
Feat: test alert rules
This commit is contained in:
@@ -45,7 +45,14 @@ import {
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Bell, ChevronsUpDown, Globe, Plus, Trash2 } from "lucide-react";
|
||||
import {
|
||||
Bell,
|
||||
ChevronRightIcon,
|
||||
ChevronsUpDown,
|
||||
Globe,
|
||||
Plus,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { Control, UseFormReturn } from "react-hook-form";
|
||||
@@ -53,6 +60,7 @@ import { useFormContext, useWatch } from "react-hook-form";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { RolesSelector } from "../roles-selector";
|
||||
import { UsersSelector } from "../users-selector";
|
||||
import { cn } from "@app/lib/cn";
|
||||
|
||||
export function AddActionPanel({
|
||||
onAdd
|
||||
@@ -95,6 +103,7 @@ export function AddActionPanel({
|
||||
const EXTERNAL_IDS = EXTERNAL_INTEGRATIONS.map((i) => i.id);
|
||||
|
||||
const [selected, setSelected] = useState<string | null>("notify");
|
||||
const [isPopoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
const isPremiumSelected =
|
||||
selected !== null && EXTERNAL_IDS.includes(selected as any);
|
||||
@@ -131,27 +140,46 @@ export function AddActionPanel({
|
||||
if (!isBuiltInSelected) return;
|
||||
onAdd(selected as AlertRuleFormAction["type"]);
|
||||
setSelected(null);
|
||||
setPopoverOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<StrategySelect
|
||||
options={actionTypeOptions}
|
||||
value={selected}
|
||||
cols={2}
|
||||
onChange={(v) => setSelected(v)}
|
||||
/>
|
||||
{isPremiumSelected && <ContactSalesBanner />}
|
||||
{!isPremiumSelected && (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!isBuiltInSelected}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t("alertingAddAction")}
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex flex-col gap-3 items-start">
|
||||
<h3 className="font-medium">{t("alertingAddActionHeading")}</h3>
|
||||
<Popover open={isPopoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="button" variant="outline">
|
||||
{t("alertingSelectActionType")}
|
||||
<ChevronRightIcon
|
||||
className={cn(
|
||||
"size-4 transition-transform duration-150",
|
||||
isPopoverOpen && "rotate-90"
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="shadow-md flex flex-col gap-3 w-150">
|
||||
<StrategySelect
|
||||
options={actionTypeOptions}
|
||||
value={selected}
|
||||
cols={2}
|
||||
onChange={(v) => setSelected(v)}
|
||||
/>
|
||||
|
||||
{isPremiumSelected ? (
|
||||
<ContactSalesBanner />
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
disabled={!isBuiltInSelected}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
{t("alertingAddAction")}
|
||||
</Button>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
AlertRuleSourceFields,
|
||||
AlertRuleTriggerFields
|
||||
} from "@app/components/alert-rule-editor/AlertRuleFields";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { SettingsContainer } from "@app/components/Settings";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Card, CardContent } from "@app/components/ui/card";
|
||||
import {
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import {
|
||||
buildFormSchema,
|
||||
@@ -27,19 +30,15 @@ import {
|
||||
type AlertRuleFormValues
|
||||
} from "@app/lib/alertRuleForm";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import type { CreateAlertRuleResponse } from "@server/routers/alertRule/types";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ChevronLeft, Cog, Flag, Zap } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useFieldArray, useForm, type Resolver } from "react-hook-form";
|
||||
import { Cog, Flag, Zap, ZapIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useActionState, useMemo, useTransition, type ReactNode } from "react";
|
||||
import { useFieldArray, useForm, type Resolver } from "react-hook-form";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
const FORM_ID = "alert-rule-form";
|
||||
@@ -115,7 +114,6 @@ export default function AlertRuleGraphEditor({
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const schema = useMemo(() => buildFormSchema(t), [t]);
|
||||
const form = useForm<AlertRuleFormValues>({
|
||||
resolver: zodResolver(schema) as Resolver<AlertRuleFormValues>,
|
||||
@@ -127,8 +125,22 @@ export default function AlertRuleGraphEditor({
|
||||
name: "actions"
|
||||
});
|
||||
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
setIsSaving(true);
|
||||
const saveAlert = async () => {
|
||||
const isValid = await form.trigger();
|
||||
if (!isValid) {
|
||||
const values = form.getValues();
|
||||
if (values.actions.length === 0) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: t("alertingNoActionsTitle"),
|
||||
description: t("alertingNoActionsSaveDescription")
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const values = form.getValues();
|
||||
|
||||
try {
|
||||
const payload = formValuesToApiPayload(values);
|
||||
if (isNew) {
|
||||
@@ -158,14 +170,48 @@ export default function AlertRuleGraphEditor({
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testAlert = async () => {
|
||||
const isValid = await form.trigger("actions");
|
||||
const values = form.getValues();
|
||||
|
||||
if (!isValid) {
|
||||
if (values.actions.length === 0) {
|
||||
toast({
|
||||
variant: "warning",
|
||||
title: t("alertingNoActionsTitle"),
|
||||
description: t("alertingNoActionsTestDescription")
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = formValuesToApiPayload(values);
|
||||
await api.post(`/org/${orgId}/test-alert-rule`, payload);
|
||||
|
||||
toast({
|
||||
title: t("alertingTestAlertSent"),
|
||||
description: t("alertingTestAlertSentDescription")
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: formatAxiosError(e),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [, formAction, isSaving] = useActionState(saveAlert, null);
|
||||
const [isTestingAlert, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form id={FORM_ID} onSubmit={onSubmit}>
|
||||
<form id={FORM_ID} action={formAction}>
|
||||
<SettingsContainer>
|
||||
<PaidFeaturesAlert tiers={tierMatrix.alertingRules} />
|
||||
<div className="flex flex-col lg:flex-row gap-6 lg:gap-8 items-start">
|
||||
@@ -263,14 +309,29 @@ export default function AlertRuleGraphEditor({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
<div className="flex flex-col items-center w-full gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSaving}
|
||||
loading={isSaving}
|
||||
>
|
||||
{t("save")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full gap-1.5"
|
||||
onClick={() =>
|
||||
startTransition(testAlert)
|
||||
}
|
||||
loading={isTestingAlert}
|
||||
>
|
||||
{t("alertingTestRule")}
|
||||
<ZapIcon className="size-3.5 flex-none" />
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export function getRandomItemInArray<T>(array: T[]) {
|
||||
// Source - https://stackoverflow.com/a/4550514
|
||||
const randomElement = array[Math.floor(Math.random() * array.length)];
|
||||
return randomElement;
|
||||
}
|
||||
+1
-1
@@ -1757,7 +1757,7 @@ export const approvalQueries = {
|
||||
},
|
||||
refetchInterval: (query) => {
|
||||
if (query.state.data) {
|
||||
return durationToMs(30, "seconds");
|
||||
return durationToMs(1.5, "minutes");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user