Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/api/types/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import { Topology } from "./topology";
import { Team, User } from "./users";
import { PlaybookRunStatus } from "./playbooks";

export type NotificationInhibition = {
// Direction specifies the traversal direction in relation to the "From" resource.
// - "outgoing": Looks for child resources originating from the "From" resource.
// Example: If "From" is "Kubernetes::Deployment", "To" could be ["Kubernetes::Pod", "Kubernetes::ReplicaSet"].
// - "incoming": Looks for parent resources related to the "From" resource.
// Example: If "From" is "Kubernetes::Deployment", "To" could be ["Kubernetes::HelmRelease", "Kubernetes::Namespace"].
// - "all": Considers both incoming and outgoing relationships.
direction: "outgoing" | "incoming" | "all";

// Soft, when true, relates using soft relationships.
// Example: Deployment to Pod is hard relationship, But Node to Pod is soft relationship.
soft?: boolean;

// Depth defines how many levels of child or parent resources to traverse.
depth?: number;

// From specifies the starting resource type (for example, "Kubernetes::Deployment").
from: string;

// To specifies the target resource types, which are determined based on the Direction.
// Example:
// - If Direction is "outgoing", these are child resources.
// - If Direction is "incoming", these are parent resources.
to: string[];
};

export type NotificationRules = {
id: string;
namespace?: string;
Expand Down Expand Up @@ -44,6 +70,7 @@ export type NotificationRules = {
repeat_interval?: string;
error?: string;
wait_for?: number;
inhibitions?: NotificationInhibition[];
};

export type SilenceNotificationResponse = {
Expand Down
158 changes: 158 additions & 0 deletions src/components/Forms/Formik/FormikNotificationInhibitionsField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { NotificationInhibition } from "@flanksource-ui/api/types/notifications";
import { useFormikContext } from "formik";
import { Button } from "@flanksource-ui/ui/Buttons/Button";
import { FaPlus, FaTrash } from "react-icons/fa";
import FormikTextInput from "./FormikTextInput";
import FormikAutocompleteDropdown from "./FormikAutocompleteDropdown";
import FormikNumberInput from "./FormikNumberInput";
import { FormikCodeEditor } from "./FormikCodeEditor";
import ErrorMessage from "@flanksource-ui/ui/FormControls/ErrorMessage";
import { FormikErrors } from "formik";
import { Toggle } from "../../../ui/FormControls/Toggle";

type FormikNotificationInhibitionsFieldProps = {
name: string;
label?: string;
hint?: string;
};

const directionOptions = [
{ label: "Outgoing", value: "outgoing" },
{ label: "Incoming", value: "incoming" },
{ label: "All", value: "all" }
];

type FormValues = {
[key: string]: NotificationInhibition[];
};

type FormErrors = {
[key: string]: (FormikErrors<NotificationInhibition> & { to?: string })[];
};

const FormikNotificationInhibitionsField = ({
name,
label = "Inhibitions",
hint = "Configure inhibition rules to prevent notification storms"
}: FormikNotificationInhibitionsFieldProps) => {
const { values, setFieldValue, errors } = useFormikContext<FormValues>();

const inhibitions = values[name] || [];
const fieldErrors = errors as FormErrors;

const addInhibition = () => {
const newInhibition: NotificationInhibition = {
direction: "outgoing",
from: "",
to: []
};
setFieldValue(name, [...inhibitions, newInhibition]);
};

const removeInhibition = (index: number) => {
const newInhibitions = [...inhibitions];
newInhibitions.splice(index, 1);
setFieldValue(name, newInhibitions);
};

const updateInhibition = (
index: number,
field: keyof NotificationInhibition,
value: string | number | boolean | string[] | undefined
) => {
const newInhibitions = [...inhibitions];
newInhibitions[index] = {
...newInhibitions[index],
[field]: value
};
setFieldValue(name, newInhibitions);
};

return (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<div>
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
{hint && <p className="mt-1 text-sm text-gray-500">{hint}</p>}
</div>
<Button
icon={<FaPlus />}
onClick={addInhibition}
className="btn-primary"
>
Add Inhibition
</Button>
</div>

{inhibitions.map((inhibition, index) => (
<div key={index} className="rounded-lg border border-gray-200 p-4">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-medium">Inhibition Rule {index + 1}</h3>
<Button
icon={<FaTrash />}
onClick={() => removeInhibition(index)}
className="btn-danger"
/>
</div>

<div className="grid grid-cols-2 gap-4">
<FormikAutocompleteDropdown
name={`${name}.${index}.direction`}
options={directionOptions}
label="Direction"
hint="Specify the traversal direction for related resources"
/>

<FormikTextInput
name={`${name}.${index}.from`}
label="From Resource Type"
hint="e.g., Kubernetes::Pod"
/>

<div className="col-span-2">
<FormikCodeEditor
fieldName={`${name}.${index}.to`}
format="yaml"
label="To Resource Types"
hint="List of resource types to traverse to (e.g., ['Kubernetes::Deployment', 'Kubernetes::ReplicaSet'])"
lines={5}
className="flex h-32 flex-col"
/>
{fieldErrors?.[name]?.[index]?.to && (
<ErrorMessage
message={fieldErrors[name][index].to}
className="mt-1"
/>
)}
</div>

<div className="flex items-end gap-4">
<FormikNumberInput
value={inhibition.depth}
onChange={(value) => updateInhibition(index, "depth", value)}
label="Depth"
hint="Number of levels to traverse"
/>
<div className="flex flex-col">
<Toggle
onChange={(value: boolean) => {
setFieldValue(`${name}.${index}.soft`, value);
}}
label="Soft Relationships"
value={!!inhibition.soft}
/>
<p className="mt-1 text-sm text-gray-500">
Use soft relationships for traversal
</p>
</div>
</div>
</div>
</div>
))}
</div>
);
};

export default FormikNotificationInhibitionsField;
76 changes: 34 additions & 42 deletions src/components/Forms/Formik/FormikNumberInput.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,48 @@
import { useField } from "formik";
import React from "react";
import { TextInput } from "../../../ui/FormControls/TextInput";
import { InputHTMLAttributes } from "react";

type FormikNumberInputProps = {
name: string;
required?: boolean;
type CustomNumberInputProps = {
label?: string;
className?: string;
hint?: string;
} & Omit<React.ComponentProps<typeof TextInput>, "id">;
value?: number;
onChange?: (value: number | undefined) => void;
};

type FormikNumberInputProps = Omit<
InputHTMLAttributes<HTMLInputElement>,
"onChange" | "value"
> &
CustomNumberInputProps;

export default function FormikNumberInput({
name,
required = false,
label,
className = "flex flex-col",
hint,
value,
onChange,
...props
}: FormikNumberInputProps) {
const [field, meta] = useField({
name,
type: "number",
required,
validate: (value) => {
if (required && !value) {
return "This field is required";
}
if (value && isNaN(value)) {
return "This field must be a number";
}
}
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val =
e.target.value === "" ? undefined : parseInt(e.target.value, 10);
onChange?.(val);
};

return (
<div className={className}>
<TextInput
label={label}
{...props}
id={name}
type="number"
{...field}
onChange={() => {
const value = field.value;
if (value) {
field.onChange({ target: { value: parseInt(value) } });
}
}}
/>
{hint && <p className="py-1 text-sm text-gray-500">{hint}</p>}
{meta.touched && meta.error ? (
<p className="w-full py-1 text-sm text-red-500">{meta.error}</p>
) : null}
<div>
{label && (
<label className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
<div className="mt-1">
<input
type="number"
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
value={value ?? ""}
onChange={handleChange}
{...props}
/>
</div>
{hint && <p className="mt-1 text-sm text-gray-500">{hint}</p>}
</div>
);
}
Loading
Loading