chore: add routing polices page (#9198)

This commit is contained in:
Amlan Kumar Nandy 2025-09-27 22:32:14 +07:00 committed by GitHub
parent 735b90722d
commit 411414fa45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 2729 additions and 5 deletions

View File

@ -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<CreateRoutingPolicyResponse> | ErrorResponseV2
> => {
try {
const response = await axios.post(`/notification-policy`, props);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default createRoutingPolicy;

View File

@ -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<DeleteRoutingPolicyResponse> | ErrorResponseV2
> => {
try {
const response = await axios.delete(
`/notification-policy/${routingPolicyId}`,
);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default deleteRoutingPolicy;

View File

@ -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<string, string>,
): Promise<SuccessResponseV2<GetRoutingPoliciesResponse> | 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<ErrorV2Resp>);
}
};

View File

@ -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<UpdateRoutingPolicyResponse> | 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<ErrorV2Resp>);
}
};
export default updateRoutingPolicy;

View File

@ -86,4 +86,7 @@ export const REACT_QUERY_KEY = {
SPAN_LOGS: 'SPAN_LOGS', SPAN_LOGS: 'SPAN_LOGS',
SPAN_BEFORE_LOGS: 'SPAN_BEFORE_LOGS', SPAN_BEFORE_LOGS: 'SPAN_BEFORE_LOGS',
SPAN_AFTER_LOGS: 'SPAN_AFTER_LOGS', SPAN_AFTER_LOGS: 'SPAN_AFTER_LOGS',
// Routing Policies Query Keys
GET_ROUTING_POLICIES: 'GET_ROUTING_POLICIES',
} as const; } as const;

View File

@ -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 (
<Modal
className="delete-policy-modal"
title={<span className="title">Delete Routing Policy</span>}
open
closable={false}
onCancel={handleClose}
footer={[
<Button
key="cancel"
onClick={handleClose}
className="cancel-btn"
icon={<X size={16} />}
disabled={isDeletingRoutingPolicy}
>
Cancel
</Button>,
<Button
key="submit"
icon={<Trash2 size={16} />}
onClick={handleDelete}
className="delete-btn"
disabled={isDeletingRoutingPolicy}
>
Delete Routing Policy
</Button>,
]}
>
<Typography.Text className="delete-text">
{`Are you sure you want to delete ${routingPolicy?.name} routing policy? Deleting a routing policy is irreversible and cannot be undone.`}
</Typography.Text>
</Modal>
);
}
export default DeleteRoutingPolicy;

View File

@ -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<HTMLInputElement>): void => {
setSearchTerm(e.target.value || '');
};
return (
<div className="routing-policies-container">
<div className="routing-policies-content">
<Typography.Title className="title">Routing Policies</Typography.Title>
<Typography.Text className="subtitle">
Create and manage routing policies.
</Typography.Text>
<Flex className="toolbar">
<Input
placeholder="Search for a routing policy..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchTerm}
onChange={handleSearch}
/>
<Tooltip title={tooltipTitle}>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={(): void => handlePolicyDetailsModalOpen('create', null)}
disabled={disableCreateButton}
>
New routing policy
</Button>
</Tooltip>
</Flex>
<br />
<RoutingPolicyList
routingPolicies={routingPoliciesData}
isRoutingPoliciesLoading={isLoadingRoutingPolicies}
isRoutingPoliciesError={isErrorRoutingPolicies}
handlePolicyDetailsModalOpen={handlePolicyDetailsModalOpen}
handleDeleteModalOpen={handleDeleteModalOpen}
/>
{policyDetailsModalState.isOpen && (
<RoutingPolicyDetails
routingPolicy={selectedRoutingPolicy}
closeModal={handlePolicyDetailsModalClose}
mode={policyDetailsModalState.mode}
channels={channels}
isErrorChannels={isErrorChannels}
isLoadingChannels={isLoadingChannels}
handlePolicyDetailsModalAction={handlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={isPolicyDetailsModalActionLoading}
refreshChannels={refreshChannels}
/>
)}
{isDeleteModalOpen && (
<DeleteRoutingPolicy
isDeletingRoutingPolicy={isDeletingRoutingPolicy}
handleDelete={handleDeleteRoutingPolicy}
handleClose={handleDeleteModalClose}
routingPolicy={selectedRoutingPolicy}
/>
)}
</div>
</div>
);
}
export default RoutingPolicies;

View File

@ -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 = (
<Flex justify="space-between">
<Flex gap={4} align="center">
<Typography.Text>No channels yet.</Typography.Text>
{user?.role === USER_ROLES.ADMIN ? (
<Typography.Text>
Create one
<Button
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
}}
>
here.
</Button>
</Typography.Text>
) : (
<Typography.Text>Please ask your admin to create one.</Typography.Text>
)}
</Flex>
<Button type="text" onClick={refreshChannels}>
Refresh
</Button>
</Flex>
);
return (
<Modal
title={<ModalTitle level={4}>{modalTitle}</ModalTitle>}
centered
open
className="create-policy-modal"
width={600}
onCancel={closeModal}
footer={null}
maskClosable={false}
>
<Divider plain />
<Form<RoutingPolicyDetailsFormState>
form={form}
initialValues={initialFormState}
onFinish={handleSave}
>
<div className="create-policy-container">
<div className="input-group">
<Typography.Text>Routing Policy Name</Typography.Text>
<Form.Item
name="name"
rules={[
{
required: true,
message: 'Please provide a name for the routing policy',
},
]}
>
<Input placeholder="e.g. Base routing policy..." />
</Form.Item>
</div>
<div className="input-group">
<Typography.Text>Description</Typography.Text>
<Form.Item
name="description"
rules={[
{
required: false,
},
]}
>
<Input.TextArea
placeholder="e.g. This is a routing policy that..."
autoSize={{ minRows: 1, maxRows: 6 }}
style={{ resize: 'none' }}
/>
</Form.Item>
</div>
<div className="input-group">
<Typography.Text>Expression</Typography.Text>
<Form.Item
name="expression"
rules={[
{
required: true,
message: 'Please provide an expression for the routing policy',
},
]}
>
<Input.TextArea
placeholder='e.g. service.name == "payment" && threshold.name == "critical"'
autoSize={{ minRows: 1, maxRows: 6 }}
style={{ resize: 'none' }}
/>
</Form.Item>
</div>
<div className="input-group">
<Typography.Text>Notification Channels</Typography.Text>
<Form.Item
name="channels"
rules={[
{
required: true,
message: 'Please select at least one notification channel',
},
]}
>
<Select
options={channels.map((channel) => ({
value: channel.name,
label: channel.name,
}))}
mode="multiple"
placeholder="Select notification channels"
showSearch
maxTagCount={3}
maxTagPlaceholder={(omittedValues): string =>
`+${omittedValues.length} more`
}
maxTagTextLength={10}
filterOption={(input, option): boolean =>
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
}
status={isErrorChannels ? 'error' : undefined}
disabled={isLoadingChannels}
notFoundContent={notificationChannelsNotFoundContent}
/>
</Form.Item>
</div>
</div>
<Flex className="create-policy-footer" justify="space-between">
<Button onClick={closeModal} disabled={isPolicyDetailsModalActionLoading}>
Cancel
</Button>
<Button
type="primary"
htmlType="submit"
loading={isPolicyDetailsModalActionLoading}
disabled={isPolicyDetailsModalActionLoading}
>
Save Routing Policy
</Button>
</Flex>
</Form>
</Modal>
);
}
export default RoutingPolicyDetails;

View File

@ -0,0 +1,73 @@
import { Table, TableProps, Typography } from 'antd';
import { useMemo } from 'react';
import RoutingPolicyListItem from './RoutingPolicyListItem';
import { RoutingPolicy, RoutingPolicyListProps } from './types';
function RoutingPolicyList({
routingPolicies,
isRoutingPoliciesLoading,
isRoutingPoliciesError,
handlePolicyDetailsModalOpen,
handleDeleteModalOpen,
}: RoutingPolicyListProps): JSX.Element {
const columns: TableProps<RoutingPolicy>['columns'] = [
{
title: 'Routing Policy',
key: 'routingPolicy',
render: (data: RoutingPolicy): JSX.Element => (
<RoutingPolicyListItem
routingPolicy={data}
handlePolicyDetailsModalOpen={handlePolicyDetailsModalOpen}
handleDeleteModalOpen={handleDeleteModalOpen}
/>
),
},
];
const localeEmptyState = useMemo(
() => (
<div className="no-routing-policies-message-container">
{isRoutingPoliciesError ? (
<img src="/Icons/awwSnap.svg" alt="aww-snap" className="error-state-svg" />
) : (
<img
src="/Icons/emptyState.svg"
alt="thinking-emoji"
className="empty-state-svg"
/>
)}
{isRoutingPoliciesError ? (
<Typography.Text>
Something went wrong while fetching routing policies.
</Typography.Text>
) : (
<Typography.Text>No routing policies found.</Typography.Text>
)}
</div>
),
[isRoutingPoliciesError],
);
return (
<Table<RoutingPolicy>
columns={columns}
className="routing-policies-table"
bordered={false}
dataSource={routingPolicies}
loading={isRoutingPoliciesLoading}
showHeader={false}
rowKey="id"
pagination={{
pageSize: 5,
showSizeChanger: false,
hideOnSinglePage: true,
}}
locale={{
emptyText: isRoutingPoliciesLoading ? null : localeEmptyState,
}}
/>
);
}
export default RoutingPolicyList;

View File

@ -0,0 +1,137 @@
import { Color } from '@signozhq/design-tokens';
import { Collapse, Flex, Tag, Typography } from 'antd';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { PenLine, Trash2 } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { USER_ROLES } from 'types/roles';
import {
PolicyListItemContentProps,
PolicyListItemHeaderProps,
RoutingPolicyListItemProps,
} from './types';
function PolicyListItemHeader({
name,
handleEdit,
handleDelete,
}: PolicyListItemHeaderProps): JSX.Element {
const { user } = useAppContext();
const isEditEnabled = user?.role !== USER_ROLES.VIEWER;
return (
<Flex className="policy-list-item-header" justify="space-between">
<Typography>{name}</Typography>
{isEditEnabled && (
<div className="action-btn">
<PenLine
size={14}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleEdit();
}}
data-testid="edit-routing-policy"
/>
<Trash2
size={14}
color={Color.BG_CHERRY_500}
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
handleDelete();
}}
data-testid="delete-routing-policy"
/>
</div>
)}
</Flex>
);
}
function PolicyListItemContent({
routingPolicy,
}: PolicyListItemContentProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return (
<div className="policy-list-item-content">
<div className="policy-list-item-content-row">
<Typography>Created by</Typography>
<Typography>{routingPolicy.createdBy}</Typography>
</div>
<div className="policy-list-item-content-row">
<Typography>Created on</Typography>
<Typography>
{routingPolicy.createdAt
? formatTimezoneAdjustedTimestamp(
routingPolicy.createdAt,
DATE_TIME_FORMATS.MONTH_DATETIME,
)
: '-'}
</Typography>
</div>
<div className="policy-list-item-content-row">
<Typography>Updated by</Typography>
<Typography>{routingPolicy.updatedBy || '-'}</Typography>
</div>
<div className="policy-list-item-content-row">
<Typography>Updated on</Typography>
<Typography>
{routingPolicy.updatedAt
? formatTimezoneAdjustedTimestamp(
routingPolicy.updatedAt,
DATE_TIME_FORMATS.MONTH_DATETIME,
)
: '-'}
</Typography>
</div>
<div className="policy-list-item-content-row">
<Typography>Expression</Typography>
<Typography>{routingPolicy.expression}</Typography>
</div>
<div className="policy-list-item-content-row">
<Typography>Description</Typography>
<Typography>{routingPolicy.description || '-'}</Typography>
</div>
<div className="policy-list-item-content-row">
<Typography>Channels</Typography>
<div>
{routingPolicy.channels.map((channel) => (
<Tag key={channel}>{channel}</Tag>
))}
</div>
</div>
</div>
);
}
function RoutingPolicyListItem({
routingPolicy,
handlePolicyDetailsModalOpen,
handleDeleteModalOpen,
}: RoutingPolicyListItemProps): JSX.Element {
return (
<Collapse accordion className="policy-list-item">
<Collapse.Panel
header={
<PolicyListItemHeader
name={routingPolicy.name}
handleEdit={(): void =>
handlePolicyDetailsModalOpen('edit', routingPolicy)
}
handleDelete={(): void => handleDeleteModalOpen(routingPolicy)}
/>
}
key={routingPolicy.id}
>
<PolicyListItemContent routingPolicy={routingPolicy} />
</Collapse.Panel>
</Collapse>
);
}
export default RoutingPolicyListItem;

View File

@ -0,0 +1,81 @@
import { fireEvent, render, screen } from '@testing-library/react';
import DeleteRoutingPolicy from '../DeleteRoutingPolicy';
import { MOCK_ROUTING_POLICY_1 } from './testUtils';
const mockRoutingPolicy = MOCK_ROUTING_POLICY_1;
const mockHandleDelete = jest.fn();
const mockHandleClose = jest.fn();
const DELETE_BUTTON_TEXT = 'Delete Routing Policy';
const CANCEL_BUTTON_TEXT = 'Cancel';
describe('DeleteRoutingPolicy', () => {
it('renders base layout with routing policy', () => {
render(
<DeleteRoutingPolicy
routingPolicy={mockRoutingPolicy}
isDeletingRoutingPolicy={false}
handleDelete={mockHandleDelete}
handleClose={mockHandleClose}
/>,
);
expect(
screen.getByRole('dialog', { name: DELETE_BUTTON_TEXT }),
).toBeInTheDocument();
expect(
screen.getByText(
`Are you sure you want to delete ${mockRoutingPolicy.name} routing policy? Deleting a routing policy is irreversible and cannot be undone.`,
),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: CANCEL_BUTTON_TEXT }),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: DELETE_BUTTON_TEXT }),
).toBeInTheDocument();
});
it('should call handleDelete when delete button is clicked', () => {
render(
<DeleteRoutingPolicy
routingPolicy={mockRoutingPolicy}
isDeletingRoutingPolicy={false}
handleDelete={mockHandleDelete}
handleClose={mockHandleClose}
/>,
);
fireEvent.click(screen.getByRole('button', { name: DELETE_BUTTON_TEXT }));
expect(mockHandleDelete).toHaveBeenCalled();
});
it('should call handleClose when cancel button is clicked', () => {
render(
<DeleteRoutingPolicy
routingPolicy={mockRoutingPolicy}
isDeletingRoutingPolicy={false}
handleDelete={mockHandleDelete}
handleClose={mockHandleClose}
/>,
);
fireEvent.click(screen.getByRole('button', { name: CANCEL_BUTTON_TEXT }));
expect(mockHandleClose).toHaveBeenCalled();
});
it('should be disabled when deleting routing policy', () => {
render(
<DeleteRoutingPolicy
routingPolicy={mockRoutingPolicy}
isDeletingRoutingPolicy
handleDelete={mockHandleDelete}
handleClose={mockHandleClose}
/>,
);
expect(
screen.getByRole('button', { name: DELETE_BUTTON_TEXT }),
).toBeDisabled();
expect(
screen.getByRole('button', { name: CANCEL_BUTTON_TEXT }),
).toBeDisabled();
});
});

View File

@ -0,0 +1,126 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as appHooks from 'providers/App/App';
import RoutingPolicies from '../RoutingPolicies';
import * as routingPoliciesHooks from '../useRoutingPolicies';
import {
getAppContextMockState,
getUseRoutingPoliciesMockData,
MOCK_ROUTING_POLICY_1,
} from './testUtils';
const ROUTING_POLICY_DETAILS_TEST_ID = 'routing-policy-details';
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
jest.mock('../RoutingPolicyList', () => ({
__esModule: true,
default: jest.fn(() => (
<div data-testid="routing-policy-list">RoutingPolicyList</div>
)),
}));
jest.mock('../RoutingPolicyDetails', () => ({
__esModule: true,
default: jest.fn(() => (
<div data-testid="routing-policy-details">RoutingPolicyDetails</div>
)),
}));
jest.mock('../DeleteRoutingPolicy', () => ({
__esModule: true,
default: jest.fn(() => (
<div data-testid="delete-routing-policy">DeleteRoutingPolicy</div>
)),
}));
const mockHandleSearch = jest.fn();
const mockHandlePolicyDetailsModalOpen = jest.fn();
jest.spyOn(routingPoliciesHooks, 'default').mockReturnValue(
getUseRoutingPoliciesMockData({
setSearchTerm: mockHandleSearch,
handlePolicyDetailsModalOpen: mockHandlePolicyDetailsModalOpen,
}),
);
describe('RoutingPolicies', () => {
it('should render components properly', () => {
render(<RoutingPolicies />);
expect(screen.getByText('Routing Policies')).toBeInTheDocument();
expect(
screen.getByText('Create and manage routing policies.'),
).toBeInTheDocument();
expect(
screen.getByPlaceholderText('Search for a routing policy...'),
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /New routing policy/ }),
).toBeInTheDocument();
expect(screen.getByTestId('routing-policy-list')).toBeInTheDocument();
expect(
screen.queryByTestId(ROUTING_POLICY_DETAILS_TEST_ID),
).not.toBeInTheDocument();
expect(screen.queryByTestId('delete-routing-policy')).not.toBeInTheDocument();
});
it('should enable the "New routing policy" button for users with ADMIN role', () => {
render(<RoutingPolicies />);
expect(
screen.getByRole('button', { name: /New routing policy/ }),
).toBeEnabled();
});
it('should disable the "New routing policy" button for users with VIEWER role', () => {
jest
.spyOn(appHooks, 'useAppContext')
.mockReturnValueOnce(getAppContextMockState({ role: 'VIEWER' }));
render(<RoutingPolicies />);
expect(
screen.getByRole('button', { name: /New routing policy/ }),
).toBeDisabled();
});
it('filters routing policies by search term', () => {
render(<RoutingPolicies />);
const searchInput = screen.getByPlaceholderText(
'Search for a routing policy...',
);
fireEvent.change(searchInput, {
target: { value: MOCK_ROUTING_POLICY_1.name },
});
expect(mockHandleSearch).toHaveBeenCalledWith(MOCK_ROUTING_POLICY_1.name);
});
it('clicking on the "New routing policy" button opens the policy details modal', () => {
render(<RoutingPolicies />);
const newRoutingPolicyButton = screen.getByRole('button', {
name: /New routing policy/,
});
fireEvent.click(newRoutingPolicyButton);
expect(mockHandlePolicyDetailsModalOpen).toHaveBeenCalledWith('create', null);
});
it('policy details modal is open based on modal state', () => {
jest.spyOn(routingPoliciesHooks, 'default').mockReturnValue(
getUseRoutingPoliciesMockData({
policyDetailsModalState: {
mode: 'create',
isOpen: true,
},
}),
);
render(<RoutingPolicies />);
expect(
screen.getByTestId(ROUTING_POLICY_DETAILS_TEST_ID),
).toBeInTheDocument();
});
it('delete modal is open based on modal state', () => {
jest.spyOn(routingPoliciesHooks, 'default').mockReturnValue(
getUseRoutingPoliciesMockData({
isDeleteModalOpen: true,
}),
);
render(<RoutingPolicies />);
expect(screen.getByTestId('delete-routing-policy')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,89 @@
import { render, screen } from '@testing-library/react';
import RoutingPoliciesList from '../RoutingPolicyList';
import { RoutingPolicyListItemProps } from '../types';
import { getUseRoutingPoliciesMockData } from './testUtils';
const useRoutingPolicesMockData = getUseRoutingPoliciesMockData();
const mockHandlePolicyDetailsModalOpen = jest.fn();
const mockHandleDeleteModalOpen = jest.fn();
jest.mock('../RoutingPolicyListItem', () => ({
__esModule: true,
default: jest.fn(({ routingPolicy }: RoutingPolicyListItemProps) => (
<div data-testid="routing-policy-list-item">{routingPolicy.name}</div>
)),
}));
const ROUTING_POLICY_LIST_ITEM_TEST_ID = 'routing-policy-list-item';
describe('RoutingPoliciesList', () => {
it('renders base layout with routing policies', () => {
render(
<RoutingPoliciesList
routingPolicies={useRoutingPolicesMockData.routingPoliciesData}
isRoutingPoliciesLoading={
useRoutingPolicesMockData.isLoadingRoutingPolicies
}
isRoutingPoliciesError={useRoutingPolicesMockData.isErrorRoutingPolicies}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
const routingPolicyItems = screen.getAllByTestId(
ROUTING_POLICY_LIST_ITEM_TEST_ID,
);
expect(routingPolicyItems).toHaveLength(2);
expect(routingPolicyItems[0]).toHaveTextContent(
useRoutingPolicesMockData.routingPoliciesData[0].name,
);
expect(routingPolicyItems[1]).toHaveTextContent(
useRoutingPolicesMockData.routingPoliciesData[1].name,
);
});
it('renders loading state', () => {
render(
<RoutingPoliciesList
routingPolicies={useRoutingPolicesMockData.routingPoliciesData}
isRoutingPoliciesLoading
isRoutingPoliciesError={false}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
// Check for loading spinner by class name
expect(document.querySelector('.ant-spin-spinning')).toBeInTheDocument();
// Check that the table is in loading state (blurred)
expect(document.querySelector('.ant-spin-blur')).toBeInTheDocument();
});
it('renders error state', () => {
render(
<RoutingPoliciesList
routingPolicies={[]}
isRoutingPoliciesLoading={false}
isRoutingPoliciesError
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
expect(
screen.getByText('Something went wrong while fetching routing policies.'),
).toBeInTheDocument();
});
it('renders empty state', () => {
render(
<RoutingPoliciesList
routingPolicies={[]}
isRoutingPoliciesLoading={false}
isRoutingPoliciesError={false}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
expect(screen.getByText('No routing policies found.')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,423 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import * as appHooks from 'providers/App/App';
import RoutingPolicyDetails from '../RoutingPolicyDetails';
import {
getAppContextMockState,
MOCK_CHANNEL_1,
MOCK_CHANNEL_2,
MOCK_ROUTING_POLICY_1,
} from './testUtils';
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
const mockHandlePolicyDetailsModalAction = jest.fn();
const mockCloseModal = jest.fn();
const mockChannels = [MOCK_CHANNEL_1, MOCK_CHANNEL_2];
const mockRoutingPolicy = MOCK_ROUTING_POLICY_1;
const mockRefreshChannels = jest.fn();
const NEW_NAME = 'New Name';
const NEW_EXPRESSION = 'New Expression';
const NEW_DESCRIPTION = 'New Description';
const SAVE_BUTTON_TEXT = 'Save Routing Policy';
const NO_CHANNELS_FOUND_TEXT = 'No channels yet.';
describe('RoutingPolicyDetails', () => {
it('renders base create layout with header, 3 inputs and footer', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Create routing policy')).toBeInTheDocument();
expect(screen.getByText('Routing Policy Name')).toBeInTheDocument();
expect(screen.getByText('Expression')).toBeInTheDocument();
expect(screen.getByText('Notification Channels')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: SAVE_BUTTON_TEXT }),
).toBeInTheDocument();
});
it('renders base edit layout with header, 3 inputs and footer', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="edit"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Edit routing policy')).toBeInTheDocument();
expect(screen.getByText('Routing Policy Name')).toBeInTheDocument();
expect(screen.getByText('Expression')).toBeInTheDocument();
expect(screen.getByText('Notification Channels')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: SAVE_BUTTON_TEXT }),
).toBeInTheDocument();
});
it('prefills inputs with existing policy values in edit mode', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="edit"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const nameInput = screen.getByDisplayValue(mockRoutingPolicy.name);
expect(nameInput).toBeInTheDocument();
const expressionTextarea = screen.getByDisplayValue(
mockRoutingPolicy.expression,
);
expect(expressionTextarea).toBeInTheDocument();
expect(screen.getByText(MOCK_CHANNEL_1.name)).toBeInTheDocument();
});
it('creating and saving the routing policy works correctly', async () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const nameInput = screen.getByPlaceholderText('e.g. Base routing policy...');
expect(nameInput).toBeInTheDocument();
const expressionTextarea = screen.getByPlaceholderText(
'e.g. service.name == "payment" && threshold.name == "critical"',
);
expect(expressionTextarea).toBeInTheDocument();
const descriptionTextarea = screen.getByPlaceholderText(
'e.g. This is a routing policy that...',
);
expect(descriptionTextarea).toBeInTheDocument();
fireEvent.change(nameInput, { target: { value: NEW_NAME } });
fireEvent.change(expressionTextarea, { target: { value: NEW_EXPRESSION } });
fireEvent.change(descriptionTextarea, { target: { value: NEW_DESCRIPTION } });
const channelSelect = screen.getByRole('combobox');
fireEvent.mouseDown(channelSelect);
const channelOptions = await screen.findAllByText('Channel 1');
fireEvent.click(channelOptions[1]);
// Wait for the form to be valid before submitting
await waitFor(() => {
expect(screen.getByDisplayValue(NEW_NAME)).toBeInTheDocument();
});
const saveButton = screen.getByRole('button', {
name: 'Save Routing Policy',
});
fireEvent.click(saveButton);
// Wait for the form submission to complete
await waitFor(() => {
expect(mockHandlePolicyDetailsModalAction).toHaveBeenCalled();
});
expect(mockHandlePolicyDetailsModalAction).toHaveBeenCalledWith('create', {
name: NEW_NAME,
expression: NEW_EXPRESSION,
description: NEW_DESCRIPTION,
channels: ['Channel 1'],
});
});
it('editing and saving the routing policy works correctly', async () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="edit"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const nameInput = screen.getByDisplayValue(mockRoutingPolicy.name);
expect(nameInput).toBeInTheDocument();
const expressionTextarea = screen.getByDisplayValue(
mockRoutingPolicy.expression,
);
expect(expressionTextarea).toBeInTheDocument();
const descriptionTextarea = screen.getByDisplayValue(
mockRoutingPolicy.description || 'description 1',
);
expect(descriptionTextarea).toBeInTheDocument();
fireEvent.change(nameInput, { target: { value: NEW_NAME } });
fireEvent.change(expressionTextarea, { target: { value: NEW_EXPRESSION } });
fireEvent.change(descriptionTextarea, { target: { value: NEW_DESCRIPTION } });
// Wait for the form to be valid before submitting
await waitFor(() => {
expect(screen.getByDisplayValue(NEW_NAME)).toBeInTheDocument();
});
const saveButton = screen.getByRole('button', {
name: SAVE_BUTTON_TEXT,
});
fireEvent.click(saveButton);
// Wait for the form submission to complete
await waitFor(() => {
expect(mockHandlePolicyDetailsModalAction).toHaveBeenCalled();
});
expect(mockHandlePolicyDetailsModalAction).toHaveBeenCalledWith('edit', {
name: NEW_NAME,
expression: NEW_EXPRESSION,
description: NEW_DESCRIPTION,
channels: ['Channel 1'],
});
});
it('should close modal when cancel button is clicked', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="edit"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
fireEvent.click(cancelButton);
expect(mockCloseModal).toHaveBeenCalled();
});
it('buttons should be disabled when loading', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="edit"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const cancelButton = screen.getByRole('button', { name: 'Cancel' });
expect(cancelButton).toBeDisabled();
const saveButton = screen.getByRole('button', {
name: new RegExp(SAVE_BUTTON_TEXT, 'i'),
});
expect(saveButton).toBeDisabled();
});
it('submit should not be called when inputs are invalid', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={mockChannels}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const saveButton = screen.getByRole('button', {
name: SAVE_BUTTON_TEXT,
});
fireEvent.click(saveButton);
expect(mockHandlePolicyDetailsModalAction).not.toHaveBeenCalled();
});
it('notification channels select should be disabled when channels are loading', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={[]}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels
refreshChannels={mockRefreshChannels}
/>,
);
const channelSelect = screen.getByRole('combobox');
expect(channelSelect).toBeDisabled();
});
it('should show error state when channels fail to load', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={[]}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const channelSelect = screen.getByRole('combobox');
const selectContainer = channelSelect.closest('.ant-select');
expect(selectContainer).toHaveClass('ant-select-status-error');
});
it('should show empty state when no channels are available', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={[]}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const channelSelect = screen.getByRole('combobox');
fireEvent.mouseDown(channelSelect);
expect(screen.getByText(NO_CHANNELS_FOUND_TEXT)).toBeInTheDocument();
});
it('should show create channel button for admin users in empty state', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={[]}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const channelSelect = screen.getByRole('combobox');
fireEvent.mouseDown(channelSelect);
expect(screen.getByText(NO_CHANNELS_FOUND_TEXT)).toBeInTheDocument();
expect(screen.getByText('Create one')).toBeInTheDocument();
});
it('should show admin message for non-admin users in empty state', () => {
jest
.spyOn(appHooks, 'useAppContext')
.mockReturnValue(getAppContextMockState({ role: 'VIEWER' }));
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={[]}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const channelSelect = screen.getByRole('combobox');
fireEvent.mouseDown(channelSelect);
expect(screen.getByText(NO_CHANNELS_FOUND_TEXT)).toBeInTheDocument();
expect(
screen.getByText('Please ask your admin to create one.'),
).toBeInTheDocument();
expect(screen.queryByText('Create one')).not.toBeInTheDocument();
});
it('should call refreshChannels when refresh button is clicked in empty state', () => {
render(
<RoutingPolicyDetails
routingPolicy={mockRoutingPolicy}
closeModal={mockCloseModal}
mode="create"
channels={[]}
handlePolicyDetailsModalAction={mockHandlePolicyDetailsModalAction}
isPolicyDetailsModalActionLoading={false}
isErrorChannels={false}
isLoadingChannels={false}
refreshChannels={mockRefreshChannels}
/>,
);
const channelSelect = screen.getByRole('combobox');
fireEvent.mouseDown(channelSelect);
const refreshButton = screen.getByText('Refresh');
expect(refreshButton).toBeInTheDocument();
fireEvent.click(refreshButton);
expect(mockRefreshChannels).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,126 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as appHooks from 'providers/App/App';
import { ROLES, USER_ROLES } from 'types/roles';
import RoutingPolicyListItem from '../RoutingPolicyListItem';
import { getAppContextMockState, MOCK_ROUTING_POLICY_1 } from './testUtils';
const mockFormatTimezoneAdjustedTimestamp = jest.fn();
jest.mock('providers/Timezone', () => ({
useTimezone: (): any => ({
formatTimezoneAdjustedTimestamp: mockFormatTimezoneAdjustedTimestamp,
}),
}));
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
const mockRoutingPolicy = MOCK_ROUTING_POLICY_1;
const mockHandlePolicyDetailsModalOpen = jest.fn();
const mockHandleDeleteModalOpen = jest.fn();
const EDIT_ROUTING_POLICY_TEST_ID = 'edit-routing-policy';
const DELETE_ROUTING_POLICY_TEST_ID = 'delete-routing-policy';
describe('RoutingPolicyListItem', () => {
it('should render properly in collapsed state', () => {
render(
<RoutingPolicyListItem
routingPolicy={mockRoutingPolicy}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
expect(screen.getByText(mockRoutingPolicy.name)).toBeInTheDocument();
expect(screen.getByTestId(EDIT_ROUTING_POLICY_TEST_ID)).toBeInTheDocument();
expect(screen.getByTestId(DELETE_ROUTING_POLICY_TEST_ID)).toBeInTheDocument();
});
it('should render properly in expanded state', () => {
render(
<RoutingPolicyListItem
routingPolicy={mockRoutingPolicy}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
expect(screen.getByText(mockRoutingPolicy.name)).toBeInTheDocument();
fireEvent.click(screen.getByText(mockRoutingPolicy.name));
expect(screen.getByText(mockRoutingPolicy.expression)).toBeInTheDocument();
expect(screen.getByText(mockRoutingPolicy.channels[0])).toBeInTheDocument();
expect(
screen.getByText(mockRoutingPolicy.createdBy || 'user1@signoz.io'),
).toBeInTheDocument();
expect(
screen.getByText(mockRoutingPolicy.description || 'description 1'),
).toBeInTheDocument();
});
it('should call handlePolicyDetailsModalOpen when edit button is clicked', () => {
render(
<RoutingPolicyListItem
routingPolicy={mockRoutingPolicy}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
fireEvent.click(screen.getByTestId(EDIT_ROUTING_POLICY_TEST_ID));
expect(mockHandlePolicyDetailsModalOpen).toHaveBeenCalledWith(
'edit',
mockRoutingPolicy,
);
});
it('should call handleDeleteModalOpen when delete button is clicked', () => {
render(
<RoutingPolicyListItem
routingPolicy={mockRoutingPolicy}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
fireEvent.click(screen.getByTestId(DELETE_ROUTING_POLICY_TEST_ID));
expect(mockHandleDeleteModalOpen).toHaveBeenCalledWith(mockRoutingPolicy);
});
it('edit and delete buttons should not be rendered for viewer role', () => {
jest
.spyOn(appHooks, 'useAppContext')
.mockReturnValue(
getAppContextMockState({ role: USER_ROLES.VIEWER as ROLES }),
);
render(
<RoutingPolicyListItem
routingPolicy={mockRoutingPolicy}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
expect(
screen.queryByTestId(EDIT_ROUTING_POLICY_TEST_ID),
).not.toBeInTheDocument();
expect(
screen.queryByTestId(DELETE_ROUTING_POLICY_TEST_ID),
).not.toBeInTheDocument();
});
it('in details panel, show "-" for undefined values', () => {
render(
<RoutingPolicyListItem
routingPolicy={mockRoutingPolicy}
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
handleDeleteModalOpen={mockHandleDeleteModalOpen}
/>,
);
// Expand the details panel
fireEvent.click(screen.getByText(mockRoutingPolicy.name));
const updatedByRow = screen.getByText('Updated by').parentElement;
expect(updatedByRow).toHaveTextContent('-');
const updatedOnRow = screen.getByText('Updated on').parentElement;
expect(updatedOnRow).toHaveTextContent('-');
});
});

View File

@ -0,0 +1,121 @@
import { IAppContext, IUser } from 'providers/App/types';
import { Channels } from 'types/api/channels/getAll';
import { RoutingPolicy, UseRoutingPoliciesReturn } from '../types';
export const MOCK_ROUTING_POLICY_1: RoutingPolicy = {
id: '1',
name: 'Routing Policy 1',
expression: 'expression 1',
description: 'description 1',
channels: ['Channel 1'],
createdAt: '2021-01-04',
updatedAt: undefined,
createdBy: 'user1@signoz.io',
updatedBy: undefined,
};
export const MOCK_ROUTING_POLICY_2: RoutingPolicy = {
id: '2',
name: 'Routing Policy 2',
expression: 'expression 2',
description: 'description 2',
channels: ['Channel 2'],
createdAt: '2021-01-05',
updatedAt: '2021-01-05',
createdBy: 'user2@signoz.io',
updatedBy: 'user2@signoz.io',
};
export const MOCK_CHANNEL_1: Channels = {
name: 'Channel 1',
created_at: '2021-01-01',
data: 'data 1',
id: '1',
type: 'type 1',
updated_at: '2021-01-01',
};
export const MOCK_CHANNEL_2: Channels = {
name: 'Channel 2',
created_at: '2021-01-02',
data: 'data 2',
id: '2',
type: 'type 2',
updated_at: '2021-01-02',
};
export function getUseRoutingPoliciesMockData(
overrides?: Partial<UseRoutingPoliciesReturn>,
): UseRoutingPoliciesReturn {
return {
selectedRoutingPolicy: MOCK_ROUTING_POLICY_1,
routingPoliciesData: [MOCK_ROUTING_POLICY_1, MOCK_ROUTING_POLICY_2],
isLoadingRoutingPolicies: false,
isErrorRoutingPolicies: false,
channels: [MOCK_CHANNEL_1, MOCK_CHANNEL_2],
isLoadingChannels: false,
searchTerm: '',
setSearchTerm: jest.fn(),
isDeleteModalOpen: false,
handleDeleteModalOpen: jest.fn(),
handleDeleteModalClose: jest.fn(),
handleDeleteRoutingPolicy: jest.fn(),
isDeletingRoutingPolicy: false,
policyDetailsModalState: {
mode: null,
isOpen: false,
},
handlePolicyDetailsModalClose: jest.fn(),
handlePolicyDetailsModalOpen: jest.fn(),
handlePolicyDetailsModalAction: jest.fn(),
isPolicyDetailsModalActionLoading: false,
isErrorChannels: false,
refreshChannels: jest.fn(),
...overrides,
};
}
export function getAppContextMockState(
overrides?: Partial<IUser>,
): IAppContext {
return {
user: {
accessJwt: 'some-token',
refreshJwt: 'some-refresh-token',
id: 'some-user-id',
email: 'user@signoz.io',
displayName: 'John Doe',
createdAt: 1732544623,
organization: 'Nightswatch',
orgId: 'does-not-matter-id',
role: 'ADMIN',
...overrides,
},
activeLicense: null,
trialInfo: null,
featureFlags: null,
orgPreferences: null,
userPreferences: null,
isLoggedIn: false,
org: null,
isFetchingUser: false,
isFetchingActiveLicense: false,
isFetchingFeatureFlags: false,
isFetchingOrgPreferences: false,
userFetchError: undefined,
activeLicenseFetchError: null,
featureFlagsFetchError: undefined,
orgPreferencesFetchError: undefined,
changelog: null,
showChangelogModal: false,
activeLicenseRefetch: jest.fn(),
updateUser: jest.fn(),
updateOrgPreferences: jest.fn(),
updateUserPreferenceInContext: jest.fn(),
updateOrg: jest.fn(),
updateChangelog: jest.fn(),
toggleChangelogModal: jest.fn(),
versionData: null,
hasEditPermission: false,
};
}

View File

@ -0,0 +1,8 @@
import { RoutingPolicyDetailsFormState } from './types';
export const INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE: RoutingPolicyDetailsFormState = {
name: '',
expression: '',
channels: [],
description: '',
};

View File

@ -0,0 +1,3 @@
import RoutingPolicies from './RoutingPolicies';
export default RoutingPolicies;

View File

@ -0,0 +1,452 @@
.routing-policies-container {
display: flex;
justify-content: center;
width: 100%;
margin-top: 8px;
.routing-policies-content {
width: calc(100% - 30px);
max-width: 736px;
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-lg);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 28px;
letter-spacing: -0.09px;
}
.subtitle {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.ant-input-affix-wrapper {
margin-top: 16px;
margin-bottom: 8px;
}
.toolbar {
display: flex;
align-items: center;
gap: 8px;
.ant-btn {
margin-top: 8px;
}
}
.routing-policies-table {
.no-routing-policies-message-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
min-height: 200px;
.empty-state-svg {
width: 40px;
height: 40px;
}
.ant-typography {
color: var(--bg-vanilla-400);
}
}
}
}
}
.routing-policies-table {
.ant-table {
background: none !important;
}
.ant-table-cell {
padding: 0 !important;
border: 0 !important;
width: 736px;
}
.ant-table-tbody {
display: flex;
flex-direction: column;
gap: 16px;
.policy-list-item {
border: 1px solid var(--bg-slate-500);
background-color: var(--bg-ink-400);
border-radius: 6px;
.ant-collapse-header {
height: 56px;
align-items: center;
}
.policy-list-item-header {
.ant-typography {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 18px;
}
.action-btn {
display: flex;
align-items: center;
gap: 20px;
cursor: pointer;
}
}
.policy-list-item-content {
.policy-list-item-content-row {
display: grid;
grid-template-columns: 128px 1fr;
margin-bottom: 13px;
.ant-typography:first-child {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.ant-typography:last-child,
div .ant-typography {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-weight: 400;
line-height: 18px;
}
}
}
.ant-collapse-content-box {
padding: 12px 20px 12px 38px;
}
.ant-collapse-content-active {
border-top: none;
}
.ant-collapse-item {
border: none;
}
}
}
}
.create-policy-modal {
.ant-modal-content {
padding: 16px;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
background-color: var(--bg-ink-400);
.ant-typography {
margin: 0;
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
}
.ant-modal-body {
.ant-divider {
margin: 16px 0;
border: 0.5px solid var(--bg-slate-500);
}
}
}
.create-policy-container {
display: flex;
flex-direction: column;
margin-bottom: 32px;
.input-group {
display: flex;
flex-direction: column;
gap: 4px;
.ant-typography {
color: var(--bg-vanilla-500);
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 20px;
padding-bottom: 6px;
}
.ant-input {
width: 100%;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
.ant-select {
width: 100%;
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
&.ant-select-focused .ant-select-selector {
border-color: var(--bg-slate-400) !important;
box-shadow: none !important;
}
&:hover .ant-select-selector {
border-color: var(--bg-slate-400) !important;
}
}
}
}
.create-policy-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
.ant-btn {
border-radius: 2px;
}
}
}
.delete-policy-modal {
width: calc(100% - 30px) !important; /* Adjust the 20px as needed */
max-width: 384px;
.ant-modal-content {
padding: 0;
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-header {
padding: 16px;
background: var(--bg-ink-400);
}
.ant-modal-body {
padding: 0px 16px 28px 16px;
.ant-typography {
color: var(--bg-vanilla-400);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-normal);
line-height: 20px;
letter-spacing: -0.07px;
}
.save-view-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.ant-color-picker-trigger {
padding: 6px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
width: 32px;
height: 32px;
.ant-color-picker-color-block {
border-radius: 50px;
width: 16px;
height: 16px;
flex-shrink: 0;
.ant-color-picker-color-block-inner {
display: flex;
justify-content: center;
align-items: center;
}
}
}
}
.ant-modal-footer {
display: flex;
justify-content: flex-end;
padding: 16px 16px;
margin: 0;
.cancel-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-slate-500);
}
.delete-btn {
display: flex;
align-items: center;
border: none;
border-radius: 2px;
background: var(--bg-cherry-500);
margin-left: 12px;
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
}
}
.title {
color: var(--bg-vanilla-100);
font-size: var(--font-size-sm);
font-style: normal;
font-weight: var(--font-weight-medium);
line-height: 20px; /* 142.857% */
}
}
.lightMode {
.routing-policies-container {
.routing-policies-content {
.title {
color: var(--bg-ink-500);
}
}
}
.routing-policies-table {
.ant-table-tbody {
.policy-list-item {
border: 1px solid var(--bg-vanilla-300);
background-color: var(--bg-vanilla-100);
.policy-list-item-header {
.ant-typography {
color: var(--bg-slate-400);
}
}
.policy-list-item-content {
.policy-list-item-content-row {
.ant-typography:first-child {
color: var(--bg-slate-400);
}
.ant-typography:last-child,
div .ant-typography {
color: var(--bg-slate-100);
}
}
}
}
}
}
.create-policy-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-modal-header {
background-color: var(--bg-vanilla-100);
.ant-typography {
color: var(--bg-slate-100);
}
}
.ant-modal-body {
.ant-divider {
border: 0.5px solid var(--bg-vanilla-300);
}
}
}
.create-policy-container {
.input-group {
.ant-typography {
color: var(--bg-slate-100);
}
.ant-input {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
.ant-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
&.ant-select-focused .ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
box-shadow: none !important;
}
&:hover .ant-select-selector {
border-color: var(--bg-vanilla-300) !important;
}
}
}
}
}
.delete-policy-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.ant-modal-header {
background: var(--bg-vanilla-100);
}
.ant-modal-body {
.ant-typography {
color: var(--bg-slate-400);
}
}
.ant-modal-footer {
.cancel-btn {
background: var(--bg-vanilla-300);
color: var(--bg-slate-100);
}
.delete-btn {
background: var(--bg-cherry-500);
color: var(--bg-vanilla-100);
}
.delete-btn:hover {
color: var(--bg-vanilla-100);
background: var(--bg-cherry-600);
}
}
}
.title {
color: var(--bg-slate-100);
}
}
}

View File

@ -0,0 +1,115 @@
import { Channels } from 'types/api/channels/getAll';
export interface RoutingPolicy {
id: string;
name: string;
expression: string;
channels: string[];
description: string | undefined;
createdAt: string | undefined;
updatedAt: string | undefined;
createdBy: string | undefined;
updatedBy: string | undefined;
}
type HandlePolicyDetailsModalOpen = (
mode: PolicyDetailsModalMode,
routingPolicy: RoutingPolicy | null,
) => void;
type HandlePolicyDetailsModalAction = (
mode: PolicyDetailsModalMode,
routingPolicyData: {
name: string;
expression: string;
channels: string[];
description: string;
},
) => void;
type HandleDeleteModalOpen = (routingPolicy: RoutingPolicy) => void;
export type PolicyDetailsModalMode = 'create' | 'edit' | null;
export interface RoutingPolicyListProps {
routingPolicies: RoutingPolicy[];
isRoutingPoliciesLoading: boolean;
isRoutingPoliciesError: boolean;
handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen;
handleDeleteModalOpen: HandleDeleteModalOpen;
}
export interface RoutingPolicyListItemProps {
routingPolicy: RoutingPolicy;
handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen;
handleDeleteModalOpen: HandleDeleteModalOpen;
}
export interface PolicyListItemHeaderProps {
name: string;
handleEdit: () => void;
handleDelete: () => void;
}
export interface PolicyListItemContentProps {
routingPolicy: RoutingPolicy;
}
export interface RoutingPolicyDetailsProps {
routingPolicy: RoutingPolicy | null;
closeModal: () => void;
mode: PolicyDetailsModalMode;
channels: Channels[];
isErrorChannels: boolean;
isLoadingChannels: boolean;
handlePolicyDetailsModalAction: HandlePolicyDetailsModalAction;
isPolicyDetailsModalActionLoading: boolean;
refreshChannels: () => void;
}
export interface DeleteRoutingPolicyProps {
routingPolicy: RoutingPolicy | null;
isDeletingRoutingPolicy: boolean;
handleDelete: () => void;
handleClose: () => void;
}
export interface UseRoutingPoliciesReturn {
// Routing Policies
selectedRoutingPolicy: RoutingPolicy | null;
routingPoliciesData: RoutingPolicy[];
isLoadingRoutingPolicies: boolean;
isErrorRoutingPolicies: boolean;
// Channels
channels: Channels[];
isLoadingChannels: boolean;
isErrorChannels: boolean;
refreshChannels: () => void;
// Search
searchTerm: string;
setSearchTerm: (searchTerm: string) => void;
// Delete Modal
isDeleteModalOpen: boolean;
handleDeleteModalOpen: (routingPolicy: RoutingPolicy) => void;
handleDeleteModalClose: () => void;
handleDeleteRoutingPolicy: () => void;
isDeletingRoutingPolicy: boolean;
// Policy Details Modal
policyDetailsModalState: PolicyDetailsModalState;
handlePolicyDetailsModalClose: () => void;
handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen;
handlePolicyDetailsModalAction: HandlePolicyDetailsModalAction;
isPolicyDetailsModalActionLoading: boolean;
}
export interface PolicyDetailsModalState {
mode: PolicyDetailsModalMode;
isOpen: boolean;
}
export interface RoutingPolicyDetailsFormState {
name: string;
expression: string;
channels: string[];
description: string;
}

View File

@ -0,0 +1,240 @@
import './styles.scss';
import { toast } from '@signozhq/sonner';
import getAllChannels from 'api/channels/getAll';
import { GetRoutingPoliciesResponse } from 'api/routingPolicies/getRoutingPolicies';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useCreateRoutingPolicy } from 'hooks/routingPolicies/useCreateRoutingPolicy';
import { useDeleteRoutingPolicy } from 'hooks/routingPolicies/useDeleteRoutingPolicy';
import { useGetRoutingPolicies } from 'hooks/routingPolicies/useGetRoutingPolicies';
import { useUpdateRoutingPolicy } from 'hooks/routingPolicies/useUpdateRoutingPolicy';
import { useMemo, useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import {
PolicyDetailsModalMode,
PolicyDetailsModalState,
RoutingPolicy,
UseRoutingPoliciesReturn,
} from './types';
import {
mapApiResponseToRoutingPolicies,
mapRoutingPolicyToCreateApiPayload,
mapRoutingPolicyToUpdateApiPayload,
} from './utils';
function useRoutingPolicies(): UseRoutingPoliciesReturn {
const queryClient = useQueryClient();
// Local state
const [searchTerm, setSearchTerm] = useState('');
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [
policyDetailsModalState,
setPolicyDetailsModalState,
] = useState<PolicyDetailsModalState>({
mode: null,
isOpen: false,
});
const [
selectedRoutingPolicy,
setSelectedRoutingPolicy,
] = useState<RoutingPolicy | null>(null);
// Routing Policies list
const {
data: routingPolicies,
isLoading: isLoadingRoutingPolicies,
isError: isErrorRoutingPolicies,
} = useGetRoutingPolicies();
const routingPoliciesData = useMemo(() => {
const unfilteredRoutingPolicies = mapApiResponseToRoutingPolicies(
routingPolicies as SuccessResponseV2<GetRoutingPoliciesResponse>,
);
return unfilteredRoutingPolicies.filter((routingPolicy) =>
routingPolicy.name.toLowerCase().includes(searchTerm.toLowerCase()),
);
}, [routingPolicies, searchTerm]);
// Channels list
const {
data,
isLoading: isLoadingChannels,
isError: isErrorChannels,
refetch: refetchChannels,
} = useQuery<SuccessResponseV2<Channels[]>, APIError>(['getChannels'], {
queryFn: () => getAllChannels(),
});
const channels = data?.data || [];
const refreshChannels = (): void => {
refetchChannels();
};
// Handlers
const handlePolicyDetailsModalOpen = (
mode: PolicyDetailsModalMode,
routingPolicy: RoutingPolicy | null,
): void => {
if (routingPolicy) {
setSelectedRoutingPolicy(routingPolicy);
}
setPolicyDetailsModalState({
isOpen: true,
mode,
});
};
const handlePolicyDetailsModalClose = (): void => {
setSelectedRoutingPolicy(null);
setPolicyDetailsModalState({
isOpen: false,
mode: null,
});
};
const handleDeleteModalOpen = (routingPolicy: RoutingPolicy): void => {
setSelectedRoutingPolicy(routingPolicy);
setIsDeleteModalOpen(true);
};
const handleDeleteModalClose = (): void => {
setSelectedRoutingPolicy(null);
setIsDeleteModalOpen(false);
};
// Create Routing Policy
const {
mutate: createRoutingPolicy,
isLoading: isCreating,
} = useCreateRoutingPolicy();
// Update Routing Policy
const {
mutate: updateRoutingPolicy,
isLoading: isUpdating,
} = useUpdateRoutingPolicy();
// Policy Details Modal Action (Create or Update)
const handlePolicyDetailsModalAction = (
mode: PolicyDetailsModalMode,
routingPolicyData: {
name: string;
expression: string;
channels: string[];
description: string;
},
): void => {
if (mode === 'create') {
createRoutingPolicy(
{
payload: mapRoutingPolicyToCreateApiPayload(
routingPolicyData.name,
routingPolicyData.expression,
routingPolicyData.channels,
routingPolicyData.description,
),
},
{
onSuccess: () => {
toast.success('Routing policy created successfully');
queryClient.invalidateQueries(REACT_QUERY_KEY.GET_ROUTING_POLICIES);
handlePolicyDetailsModalClose();
},
onError: (error) => {
toast.error(`Error: ${error.message}`);
},
},
);
} else if (mode === 'edit' && selectedRoutingPolicy) {
updateRoutingPolicy(
{
id: selectedRoutingPolicy.id,
payload: mapRoutingPolicyToUpdateApiPayload(
routingPolicyData.name,
routingPolicyData.expression,
routingPolicyData.channels,
routingPolicyData.description,
),
},
{
onSuccess: () => {
toast.success('Routing policy updated successfully');
queryClient.invalidateQueries(REACT_QUERY_KEY.GET_ROUTING_POLICIES);
handlePolicyDetailsModalClose();
},
onError: () => {
toast.error('Failed to update routing policy');
},
},
);
}
};
// Policy Details Modal Action Loading (Creating or Updating)
const isPolicyDetailsModalActionLoading = useMemo(() => {
if (policyDetailsModalState.mode === 'create') {
return isCreating;
}
if (policyDetailsModalState.mode === 'edit') {
return isUpdating;
}
return false;
}, [policyDetailsModalState.mode, isCreating, isUpdating]);
// Delete Routing Policy
const {
mutate: deleteRoutingPolicy,
isLoading: isDeletingRoutingPolicy,
} = useDeleteRoutingPolicy();
const handleDeleteRoutingPolicy = (): void => {
if (!selectedRoutingPolicy) {
return;
}
deleteRoutingPolicy(selectedRoutingPolicy.id, {
onSuccess: () => {
toast.success('Routing policy deleted successfully');
queryClient.invalidateQueries(REACT_QUERY_KEY.GET_ROUTING_POLICIES);
handleDeleteModalClose();
},
onError: () => {
toast.error('Failed to delete routing policy');
},
});
};
return {
// 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,
isPolicyDetailsModalActionLoading,
handlePolicyDetailsModalAction,
handlePolicyDetailsModalOpen,
handlePolicyDetailsModalClose,
};
}
export default useRoutingPolicies;

View File

@ -0,0 +1,61 @@
import { CreateRoutingPolicyBody } from 'api/routingPolicies/createRoutingPolicy';
import { GetRoutingPoliciesResponse } from 'api/routingPolicies/getRoutingPolicies';
import { UpdateRoutingPolicyBody } from 'api/routingPolicies/updateRoutingPolicy';
import { SuccessResponseV2 } from 'types/api';
import { RoutingPolicy } from './types';
export function showRoutingPoliciesPage(): boolean {
return localStorage.getItem('showRoutingPoliciesPage') === 'true';
}
export function mapApiResponseToRoutingPolicies(
response: SuccessResponseV2<GetRoutingPoliciesResponse>,
): RoutingPolicy[] {
return (
response?.data?.data?.map((policyData) => ({
id: policyData.id,
name: policyData.name,
expression: policyData.expression,
description: policyData.description,
channels: policyData.channels,
createdAt: policyData.createdAt,
updatedAt: policyData.updatedAt,
createdBy: policyData.createdBy,
updatedBy: policyData.updatedBy,
})) || []
);
}
export function mapRoutingPolicyToCreateApiPayload(
name: string,
expression: string,
channels: string[],
description: string,
): CreateRoutingPolicyBody {
return {
name,
expression,
actions: {
channels,
},
description,
};
}
// eslint-disable-next-line sonarjs/no-identical-functions
export function mapRoutingPolicyToUpdateApiPayload(
name: string,
expression: string,
channels: string[],
description: string,
): UpdateRoutingPolicyBody {
return {
name,
expression,
actions: {
channels,
},
description,
};
}

View File

@ -0,0 +1,24 @@
import createRoutingPolicy, {
CreateRoutingPolicyBody,
CreateRoutingPolicyResponse,
} from 'api/routingPolicies/createRoutingPolicy';
import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponseV2, SuccessResponseV2 } from 'types/api';
interface UseCreateRoutingPolicyProps {
payload: CreateRoutingPolicyBody;
}
export function useCreateRoutingPolicy(): UseMutationResult<
SuccessResponseV2<CreateRoutingPolicyResponse> | ErrorResponseV2,
Error,
UseCreateRoutingPolicyProps
> {
return useMutation<
SuccessResponseV2<CreateRoutingPolicyResponse> | ErrorResponseV2,
Error,
UseCreateRoutingPolicyProps
>({
mutationFn: ({ payload }) => createRoutingPolicy(payload),
});
}

View File

@ -0,0 +1,19 @@
import deleteRoutingPolicy, {
DeleteRoutingPolicyResponse,
} from 'api/routingPolicies/deleteRoutingPolicy';
import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponseV2, SuccessResponseV2 } from 'types/api';
export function useDeleteRoutingPolicy(): UseMutationResult<
SuccessResponseV2<DeleteRoutingPolicyResponse> | ErrorResponseV2,
Error,
string
> {
return useMutation<
SuccessResponseV2<DeleteRoutingPolicyResponse> | ErrorResponseV2,
Error,
string
>({
mutationFn: (policyId) => deleteRoutingPolicy(policyId),
});
}

View File

@ -0,0 +1,39 @@
import {
getRoutingPolicies,
GetRoutingPoliciesResponse,
} from 'api/routingPolicies/getRoutingPolicies';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponseV2, SuccessResponseV2 } from 'types/api';
type UseGetRoutingPolicies = (
options?: UseQueryOptions<
SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2,
Error
>,
headers?: Record<string, string>,
) => UseQueryResult<
SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2,
Error
>;
export const useGetRoutingPolicies: UseGetRoutingPolicies = (
options,
headers,
) => {
const queryKey = useMemo(
() => options?.queryKey || [REACT_QUERY_KEY.GET_ROUTING_POLICIES],
[options?.queryKey],
);
return useQuery<
SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2,
Error
>({
queryFn: ({ signal }) => getRoutingPolicies(signal, headers),
...options,
queryKey,
});
};

View File

@ -0,0 +1,25 @@
import updateRoutingPolicy, {
UpdateRoutingPolicyBody,
UpdateRoutingPolicyResponse,
} from 'api/routingPolicies/updateRoutingPolicy';
import { useMutation, UseMutationResult } from 'react-query';
import { ErrorResponseV2, SuccessResponseV2 } from 'types/api';
interface UseUpdateRoutingPolicyProps {
id: string;
payload: UpdateRoutingPolicyBody;
}
export function useUpdateRoutingPolicy(): UseMutationResult<
SuccessResponseV2<UpdateRoutingPolicyResponse> | ErrorResponseV2,
Error,
UseUpdateRoutingPolicyProps
> {
return useMutation<
SuccessResponseV2<UpdateRoutingPolicyResponse> | ErrorResponseV2,
Error,
UseUpdateRoutingPolicyProps
>({
mutationFn: ({ id, payload }) => updateRoutingPolicy(id, payload),
});
}

View File

@ -2,4 +2,14 @@
.ant-tabs-nav { .ant-tabs-nav {
padding: 0 8px; padding: 0 8px;
} }
.configuration-tabs {
margin-top: -16px;
.ant-tabs-nav {
.ant-tabs-nav-wrap {
padding: 0 8px;
}
}
}
} }

View File

@ -7,11 +7,14 @@ import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import AllAlertRules from 'container/ListAlertRules'; import AllAlertRules from 'container/ListAlertRules';
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime'; import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
import RoutingPolicies from 'container/RoutingPolicies';
import { showRoutingPoliciesPage } from 'container/RoutingPolicies/utils';
import TriggeredAlerts from 'container/TriggeredAlerts'; import TriggeredAlerts from 'container/TriggeredAlerts';
import { useSafeNavigate } from 'hooks/useSafeNavigate'; import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { GalleryVerticalEnd, Pyramid } from 'lucide-react'; import { GalleryVerticalEnd, Pyramid } from 'lucide-react';
import AlertDetails from 'pages/AlertDetails'; import AlertDetails from 'pages/AlertDetails';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
function AllAlertList(): JSX.Element { function AllAlertList(): JSX.Element {
@ -25,6 +28,37 @@ function AllAlertList(): JSX.Element {
const search = urlQuery.get('search'); const search = urlQuery.get('search');
const showRoutingPoliciesPageFlag = showRoutingPoliciesPage();
const configurationTab = useMemo(() => {
if (showRoutingPoliciesPageFlag) {
const tabs = [
{
label: 'Planned Downtime',
key: 'planned-downtime',
children: <PlannedDowntime />,
},
{
label: 'Routing Policies',
key: 'routing-policies',
children: <RoutingPolicies />,
},
];
return (
<Tabs
className="configuration-tabs"
defaultActiveKey="planned-downtime"
items={tabs}
/>
);
}
return (
<div className="planned-downtime-container">
<PlannedDowntime />
</div>
);
}, [showRoutingPoliciesPageFlag]);
const items: TabsProps['items'] = [ const items: TabsProps['items'] = [
{ {
label: ( label: (
@ -58,11 +92,7 @@ function AllAlertList(): JSX.Element {
</div> </div>
), ),
key: 'Configuration', key: 'Configuration',
children: ( children: configurationTab,
<div className="planned-downtime-container">
<PlannedDowntime />
</div>
),
}, },
]; ];