Skip to content

Commit

Permalink
feat: add silence notification form page
Browse files Browse the repository at this point in the history
Fixes #2284
  • Loading branch information
mainawycliffe committed Sep 16, 2024
1 parent 43c30a7 commit e03d982
Show file tree
Hide file tree
Showing 10 changed files with 525 additions and 10 deletions.
30 changes: 22 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
import { ConnectionsPage } from "./pages/Settings/ConnectionsPage";
import { EventQueueStatusPage } from "./pages/Settings/EventQueueStatus";
import { FeatureFlagsPage } from "./pages/Settings/FeatureFlagsPage";
import NotificationSilencePage from "./pages/Settings/NotificationSilencePage";
import { TopologyCardPage } from "./pages/TopologyCard";
import { UsersPage } from "./pages/UsersPage";
import { ConfigInsightsPage } from "./pages/config/ConfigInsightsList";
Expand Down Expand Up @@ -374,14 +375,27 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
true
)}
/>
<Route
path="notifications"
element={withAuthorizationAccessCheck(
<NotificationsPage />,
tables.database,
"read"
)}
/>
<Route path="notifications">
<Route
index
element={withAuthorizationAccessCheck(
<NotificationsPage />,
tables.database,
"read",
true
)}
/>

<Route
path="silence"
element={withAuthorizationAccessCheck(
<NotificationSilencePage />,
tables.database,
"write",
true
)}
/>
</Route>
<Route
path="feature-flags"
element={withAuthorizationAccessCheck(
Expand Down
9 changes: 9 additions & 0 deletions src/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ export const Rback = axios.create({
}
});

export const NotificationAPI = axios.create({
baseURL: `${API_BASE}/notification`,
headers: {
Accept: "application/json",
Prefer: "return=representation",
"Content-Type": "application/json"
}
});

for (const client of [
Auth,
IncidentCommander,
Expand Down
11 changes: 9 additions & 2 deletions src/api/services/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Notification } from "../../components/Notifications/notificationsTableColumns";
import { AVATAR_INFO } from "../../constants";
import { IncidentCommander } from "../axios";
import { IncidentCommander, NotificationAPI } from "../axios";
import { resolvePostGrestRequestWithPagination } from "../resolve";
import { SilenceNotificationResponse } from "../types/notifications";

export const getNotificationsSummary = async () => {
return resolvePostGrestRequestWithPagination(
Expand All @@ -22,3 +22,10 @@ export const getNotificationById = async (id: string) => {
);
return res.data ? res.data?.[0] : undefined;
};

export const silenceNotification = async (
data: SilenceNotificationResponse
) => {
const res = await NotificationAPI.post("/silence", data);
return res.data;
};
10 changes: 10 additions & 0 deletions src/api/services/topology.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,13 @@ export const getCheckNames = async () => {
const res = await IncidentCommander.get<HealthCheckNames[]>(`/check_names`);
return res.data;
};

export const getCanaryNames = async () => {
const res = await IncidentCommander.get<
{
id: string;
name: string;
}[]
>(`/canary_names`);
return res.data;
};
11 changes: 11 additions & 0 deletions src/api/types/notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type SilenceNotificationResponse = {
id: string;
component_id: string;
config_id: string;
check_id: string;
canary_id: string;
from: string;
until: string;
description: string;
recursive: boolean;
};
47 changes: 47 additions & 0 deletions src/components/Forms/Formik/FormikCanaryDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getCanaryNames } from "@flanksource-ui/api/services/topology";
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import FormikSelectDropdown from "./FormikSelectDropdown";

type FormikCanaryDropdownProps = {
name: string;
label?: string;
required?: boolean;
hint?: string;

className?: string;
};

export default function FormikCanaryDropdown({
name,
label,
required = false,
hint,
className = "flex flex-col space-y-2 py-2"
}: FormikCanaryDropdownProps) {
const { isLoading, data: canary } = useQuery({
queryKey: ["canaries", "canary_names"],
queryFn: () => getCanaryNames()
});

const options = useMemo(
() =>
canary?.map((canary) => ({
label: canary.name,
value: canary.id
})),
[canary]
);

return (
<FormikSelectDropdown
name={name}
className={className}
options={options}
label={label}
isLoading={isLoading}
required={required}
hint={hint}
/>
);
}
220 changes: 220 additions & 0 deletions src/components/Forms/Formik/FormikDurationPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import clsx from "clsx";
import dayjs from "dayjs";
import { useFormikContext } from "formik";
import { useCallback } from "react";
import { MdOutlineKeyboardArrowDown } from "react-icons/md";
import FormikTextInput from "./FormikTextInput";

const commonDurations = [
{
label: "1 Day",
values: {
from: dayjs().toISOString(),
to: dayjs().add(1, "day").toISOString()
}
},
{
label: "2 Days",
values: {
from: dayjs().toISOString(),
to: dayjs().add(2, "day").toISOString()
}
},
{
label: "3 Days",
values: {
from: dayjs().toISOString(),
to: dayjs().add(3, "day").toISOString()
}
},
{
label: "1 Week",
values: {
from: dayjs().toISOString(),
to: dayjs().add(1, "week").toISOString()
}
},
{
label: "2 Weeks",
values: {
from: dayjs().toISOString(),
to: dayjs().add(2, "week").toISOString()
}
},
{
label: "1 Month",
values: {
from: dayjs().toISOString(),
to: dayjs().add(1, "month").toISOString()
}
},
{
label: "3 Months",
values: {
from: dayjs().toISOString(),
to: dayjs().add(3, "month").toISOString()
}
},
{
label: "6 Months",
values: {
from: dayjs().toISOString(),
to: dayjs().add(6, "month").toISOString()
}
},
{
label: "1 Year",
values: {
from: dayjs().toISOString(),
to: dayjs().add(1, "year").toISOString()
}
}
];

type FormikDurationPickerProps = {
fieldNames: {
from: string;
to: string;
};
label: string;
className?: string;
placeholder?: string;
};

export default function FormikDurationPicker({
fieldNames: { from, to },
label,
placeholder = "Select duration",
className
}: FormikDurationPickerProps) {
const { values, setFieldValue } =
useFormikContext<Record<string, string | undefined>>();

const updateFrom = useCallback(
(value: string) => {
setFieldValue(from, dayjs(value).format("YYYY-MM-DDTHH:mm"));
},
[from, setFieldValue]
);

const updateTo = useCallback(
(value: string) => {
setFieldValue(to, dayjs(value).format("YYYY-MM-DDTHH:mm"));
},
[to, setFieldValue]
);

const clearFields = useCallback(() => {
setFieldValue(from, undefined);
setFieldValue(to, undefined);
}, [from, to, setFieldValue]);

return (
<Popover className={clsx("relative py-2 text-sm", className)}>
<PopoverButton as="div" className="w-full">
{({ open }) => {
return (
<div className="flex w-full flex-col gap-2">
{label && (
<label className="text-sm font-medium text-gray-700">
{label}
</label>
)}
<div className="flex w-full flex-row items-center rounded-md border border-gray-300 bg-white px-2 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
{values?.[to] || values?.[from] ? (
<div className="flex flex-1 flex-row text-sm">
<span>
{dayjs(values?.[from]).format("DD/MM/YYYY HH:mm")}
</span>
<span className="mx-2">-</span>
<span>
{dayjs(values?.[to]).format("DD/MM/YYYY HH:mm")}
</span>
</div>
) : (
<span>{placeholder || "Select duration"}</span>
)}
<div
className={clsx("ml-2 mt-1", {
"rotate-180": open
})}
>
<MdOutlineKeyboardArrowDown size={18} />
</div>
</div>
</div>
);
}}
</PopoverButton>
<PopoverPanel
className={`absolute z-50 flex w-full origin-top-right flex-col divide-y divide-gray-100 rounded-md bg-slate-50 ring-1 ring-black ring-opacity-5 drop-shadow-xl focus:outline-none`}
>
{({ close }) => {
return (
<div
className={clsx(
"z-50 flex max-h-96 w-full cursor-auto rounded-md bg-white py-2 shadow-lg shadow-gray-200 ring-1 ring-black ring-opacity-5 focus:outline-none"
)}
>
<div className="w-1/2 overflow-hidden border-r border-gray-300 px-1">
<div className="p-3">
<div>
<div className="mb-2">
<div className="my-3">
<FormikTextInput
label="From"
type="datetime-local"
name={from}
/>
</div>
<div className="my-3">
<FormikTextInput
label="To"
type="datetime-local"
name={to}
/>
</div>
</div>
<div className="flex justify-end gap-2">
<button
title="Clear Time Range"
className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-2 py-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-100"
onClick={clearFields}
>
Clear
</button>
</div>
</div>
</div>
</div>
<div className="flex w-1/2 flex-col overflow-y-hidden p-2">
<div className="mb-2 whitespace-nowrap text-sm font-medium sm:space-x-2">
Common Durations
</div>
{commonDurations.map((option) => {
return (
<button
type="button"
onClick={() => {
updateFrom(option.values.from);
updateTo(option.values.to);
close();
}}
key={option.label}
className={clsx(
"option-item flex w-full items-center justify-between px-2 py-1 text-left hover:bg-gray-100"
)}
>
{option.label}
</button>
);
})}
</div>
</div>
);
}}
</PopoverPanel>
</Popover>
);
}
Loading

0 comments on commit e03d982

Please sign in to comment.