Skip to content

Commit

Permalink
feat: add Notification List with settings and selenced tabs
Browse files Browse the repository at this point in the history
Fixes #2286
  • Loading branch information
mainawycliffe committed Sep 18, 2024
1 parent 4285158 commit 7efb449
Show file tree
Hide file tree
Showing 21 changed files with 1,026 additions and 483 deletions.
5 changes: 5 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const config = {
source: "/config/:path*",
destination: "/catalog/:path*",
permanent: true
},
{
source: "/settings/notifications/:path*",
destination: "/notifications/:path*",
permanent: true
}
];
},
Expand Down
55 changes: 46 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import React, { ReactNode, useEffect, useState } from "react";
import { IconType } from "react-icons";
import { AiFillHeart } from "react-icons/ai";
import { BsLink, BsToggles } from "react-icons/bs";
import { FaBell, FaTasks } from "react-icons/fa";
import { FaTasks } from "react-icons/fa";
import { HiUser } from "react-icons/hi";
import { ImLifebuoy } from "react-icons/im";
import {
Expand All @@ -27,7 +27,6 @@ import { ErrorBoundary } from "./components/ErrorBoundary";
import EditIntegrationPage from "./components/Integrations/EditIntegrationPage";
import IntegrationsPage from "./components/Integrations/IntegrationsPage";
import JobsHistorySettingsPage from "./components/JobsHistory/JobsHistorySettingsPage";
import NotificationsPage from "./components/Notifications/NotificationsSettingsPage";
import { withAuthorizationAccessCheck } from "./components/Permissions/AuthorizationAccessCheck";
import { SchemaResourcePage } from "./components/SchemaResourcePage";
import { SchemaResource } from "./components/SchemaResourcePage/SchemaResource";
Expand Down Expand Up @@ -55,6 +54,9 @@ 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 NotificationsPage from "./pages/Settings/notifications/NotificationsPage";
import NotificationRulesPage from "./pages/Settings/notifications/NotificationsRulesPage";
import NotificationsSilencedPage from "./pages/Settings/notifications/NotificationsSilencedPage";
import { TopologyCardPage } from "./pages/TopologyCard";
import { UsersPage } from "./pages/UsersPage";
import { ConfigInsightsPage } from "./pages/config/ConfigInsightsList";
Expand Down Expand Up @@ -187,13 +189,6 @@ const settingsNav: SettingsNavigationItems = {
featureName: features["settings.job_history"],
resourceName: tables.database
},
{
name: "Notifications",
href: "/settings/notifications",
icon: FaBell,
featureName: features["settings.notifications"],
resourceName: tables.database
},
{
name: "Feature Flags",
href: "/settings/feature-flags",
Expand Down Expand Up @@ -348,6 +343,48 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
</Route>
</Route>

<Route path="notifications" element={sidebar}>
<Route
index
element={withAuthorizationAccessCheck(
<NotificationsPage />,
tables.database,
"read",
true
)}
/>

<Route
path="rules"
element={withAuthorizationAccessCheck(
<NotificationRulesPage />,
tables.database,
"read",
true
)}
/>

<Route
path="silenced"
element={withAuthorizationAccessCheck(
<NotificationsSilencedPage />,
tables.database,
"read",
true
)}
/>

<Route
path="silence"
element={withAuthorizationAccessCheck(
<NotificationSilencePage />,
tables.database,
"write",
true
)}
/>
</Route>

<Route path="settings" element={sidebar}>
<Route
path="connections"
Expand Down
20 changes: 10 additions & 10 deletions src/api/query-hooks/useNotificationsQuery.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useMutation, useQuery, UseQueryOptions } from "@tanstack/react-query";
import {
NewNotification,
Notification,
UpdateNotification
} from "../../components/Notifications/notificationsTableColumns";
NewNotificationRule,
NotificationRules,
UpdateNotificationRule
} from "../../components/Notifications/Rules/notificationsRulesTableColumns";
import { toastError, toastSuccess } from "../../components/Toast/toast";
import { useUser } from "../../context";
import { createResource, updateResource } from "../schemaResources";
Expand All @@ -21,9 +21,9 @@ export type DatabaseResponse<T extends Record<string, any>> =
};

export function useNotificationsSummaryQuery(
options?: UseQueryOptions<DatabaseResponse<Notification>, Error>
options?: UseQueryOptions<DatabaseResponse<NotificationRules>, Error>
) {
return useQuery<DatabaseResponse<Notification>, Error>(
return useQuery<DatabaseResponse<NotificationRules>, Error>(
["notifications", "settings"],
() => getNotificationsSummary(),
options
Expand All @@ -32,9 +32,9 @@ export function useNotificationsSummaryQuery(

export function useGetNotificationsByIDQuery(
id: string,
options?: UseQueryOptions<Notification | undefined, Error>
options?: UseQueryOptions<NotificationRules | undefined, Error>
) {
return useQuery<Notification | undefined, Error>(
return useQuery<NotificationRules | undefined, Error>(
["notifications", "settings", id],
() => getNotificationById(id),
options
Expand All @@ -43,7 +43,7 @@ export function useGetNotificationsByIDQuery(

export const useUpdateNotification = (onSuccess = () => {}) => {
return useMutation(
async (props: Partial<UpdateNotification>) => {
async (props: Partial<UpdateNotificationRule>) => {
const payload = {
...props,
// we want to remove person id, team id and custom services if they are
Expand Down Expand Up @@ -78,7 +78,7 @@ export const useCreateNotification = (onSuccess = () => {}) => {
const { user } = useUser();

return useMutation(
async (data: Partial<NewNotification>) => {
async (data: Partial<NewNotificationRule>) => {
await createResource(
{
api: "config-db",
Expand Down
66 changes: 62 additions & 4 deletions src/api/services/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
import { Notification } from "@flanksource-ui/components/Notifications/notificationsTableColumns";
import { NotificationRules } from "@flanksource-ui/components/Notifications/Rules/notificationsRulesTableColumns";
import { AVATAR_INFO } from "../../constants";
import { IncidentCommander, NotificationAPI } from "../axios";
import { resolvePostGrestRequestWithPagination } from "../resolve";
import { SilenceNotificationResponse } from "../types/notifications";
import {
NotificationSendHistoryApiResponse,
NotificationSilenceItemApiResponse,
SilenceNotificationResponse
} from "../types/notifications";

export function getPagingParams({
pageIndex,
pageSize
}: {
pageIndex?: number;
pageSize?: number;
}) {
const pagingParams =
pageIndex || pageSize
? `&limit=${pageSize}&offset=${pageIndex! * pageSize!}`
: "";
return pagingParams;
}

export const getNotificationsSummary = async () => {
return resolvePostGrestRequestWithPagination(
IncidentCommander.get<Notification[] | null>(
IncidentCommander.get<NotificationRules[] | null>(
`/notifications_summary?select=*,person:person_id(${AVATAR_INFO}),team:team_id(id,name,icon),created_by(${AVATAR_INFO})&order=created_at.desc`,
{
headers: {
Expand All @@ -18,7 +36,7 @@ export const getNotificationsSummary = async () => {
};

export const getNotificationById = async (id: string) => {
const res = await IncidentCommander.get<Notification[] | null>(
const res = await IncidentCommander.get<NotificationRules[] | null>(
`/notifications?id=eq.${id}&select=*,person:person_id(${AVATAR_INFO}),team:team_id(id,name,icon),created_by(${AVATAR_INFO})`
);
return res.data ? res.data?.[0] : undefined;
Expand All @@ -30,3 +48,43 @@ export const silenceNotification = async (
const res = await NotificationAPI.post("/silence", data);
return res.data;
};

export const getNotificationSendHistory = async ({
pageIndex,
pageSize
}: {
pageIndex: number;
pageSize: number;
}) => {
const pagingParams = getPagingParams({ pageIndex, pageSize });
return resolvePostGrestRequestWithPagination(
IncidentCommander.get<NotificationSendHistoryApiResponse[] | null>(
`/notification_send_history?select=*,person:person_id(${AVATAR_INFO})&order=created_at.desc${pagingParams}`,
{
headers: {
Prefer: "count=exact"
}
}
)
);
};

export const getNotificationSilences = async ({
pageIndex,
pageSize
}: {
pageIndex: number;
pageSize: number;
}) => {
const pagingParams = getPagingParams({ pageIndex, pageSize });
return resolvePostGrestRequestWithPagination(
IncidentCommander.get<NotificationSilenceItemApiResponse[] | null>(
`/notification_silences?select=*,checks:check_id(id,name,type,status),catalog:config_id(id,name,type,config_class),component:component_id(id,name,icon),createdBy:created_by(${AVATAR_INFO})&order=created_at.desc${pagingParams}`,
{
headers: {
Prefer: "count=exact"
}
}
)
);
};
49 changes: 49 additions & 0 deletions src/api/types/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { ConfigItem } from "./configs";
import { HealthCheck } from "./health";
import { Topology } from "./topology";
import { User } from "./users";

export type SilenceNotificationResponse = {
id: string;
component_id: string;
Expand All @@ -9,3 +14,47 @@ export type SilenceNotificationResponse = {
description: string;
recursive: boolean;
};

export type NotificationSendHistory = {
id: string;
notification_id: string;
body?: string | undefined;
status?: string | null;
count: number;
first_observed: string;
source_event: string;
resource_id: string;
person_id?: string | undefined;
error?: string | undefined;
duration_millis?: number | undefined;
created_at: string;
};

export type NotificationSendHistoryApiResponse = NotificationSendHistory & {
person: User;
};

export type NotificationSilenceItem = {
id: string;
namespace: string;
description?: string;
from: string;
until: string;
recursive?: boolean;
config_id?: string;
check_id?: string;
canary_id?: string;
component_id?: string;
source?: "source1" | "source2" | "source3";
created_by?: string;
created_at: string;
updated_at: string;
deleted_at?: string;
};

export type NotificationSilenceItemApiResponse = NotificationSilenceItem & {
checks: Pick<HealthCheck, "id" | "name" | "type" | "status">[];
catalog: Pick<ConfigItem, "id" | "name" | "type" | "config_class">[];
component: Pick<Topology, "id" | "name" | "icon">[];
createdBy: User;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { notificationEvents } from "../../Notifications/notificationsTableColumns";
import { notificationEvents } from "../../Notifications/Rules/notificationsRulesTableColumns";
import FormikSelectDropdown from "./FormikSelectDropdown";

type FormikEventsDropdownProps = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Notification } from "@flanksource-ui/components/Notifications/notificationsTableColumns";
import { NotificationRules } from "@flanksource-ui/components/Notifications/Rules/notificationsRulesTableColumns";
import { useFormikContext } from "formik";
import FormikConnectionField from "../Formik/FormikConnectionField";
import FormikTextArea from "../Formik/FormikTextArea";

export default function NotificationConfigurationForm() {
const { values } = useFormikContext<Notification>();
const { values } = useFormikContext<NotificationRules>();
return (
<div className="flex flex-col space-y-2">
{values.source === "UI" ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useFormikContext } from "formik";
import { useEffect, useState } from "react";
import { Tab, Tabs } from "../../../ui/Tabs/Tabs";
import { Notification } from "../../Notifications/notificationsTableColumns";
import { NotificationRules } from "../../Notifications/Rules/notificationsRulesTableColumns";
import FormikPeopleDropdown from "../Formik/FormikPeopleDropdown";
import FormikTeamsDropdown from "../Formik/FormikTeamsDropdown";
import NotificationConfigurationForm from "./NotificationConfigurationForm";
Expand All @@ -24,7 +24,7 @@ const subjectTabs = [
];

export default function NotificationsRecipientsTabs() {
const { values, setFieldValue } = useFormikContext<Notification>();
const { values, setFieldValue } = useFormikContext<NotificationRules>();

const [activeTab, setActiveTab] = useState<ActiveTab>(() => {
if (values.person_id) {
Expand Down
9 changes: 9 additions & 0 deletions src/components/Layout/SearchLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RefreshButton } from "@flanksource-ui/ui/Buttons/RefreshButton";
import { HelpDropdown } from "@flanksource-ui/ui/MenuBar/HelpDropdown";
import { FaBell } from "react-icons/fa";
import { Link } from "react-router-dom";
import DashboardErrorBoundary from "../../components/Errors/DashboardErrorBoundary";
import { UserProfileDropdown } from "../../components/Users/UserProfile";

Expand Down Expand Up @@ -36,6 +38,13 @@ export function SearchLayout({
{onRefresh && (
<RefreshButton onClick={onRefresh} animate={loading} />
)}
<Link
to={{
pathname: "/notifications"
}}
>
<FaBell className="cursor-pointer text-gray-500" size={20} />
</Link>
<HelpDropdown />
<UserProfileDropdown />
</div>
Expand Down
Loading

0 comments on commit 7efb449

Please sign in to comment.