diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index 2214e9487c..a6e9ee41c4 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -37,4 +37,5 @@ export enum QueryParams { partition = 'partition', selectedTimelineQuery = 'selectedTimelineQuery', ruleType = 'ruleType', + configDetail = 'configDetail', } diff --git a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx index 8fa697f6af..931502b8e1 100644 --- a/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetailPage/MQDetailPage.tsx @@ -5,17 +5,33 @@ import logEvent from 'api/common/logEvent'; import ROUTES from 'constants/routes'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import { ListMinus } from 'lucide-react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useHistory } from 'react-router-dom'; -import { MessagingQueuesViewType } from '../MessagingQueuesUtils'; +import { + MessagingQueuesViewType, + MessagingQueuesViewTypeOptions, + ProducerLatencyOptions, +} from '../MessagingQueuesUtils'; import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon'; +import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview'; import MessagingQueuesDetails from '../MQDetails/MQDetails'; import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions'; import MessagingQueuesGraph from '../MQGraph/MQGraph'; function MQDetailPage(): JSX.Element { const history = useHistory(); + const [ + selectedView, + setSelectedView, + ] = useState( + MessagingQueuesViewType.consumerLag.value, + ); + + const [ + producerLatencyOption, + setproducerLatencyOption, + ] = useState(ProducerLatencyOptions.Producers); useEffect(() => { logEvent('Messaging Queues: Detail page visited', {}); @@ -39,28 +55,19 @@ function MQDetailPage(): JSX.Element { className="messaging-queue-options" defaultValue={MessagingQueuesViewType.consumerLag.value} popupClassName="messaging-queue-options-popup" + onChange={(value): void => setSelectedView(value)} options={[ { label: MessagingQueuesViewType.consumerLag.label, value: MessagingQueuesViewType.consumerLag.value, }, { - label: ( - - ), + label: MessagingQueuesViewType.partitionLatency.label, value: MessagingQueuesViewType.partitionLatency.value, - disabled: true, }, { - label: ( - - ), + label: MessagingQueuesViewType.producerLatency.label, value: MessagingQueuesViewType.producerLatency.value, - disabled: true, }, { label: ( @@ -78,10 +85,21 @@ function MQDetailPage(): JSX.Element {
- + {selectedView === MessagingQueuesViewType.consumerLag.value ? ( + + ) : ( + + )}
- +
); diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss index 68014823da..c4995a1812 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.style.scss @@ -4,3 +4,42 @@ flex-direction: column; gap: 24px; } + +.mq-overview-container { + display: flex; + padding: 24px; + flex-direction: column; + align-items: start; + gap: 16px; + + border-radius: 6px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-500); + + .mq-overview-title { + color: var(--bg-vanilla-200); + + font-family: Inter; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 28px; + } + + .mq-details-options { + letter-spacing: -0.06px; + cursor: pointer; + + .ant-radio-button-wrapper { + border-color: var(--bg-slate-400); + color: var(--bg-vanilla-400); + } + .ant-radio-button-wrapper-checked { + background: var(--bg-slate-400); + color: var(--bg-vanilla-100); + } + .ant-radio-button-wrapper::before { + width: 0px; + } + } +} diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx index df1f643daa..6ec3d45f28 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQDetails.tsx @@ -1,65 +1,227 @@ import './MQDetails.style.scss'; import { Radio } from 'antd'; -import { Dispatch, SetStateAction, useState } from 'react'; +import { QueryParams } from 'constants/query'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { isEmpty } from 'lodash-es'; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; import { ConsumerLagDetailTitle, - ConsumerLagDetailType, + getMetaDataAndAPIPerView, + MessagingQueueServiceDetailType, + MessagingQueuesViewType, + MessagingQueuesViewTypeOptions, + ProducerLatencyOptions, + SelectedTimelineQuery, } from '../MessagingQueuesUtils'; import { ComingSoon } from '../MQCommon/MQCommon'; import MessagingQueuesTable from './MQTables/MQTables'; +const MQServiceDetailTypePerView = ( + producerLatencyOption: ProducerLatencyOptions, +): Record => ({ + [MessagingQueuesViewType.consumerLag.value]: [ + MessagingQueueServiceDetailType.ConsumerDetails, + MessagingQueueServiceDetailType.ProducerDetails, + MessagingQueueServiceDetailType.NetworkLatency, + MessagingQueueServiceDetailType.PartitionHostMetrics, + ], + [MessagingQueuesViewType.partitionLatency.value]: [ + MessagingQueueServiceDetailType.ConsumerDetails, + MessagingQueueServiceDetailType.ProducerDetails, + ], + [MessagingQueuesViewType.producerLatency.value]: [ + producerLatencyOption === ProducerLatencyOptions.Consumers + ? MessagingQueueServiceDetailType.ConsumerDetails + : MessagingQueueServiceDetailType.ProducerDetails, + ], +}); + +interface MessagingQueuesOptionsProps { + currentTab: MessagingQueueServiceDetailType; + setCurrentTab: Dispatch>; + selectedView: MessagingQueuesViewTypeOptions; + producerLatencyOption: ProducerLatencyOptions; +} + function MessagingQueuesOptions({ currentTab, setCurrentTab, -}: { - currentTab: ConsumerLagDetailType; - setCurrentTab: Dispatch>; -}): JSX.Element { - const [option, setOption] = useState(currentTab); + selectedView, + producerLatencyOption, +}: MessagingQueuesOptionsProps): JSX.Element { + const handleChange = (value: MessagingQueueServiceDetailType): void => { + setCurrentTab(value); + }; + + const renderRadioButtons = (): JSX.Element[] => { + const detailTypes = + MQServiceDetailTypePerView(producerLatencyOption)[selectedView] || []; + return detailTypes.map((detailType) => ( + + {ConsumerLagDetailTitle[detailType]} + {detailType === MessagingQueueServiceDetailType.PartitionHostMetrics && ( + + )} + + )); + }; return ( { - setOption(value.target.value); - setCurrentTab(value.target.value); - }} - value={option} + onChange={(e): void => handleChange(e.target.value)} + value={currentTab} className="mq-details-options" > - - {ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]} - - - {ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]} - - - {ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]} - - - {ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]} - - + {renderRadioButtons()} ); } -function MessagingQueuesDetails(): JSX.Element { - const [currentTab, setCurrentTab] = useState( - ConsumerLagDetailType.ConsumerDetails, +const checkValidityOfDetailConfigs = ( + selectedTimelineQuery: SelectedTimelineQuery, + selectedView: MessagingQueuesViewTypeOptions, + currentTab: MessagingQueueServiceDetailType, + configDetails?: { + [key: string]: string; + }, + // eslint-disable-next-line sonarjs/cognitive-complexity +): boolean => { + if (selectedView === MessagingQueuesViewType.consumerLag.value) { + return !( + isEmpty(selectedTimelineQuery) || + (!selectedTimelineQuery?.group && + !selectedTimelineQuery?.topic && + !selectedTimelineQuery?.partition) + ); + } + + if (selectedView === MessagingQueuesViewType.partitionLatency.value) { + if (isEmpty(configDetails)) { + return false; + } + + if (currentTab === MessagingQueueServiceDetailType.ConsumerDetails) { + return Boolean(configDetails?.topic && configDetails?.partition); + } + return Boolean( + configDetails?.group && configDetails?.topic && configDetails?.partition, + ); + } + + if (selectedView === MessagingQueuesViewType.producerLatency.value) { + if (isEmpty(configDetails)) { + return false; + } + + if (currentTab === MessagingQueueServiceDetailType.ProducerDetails) { + return Boolean( + configDetails?.topic && + configDetails?.partition && + configDetails?.service_name, + ); + } + return Boolean(configDetails?.topic && configDetails?.service_name); + } + + return false; +}; + +function MessagingQueuesDetails({ + selectedView, + producerLatencyOption, +}: { + selectedView: MessagingQueuesViewTypeOptions; + producerLatencyOption: ProducerLatencyOptions; +}): JSX.Element { + const [currentTab, setCurrentTab] = useState( + MessagingQueueServiceDetailType.ConsumerDetails, ); + + useEffect(() => { + if ( + producerLatencyOption && + selectedView === MessagingQueuesViewType.producerLatency.value + ) { + setCurrentTab( + producerLatencyOption === ProducerLatencyOptions.Consumers + ? MessagingQueueServiceDetailType.ConsumerDetails + : MessagingQueueServiceDetailType.ProducerDetails, + ); + } + }, [selectedView, producerLatencyOption]); + + const urlQuery = useUrlQuery(); + const timelineQuery = decodeURIComponent( + urlQuery.get(QueryParams.selectedTimelineQuery) || '', + ); + + const timelineQueryData: SelectedTimelineQuery = useMemo( + () => (timelineQuery ? JSON.parse(timelineQuery) : {}), + [timelineQuery], + ); + + const configDetails = decodeURIComponent( + urlQuery.get(QueryParams.configDetail) || '', + ); + + const configDetailQueryData: { + [key: string]: string; + } = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [ + configDetails, + ]); + + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const serviceConfigDetails = useMemo( + () => + getMetaDataAndAPIPerView({ + detailType: currentTab, + minTime, + maxTime, + selectedTimelineQuery: timelineQueryData, + configDetails: configDetailQueryData, + }), + [configDetailQueryData, currentTab, maxTime, minTime, timelineQueryData], + ); + return (
+ -
); } diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.styles.scss b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.styles.scss index e02e19e890..ad665d61f5 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.styles.scss +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.styles.scss @@ -1,4 +1,7 @@ .mq-tables-container { + width: 100%; + height: 100%; + .mq-table-title { display: flex; align-items: center; @@ -31,9 +34,6 @@ .ant-table-tbody { .ant-table-cell { max-width: 250px; - - background-color: var(--bg-ink-400); - border-bottom: none; } } @@ -63,6 +63,21 @@ } } +.mq-table { + &.mq-overview-row-clickable { + .ant-table-row { + background-color: var(--bg-ink-400); + + &:hover { + cursor: pointer; + background-color: var(--bg-slate-400) !important; + color: var(--bg-vanilla-400); + transition: background-color 0.3s ease, color 0.3s ease; + } + } + } +} + .lightMode { .mq-tables-container { .mq-table-title { diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx index 9555f7f228..52c01fce45 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/MQTables.tsx @@ -1,9 +1,10 @@ +/* eslint-disable react/require-default-props */ import './MQTables.styles.scss'; import { Skeleton, Table, Typography } from 'antd'; -import logEvent from 'api/common/logEvent'; import axios from 'axios'; import { isNumber } from 'chart.js/helpers'; +import cx from 'classnames'; import { ColumnTypeRender } from 'components/Logs/TableView/types'; import { SOMETHING_WENT_WRONG } from 'constants/api'; import { QueryParams } from 'constants/query'; @@ -13,18 +14,21 @@ import useUrlQuery from 'hooks/useUrlQuery'; import { isEmpty } from 'lodash-es'; import { ConsumerLagDetailTitle, - ConsumerLagDetailType, convertToTitleCase, + MessagingQueueServiceDetailType, + MessagingQueuesViewType, + MessagingQueuesViewTypeOptions, RowData, SelectedTimelineQuery, + setConfigDetail, } from 'pages/MessagingQueues/MessagingQueuesUtils'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useMutation } from 'react-query'; -import { useHistory } from 'react-router-dom'; +import { useHistory, useLocation } from 'react-router-dom'; +import { ErrorResponse, SuccessResponse } from 'types/api'; import { - ConsumerLagPayload, - getConsumerLagDetails, + MessagingQueueServicePayload, MessagingQueuesPayloadProps, } from './getConsumerLagDetails'; @@ -33,7 +37,6 @@ export function getColumns( data: MessagingQueuesPayloadProps['payload'], history: History, ): RowData[] { - console.log(data); if (data?.result?.length === 0) { return []; } @@ -105,10 +108,25 @@ const showPaginationItem = (total: number, range: number[]): JSX.Element => ( ); +// eslint-disable-next-line sonarjs/cognitive-complexity function MessagingQueuesTable({ currentTab, + selectedView, + tableApiPayload, + tableApi, + validConfigPresent = false, + type = 'Detail', }: { - currentTab: ConsumerLagDetailType; + currentTab?: MessagingQueueServiceDetailType; + selectedView: MessagingQueuesViewTypeOptions; + tableApiPayload?: MessagingQueueServicePayload; + tableApi: ( + props: MessagingQueueServicePayload, + ) => Promise< + SuccessResponse | ErrorResponse + >; + validConfigPresent?: boolean; + type?: 'Detail' | 'Overview'; }): JSX.Element { const [columns, setColumns] = useState([]); const [tableData, setTableData] = useState([]); @@ -118,11 +136,22 @@ function MessagingQueuesTable({ const timelineQuery = decodeURIComponent( urlQuery.get(QueryParams.selectedTimelineQuery) || '', ); + const timelineQueryData: SelectedTimelineQuery = useMemo( () => (timelineQuery ? JSON.parse(timelineQuery) : {}), [timelineQuery], ); + const configDetails = decodeURIComponent( + urlQuery.get(QueryParams.configDetail) || '', + ); + + const configDetailQueryData: { + [key: string]: string; + } = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [ + configDetails, + ]); + const paginationConfig = useMemo( () => tableData?.length > 20 && { @@ -134,90 +163,104 @@ function MessagingQueuesTable({ [tableData], ); - const props: ConsumerLagPayload = useMemo( - () => ({ - start: (timelineQueryData?.start || 0) * 1e9, - end: (timelineQueryData?.end || 0) * 1e9, - variables: { - partition: timelineQueryData?.partition, - topic: timelineQueryData?.topic, - consumer_group: timelineQueryData?.group, - }, - detailType: currentTab, - }), - [currentTab, timelineQueryData], - ); - const handleConsumerDetailsOnError = (error: Error): void => { notifications.error({ message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG, }); }; - const { mutate: getConsumerDetails, isLoading } = useMutation( - getConsumerLagDetails, - { - onSuccess: (data) => { - if (data.payload) { - setColumns(getColumns(data?.payload, history)); - setTableData(getTableData(data?.payload)); - } - }, - onError: handleConsumerDetailsOnError, + const { mutate: getViewDetails, isLoading } = useMutation(tableApi, { + onSuccess: (data) => { + if (data.payload) { + setColumns(getColumns(data?.payload, history)); + setTableData(getTableData(data?.payload)); + } }, + onError: handleConsumerDetailsOnError, + }); + + useEffect( + () => { + if (validConfigPresent && tableApiPayload) { + getViewDetails(tableApiPayload); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [currentTab, selectedView, tableApiPayload], ); - // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => getConsumerDetails(props), [currentTab, props]); + const [selectedRowKey, setSelectedRowKey] = useState(); + const [, setSelectedRows] = useState(); + const location = useLocation(); - const isLogEventCalled = useRef(false); + const onRowClick = (record: { [key: string]: string }): void => { + const selectedKey = record.key; - const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean => { - const isEmptyDetail = - isEmpty(timelineQueryData) || - (!timelineQueryData?.group && - !timelineQueryData?.topic && - !timelineQueryData?.partition); + if (`${selectedKey}_${selectedView}` === selectedRowKey) { + setSelectedRowKey(undefined); + setSelectedRows({}); + setConfigDetail(urlQuery, location, history, {}); + } else { + setSelectedRowKey(`${selectedKey}_${selectedView}`); + setSelectedRows(record); - if (!isEmptyDetail && !isLogEventCalled.current) { - logEvent('Messaging Queues: More details viewed', { - 'tab-option': ConsumerLagDetailTitle[currentTab], - variables: { - group: timelineQueryData?.group, - topic: timelineQueryData?.topic, - partition: timelineQueryData?.partition, - }, - }); - isLogEventCalled.current = true; + if (!isEmpty(record)) { + setConfigDetail(urlQuery, location, history, record); + } } - return isEmptyDetail; }; + const subtitle = + selectedView === MessagingQueuesViewType.consumerLag.value + ? `${timelineQueryData?.group || ''} ${timelineQueryData?.topic || ''} ${ + timelineQueryData?.partition || '' + }` + : `${configDetailQueryData?.service_name || ''} ${ + configDetailQueryData?.topic || '' + } ${configDetailQueryData?.partition || ''}`; + return (
- {isEmptyDetails(timelineQueryData) ? ( + {!validConfigPresent ? (
- Click on a co-ordinate above to see the details + {selectedView === MessagingQueuesViewType.consumerLag.value + ? 'Click on a co-ordinate above to see the details' + : 'Click on a row above to see the details'}
) : ( <> -
- {ConsumerLagDetailTitle[currentTab]} -
{`${timelineQueryData?.group || ''} ${ - timelineQueryData?.topic || '' - } ${timelineQueryData?.partition || ''}`}
-
+ {currentTab && ( +
+ {ConsumerLagDetailTitle[currentTab]} +
{subtitle}
+
+ )} + type !== 'Detail' + ? { + onClick: (): void => onRowClick(record), + } + : {} + } + rowClassName={(record): any => + `${record.key}_${selectedView}` === selectedRowKey + ? 'ant-table-row-selected' + : '' + } /> )} diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails.ts b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails.ts index fc6a273e5d..ad8a290dd7 100644 --- a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails.ts +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getConsumerLagDetails.ts @@ -2,18 +2,19 @@ import axios from 'api'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { AxiosError } from 'axios'; import { SOMETHING_WENT_WRONG } from 'constants/api'; -import { ConsumerLagDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils'; +import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils'; import { ErrorResponse, SuccessResponse } from 'types/api'; -export interface ConsumerLagPayload { +export interface MessagingQueueServicePayload { start?: number | string; end?: number | string; variables: { partition?: string; topic?: string; consumer_group?: string; + service_name?: string; }; - detailType: ConsumerLagDetailType; + detailType?: MessagingQueueServiceDetailType | 'producer' | 'consumer'; } export interface MessagingQueuesPayloadProps { @@ -36,7 +37,7 @@ export interface MessagingQueuesPayloadProps { } export const getConsumerLagDetails = async ( - props: ConsumerLagPayload, + props: MessagingQueueServicePayload, ): Promise< SuccessResponse | ErrorResponse > => { diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getPartitionLatencyDetails.ts b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getPartitionLatencyDetails.ts new file mode 100644 index 0000000000..8c0b26f594 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getPartitionLatencyDetails.ts @@ -0,0 +1,39 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { MessagingQueueServiceDetailType } from 'pages/MessagingQueues/MessagingQueuesUtils'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +import { + MessagingQueueServicePayload, + MessagingQueuesPayloadProps, +} from './getConsumerLagDetails'; + +export const getPartitionLatencyDetails = async ( + props: MessagingQueueServicePayload, +): Promise< + SuccessResponse | ErrorResponse +> => { + const { detailType, ...rest } = props; + let endpoint = ''; + if (detailType === MessagingQueueServiceDetailType.ConsumerDetails) { + endpoint = `/messaging-queues/kafka/partition-latency/consumer`; + } else { + endpoint = `/messaging-queues/kafka/consumer-lag/producer-details`; + } + try { + const response = await axios.post(endpoint, { + ...rest, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG); + } +}; diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getPartitionLatencyOverview.ts b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getPartitionLatencyOverview.ts new file mode 100644 index 0000000000..49c8eed757 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getPartitionLatencyOverview.ts @@ -0,0 +1,34 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +import { + MessagingQueueServicePayload, + MessagingQueuesPayloadProps, +} from './getConsumerLagDetails'; + +export const getPartitionLatencyOverview = async ( + props: Omit, +): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.post( + `/messaging-queues/kafka/partition-latency/overview`, + { + ...props, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG); + } +}; diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputDetails.ts b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputDetails.ts new file mode 100644 index 0000000000..3a995ef590 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputDetails.ts @@ -0,0 +1,33 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +import { + MessagingQueueServicePayload, + MessagingQueuesPayloadProps, +} from './getConsumerLagDetails'; + +export const getTopicThroughputDetails = async ( + props: MessagingQueueServicePayload, +): Promise< + SuccessResponse | ErrorResponse +> => { + const { detailType, ...rest } = props; + const endpoint = `/messaging-queues/kafka/topic-throughput/${detailType}`; + try { + const response = await axios.post(endpoint, { + ...rest, + }); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG); + } +}; diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts new file mode 100644 index 0000000000..7ed896d8ca --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetails/MQTables/getTopicThroughputOverview.ts @@ -0,0 +1,37 @@ +import axios from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; +import { ErrorResponse, SuccessResponse } from 'types/api'; + +import { + MessagingQueueServicePayload, + MessagingQueuesPayloadProps, +} from './getConsumerLagDetails'; + +export const getTopicThroughputOverview = async ( + props: Omit, +): Promise< + SuccessResponse | ErrorResponse +> => { + const { detailType, start, end } = props; + console.log(detailType); + try { + const response = await axios.post( + `messaging-queues/kafka/topic-throughput/${detailType}`, + { + start, + end, + }, + ); + + return { + statusCode: 200, + error: null, + message: response.data.status, + payload: response.data.data, + }; + } catch (error) { + return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG); + } +}; diff --git a/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx b/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx new file mode 100644 index 0000000000..104b7ec237 --- /dev/null +++ b/frontend/src/pages/MessagingQueues/MQDetails/MessagingQueueOverview.tsx @@ -0,0 +1,102 @@ +import './MQDetails.style.scss'; + +import { Radio } from 'antd'; +import { Dispatch, SetStateAction } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { + MessagingQueuesViewType, + MessagingQueuesViewTypeOptions, + ProducerLatencyOptions, +} from '../MessagingQueuesUtils'; +import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails'; +import { getPartitionLatencyOverview } from './MQTables/getPartitionLatencyOverview'; +import { getTopicThroughputOverview } from './MQTables/getTopicThroughputOverview'; +import MessagingQueuesTable from './MQTables/MQTables'; + +type SelectedViewType = keyof typeof MessagingQueuesViewType; + +function PartitionLatencyTabs({ + option, + setOption, +}: { + option: ProducerLatencyOptions; + setOption: Dispatch>; +}): JSX.Element { + return ( + setOption(e.target.value)} + value={option} + className="mq-details-options" + > + + {ProducerLatencyOptions.Producers} + + + {ProducerLatencyOptions.Consumers} + + + ); +} + +const getTableApi = (selectedView: MessagingQueuesViewTypeOptions): any => { + if (selectedView === MessagingQueuesViewType.producerLatency.value) { + return getTopicThroughputOverview; + } + return getPartitionLatencyOverview; +}; + +function MessagingQueueOverview({ + selectedView, + option, + setOption, +}: { + selectedView: MessagingQueuesViewTypeOptions; + option: ProducerLatencyOptions; + setOption: Dispatch>; +}): JSX.Element { + const { maxTime, minTime } = useSelector( + (state) => state.globalTime, + ); + + const tableApiPayload: MessagingQueueServicePayload = { + variables: {}, + start: minTime, + end: maxTime, + detailType: + // eslint-disable-next-line no-nested-ternary + selectedView === MessagingQueuesViewType.producerLatency.value + ? option === ProducerLatencyOptions.Producers + ? 'producer' + : 'consumer' + : undefined, + }; + + return ( +
+ {selectedView === MessagingQueuesViewType.producerLatency.value ? ( + + ) : ( +
+ {MessagingQueuesViewType[selectedView as SelectedViewType].label} +
+ )} + +
+ ); +} +export default MessagingQueueOverview; diff --git a/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss index be2a27b50b..d4a1d1165c 100644 --- a/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss +++ b/frontend/src/pages/MessagingQueues/MessagingQueues.styles.scss @@ -106,6 +106,8 @@ .mq-details-options { letter-spacing: -0.06px; + cursor: pointer; + .ant-radio-button-wrapper { border-color: var(--bg-slate-400); color: var(--bg-vanilla-400); diff --git a/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts b/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts index d215b1025e..062e4317c2 100644 --- a/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts +++ b/frontend/src/pages/MessagingQueues/MessagingQueuesUtils.ts @@ -3,12 +3,21 @@ import { PANEL_TYPES } from 'constants/queryBuilder'; import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types'; import { History, Location } from 'history'; import { isEmpty } from 'lodash-es'; +import { ErrorResponse, SuccessResponse } from 'types/api'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import { DataSource } from 'types/common/queryBuilder'; import { v4 as uuid } from 'uuid'; +import { + getConsumerLagDetails, + MessagingQueueServicePayload, + MessagingQueuesPayloadProps, +} from './MQDetails/MQTables/getConsumerLagDetails'; +import { getPartitionLatencyDetails } from './MQDetails/MQTables/getPartitionLatencyDetails'; +import { getTopicThroughputDetails } from './MQDetails/MQTables/getTopicThroughputDetails'; + export const KAFKA_SETUP_DOC_LINK = 'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started'; @@ -24,14 +33,17 @@ export type RowData = { [key: string]: string | number; }; -export enum ConsumerLagDetailType { +export enum MessagingQueueServiceDetailType { ConsumerDetails = 'consumer-details', ProducerDetails = 'producer-details', NetworkLatency = 'network-latency', PartitionHostMetrics = 'partition-host-metric', } -export const ConsumerLagDetailTitle: Record = { +export const ConsumerLagDetailTitle: Record< + MessagingQueueServiceDetailType, + string +> = { 'consumer-details': 'Consumer Groups Details', 'producer-details': 'Producer Details', 'network-latency': 'Network Latency', @@ -205,21 +217,130 @@ export function setSelectedTimelineQuery( history.replace(generatedUrl); } +export enum MessagingQueuesViewTypeOptions { + ConsumerLag = 'consumerLag', + PartitionLatency = 'partitionLatency', + ProducerLatency = 'producerLatency', + ConsumerLatency = 'consumerLatency', +} + export const MessagingQueuesViewType = { consumerLag: { label: 'Consumer Lag view', - value: 'consumerLag', + value: MessagingQueuesViewTypeOptions.ConsumerLag, }, partitionLatency: { label: 'Partition Latency view', - value: 'partitionLatency', + value: MessagingQueuesViewTypeOptions.PartitionLatency, }, producerLatency: { label: 'Producer Latency view', - value: 'producerLatency', + value: MessagingQueuesViewTypeOptions.ProducerLatency, }, consumerLatency: { label: 'Consumer latency view', - value: 'consumerLatency', + value: MessagingQueuesViewTypeOptions.ConsumerLatency, }, }; + +export function setConfigDetail( + urlQuery: URLSearchParams, + location: Location, + history: History, + paramsToSet?: { + [key: string]: string; + }, +): void { + // remove "key" and its value from the paramsToSet object + const { key, ...restParamsToSet } = paramsToSet || {}; + + if (!isEmpty(restParamsToSet)) { + const configDetail = { + ...restParamsToSet, + }; + urlQuery.set( + QueryParams.configDetail, + encodeURIComponent(JSON.stringify(configDetail)), + ); + } else { + urlQuery.delete(QueryParams.configDetail); + } + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + history.replace(generatedUrl); +} + +export enum ProducerLatencyOptions { + Producers = 'Producers', + Consumers = 'Consumers', +} + +interface MetaDataAndAPI { + tableApiPayload: MessagingQueueServicePayload; + tableApi: ( + props: MessagingQueueServicePayload, + ) => Promise< + SuccessResponse | ErrorResponse + >; +} +interface MetaDataAndAPIPerView { + detailType: MessagingQueueServiceDetailType; + selectedTimelineQuery: SelectedTimelineQuery; + configDetails?: { + [key: string]: string; + }; + minTime: number; + maxTime: number; +} + +export const getMetaDataAndAPIPerView = ( + metaDataProps: MetaDataAndAPIPerView, +): Record => { + const { + detailType, + minTime, + maxTime, + selectedTimelineQuery, + configDetails, + } = metaDataProps; + return { + [MessagingQueuesViewType.consumerLag.value]: { + tableApiPayload: { + start: (selectedTimelineQuery?.start || 0) * 1e9, + end: (selectedTimelineQuery?.end || 0) * 1e9, + variables: { + partition: selectedTimelineQuery?.partition, + topic: selectedTimelineQuery?.topic, + consumer_group: selectedTimelineQuery?.group, + }, + detailType, + }, + tableApi: getConsumerLagDetails, + }, + [MessagingQueuesViewType.partitionLatency.value]: { + tableApiPayload: { + start: minTime, + end: maxTime, + variables: { + partition: configDetails?.partition, + topic: configDetails?.topic, + consumer_group: configDetails?.group, + }, + detailType, + }, + tableApi: getPartitionLatencyDetails, + }, + [MessagingQueuesViewType.producerLatency.value]: { + tableApiPayload: { + start: minTime, + end: maxTime, + variables: { + partition: configDetails?.partition, + topic: configDetails?.topic, + service_name: configDetails?.service_name, + }, + detailType, + }, + tableApi: getTopicThroughputDetails, + }, + }; +};