Skip to content

Commit

Permalink
feat: show attributes in the silence form
Browse files Browse the repository at this point in the history
  • Loading branch information
adityathebe committed Feb 13, 2025
1 parent 1d2fad6 commit fb708da
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 82 deletions.
1 change: 1 addition & 0 deletions src/api/types/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export type SilenceNotificationResponse = {
id: string;
name: string;
filter?: string;
selectors?: string;
component_id?: string;
config_id?: string;
check_id?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useComponentDetail } from "@flanksource-ui/api/query-hooks/useComponent";
import { useCheckDetail } from "@flanksource-ui/api/query-hooks/useCheck";
import { useConfigHardParents } from "@flanksource-ui/api/query-hooks/useConfigRelationshipsQuery";
import ConfigsTypeIcon from "@flanksource-ui/components/Configs/ConfigsTypeIcon";
import { Badge } from "@flanksource-ui/ui/Badge/Badge";
import { Label } from "@flanksource-ui/ui/FormControls/Label";
import { Icon } from "@flanksource-ui/ui/Icons/Icon";
Expand All @@ -14,11 +13,20 @@ type FormikNotificationResourceFieldProps = {
type RadioItem = {
id: string;
name: string;
path: string;
badge?: string;
path?: string;
type: string;
resource_id_field: "config_id" | "component_id" | "check_id" | "canary_id";
recursive?: boolean;
selected?: boolean;
radioItemType: "resource" | "Kind" | "Tag";

// which field to set in the formik values
form_field:
| "config_id"
| "component_id"
| "check_id"
| "canary_id"
| "selectors";
};

export default function FormikNotificationDirectResourceField({
Expand All @@ -33,110 +41,188 @@ export default function FormikNotificationDirectResourceField({
prepopulatedConfigID || ""
);

let radioItems: RadioItem[] = [];
const resourcesRadioItems: RadioItem[] = [];
const attributesRadioItems: RadioItem[] = [];
const seenAttributes = new Set<string>();

if (config_id) {
radioItems =
hardParents?.map((parent) => {
const radioItem: RadioItem = {
id: parent.id,
name: parent.name,
hardParents?.forEach((parent) => {
const radioItem: RadioItem = {
id: parent.id,
name: parent.name,
type: parent.type,
radioItemType: "resource",
form_field: "config_id",
path: parent.path || "",
badge: "Recursive",
recursive: true
};

if (parent.id === prepopulatedConfigID) {
radioItem.recursive = false;
radioItem.badge = undefined;
radioItem.selected = true;
}

resourcesRadioItems.push(radioItem);

if (!seenAttributes.has(parent.type)) {
attributesRadioItems.push({
id: parent.type,
name: parent.type,
type: parent.type,
resource_id_field: "config_id",
path: parent.path || "",
recursive: true
};

if (parent.id === prepopulatedConfigID) {
radioItem.recursive = false;
radioItem.selected = true;
}
radioItemType: "Kind",
badge: "Kind",
form_field: "selectors"
});

seenAttributes.add(parent.type);
}

for (const tag in parent.tags) {
const tagDisplay = `${tag}: ${parent.tags[tag]}`;
if (!seenAttributes.has(tagDisplay)) {
attributesRadioItems.push({
id: tagDisplay,
name: tagDisplay,
type: "",
radioItemType: "Tag",
badge: "Tag",
form_field: "selectors"
});

return radioItem;
}) || [];
seenAttributes.add(tagDisplay);
}
}
});
} else if (component_id && components) {
const component = components[0] || {};
radioItems.push({
resourcesRadioItems.push({
id: component_id,
name: component.name,
resource_id_field: "component_id",
form_field: "component_id",
type: component.type || "",
path: "",
selected: true
selected: true,
radioItemType: "resource"
});
} else if (check_id && healthChecks) {
const check = healthChecks[0];
radioItems.push({
resourcesRadioItems.push({
id: check_id,
name: check.name,
resource_id_field: "check_id",
form_field: "check_id",
type: check.type,
path: "",
selected: true
selected: true,
radioItemType: "resource"
});
} else if (canary_id) {
radioItems.push({
resourcesRadioItems.push({
id: canary_id,
name: "Canary",
resource_id_field: "canary_id",
type: "Canary",
radioItemType: "resource",
form_field: "canary_id",
selected: true,
path: ""
type: "Canary"
});
}

const onResourceSelect = (radioItem: RadioItem) => {
return () => {
setFieldValue(radioItem.form_field, radioItem.id);
if (!radioItem.selected) {
// only set recursive if it isn't the pre-selected item
setFieldValue("recursive", true);
}
};
};

const onAttributeSelect = (radioItem: RadioItem) => {
return () => {
if (radioItem.radioItemType === "Kind") {
setFieldValue(radioItem.form_field, [{ types: [radioItem.type] }]);
} else if (radioItem.radioItemType === "Tag") {
const [tag, value] = radioItem.name.split(": ");
setFieldValue(radioItem.form_field, [
{ tagSelector: `${tag}=${value}` }
]);
}
};
};

return (
<div className="flex flex-col gap-2">
<Label label="Resource" required={true} />
<div>
{radioItems &&
radioItems
.sort((a, b) => a.path.length - b.path.length) // higher level first in the list
{resourcesRadioItems &&
resourcesRadioItems
.sort((a, b) => (a.path?.length ?? 0) - (b.path?.length ?? 0)) // higher level first in the list
.map((radioItem) => (
<label
key={radioItem.id}
title={radioItem.name}
aria-label={radioItem.name}
aria-description={radioItem.type || ""}
onChange={onResourceSelect(radioItem)}
className="group flex cursor-pointer border border-gray-200 p-4 first:rounded-tl-md first:rounded-tr-md last:rounded-bl-md last:rounded-br-md focus:outline-none has-[:checked]:relative has-[:checked]:border-indigo-200 has-[:checked]:bg-indigo-50"
>
<input
defaultValue={radioItem.name}
defaultChecked={radioItem.selected}
value={radioItem.id}
name="resource"
type="radio"
className="relative mt-0.5 size-4 shrink-0 appearance-none rounded-full border border-gray-300 bg-white before:absolute before:inset-1 before:rounded-full before:bg-white checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:before:bg-gray-400 forced-colors:appearance-auto forced-colors:before:hidden [&:not(:checked)]:before:hidden"
/>
<span className="ml-3 flex flex-col">
<span className="block text-sm font-medium text-gray-900 group-has-[:checked]:text-indigo-900">
<div className="flex flex-row items-center gap-2">
<Icon
name={radioItem.type}
className={"flex h-auto w-6 flex-row"}
></Icon>
<span>{radioItem.name}</span>
{radioItem.badge && <Badge text={radioItem.badge} />}
</div>
</span>
</span>
</label>
))}
</div>

{attributesRadioItems.length > 0 && (
<Label label="Attributes" required={false} />
)}
<div>
{attributesRadioItems &&
attributesRadioItems
.sort((a, b) => a.radioItemType.length - b.radioItemType.length) // group by kind and tags
.map((radioItem) => (
<label
key={radioItem.id}
title={radioItem.name}
aria-label={radioItem.name}
aria-description={radioItem.type || ""}
onChange={() => {
setFieldValue(radioItem.resource_id_field, radioItem.id);
if (!radioItem.selected) {
// only set recursive if it isn't the pre-selected item
setFieldValue("recursive", true);
}
}}
onChange={onAttributeSelect(radioItem)}
className="group flex cursor-pointer border border-gray-200 p-4 first:rounded-tl-md first:rounded-tr-md last:rounded-bl-md last:rounded-br-md focus:outline-none has-[:checked]:relative has-[:checked]:border-indigo-200 has-[:checked]:bg-indigo-50"
>
<input
defaultValue={radioItem.name}
defaultChecked={radioItem.selected}
value={radioItem.id}
name="parent"
name="resource_attributes"
type="radio"
className="relative mt-0.5 size-4 shrink-0 appearance-none rounded-full border border-gray-300 bg-white before:absolute before:inset-1 before:rounded-full before:bg-white checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:before:bg-gray-400 forced-colors:appearance-auto forced-colors:before:hidden [&:not(:checked)]:before:hidden"
/>
<span className="ml-3 flex flex-col">
<span className="block text-sm font-medium text-gray-900 group-has-[:checked]:text-indigo-900">
{config_id ? (
<ConfigsTypeIcon
config={{ type: radioItem.type }}
showPrimaryIcon={false}
>
<span>
{radioItem.name}{" "}
{radioItem.recursive && (
<Badge text="Recursive" className="ml-2" />
)}
</span>
</ConfigsTypeIcon>
) : (
<div className="flex flex-row items-center gap-2">
<Icon
name={radioItem.type}
className={"flex h-auto w-6 flex-row"}
></Icon>
<span>{radioItem.name}</span>
</div>
)}
<div className="flex flex-row items-center gap-2">
<Icon
name={radioItem.type}
className={"flex h-auto w-6 flex-row"}
></Icon>
<span>{radioItem.name}</span>
{radioItem.badge && <Badge text={radioItem.badge} />}
</div>
</span>
</span>
</label>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { omit } from "lodash";
import { FaCircleNotch } from "react-icons/fa";
import { useSearchParams } from "react-router-dom";
import FormikNotificationDirectResourceField from "./FormikNotificationDirectResourceField";
import { FormikCodeEditor } from "@flanksource-ui/components/Forms/Formik/FormikCodeEditor";

type NotificationSilenceFormProps = {
data?: SilenceNotificationRequest;
Expand Down Expand Up @@ -100,9 +101,11 @@ export default function NotificationSilenceDirectForm({
v.check_id == null &&
v.component_id == null &&
v.config_id == null &&
v.filter == null
v.filter == null &&
v.selectors == null
) {
errors.form = "Must specify either a resource and/or a filter";
errors.form =
"You must specify at least one of the following: a resource, a filter, or selectors";
}
if (v.until == null) {
errors.until = "Must specify a silence duration";
Expand Down Expand Up @@ -176,7 +179,15 @@ export default function NotificationSilenceDirectForm({
<FormikTextArea
name="filter"
label="Filter"
hint="CEL expression for the silence to match against"
hint="Notifications for resources matching this CEL expression will be silenced"
/>

<FormikCodeEditor
fieldName="selectors"
format={"yaml"}
label="Selectors"
lines={10}
hint="List of resource selectors. Notifications for resources matching these selectors will be silenced"
/>

<ErrorMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { AxiosError } from "axios";
import { Formik, Form, FormikBag } from "formik";
import { omit } from "lodash";
import { FaCircleNotch } from "react-icons/fa";
import { useSearchParams } from "react-router-dom";
import { FormikCodeEditor } from "@flanksource-ui/components/Forms/Formik/FormikCodeEditor";

type NotificationSilenceFormProps = {
data?: SilenceNotificationRequest;
Expand All @@ -37,20 +37,13 @@ export default function NotificationSilenceForm({
onSuccess = () => {},
onCancel = () => {}
}: NotificationSilenceFormProps) {
const [searchParam] = useSearchParams();

const component_id = searchParam.get("component_id") ?? undefined;
const config_id = searchParam.get("config_id") ?? undefined;
const check_id = searchParam.get("check_id") ?? undefined;
const canary_id = searchParam.get("canary_id") ?? undefined;

const initialValues: Partial<SilenceNotificationRequest> = {
...data,
name: data?.name,
component_id: data?.component_id ?? component_id,
config_id: data?.config_id ?? config_id,
check_id: data?.check_id ?? check_id,
canary_id: data?.canary_id ?? canary_id
component_id: data?.component_id,
config_id: data?.config_id,
check_id: data?.check_id,
canary_id: data?.canary_id
};

const { isLoading, mutate } = useMutation({
Expand Down Expand Up @@ -101,9 +94,11 @@ export default function NotificationSilenceForm({
v.check_id == null &&
v.component_id == null &&
v.config_id == null &&
v.selectors == null &&
v.filter == null
) {
errors.form = "Must specify either a resource and/or a filter";
errors.form =
"You must specify at least one of the following: a resource, a filter, or selectors";
}
if (v.until == null) {
errors.until = "Must specify a silence duration";
Expand Down Expand Up @@ -182,7 +177,15 @@ export default function NotificationSilenceForm({
<FormikTextArea
name="filter"
label="Filter"
hint="CEL expression for the silence to match against"
hint="Notifications for resources matching this CEL expression will be silenced"
/>

<FormikCodeEditor
fieldName="selectors"
format={"yaml"}
label="Selectors"
lines={10}
hint="List of resource selectors. Notifications for resources matching these selectors will be silenced"
/>

<ErrorMessage
Expand Down
Loading

0 comments on commit fb708da

Please sign in to comment.