diff --git a/apps/api/prisma/migrations/20260221000000_add_incident_detection_automation_type/migration.sql b/apps/api/prisma/migrations/20260221000000_add_incident_detection_automation_type/migration.sql new file mode 100644 index 00000000..2b885a53 --- /dev/null +++ b/apps/api/prisma/migrations/20260221000000_add_incident_detection_automation_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "AutomationType" ADD VALUE 'INCIDENT_DETECTION'; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 4af2250a..63febf04 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -58,6 +58,7 @@ enum IntegrationApp { enum AutomationType { PR_TITLE_CHECK PR_SIZE_LABELER + INCIDENT_DETECTION } enum DigestType { diff --git a/apps/api/src/app/automations/resolvers/automations.schema.ts b/apps/api/src/app/automations/resolvers/automations.schema.ts index 875f7b4b..1a5c3aba 100644 --- a/apps/api/src/app/automations/resolvers/automations.schema.ts +++ b/apps/api/src/app/automations/resolvers/automations.schema.ts @@ -2,6 +2,7 @@ export default /* GraphQL */ ` enum AutomationType { PR_TITLE_CHECK PR_SIZE_LABELER + INCIDENT_DETECTION } type Automation { diff --git a/apps/api/src/app/automations/services/automation.types.ts b/apps/api/src/app/automations/services/automation.types.ts index af3db34b..b8eda2d5 100644 --- a/apps/api/src/app/automations/services/automation.types.ts +++ b/apps/api/src/app/automations/services/automation.types.ts @@ -40,6 +40,7 @@ export interface CanRunAutomationArgs { export type AutomationTypeMap = { [AutomationType.PR_TITLE_CHECK]: AutomationPrTitleCheck; [AutomationType.PR_SIZE_LABELER]: AutomationPrSizeLabeler; + [AutomationType.INCIDENT_DETECTION]: AutomationIncidentDetection; }; export interface AutomationPrTitleCheck extends Omit { @@ -54,13 +55,35 @@ export interface AutomationPrSizeLabeler extends Omit { export type AutomationSettings = | AutomationPrTitleCheck - | AutomationPrSizeLabeler; + | AutomationPrSizeLabeler + | AutomationIncidentDetection; export interface PrTitleCheckSettings extends Prisma.JsonObject { regex?: string; regexExample?: string; } +export interface AutomationIncidentDetection + extends Omit { + type: typeof AutomationType.INCIDENT_DETECTION; + settings: IncidentDetectionSettings; +} + +export interface IncidentDetectionSettings { + revert?: { + enabled?: boolean; + }; + hotfix?: { + enabled?: boolean; + prTitleRegEx?: string; + branchRegEx?: string; + prLabelRegEx?: string; + }; + rollback?: { + enabled?: boolean; + }; +} + export interface PrSizeLabelerSettings { repositories?: string[]; labels?: { diff --git a/apps/web/src/app/automations/components/card-automation/card-automation.tsx b/apps/web/src/app/automations/components/card-automation/card-automation.tsx index 6cc606ca..bb1c0d8b 100644 --- a/apps/web/src/app/automations/components/card-automation/card-automation.tsx +++ b/apps/web/src/app/automations/components/card-automation/card-automation.tsx @@ -45,28 +45,32 @@ export const CardAutomation = ({ p="md" style={{ borderBottom: "1px solid #303030", flexGrow: 1 }} > - - - {benefits && - Object.entries(benefits).map( - ([key, value]) => - value && ( - - ), - )} - - - - {title} - - {description} - + + + + {benefits && + Object.entries(benefits).map( + ([key, value]) => + value && ( + + ), + )} + + + + {title} + + {description} + + - + + + diff --git a/apps/web/src/app/automations/settings/components/header-automation/header-automation.tsx b/apps/web/src/app/automations/settings/components/header-automation/header-automation.tsx index 2bf06bca..a24f0f72 100644 --- a/apps/web/src/app/automations/settings/components/header-automation/header-automation.tsx +++ b/apps/web/src/app/automations/settings/components/header-automation/header-automation.tsx @@ -11,7 +11,9 @@ export const HeaderAutomation = ({ automation }: HeaderAutomationProps) => { return ( <> - + {automation.demoUrl && ( + + )} {automation.description} diff --git a/apps/web/src/app/automations/settings/incident-detection/components/form-incident-detection-settings/form-incident-detection-settings.tsx b/apps/web/src/app/automations/settings/incident-detection/components/form-incident-detection-settings/form-incident-detection-settings.tsx new file mode 100644 index 00000000..84226478 --- /dev/null +++ b/apps/web/src/app/automations/settings/incident-detection/components/form-incident-detection-settings/form-incident-detection-settings.tsx @@ -0,0 +1,128 @@ +import { + Divider, + SimpleGrid, + Stack, + Switch, + TextInput, + Title, +} from "@mantine/core"; +import { BoxSetting } from "../../../../../../components/box-setting"; +import { UseFormReturnType } from "@mantine/form"; +import { FormIncidentDetection } from "../../types"; + +interface FormIncidentDetectionSettingsProps { + form: UseFormReturnType; +} + +export const FormIncidentDetectionSettings = ({ + form, +}: FormIncidentDetectionSettingsProps) => { + return ( + <> + + Settings + + + + + + + {form.values.enabled && ( + <> + + + + Reverts + + + + + + + + + + Rollback + + + + + + + + + + Hotfixes + + + + {form.values.settings.hotfix?.enabled && ( + + + + + + )} + + + )} + + ); +}; diff --git a/apps/web/src/app/automations/settings/incident-detection/components/form-incident-detection-settings/index.ts b/apps/web/src/app/automations/settings/incident-detection/components/form-incident-detection-settings/index.ts new file mode 100644 index 00000000..ef629789 --- /dev/null +++ b/apps/web/src/app/automations/settings/incident-detection/components/form-incident-detection-settings/index.ts @@ -0,0 +1 @@ +export { FormIncidentDetectionSettings } from "./form-incident-detection-settings"; diff --git a/apps/web/src/app/automations/settings/incident-detection/page.tsx b/apps/web/src/app/automations/settings/incident-detection/page.tsx new file mode 100644 index 00000000..c594145c --- /dev/null +++ b/apps/web/src/app/automations/settings/incident-detection/page.tsx @@ -0,0 +1,141 @@ +import { Stack, Title, Skeleton, Group, Text, Button } from "@mantine/core"; +import { LoadableContent } from "../../../../components/loadable-content"; +import { AutomationType } from "@sweetr/graphql-types/frontend/graphql"; +import { useAutomationSettings } from "../use-automation"; +import { FormIncidentDetectionSettings } from "./components/form-incident-detection-settings"; +import { useDrawerPage } from "../../../../providers/drawer-page.provider"; +import { DrawerScrollable } from "../../../../components/drawer-scrollable"; +import { ButtonDocs } from "../../../../components/button-docs"; +import { useForm, zodResolver } from "@mantine/form"; +import { FormEventHandler, useEffect, useMemo } from "react"; +import { FormIncidentDetection } from "./types"; +import { HeaderAutomation } from "../components/header-automation"; + +const defaultValues: FormIncidentDetection = { + enabled: false, + settings: { + revert: { + enabled: true, + }, + hotfix: { + enabled: true, + prTitleRegEx: "hotfix", + branchRegEx: "^hotfix", + prLabelRegEx: "hotfix", + }, + rollback: { + enabled: true, + }, + }, +}; + +export const AutomationIncidentDetectionPage = () => { + const { automation, automationSettings, query, mutation, mutate } = + useAutomationSettings(AutomationType.INCIDENT_DETECTION); + + const drawerProps = useDrawerPage({ + closeUrl: `/automations`, + }); + + const form = useForm({ + validate: zodResolver(FormIncidentDetection), + }); + + useEffect(() => { + const settings = automationSettings?.settings as + | FormIncidentDetection["settings"] + | undefined + | null; + + form.setValues({ + enabled: automationSettings?.enabled || false, + settings: { + revert: { + enabled: + settings?.revert?.enabled ?? defaultValues.settings.revert.enabled, + }, + rollback: { + enabled: + settings?.rollback?.enabled ?? + defaultValues.settings.rollback.enabled, + }, + hotfix: { + enabled: + settings?.hotfix?.enabled ?? defaultValues.settings.hotfix.enabled, + prTitleRegEx: + settings?.hotfix?.prTitleRegEx ?? + defaultValues.settings.hotfix.prTitleRegEx, + branchRegEx: + settings?.hotfix?.branchRegEx ?? + defaultValues.settings.hotfix.branchRegEx, + prLabelRegEx: + settings?.hotfix?.prLabelRegEx ?? + defaultValues.settings.hotfix.prLabelRegEx, + }, + }, + }); + form.resetDirty(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [automationSettings]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const isFormValid = useMemo(() => !form.validate().hasErrors, [form.values]); + + const handleSave: FormEventHandler = async (event) => { + event.preventDefault(); + + if (form.validate().hasErrors) return; + + await mutate({ + settings: form.values.settings, + enabled: form.values.enabled, + }); + }; + + return ( + + + {automation?.icon} + + + {automation?.title} + + + } + toolbar={automation?.docsUrl && } + actions={ + + } + onSubmit={handleSave} + > + + + + + + + } + isLoading={query.isLoading} + content={ + <> + + + + } + /> + + ); +}; diff --git a/apps/web/src/app/automations/settings/incident-detection/types.ts b/apps/web/src/app/automations/settings/incident-detection/types.ts new file mode 100644 index 00000000..b8736c03 --- /dev/null +++ b/apps/web/src/app/automations/settings/incident-detection/types.ts @@ -0,0 +1,31 @@ +import { z } from "zod"; +import { validateRegEx } from "../../../../providers/validation.provider"; + +export const FormIncidentDetection = z.object({ + enabled: z.boolean(), + settings: z.object({ + revert: z.object({ + enabled: z.boolean(), + }), + hotfix: z.object({ + enabled: z.boolean(), + prTitleRegEx: z + .string() + .refine(validateRegEx, "Invalid regular expression") + .optional(), + branchRegEx: z + .string() + .refine(validateRegEx, "Invalid regular expression") + .optional(), + prLabelRegEx: z + .string() + .refine(validateRegEx, "Invalid regular expression") + .optional(), + }), + rollback: z.object({ + enabled: z.boolean(), + }), + }), +}); + +export type FormIncidentDetection = z.infer; diff --git a/apps/web/src/app/automations/settings/pr-size-labeler/page.tsx b/apps/web/src/app/automations/settings/pr-size-labeler/page.tsx index 8aa943d7..3fc2f65e 100644 --- a/apps/web/src/app/automations/settings/pr-size-labeler/page.tsx +++ b/apps/web/src/app/automations/settings/pr-size-labeler/page.tsx @@ -90,7 +90,7 @@ export const AutomationPrSizeLabelerPage = () => { } - toolbar={automation.docsUrl && } + toolbar={automation?.docsUrl && } actions={