feat: added generic UI for scenario 1,3,4 (#6287)

* feat: added generic table component for scenario 1,3,4

* feat: added generic logic for mq detail tables and consumed for sc-1,2

* feat: added overview and details table for scenario-3

* feat: added table row clicks func

* feat: resolved comments
This commit is contained in:
SagarRajput-7 2024-11-05 19:26:41 +05:30 committed by GitHub
parent 9d90b8d19c
commit 12377be809
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 770 additions and 123 deletions

View File

@ -37,4 +37,5 @@ export enum QueryParams {
partition = 'partition', partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery', selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType', ruleType = 'ruleType',
configDetail = 'configDetail',
} }

View File

@ -5,17 +5,33 @@ import logEvent from 'api/common/logEvent';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { ListMinus } from 'lucide-react'; import { ListMinus } from 'lucide-react';
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { MessagingQueuesViewType } from '../MessagingQueuesUtils'; import {
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon'; import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
import MessagingQueuesDetails from '../MQDetails/MQDetails'; import MessagingQueuesDetails from '../MQDetails/MQDetails';
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions'; import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
import MessagingQueuesGraph from '../MQGraph/MQGraph'; import MessagingQueuesGraph from '../MQGraph/MQGraph';
function MQDetailPage(): JSX.Element { function MQDetailPage(): JSX.Element {
const history = useHistory(); const history = useHistory();
const [
selectedView,
setSelectedView,
] = useState<MessagingQueuesViewTypeOptions>(
MessagingQueuesViewType.consumerLag.value,
);
const [
producerLatencyOption,
setproducerLatencyOption,
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
useEffect(() => { useEffect(() => {
logEvent('Messaging Queues: Detail page visited', {}); logEvent('Messaging Queues: Detail page visited', {});
@ -39,28 +55,19 @@ function MQDetailPage(): JSX.Element {
className="messaging-queue-options" className="messaging-queue-options"
defaultValue={MessagingQueuesViewType.consumerLag.value} defaultValue={MessagingQueuesViewType.consumerLag.value}
popupClassName="messaging-queue-options-popup" popupClassName="messaging-queue-options-popup"
onChange={(value): void => setSelectedView(value)}
options={[ options={[
{ {
label: MessagingQueuesViewType.consumerLag.label, label: MessagingQueuesViewType.consumerLag.label,
value: MessagingQueuesViewType.consumerLag.value, value: MessagingQueuesViewType.consumerLag.value,
}, },
{ {
label: ( label: MessagingQueuesViewType.partitionLatency.label,
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.partitionLatency.label}
/>
),
value: MessagingQueuesViewType.partitionLatency.value, value: MessagingQueuesViewType.partitionLatency.value,
disabled: true,
}, },
{ {
label: ( label: MessagingQueuesViewType.producerLatency.label,
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.producerLatency.label}
/>
),
value: MessagingQueuesViewType.producerLatency.value, value: MessagingQueuesViewType.producerLatency.value,
disabled: true,
}, },
{ {
label: ( label: (
@ -78,10 +85,21 @@ function MQDetailPage(): JSX.Element {
</div> </div>
<div className="messaging-queue-main-graph"> <div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions /> <MessagingQueuesConfigOptions />
<MessagingQueuesGraph /> {selectedView === MessagingQueuesViewType.consumerLag.value ? (
<MessagingQueuesGraph />
) : (
<MessagingQueueOverview
selectedView={selectedView}
option={producerLatencyOption}
setOption={setproducerLatencyOption}
/>
)}
</div> </div>
<div className="messaging-queue-details"> <div className="messaging-queue-details">
<MessagingQueuesDetails /> <MessagingQueuesDetails
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
</div> </div>
</div> </div>
); );

View File

@ -4,3 +4,42 @@
flex-direction: column; flex-direction: column;
gap: 24px; 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;
}
}
}

View File

@ -1,65 +1,227 @@
import './MQDetails.style.scss'; import './MQDetails.style.scss';
import { Radio } from 'antd'; 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 { import {
ConsumerLagDetailTitle, ConsumerLagDetailTitle,
ConsumerLagDetailType, getMetaDataAndAPIPerView,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
ProducerLatencyOptions,
SelectedTimelineQuery,
} from '../MessagingQueuesUtils'; } from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon'; import { ComingSoon } from '../MQCommon/MQCommon';
import MessagingQueuesTable from './MQTables/MQTables'; import MessagingQueuesTable from './MQTables/MQTables';
const MQServiceDetailTypePerView = (
producerLatencyOption: ProducerLatencyOptions,
): Record<string, MessagingQueueServiceDetailType[]> => ({
[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<SetStateAction<MessagingQueueServiceDetailType>>;
selectedView: MessagingQueuesViewTypeOptions;
producerLatencyOption: ProducerLatencyOptions;
}
function MessagingQueuesOptions({ function MessagingQueuesOptions({
currentTab, currentTab,
setCurrentTab, setCurrentTab,
}: { selectedView,
currentTab: ConsumerLagDetailType; producerLatencyOption,
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>; }: MessagingQueuesOptionsProps): JSX.Element {
}): JSX.Element { const handleChange = (value: MessagingQueueServiceDetailType): void => {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab); setCurrentTab(value);
};
const renderRadioButtons = (): JSX.Element[] => {
const detailTypes =
MQServiceDetailTypePerView(producerLatencyOption)[selectedView] || [];
return detailTypes.map((detailType) => (
<Radio.Button
key={detailType}
value={detailType}
disabled={
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
}
className={
detailType === MessagingQueueServiceDetailType.PartitionHostMetrics
? 'disabled-option'
: ''
}
>
{ConsumerLagDetailTitle[detailType]}
{detailType === MessagingQueueServiceDetailType.PartitionHostMetrics && (
<ComingSoon />
)}
</Radio.Button>
));
};
return ( return (
<Radio.Group <Radio.Group
onChange={(value): void => { onChange={(e): void => handleChange(e.target.value)}
setOption(value.target.value); value={currentTab}
setCurrentTab(value.target.value);
}}
value={option}
className="mq-details-options" className="mq-details-options"
> >
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked> {renderRadioButtons()}
{ConsumerLagDetailTitle[ConsumerLagDetailType.ConsumerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.ProducerDetails}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.ProducerDetails]}
</Radio.Button>
<Radio.Button value={ConsumerLagDetailType.NetworkLatency}>
{ConsumerLagDetailTitle[ConsumerLagDetailType.NetworkLatency]}
</Radio.Button>
<Radio.Button
value={ConsumerLagDetailType.PartitionHostMetrics}
disabled
className="disabled-option"
>
{ConsumerLagDetailTitle[ConsumerLagDetailType.PartitionHostMetrics]}
<ComingSoon />
</Radio.Button>
</Radio.Group> </Radio.Group>
); );
} }
function MessagingQueuesDetails(): JSX.Element { const checkValidityOfDetailConfigs = (
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>( selectedTimelineQuery: SelectedTimelineQuery,
ConsumerLagDetailType.ConsumerDetails, 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>(
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<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const serviceConfigDetails = useMemo(
() =>
getMetaDataAndAPIPerView({
detailType: currentTab,
minTime,
maxTime,
selectedTimelineQuery: timelineQueryData,
configDetails: configDetailQueryData,
}),
[configDetailQueryData, currentTab, maxTime, minTime, timelineQueryData],
);
return ( return (
<div className="mq-details"> <div className="mq-details">
<MessagingQueuesOptions <MessagingQueuesOptions
currentTab={currentTab} currentTab={currentTab}
setCurrentTab={setCurrentTab} setCurrentTab={setCurrentTab}
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
<MessagingQueuesTable
currentTab={currentTab}
selectedView={selectedView}
tableApi={serviceConfigDetails[selectedView].tableApi}
validConfigPresent={checkValidityOfDetailConfigs(
timelineQueryData,
selectedView,
currentTab,
configDetailQueryData,
)}
tableApiPayload={serviceConfigDetails[selectedView].tableApiPayload}
/> />
<MessagingQueuesTable currentTab={currentTab} />
</div> </div>
); );
} }

View File

@ -1,4 +1,7 @@
.mq-tables-container { .mq-tables-container {
width: 100%;
height: 100%;
.mq-table-title { .mq-table-title {
display: flex; display: flex;
align-items: center; align-items: center;
@ -31,9 +34,6 @@
.ant-table-tbody { .ant-table-tbody {
.ant-table-cell { .ant-table-cell {
max-width: 250px; max-width: 250px;
background-color: var(--bg-ink-400);
border-bottom: none; 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 { .lightMode {
.mq-tables-container { .mq-tables-container {
.mq-table-title { .mq-table-title {

View File

@ -1,9 +1,10 @@
/* eslint-disable react/require-default-props */
import './MQTables.styles.scss'; import './MQTables.styles.scss';
import { Skeleton, Table, Typography } from 'antd'; import { Skeleton, Table, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import axios from 'axios'; import axios from 'axios';
import { isNumber } from 'chart.js/helpers'; import { isNumber } from 'chart.js/helpers';
import cx from 'classnames';
import { ColumnTypeRender } from 'components/Logs/TableView/types'; import { ColumnTypeRender } from 'components/Logs/TableView/types';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -13,18 +14,21 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { import {
ConsumerLagDetailTitle, ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase, convertToTitleCase,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
MessagingQueuesViewTypeOptions,
RowData, RowData,
SelectedTimelineQuery, SelectedTimelineQuery,
setConfigDetail,
} from 'pages/MessagingQueues/MessagingQueuesUtils'; } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query'; 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 { import {
ConsumerLagPayload, MessagingQueueServicePayload,
getConsumerLagDetails,
MessagingQueuesPayloadProps, MessagingQueuesPayloadProps,
} from './getConsumerLagDetails'; } from './getConsumerLagDetails';
@ -33,7 +37,6 @@ export function getColumns(
data: MessagingQueuesPayloadProps['payload'], data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>, history: History<unknown>,
): RowData[] { ): RowData[] {
console.log(data);
if (data?.result?.length === 0) { if (data?.result?.length === 0) {
return []; return [];
} }
@ -105,10 +108,25 @@ const showPaginationItem = (total: number, range: number[]): JSX.Element => (
</> </>
); );
// eslint-disable-next-line sonarjs/cognitive-complexity
function MessagingQueuesTable({ function MessagingQueuesTable({
currentTab, currentTab,
selectedView,
tableApiPayload,
tableApi,
validConfigPresent = false,
type = 'Detail',
}: { }: {
currentTab: ConsumerLagDetailType; currentTab?: MessagingQueueServiceDetailType;
selectedView: MessagingQueuesViewTypeOptions;
tableApiPayload?: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
validConfigPresent?: boolean;
type?: 'Detail' | 'Overview';
}): JSX.Element { }): JSX.Element {
const [columns, setColumns] = useState<any[]>([]); const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]); const [tableData, setTableData] = useState<any[]>([]);
@ -118,11 +136,22 @@ function MessagingQueuesTable({
const timelineQuery = decodeURIComponent( const timelineQuery = decodeURIComponent(
urlQuery.get(QueryParams.selectedTimelineQuery) || '', urlQuery.get(QueryParams.selectedTimelineQuery) || '',
); );
const timelineQueryData: SelectedTimelineQuery = useMemo( const timelineQueryData: SelectedTimelineQuery = useMemo(
() => (timelineQuery ? JSON.parse(timelineQuery) : {}), () => (timelineQuery ? JSON.parse(timelineQuery) : {}),
[timelineQuery], [timelineQuery],
); );
const configDetails = decodeURIComponent(
urlQuery.get(QueryParams.configDetail) || '',
);
const configDetailQueryData: {
[key: string]: string;
} = useMemo(() => (configDetails ? JSON.parse(configDetails) : {}), [
configDetails,
]);
const paginationConfig = useMemo( const paginationConfig = useMemo(
() => () =>
tableData?.length > 20 && { tableData?.length > 20 && {
@ -134,90 +163,104 @@ function MessagingQueuesTable({
[tableData], [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 => { const handleConsumerDetailsOnError = (error: Error): void => {
notifications.error({ notifications.error({
message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG, message: axios.isAxiosError(error) ? error?.message : SOMETHING_WENT_WRONG,
}); });
}; };
const { mutate: getConsumerDetails, isLoading } = useMutation( const { mutate: getViewDetails, isLoading } = useMutation(tableApi, {
getConsumerLagDetails, onSuccess: (data) => {
{ if (data.payload) {
onSuccess: (data) => { setColumns(getColumns(data?.payload, history));
if (data.payload) { setTableData(getTableData(data?.payload));
setColumns(getColumns(data?.payload, history)); }
setTableData(getTableData(data?.payload));
}
},
onError: handleConsumerDetailsOnError,
}, },
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 const [selectedRowKey, setSelectedRowKey] = useState<React.Key>();
useEffect(() => getConsumerDetails(props), [currentTab, props]); const [, setSelectedRows] = useState<any>();
const location = useLocation();
const isLogEventCalled = useRef<boolean>(false); const onRowClick = (record: { [key: string]: string }): void => {
const selectedKey = record.key;
const isEmptyDetails = (timelineQueryData: SelectedTimelineQuery): boolean => { if (`${selectedKey}_${selectedView}` === selectedRowKey) {
const isEmptyDetail = setSelectedRowKey(undefined);
isEmpty(timelineQueryData) || setSelectedRows({});
(!timelineQueryData?.group && setConfigDetail(urlQuery, location, history, {});
!timelineQueryData?.topic && } else {
!timelineQueryData?.partition); setSelectedRowKey(`${selectedKey}_${selectedView}`);
setSelectedRows(record);
if (!isEmptyDetail && !isLogEventCalled.current) { if (!isEmpty(record)) {
logEvent('Messaging Queues: More details viewed', { setConfigDetail(urlQuery, location, history, record);
'tab-option': ConsumerLagDetailTitle[currentTab], }
variables: {
group: timelineQueryData?.group,
topic: timelineQueryData?.topic,
partition: timelineQueryData?.partition,
},
});
isLogEventCalled.current = true;
} }
return isEmptyDetail;
}; };
const subtitle =
selectedView === MessagingQueuesViewType.consumerLag.value
? `${timelineQueryData?.group || ''} ${timelineQueryData?.topic || ''} ${
timelineQueryData?.partition || ''
}`
: `${configDetailQueryData?.service_name || ''} ${
configDetailQueryData?.topic || ''
} ${configDetailQueryData?.partition || ''}`;
return ( return (
<div className="mq-tables-container"> <div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? ( {!validConfigPresent ? (
<div className="no-data-style"> <div className="no-data-style">
<Typography.Text> <Typography.Text>
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'}
</Typography.Text> </Typography.Text>
<Skeleton /> <Skeleton />
</div> </div>
) : ( ) : (
<> <>
<div className="mq-table-title"> {currentTab && (
{ConsumerLagDetailTitle[currentTab]} <div className="mq-table-title">
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${ {ConsumerLagDetailTitle[currentTab]}
timelineQueryData?.topic || '' <div className="mq-table-subtitle">{subtitle}</div>
} ${timelineQueryData?.partition || ''}`}</div> </div>
</div> )}
<Table <Table
className="mq-table" className={cx(
'mq-table',
type !== 'Detail' ? 'mq-overview-row-clickable' : '',
)}
pagination={paginationConfig} pagination={paginationConfig}
size="middle" size="middle"
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
bordered={false} bordered={false}
loading={isLoading} loading={isLoading}
onRow={(record): any =>
type !== 'Detail'
? {
onClick: (): void => onRowClick(record),
}
: {}
}
rowClassName={(record): any =>
`${record.key}_${selectedView}` === selectedRowKey
? 'ant-table-row-selected'
: ''
}
/> />
</> </>
)} )}

View File

@ -2,18 +2,19 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api'; 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'; import { ErrorResponse, SuccessResponse } from 'types/api';
export interface ConsumerLagPayload { export interface MessagingQueueServicePayload {
start?: number | string; start?: number | string;
end?: number | string; end?: number | string;
variables: { variables: {
partition?: string; partition?: string;
topic?: string; topic?: string;
consumer_group?: string; consumer_group?: string;
service_name?: string;
}; };
detailType: ConsumerLagDetailType; detailType?: MessagingQueueServiceDetailType | 'producer' | 'consumer';
} }
export interface MessagingQueuesPayloadProps { export interface MessagingQueuesPayloadProps {
@ -36,7 +37,7 @@ export interface MessagingQueuesPayloadProps {
} }
export const getConsumerLagDetails = async ( export const getConsumerLagDetails = async (
props: ConsumerLagPayload, props: MessagingQueueServicePayload,
): Promise< ): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => { > => {

View File

@ -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<MessagingQueuesPayloadProps['payload']> | 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);
}
};

View File

@ -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<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | 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);
}
};

View File

@ -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<MessagingQueuesPayloadProps['payload']> | 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);
}
};

View File

@ -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<MessagingQueueServicePayload, 'variables'>,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | 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);
}
};

View File

@ -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<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
return (
<Radio.Group
onChange={(e): void => setOption(e.target.value)}
value={option}
className="mq-details-options"
>
<Radio.Button
value={ProducerLatencyOptions.Producers}
key={ProducerLatencyOptions.Producers}
>
{ProducerLatencyOptions.Producers}
</Radio.Button>
<Radio.Button
value={ProducerLatencyOptions.Consumers}
key={ProducerLatencyOptions.Consumers}
>
{ProducerLatencyOptions.Consumers}
</Radio.Button>
</Radio.Group>
);
}
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<SetStateAction<ProducerLatencyOptions>>;
}): JSX.Element {
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(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 (
<div className="mq-overview-container">
{selectedView === MessagingQueuesViewType.producerLatency.value ? (
<PartitionLatencyTabs option={option} setOption={setOption} />
) : (
<div className="mq-overview-title">
{MessagingQueuesViewType[selectedView as SelectedViewType].label}
</div>
)}
<MessagingQueuesTable
selectedView={selectedView}
tableApiPayload={tableApiPayload}
tableApi={getTableApi(selectedView)}
validConfigPresent
type="Overview"
/>
</div>
);
}
export default MessagingQueueOverview;

View File

@ -106,6 +106,8 @@
.mq-details-options { .mq-details-options {
letter-spacing: -0.06px; letter-spacing: -0.06px;
cursor: pointer;
.ant-radio-button-wrapper { .ant-radio-button-wrapper {
border-color: var(--bg-slate-400); border-color: var(--bg-slate-400);
color: var(--bg-vanilla-400); color: var(--bg-vanilla-400);

View File

@ -3,12 +3,21 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types'; import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
import { History, Location } from 'history'; import { History, Location } from 'history';
import { isEmpty } from 'lodash-es'; import { isEmpty } from 'lodash-es';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid'; 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 = export const KAFKA_SETUP_DOC_LINK =
'https://signoz.io/docs/messaging-queues/kafka?utm_source=product&utm_medium=kafka-get-started'; '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; [key: string]: string | number;
}; };
export enum ConsumerLagDetailType { export enum MessagingQueueServiceDetailType {
ConsumerDetails = 'consumer-details', ConsumerDetails = 'consumer-details',
ProducerDetails = 'producer-details', ProducerDetails = 'producer-details',
NetworkLatency = 'network-latency', NetworkLatency = 'network-latency',
PartitionHostMetrics = 'partition-host-metric', PartitionHostMetrics = 'partition-host-metric',
} }
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = { export const ConsumerLagDetailTitle: Record<
MessagingQueueServiceDetailType,
string
> = {
'consumer-details': 'Consumer Groups Details', 'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details', 'producer-details': 'Producer Details',
'network-latency': 'Network Latency', 'network-latency': 'Network Latency',
@ -205,21 +217,130 @@ export function setSelectedTimelineQuery(
history.replace(generatedUrl); history.replace(generatedUrl);
} }
export enum MessagingQueuesViewTypeOptions {
ConsumerLag = 'consumerLag',
PartitionLatency = 'partitionLatency',
ProducerLatency = 'producerLatency',
ConsumerLatency = 'consumerLatency',
}
export const MessagingQueuesViewType = { export const MessagingQueuesViewType = {
consumerLag: { consumerLag: {
label: 'Consumer Lag view', label: 'Consumer Lag view',
value: 'consumerLag', value: MessagingQueuesViewTypeOptions.ConsumerLag,
}, },
partitionLatency: { partitionLatency: {
label: 'Partition Latency view', label: 'Partition Latency view',
value: 'partitionLatency', value: MessagingQueuesViewTypeOptions.PartitionLatency,
}, },
producerLatency: { producerLatency: {
label: 'Producer Latency view', label: 'Producer Latency view',
value: 'producerLatency', value: MessagingQueuesViewTypeOptions.ProducerLatency,
}, },
consumerLatency: { consumerLatency: {
label: 'Consumer latency view', label: 'Consumer latency view',
value: 'consumerLatency', value: MessagingQueuesViewTypeOptions.ConsumerLatency,
}, },
}; };
export function setConfigDetail(
urlQuery: URLSearchParams,
location: Location<unknown>,
history: History<unknown>,
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<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
}
interface MetaDataAndAPIPerView {
detailType: MessagingQueueServiceDetailType;
selectedTimelineQuery: SelectedTimelineQuery;
configDetails?: {
[key: string]: string;
};
minTime: number;
maxTime: number;
}
export const getMetaDataAndAPIPerView = (
metaDataProps: MetaDataAndAPIPerView,
): Record<string, MetaDataAndAPI> => {
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,
},
};
};