From 411414fa45a9907598dc299e3a1815cac285a9d8 Mon Sep 17 00:00:00 2001 From: Amlan Kumar Nandy <45410599+amlannandy@users.noreply.github.com> Date: Sat, 27 Sep 2025 22:32:14 +0700 Subject: [PATCH] chore: add routing polices page (#9198) --- .../routingPolicies/createRoutingPolicy.ts | 36 ++ .../routingPolicies/deleteRoutingPolicy.ts | 30 ++ .../api/routingPolicies/getRoutingPolicies.ts | 40 ++ .../routingPolicies/updateRoutingPolicy.ts | 40 ++ frontend/src/constants/reactQueryKeys.ts | 3 + .../RoutingPolicies/DeleteRoutingPolicy.tsx | 47 ++ .../RoutingPolicies/RoutingPolicies.tsx | 118 +++++ .../RoutingPolicies/RoutingPolicyDetails.tsx | 208 ++++++++ .../RoutingPolicies/RoutingPolicyList.tsx | 73 +++ .../RoutingPolicies/RoutingPolicyListItem.tsx | 137 ++++++ .../__tests__/DeleteRoutingPolicy.test.tsx | 81 ++++ .../__tests__/RoutingPolicies.test.tsx | 126 +++++ .../__tests__/RoutingPoliciesList.test.tsx | 89 ++++ .../__tests__/RoutingPolicyDetails.test.tsx | 423 ++++++++++++++++ .../__tests__/RoutingPolicyListItem.test.tsx | 126 +++++ .../RoutingPolicies/__tests__/testUtils.ts | 121 +++++ .../container/RoutingPolicies/constants.ts | 8 + .../src/container/RoutingPolicies/index.ts | 3 + .../src/container/RoutingPolicies/styles.scss | 452 ++++++++++++++++++ .../src/container/RoutingPolicies/types.ts | 115 +++++ .../RoutingPolicies/useRoutingPolicies.ts | 240 ++++++++++ .../src/container/RoutingPolicies/utils.tsx | 61 +++ .../routingPolicies/useCreateRoutingPolicy.ts | 24 + .../routingPolicies/useDeleteRoutingPolicy.ts | 19 + .../routingPolicies/useGetRoutingPolicies.ts | 39 ++ .../routingPolicies/useUpdateRoutingPolicy.ts | 25 + .../src/pages/AlertList/AlertList.styles.scss | 10 + frontend/src/pages/AlertList/index.tsx | 40 +- 28 files changed, 2729 insertions(+), 5 deletions(-) create mode 100644 frontend/src/api/routingPolicies/createRoutingPolicy.ts create mode 100644 frontend/src/api/routingPolicies/deleteRoutingPolicy.ts create mode 100644 frontend/src/api/routingPolicies/getRoutingPolicies.ts create mode 100644 frontend/src/api/routingPolicies/updateRoutingPolicy.ts create mode 100644 frontend/src/container/RoutingPolicies/DeleteRoutingPolicy.tsx create mode 100644 frontend/src/container/RoutingPolicies/RoutingPolicies.tsx create mode 100644 frontend/src/container/RoutingPolicies/RoutingPolicyDetails.tsx create mode 100644 frontend/src/container/RoutingPolicies/RoutingPolicyList.tsx create mode 100644 frontend/src/container/RoutingPolicies/RoutingPolicyListItem.tsx create mode 100644 frontend/src/container/RoutingPolicies/__tests__/DeleteRoutingPolicy.test.tsx create mode 100644 frontend/src/container/RoutingPolicies/__tests__/RoutingPolicies.test.tsx create mode 100644 frontend/src/container/RoutingPolicies/__tests__/RoutingPoliciesList.test.tsx create mode 100644 frontend/src/container/RoutingPolicies/__tests__/RoutingPolicyDetails.test.tsx create mode 100644 frontend/src/container/RoutingPolicies/__tests__/RoutingPolicyListItem.test.tsx create mode 100644 frontend/src/container/RoutingPolicies/__tests__/testUtils.ts create mode 100644 frontend/src/container/RoutingPolicies/constants.ts create mode 100644 frontend/src/container/RoutingPolicies/index.ts create mode 100644 frontend/src/container/RoutingPolicies/styles.scss create mode 100644 frontend/src/container/RoutingPolicies/types.ts create mode 100644 frontend/src/container/RoutingPolicies/useRoutingPolicies.ts create mode 100644 frontend/src/container/RoutingPolicies/utils.tsx create mode 100644 frontend/src/hooks/routingPolicies/useCreateRoutingPolicy.ts create mode 100644 frontend/src/hooks/routingPolicies/useDeleteRoutingPolicy.ts create mode 100644 frontend/src/hooks/routingPolicies/useGetRoutingPolicies.ts create mode 100644 frontend/src/hooks/routingPolicies/useUpdateRoutingPolicy.ts diff --git a/frontend/src/api/routingPolicies/createRoutingPolicy.ts b/frontend/src/api/routingPolicies/createRoutingPolicy.ts new file mode 100644 index 000000000000..5ec9847b69b7 --- /dev/null +++ b/frontend/src/api/routingPolicies/createRoutingPolicy.ts @@ -0,0 +1,36 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api'; + +export interface CreateRoutingPolicyBody { + name: string; + expression: string; + actions: { + channels: string[]; + }; + description?: string; +} + +export interface CreateRoutingPolicyResponse { + success: boolean; + message: string; +} + +const createRoutingPolicy = async ( + props: CreateRoutingPolicyBody, +): Promise< + SuccessResponseV2 | ErrorResponseV2 +> => { + try { + const response = await axios.post(`/notification-policy`, props); + return { + httpStatusCode: response.status, + data: response.data, + }; + } catch (error) { + return ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default createRoutingPolicy; diff --git a/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts b/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts new file mode 100644 index 000000000000..5b0d3df14d97 --- /dev/null +++ b/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts @@ -0,0 +1,30 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api'; + +export interface DeleteRoutingPolicyResponse { + success: boolean; + message: string; +} + +const deleteRoutingPolicy = async ( + routingPolicyId: string, +): Promise< + SuccessResponseV2 | ErrorResponseV2 +> => { + try { + const response = await axios.delete( + `/notification-policy/${routingPolicyId}`, + ); + + return { + httpStatusCode: response.status, + data: response.data, + }; + } catch (error) { + return ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default deleteRoutingPolicy; diff --git a/frontend/src/api/routingPolicies/getRoutingPolicies.ts b/frontend/src/api/routingPolicies/getRoutingPolicies.ts new file mode 100644 index 000000000000..43191aebd77f --- /dev/null +++ b/frontend/src/api/routingPolicies/getRoutingPolicies.ts @@ -0,0 +1,40 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api'; + +export interface ApiRoutingPolicy { + id: string; + name: string; + expression: string; + description: string; + channels: string[]; + createdAt: string; + updatedAt: string; + createdBy: string; + updatedBy: string; +} + +export interface GetRoutingPoliciesResponse { + status: string; + data?: ApiRoutingPolicy[]; +} + +export const getRoutingPolicies = async ( + signal?: AbortSignal, + headers?: Record, +): Promise | ErrorResponseV2> => { + try { + const response = await axios.get('/notification-policy', { + signal, + headers, + }); + + return { + httpStatusCode: response.status, + data: response.data, + }; + } catch (error) { + return ErrorResponseHandlerV2(error as AxiosError); + } +}; diff --git a/frontend/src/api/routingPolicies/updateRoutingPolicy.ts b/frontend/src/api/routingPolicies/updateRoutingPolicy.ts new file mode 100644 index 000000000000..08448562cdd0 --- /dev/null +++ b/frontend/src/api/routingPolicies/updateRoutingPolicy.ts @@ -0,0 +1,40 @@ +import axios from 'api'; +import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2'; +import { AxiosError } from 'axios'; +import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api'; + +export interface UpdateRoutingPolicyBody { + name: string; + expression: string; + actions: { + channels: string[]; + }; + description: string; +} + +export interface UpdateRoutingPolicyResponse { + success: boolean; + message: string; +} + +const updateRoutingPolicy = async ( + id: string, + props: UpdateRoutingPolicyBody, +): Promise< + SuccessResponseV2 | ErrorResponseV2 +> => { + try { + const response = await axios.put(`/notification-policy/${id}`, { + ...props, + }); + + return { + httpStatusCode: response.status, + data: response.data, + }; + } catch (error) { + return ErrorResponseHandlerV2(error as AxiosError); + } +}; + +export default updateRoutingPolicy; diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 6d34aa4b29c0..f59f2c21124b 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -86,4 +86,7 @@ export const REACT_QUERY_KEY = { SPAN_LOGS: 'SPAN_LOGS', SPAN_BEFORE_LOGS: 'SPAN_BEFORE_LOGS', SPAN_AFTER_LOGS: 'SPAN_AFTER_LOGS', + + // Routing Policies Query Keys + GET_ROUTING_POLICIES: 'GET_ROUTING_POLICIES', } as const; diff --git a/frontend/src/container/RoutingPolicies/DeleteRoutingPolicy.tsx b/frontend/src/container/RoutingPolicies/DeleteRoutingPolicy.tsx new file mode 100644 index 000000000000..da2d78935231 --- /dev/null +++ b/frontend/src/container/RoutingPolicies/DeleteRoutingPolicy.tsx @@ -0,0 +1,47 @@ +import { Button, Modal, Typography } from 'antd'; +import { Trash2, X } from 'lucide-react'; + +import { DeleteRoutingPolicyProps } from './types'; + +function DeleteRoutingPolicy({ + handleClose, + handleDelete, + routingPolicy, + isDeletingRoutingPolicy, +}: DeleteRoutingPolicyProps): JSX.Element { + return ( + Delete Routing Policy} + open + closable={false} + onCancel={handleClose} + footer={[ + , + , + ]} + > + + {`Are you sure you want to delete ${routingPolicy?.name} routing policy? Deleting a routing policy is irreversible and cannot be undone.`} + + + ); +} + +export default DeleteRoutingPolicy; diff --git a/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx b/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx new file mode 100644 index 000000000000..b5cb3f08d4f5 --- /dev/null +++ b/frontend/src/container/RoutingPolicies/RoutingPolicies.tsx @@ -0,0 +1,118 @@ +import './styles.scss'; + +import { PlusOutlined } from '@ant-design/icons'; +import { Color } from '@signozhq/design-tokens'; +import { Button, Flex, Input, Tooltip, Typography } from 'antd'; +import { Search } from 'lucide-react'; +import { useAppContext } from 'providers/App/App'; +import { ChangeEvent, useMemo } from 'react'; +import { USER_ROLES } from 'types/roles'; + +import DeleteRoutingPolicy from './DeleteRoutingPolicy'; +import RoutingPolicyDetails from './RoutingPolicyDetails'; +import RoutingPolicyList from './RoutingPolicyList'; +import useRoutingPolicies from './useRoutingPolicies'; + +function RoutingPolicies(): JSX.Element { + const { user } = useAppContext(); + const { + // Routing Policies + selectedRoutingPolicy, + routingPoliciesData, + isLoadingRoutingPolicies, + isErrorRoutingPolicies, + // Channels + channels, + isLoadingChannels, + isErrorChannels, + refreshChannels, + // Search + searchTerm, + setSearchTerm, + // Delete Modal + isDeleteModalOpen, + handleDeleteModalOpen, + handleDeleteModalClose, + handleDeleteRoutingPolicy, + isDeletingRoutingPolicy, + // Policy Details Modal + policyDetailsModalState, + handlePolicyDetailsModalClose, + handlePolicyDetailsModalOpen, + handlePolicyDetailsModalAction, + isPolicyDetailsModalActionLoading, + } = useRoutingPolicies(); + + const disableCreateButton = user?.role === USER_ROLES.VIEWER; + + const tooltipTitle = useMemo(() => { + if (user?.role === USER_ROLES.VIEWER) { + return 'You need edit permissions to create a routing policy'; + } + return ''; + }, [user?.role]); + + const handleSearch = (e: ChangeEvent): void => { + setSearchTerm(e.target.value || ''); + }; + + return ( +
+
+ Routing Policies + + Create and manage routing policies. + + + } + value={searchTerm} + onChange={handleSearch} + /> + + + + +
+ + {policyDetailsModalState.isOpen && ( + + )} + {isDeleteModalOpen && ( + + )} +
+
+ ); +} + +export default RoutingPolicies; diff --git a/frontend/src/container/RoutingPolicies/RoutingPolicyDetails.tsx b/frontend/src/container/RoutingPolicies/RoutingPolicyDetails.tsx new file mode 100644 index 000000000000..0a0726fe0714 --- /dev/null +++ b/frontend/src/container/RoutingPolicies/RoutingPolicyDetails.tsx @@ -0,0 +1,208 @@ +import { + Button, + Divider, + Flex, + Form, + Input, + Modal, + Select, + Typography, +} from 'antd'; +import { useForm } from 'antd/lib/form/Form'; +import ROUTES from 'constants/routes'; +import { ModalTitle } from 'container/PipelinePage/PipelineListsView/styles'; +import { useAppContext } from 'providers/App/App'; +import { useMemo } from 'react'; +import { USER_ROLES } from 'types/roles'; + +import { INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE } from './constants'; +import { + RoutingPolicyDetailsFormState, + RoutingPolicyDetailsProps, +} from './types'; + +function RoutingPolicyDetails({ + closeModal, + mode, + channels, + isErrorChannels, + isLoadingChannels, + routingPolicy, + handlePolicyDetailsModalAction, + isPolicyDetailsModalActionLoading, + refreshChannels, +}: RoutingPolicyDetailsProps): JSX.Element { + const [form] = useForm(); + const { user } = useAppContext(); + + const initialFormState = useMemo(() => { + if (mode === 'edit') { + return { + name: routingPolicy?.name || '', + expression: routingPolicy?.expression || '', + channels: routingPolicy?.channels || [], + description: routingPolicy?.description || '', + }; + } + return INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE; + }, [routingPolicy, mode]); + + const modalTitle = + mode === 'edit' ? 'Edit routing policy' : 'Create routing policy'; + + const handleSave = (): void => { + handlePolicyDetailsModalAction(mode, { + name: form.getFieldValue('name'), + expression: form.getFieldValue('expression'), + channels: form.getFieldValue('channels'), + description: form.getFieldValue('description'), + }); + }; + + const notificationChannelsNotFoundContent = ( + + + No channels yet. + {user?.role === USER_ROLES.ADMIN ? ( + + Create one + + + ) : ( + Please ask your admin to create one. + )} + + + + ); + + return ( + {modalTitle}} + centered + open + className="create-policy-modal" + width={600} + onCancel={closeModal} + footer={null} + maskClosable={false} + > + + + form={form} + initialValues={initialFormState} + onFinish={handleSave} + > +
+
+ Routing Policy Name + + + +
+
+ Description + + + +
+
+ Expression + + + +
+
+ Notification Channels + +