diff --git a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx
index b87a5abc7a..5ee98249f6 100644
--- a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx
+++ b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx
@@ -1,3 +1,4 @@
+/* eslint-disable no-nested-ternary */
import '../MessagingQueues.styles.scss';
import { Select, Typography } from 'antd';
@@ -15,6 +16,7 @@ import {
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
+import DropRateView from '../MQDetails/DropRateView/DropRateView';
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
@@ -103,26 +105,28 @@ function MQDetailPage(): JSX.Element {
-
-
- {selectedView === MessagingQueuesViewType.consumerLag.value ? (
+ {selectedView === MessagingQueuesViewType.consumerLag.value ? (
+
+
- ) : (
-
- )}
-
-
- {selectedView !== MessagingQueuesViewType.dropRate.value && (
+
+ ) : selectedView === MessagingQueuesViewType.dropRate.value ? (
+
+ ) : (
+
+ )}
+ {selectedView !== MessagingQueuesViewType.dropRate.value && (
+
- )}
-
+
+ )}
);
}
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/DropRateView.styles.scss b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/DropRateView.styles.scss
new file mode 100644
index 0000000000..b36a46a8ec
--- /dev/null
+++ b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/DropRateView.styles.scss
@@ -0,0 +1,30 @@
+.evaluation-time-selector {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .eval-title {
+ font-family: Inter;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 28px;
+ color: var(--bg-vanilla-200);
+ }
+
+ .ant-selector {
+ background-color: var(--bg-ink-400);
+ border-radius: 4px;
+ border: 1px solid var(--bg-slate-400);
+ box-shadow: none;
+ }
+}
+
+.select-dropdown-render {
+ padding: 8px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 200px;
+ margin: 6px;
+}
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/DropRateView.tsx b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/DropRateView.tsx
new file mode 100644
index 0000000000..28d9f65d44
--- /dev/null
+++ b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/DropRateView.tsx
@@ -0,0 +1,251 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+import '../MQDetails.style.scss';
+
+import { Table, Typography } from 'antd';
+import axios from 'axios';
+import cx from 'classnames';
+import { SOMETHING_WENT_WRONG } from 'constants/api';
+import ROUTES from 'constants/routes';
+import { useNotifications } from 'hooks/useNotifications';
+import { isNumber } from 'lodash-es';
+import {
+ convertToTitleCase,
+ MessagingQueuesViewType,
+ RowData,
+} from 'pages/MessagingQueues/MessagingQueuesUtils';
+import { useEffect, useMemo, useState } from 'react';
+import { useMutation } from 'react-query';
+import { useSelector } from 'react-redux';
+import { AppState } from 'store/reducers';
+import { GlobalReducer } from 'types/reducer/globalTime';
+
+import { MessagingQueueServicePayload } from '../MQTables/getConsumerLagDetails';
+import { getKafkaSpanEval } from '../MQTables/getKafkaSpanEval';
+import {
+ convertToMilliseconds,
+ DropRateAPIResponse,
+ DropRateResponse,
+} from './dropRateViewUtils';
+import EvaluationTimeSelector from './EvaluationTimeSelector';
+
+export function getTableData(data: DropRateResponse[]): RowData[] {
+ if (data?.length === 0) {
+ return [];
+ }
+
+ const tableData: RowData[] =
+ data?.map(
+ (row: DropRateResponse, index: number): RowData => ({
+ ...(row.data as any), // todo-sagar
+ key: index,
+ }),
+ ) || [];
+
+ return tableData;
+}
+
+// eslint-disable-next-line sonarjs/cognitive-complexity
+export function getColumns(
+ data: DropRateResponse[],
+ visibleCounts: Record,
+ handleShowMore: (index: number) => void,
+): any[] {
+ if (data?.length === 0) {
+ return [];
+ }
+
+ const columnsOrder = [
+ 'producer_service',
+ 'consumer_service',
+ 'breach_percentage',
+ 'top_traceIDs',
+ 'breached_spans',
+ 'total_spans',
+ ];
+
+ const columns: {
+ title: string;
+ dataIndex: string;
+ key: string;
+ }[] = columnsOrder.map((column) => ({
+ title: convertToTitleCase(column),
+ dataIndex: column,
+ key: column,
+ render: (
+ text: string | string[],
+ _record: any,
+ index: number,
+ ): JSX.Element => {
+ if (Array.isArray(text)) {
+ const visibleCount = visibleCounts[index] || 4;
+ const visibleItems = text.slice(0, visibleCount);
+ const remainingCount = (text || []).length - visibleCount;
+
+ return (
+
+
+ {visibleItems.map((item, idx) => {
+ const shouldShowMore = remainingCount > 0 && idx === visibleCount - 1;
+ return (
+
+ {
+ window.open(`${ROUTES.TRACE}/${item}`, '_blank');
+ }}
+ >
+ {item}
+
+ {shouldShowMore && (
+ handleShowMore(index)}
+ className="remaing-count"
+ >
+ + {remainingCount} more
+
+ )}
+
+ );
+ })}
+
+
+ );
+ }
+
+ if (column === 'consumer_service' || column === 'producer_service') {
+ return (
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ window.open(`/services/${encodeURIComponent(text)}`, '_blank');
+ }}
+ >
+ {text}
+
+ );
+ }
+
+ if (column === 'breach_percentage' && text) {
+ if (!isNumber(text))
+ return {text.toString()};
+ return (
+
+ {(typeof text === 'string' ? parseFloat(text) : text).toFixed(2)} %
+
+ );
+ }
+
+ return {text};
+ },
+ }));
+
+ return columns;
+}
+
+const showPaginationItem = (total: number, range: number[]): JSX.Element => (
+ <>
+
+ {range[0]} — {range[1]}
+
+ of {total}
+ >
+);
+
+function DropRateView(): JSX.Element {
+ const [columns, setColumns] = useState([]);
+ const [tableData, setTableData] = useState([]);
+ const { notifications } = useNotifications();
+ const { maxTime, minTime } = useSelector(
+ (state) => state.globalTime,
+ );
+ const [data, setData] = useState<
+ DropRateAPIResponse['data']['result'][0]['list']
+ >([]);
+ const [interval, setInterval] = useState('');
+
+ const [visibleCounts, setVisibleCounts] = useState>({});
+
+ const paginationConfig = useMemo(
+ () =>
+ tableData?.length > 10 && {
+ pageSize: 10,
+ showTotal: showPaginationItem,
+ showSizeChanger: false,
+ hideOnSinglePage: true,
+ },
+ [tableData],
+ );
+
+ const evaluationTime = useMemo(() => convertToMilliseconds(interval), [
+ interval,
+ ]);
+ const tableApiPayload: MessagingQueueServicePayload = useMemo(
+ () => ({
+ start: minTime,
+ end: maxTime,
+ evalTime: evaluationTime * 1e6,
+ }),
+ [evaluationTime, maxTime, minTime],
+ );
+
+ const handleOnError = (error: Error): void => {
+ notifications.error({
+ message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
+ });
+ };
+
+ const handleShowMore = (index: number): void => {
+ setVisibleCounts((prevCounts) => ({
+ ...prevCounts,
+ [index]: (prevCounts[index] || 4) + 4,
+ }));
+ };
+
+ const { mutate: getViewDetails, isLoading } = useMutation(getKafkaSpanEval, {
+ onSuccess: (data) => {
+ if (data.payload) {
+ setData(data.payload.result[0].list);
+ }
+ },
+ onError: handleOnError,
+ });
+
+ useEffect(() => {
+ if (data?.length > 0) {
+ setColumns(getColumns(data, visibleCounts, handleShowMore));
+ setTableData(getTableData(data));
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data, visibleCounts]);
+
+ useEffect(() => {
+ if (evaluationTime) {
+ getViewDetails(tableApiPayload);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [minTime, maxTime, evaluationTime]);
+
+ return (
+
+
+
+ {MessagingQueuesViewType.dropRate.label}
+
+
+
+
+
+ );
+}
+
+export default DropRateView;
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/EvaluationTimeSelector.tsx b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/EvaluationTimeSelector.tsx
new file mode 100644
index 0000000000..2ca2e9c301
--- /dev/null
+++ b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/EvaluationTimeSelector.tsx
@@ -0,0 +1,111 @@
+import './DropRateView.styles.scss';
+
+import { Input, Select, Typography } from 'antd';
+import { Dispatch, SetStateAction, useEffect, useState } from 'react';
+
+const { Option } = Select;
+
+interface SelectDropdownRenderProps {
+ menu: React.ReactNode;
+ inputValue: string;
+ handleInputChange: (e: React.ChangeEvent) => void;
+ handleKeyDown: (e: React.KeyboardEvent) => void;
+ handleAddCustomValue: () => void;
+}
+
+function SelectDropdownRender({
+ menu,
+ inputValue,
+ handleInputChange,
+ handleAddCustomValue,
+ handleKeyDown,
+}: SelectDropdownRenderProps): JSX.Element {
+ return (
+ <>
+ {menu}
+
+ >
+ );
+}
+
+function EvaluationTimeSelector({
+ setInterval,
+}: {
+ setInterval: Dispatch>;
+}): JSX.Element {
+ const [inputValue, setInputValue] = useState('');
+ const [selectedInterval, setSelectedInterval] = useState('5ms');
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+
+ const handleInputChange = (e: React.ChangeEvent): void => {
+ setInputValue(e.target.value);
+ };
+
+ const handleSelectChange = (value: string): void => {
+ setSelectedInterval(value);
+ setInputValue('');
+ setDropdownOpen(false);
+ };
+
+ const handleAddCustomValue = (): void => {
+ setSelectedInterval(inputValue);
+ setInputValue(inputValue);
+ setDropdownOpen(false);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent): void => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ e.stopPropagation();
+ handleAddCustomValue();
+ }
+ };
+
+ const renderDropdown = (menu: React.ReactNode): JSX.Element => (
+
+ );
+
+ useEffect(() => {
+ if (selectedInterval) {
+ setInterval(() => selectedInterval);
+ }
+ }, [selectedInterval, setInterval]);
+
+ return (
+
+
+ Evaluation Interval:
+
+
+
+ );
+}
+
+export default EvaluationTimeSelector;
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/dropRateViewUtils.ts b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/dropRateViewUtils.ts
new file mode 100644
index 0000000000..49d751e722
--- /dev/null
+++ b/frontend/src/pages/MessagingQueues/MQDetails/DropRateView/dropRateViewUtils.ts
@@ -0,0 +1,46 @@
+export function convertToMilliseconds(timeInput: string): number {
+ if (!timeInput.trim()) {
+ return 0;
+ }
+
+ const match = timeInput.match(/^(\d+)(ms|s|ns)?$/); // Match number and optional unit
+ if (!match) {
+ throw new Error(`Invalid time format: ${timeInput}`);
+ }
+
+ const value = parseInt(match[1], 10);
+ const unit = match[2] || 'ms'; // Default to 'ms' if no unit is provided
+
+ switch (unit) {
+ case 's':
+ return value * 1e3;
+ case 'ms':
+ return value;
+ case 'ns':
+ return value / 1e6;
+ default:
+ throw new Error('Invalid time format');
+ }
+}
+
+export interface DropRateResponse {
+ timestamp: string;
+ data: {
+ breach_percentage: number;
+ breached_spans: number;
+ consumer_service: string;
+ producer_service: string;
+ top_traceIDs: string[];
+ total_spans: number;
+ };
+}
+export interface DropRateAPIResponse {
+ status: string;
+ data: {
+ resultType: string;
+ result: {
+ queryName: string;
+ list: DropRateResponse[];
+ }[];
+ };
+}
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss
index c4995a1812..5a746bbcae 100644
--- a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss
+++ b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss
@@ -17,13 +17,20 @@
background: var(--bg-ink-500);
.mq-overview-title {
- color: var(--bg-vanilla-200);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
- font-family: Inter;
- font-size: 18px;
- font-style: normal;
- font-weight: 500;
- line-height: 28px;
+ .drop-rat-title {
+ color: var(--bg-vanilla-200);
+
+ font-family: Inter;
+ font-size: 18px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 28px;
+ }
}
.mq-details-options {
@@ -43,3 +50,69 @@
}
}
}
+
+.droprate-view {
+ .mq-table {
+ width: 100%;
+
+ .ant-table-content {
+ border-radius: 6px;
+ border: 1px solid var(--bg-slate-500);
+ box-shadow: 0px 4px 12px 0px rgba(0, 0, 0, 0.1);
+ }
+
+ .ant-table-tbody {
+ .ant-table-cell {
+ max-width: 250px;
+ border-bottom: none;
+ }
+ }
+
+ .ant-table-thead {
+ .ant-table-cell {
+ background-color: var(--bg-ink-500);
+ border-bottom: 1px solid var(--bg-slate-500);
+ }
+ }
+ }
+
+ .trace-id-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ width: max-content;
+
+ .traceid-style {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+
+ .traceid-text {
+ border-radius: 2px;
+ border: 1px solid var(--bg-slate-400);
+ background: var(--bg-slate-400);
+ padding: 2px;
+ cursor: pointer;
+ }
+
+ .remaing-count {
+ cursor: pointer;
+ color: var(--bg-vanilla-100);
+ font-family: Inter;
+ font-size: 12px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ letter-spacing: -0.06px;
+ }
+ }
+ }
+}
+
+.pagination-left {
+ &.mq-table {
+ .ant-pagination {
+ justify-content: flex-start;
+ }
+ }
+}
diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx
index 34ab160553..e1e1791f32 100644
--- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx
+++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx
@@ -249,7 +249,7 @@ function MessagingQueuesTable({
,
-): Promise<
- SuccessResponse | ErrorResponse
-> => {
+): Promise | ErrorResponse> => {
const { start, end, evalTime } = props;
const response = await axios.post(`messaging-queues/kafka/span/evaluation`, {
start,