mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-17 07:26:20 +00:00
chore: add routing polices page (#9198)
This commit is contained in:
parent
735b90722d
commit
411414fa45
36
frontend/src/api/routingPolicies/createRoutingPolicy.ts
Normal file
36
frontend/src/api/routingPolicies/createRoutingPolicy.ts
Normal 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;
|
||||
30
frontend/src/api/routingPolicies/deleteRoutingPolicy.ts
Normal file
30
frontend/src/api/routingPolicies/deleteRoutingPolicy.ts
Normal 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;
|
||||
40
frontend/src/api/routingPolicies/getRoutingPolicies.ts
Normal file
40
frontend/src/api/routingPolicies/getRoutingPolicies.ts
Normal 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>);
|
||||
}
|
||||
};
|
||||
40
frontend/src/api/routingPolicies/updateRoutingPolicy.ts
Normal file
40
frontend/src/api/routingPolicies/updateRoutingPolicy.ts
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
118
frontend/src/container/RoutingPolicies/RoutingPolicies.tsx
Normal file
118
frontend/src/container/RoutingPolicies/RoutingPolicies.tsx
Normal 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;
|
||||
208
frontend/src/container/RoutingPolicies/RoutingPolicyDetails.tsx
Normal file
208
frontend/src/container/RoutingPolicies/RoutingPolicyDetails.tsx
Normal 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;
|
||||
73
frontend/src/container/RoutingPolicies/RoutingPolicyList.tsx
Normal file
73
frontend/src/container/RoutingPolicies/RoutingPolicyList.tsx
Normal 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;
|
||||
137
frontend/src/container/RoutingPolicies/RoutingPolicyListItem.tsx
Normal file
137
frontend/src/container/RoutingPolicies/RoutingPolicyListItem.tsx
Normal 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;
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -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('-');
|
||||
});
|
||||
});
|
||||
121
frontend/src/container/RoutingPolicies/__tests__/testUtils.ts
Normal file
121
frontend/src/container/RoutingPolicies/__tests__/testUtils.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
8
frontend/src/container/RoutingPolicies/constants.ts
Normal file
8
frontend/src/container/RoutingPolicies/constants.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { RoutingPolicyDetailsFormState } from './types';
|
||||
|
||||
export const INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE: RoutingPolicyDetailsFormState = {
|
||||
name: '',
|
||||
expression: '',
|
||||
channels: [],
|
||||
description: '',
|
||||
};
|
||||
3
frontend/src/container/RoutingPolicies/index.ts
Normal file
3
frontend/src/container/RoutingPolicies/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import RoutingPolicies from './RoutingPolicies';
|
||||
|
||||
export default RoutingPolicies;
|
||||
452
frontend/src/container/RoutingPolicies/styles.scss
Normal file
452
frontend/src/container/RoutingPolicies/styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
115
frontend/src/container/RoutingPolicies/types.ts
Normal file
115
frontend/src/container/RoutingPolicies/types.ts
Normal 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;
|
||||
}
|
||||
240
frontend/src/container/RoutingPolicies/useRoutingPolicies.ts
Normal file
240
frontend/src/container/RoutingPolicies/useRoutingPolicies.ts
Normal 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;
|
||||
61
frontend/src/container/RoutingPolicies/utils.tsx
Normal file
61
frontend/src/container/RoutingPolicies/utils.tsx
Normal 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,
|
||||
};
|
||||
}
|
||||
24
frontend/src/hooks/routingPolicies/useCreateRoutingPolicy.ts
Normal file
24
frontend/src/hooks/routingPolicies/useCreateRoutingPolicy.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
19
frontend/src/hooks/routingPolicies/useDeleteRoutingPolicy.ts
Normal file
19
frontend/src/hooks/routingPolicies/useDeleteRoutingPolicy.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
39
frontend/src/hooks/routingPolicies/useGetRoutingPolicies.ts
Normal file
39
frontend/src/hooks/routingPolicies/useGetRoutingPolicies.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
25
frontend/src/hooks/routingPolicies/useUpdateRoutingPolicy.ts
Normal file
25
frontend/src/hooks/routingPolicies/useUpdateRoutingPolicy.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
@ -2,4 +2,14 @@
|
||||
.ant-tabs-nav {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.configuration-tabs {
|
||||
margin-top: -16px;
|
||||
|
||||
.ant-tabs-nav {
|
||||
.ant-tabs-nav-wrap {
|
||||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,11 +7,14 @@ import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection
|
||||
import ROUTES from 'constants/routes';
|
||||
import AllAlertRules from 'container/ListAlertRules';
|
||||
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
|
||||
import RoutingPolicies from 'container/RoutingPolicies';
|
||||
import { showRoutingPoliciesPage } from 'container/RoutingPolicies/utils';
|
||||
import TriggeredAlerts from 'container/TriggeredAlerts';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { GalleryVerticalEnd, Pyramid } from 'lucide-react';
|
||||
import AlertDetails from 'pages/AlertDetails';
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
function AllAlertList(): JSX.Element {
|
||||
@ -25,6 +28,37 @@ function AllAlertList(): JSX.Element {
|
||||
|
||||
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'] = [
|
||||
{
|
||||
label: (
|
||||
@ -58,11 +92,7 @@ function AllAlertList(): JSX.Element {
|
||||
</div>
|
||||
),
|
||||
key: 'Configuration',
|
||||
children: (
|
||||
<div className="planned-downtime-container">
|
||||
<PlannedDowntime />
|
||||
</div>
|
||||
),
|
||||
children: configurationTab,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user