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,