Compare commits

...

23 Commits

Author SHA1 Message Date
SagarRajput-7
57d9713454 feat: added metric page in messaging queues 2024-11-05 13:33:03 +05:30
SagarRajput-7
9bce03e2e8 feat: code refactor 2024-11-05 13:33:03 +05:30
SagarRajput-7
f1c6a3de2c feat: added api, new table and traceid redirection 2024-11-05 13:33:03 +05:30
SagarRajput-7
8c2dbe2534 feat: added kafka - scenario - 4 - drop rate table 2024-11-05 13:33:03 +05:30
SagarRajput-7
c21b706b55 feat: fixed mq-detail page layout and pagination 2024-11-05 13:33:03 +05:30
SagarRajput-7
8990f09fca feat: fixed producer latency - producer-detail call 2024-11-05 13:33:03 +05:30
SagarRajput-7
7e7f37fc26 feat: fixed multiple fixes and chores in kafka 2.0 2024-11-05 13:33:03 +05:30
SagarRajput-7
57eeeb8046 feat: updated the design for Messaging Queue - summary section 2024-11-05 13:33:03 +05:30
SagarRajput-7
ad3e19065b feat: added kafka - scenario - 4 2024-11-05 13:33:03 +05:30
SagarRajput-7
b16d81dde0 feat: added table row clicks func 2024-11-05 13:33:03 +05:30
SagarRajput-7
1042ef3414 feat: added overview and details table for scenario-3 2024-11-05 13:33:03 +05:30
SagarRajput-7
7a5cbf1267 feat: added generic logic for mq detail tables and consumed for sc-1,2 2024-11-05 13:33:03 +05:30
SagarRajput-7
c0e3c1ee2a feat: added generic table component for scenario 1,3,4 2024-11-05 13:33:03 +05:30
SagarRajput-7
32e4ac1c85 feat: added onboarding detail for consumer setup 2024-11-05 13:33:03 +05:30
SagarRajput-7
f6f196aee3 feat: added missing configuration button at overview and onboarding flow 2024-11-05 13:33:03 +05:30
SagarRajput-7
87a0bf7565 feat: corrected the onboardingapi payload 2024-11-05 13:33:03 +05:30
SagarRajput-7
e04867cd14 feat: added healthcheck and attribute checklist component for Kafka 2024-11-05 13:33:03 +05:30
SagarRajput-7
dd9ba2d9c8 feat: changed start and end time to nanosecond for api payload 2024-11-05 13:33:03 +05:30
SagarRajput-7
e1e033b477 feat: refactoring and url query changes 2024-11-05 13:33:03 +05:30
SagarRajput-7
07af634db4 feat: added onboarding status api util for attribute data 2024-11-05 13:33:03 +05:30
SagarRajput-7
b60ca11ce0 feat: added onboarding status api with useQueury functions and updated endpoints 2024-11-05 13:33:03 +05:30
SagarRajput-7
967490499d fix: polled onboarding status api 2024-11-05 13:33:03 +05:30
SagarRajput-7
167c22652f fix: added onboarding setup for producer/consumer for Messaging queues 2024-11-05 13:33:03 +05:30
39 changed files with 3987 additions and 270 deletions

View File

@@ -1,30 +1,54 @@
{
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details"
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
}
}
"breadcrumb": "Messaging Queues",
"header": "Kafka / Overview",
"overview": {
"title": "Start sending data in as little as 20 minutes",
"subtitle": "Connect and Monitor Your Data Streams"
},
"configureConsumer": {
"title": "Configure Consumer",
"description": "Add consumer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"configureProducer": {
"title": "Configure Producer",
"description": "Add producer data sources to gain insights and enhance monitoring.",
"button": "Get Started"
},
"monitorKafka": {
"title": "Monitor kafka",
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
"button": "Get Started"
},
"summarySection": {
"viewDetailsButton": "View Details",
"consumer": {
"title": "Consumer lag view",
"description": "Connect and Monitor Your Data Streams"
},
"producer": {
"title": "Producer latency view",
"description": "Connect and Monitor Your Data Streams"
},
"partition": {
"title": "Partition Latency view",
"description": "Connect and Monitor Your Data Streams"
},
"dropRate": {
"title": "Drop Rate view",
"description": "Connect and Monitor Your Data Streams"
},
"metricPage": {
"title": "Metric View",
"description": "Connect and Monitor Your Data Streams"
}
},
"confirmModal": {
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
"okText": "Proceed"
},
"overviewSummarySection": {
"title": "Monitor Your Data Streams",
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
}
}

View File

@@ -0,0 +1,39 @@
import { ApiBaseInstance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { ErrorResponse, SuccessResponse } from 'types/api';
export interface OnboardingStatusResponse {
status: string;
data: {
attribute?: string;
error_message?: string;
status?: string;
}[];
}
const getOnboardingStatus = async (props: {
start: number;
end: number;
endpointService?: string;
}): Promise<SuccessResponse<OnboardingStatusResponse> | ErrorResponse> => {
const { endpointService, ...rest } = props;
try {
const response = await ApiBaseInstance.post(
`/messaging-queues/kafka/onboarding/${endpointService || 'consumers'}`,
rest,
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler((error as AxiosError) || SOMETHING_WENT_WRONG);
}
};
export default getOnboardingStatus;

View File

@@ -37,4 +37,8 @@ export enum QueryParams {
partition = 'partition',
selectedTimelineQuery = 'selectedTimelineQuery',
ruleType = 'ruleType',
getStartedSource = 'getStartedSource',
getStartedSourceService = 'getStartedSourceService',
configDetail = 'configDetail',
mqServiceView = 'mqServiceView',
}

View File

@@ -0,0 +1,32 @@
&nbsp;
Once you are done intrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java consumer applications: Ensure your consumer apps are compiled and ready to run.
**Run Consumer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=consumer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.instrumentation.kafka.producer-propagation.enabled=true \
-Dotel.instrumentation.kafka.experimental-span-attributes=true \
-Dotel.instrumentation.kafka.metric-reporter.enabled=true \
-jar /path/to/your/consumer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@@ -0,0 +1,29 @@
&nbsp;
Once you are done intrumenting your Java application, you can run it using the below commands
**Note:**
- Ensure you have Java and Maven installed. Compile your Java producer applications: Ensure your producer apps are compiled and ready to run.
**Run Producer App with Java Agent:**
```bash
java -javaagent:/path/to/opentelemetry-javaagent.jar \
-Dotel.service.name=producer-svc \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar /path/to/your/producer.jar
```
<path> - update it to the path where you downloaded the Java JAR agent in previous step
<my-app> - Jar file of your application
&nbsp;
**Note:**
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
&nbsp;
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.

View File

@@ -6,11 +6,16 @@ import {
LoadingOutlined,
} from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import MessagingQueueHealthCheck from 'pages/MessagingQueues/MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import { getAttributeDataFromOnboardingStatus } from 'pages/MessagingQueues/MessagingQueuesUtils';
import { useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@@ -27,6 +32,12 @@ export default function ConnectionStatus(): JSX.Element {
GlobalReducer
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const {
serviceName,
selectedDataSource,
@@ -57,8 +68,68 @@ export default function ConnectionStatus(): JSX.Element {
maxTime,
selectedTime,
selectedTags,
options: {
enabled: getStartedSource !== 'kafka',
},
});
const [pollInterval, setPollInterval] = useState<number | false>(10000);
const {
data: onbData,
error: onbErr,
isFetching: onbFetching,
} = useOnboardingStatus(
{
enabled: getStartedSource === 'kafka',
refetchInterval: pollInterval,
},
getStartedSourceService || '',
'query-key-onboarding-status',
);
const [
shouldRetryOnboardingCall,
setShouldRetryOnboardingCall,
] = useState<boolean>(false);
useEffect(() => {
if (getStartedSource === 'kafka') {
if (onbData?.statusCode !== 200) {
setShouldRetryOnboardingCall(true);
} else if (onbData?.payload?.status === 'success') {
const attributeData = getAttributeDataFromOnboardingStatus(
onbData?.payload,
);
if (attributeData.overallStatus === 'success') {
setLoading(false);
setIsReceivingData(true);
setPollInterval(false);
} else {
setShouldRetryOnboardingCall(true);
}
}
}
}, [
shouldRetryOnboardingCall,
onbData,
onbErr,
onbFetching,
getStartedSource,
]);
useEffect(() => {
if (retryCount < 0 && getStartedSource === 'kafka') {
setPollInterval(false);
setLoading(false);
}
}, [retryCount, getStartedSource]);
useEffect(() => {
if (getStartedSource === 'kafka' && !onbFetching) {
setRetryCount((prevCount) => prevCount - 1);
}
}, [getStartedSource, onbData, onbFetching]);
const renderDocsReference = (): JSX.Element => {
switch (selectedDataSource?.name) {
case 'java':
@@ -192,25 +263,27 @@ export default function ConnectionStatus(): JSX.Element {
useEffect(() => {
let pollingTimer: string | number | NodeJS.Timer | undefined;
if (loading) {
pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
const updatedMaxTime = Date.now() * 1000000;
if (getStartedSource !== 'kafka') {
if (loading) {
pollingTimer = setInterval(() => {
// Trigger a refetch with the updated parameters
const updatedMinTime = (Date.now() - 15 * 60 * 1000) * 1000000;
const updatedMaxTime = Date.now() * 1000000;
const payload = {
maxTime: updatedMaxTime,
minTime: updatedMinTime,
selectedTime,
};
const payload = {
maxTime: updatedMaxTime,
minTime: updatedMinTime,
selectedTime,
};
dispatch({
type: UPDATE_TIME_INTERVAL,
payload,
});
}, pollingInterval); // Same interval as pollingInterval
} else if (!loading && pollingTimer) {
clearInterval(pollingTimer);
dispatch({
type: UPDATE_TIME_INTERVAL,
payload,
});
}, pollingInterval); // Same interval as pollingInterval
} else if (!loading && pollingTimer) {
clearInterval(pollingTimer);
}
}
// Clean up the interval when the component unmounts
@@ -221,15 +294,24 @@ export default function ConnectionStatus(): JSX.Element {
}, [refetch, selectedTags, selectedTime, loading]);
useEffect(() => {
verifyApplicationData(data);
if (getStartedSource !== 'kafka') {
verifyApplicationData(data);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isServiceLoading, data, error, isError]);
useEffect(() => {
refetch();
if (getStartedSource !== 'kafka') {
refetch();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isQueryServiceLoading = useMemo(
() => isServiceLoading || loading || onbFetching,
[isServiceLoading, loading, onbFetching],
);
return (
<div className="connection-status-container">
<div className="full-docs-link">{renderDocsReference()}</div>
@@ -250,30 +332,42 @@ export default function ConnectionStatus(): JSX.Element {
<div className="label"> Status </div>
<div className="status">
{(loading || isServiceLoading) && <LoadingOutlined />}
{!(loading || isServiceLoading) && isReceivingData && (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
)}
{!(loading || isServiceLoading) && !isReceivingData && (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
)}
{isQueryServiceLoading && <LoadingOutlined />}
{!isQueryServiceLoading &&
isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CheckCircleTwoTone twoToneColor="#52c41a" />
<span> Success </span>
</>
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
{!isQueryServiceLoading &&
!isReceivingData &&
(getStartedSource !== 'kafka' ? (
<>
<CloseCircleTwoTone twoToneColor="#e84749" />
<span> Failed </span>
</>
) : (
<MessagingQueueHealthCheck
serviceToInclude={[getStartedSourceService || '']}
/>
))}
</div>
</div>
<div className="details-info">
<div className="label"> Details </div>
<div className="details">
{(loading || isServiceLoading) && <div> Waiting for Update </div>}
{!(loading || isServiceLoading) && isReceivingData && (
{isQueryServiceLoading && <div> Waiting for Update </div>}
{!isQueryServiceLoading && isReceivingData && (
<div> Received data from the application successfully. </div>
)}
{!(loading || isServiceLoading) && !isReceivingData && (
{!isQueryServiceLoading && !isReceivingData && (
<div> Could not detect the install </div>
)}
</div>

View File

@@ -74,4 +74,11 @@ div[class*='-setup-instructions-container'] {
.dataSourceName {
color: var(--bg-slate-500);
}
}
}
.supported-languages-container {
.disabled {
cursor: not-allowed;
opacity: 0.5;
}
}

View File

@@ -6,15 +6,21 @@ import { LoadingOutlined } from '@ant-design/icons';
import { Button, Card, Form, Input, Select, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import { useCases } from 'container/OnboardingContainer/OnboardingContainer';
import {
ModulesMap,
useCases,
} from 'container/OnboardingContainer/OnboardingContainer';
import {
getDataSources,
getSupportedFrameworks,
hasFrameworks,
messagingQueueKakfaSupportedDataSources,
} from 'container/OnboardingContainer/utils/dataSourceUtils';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Blocks, Check } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -33,6 +39,8 @@ export default function DataSource(): JSX.Element {
const { t } = useTranslation(['common']);
const history = useHistory();
const getStartedSource = useUrlQuery().get(QueryParams.getStartedSource);
const {
serviceName,
selectedModule,
@@ -44,6 +52,9 @@ export default function DataSource(): JSX.Element {
updateSelectedFramework,
} = useOnboardingContext();
const isKafkaAPM =
getStartedSource === 'kafka' && selectedModule?.id === ModulesMap.APM;
const [supportedDataSources, setSupportedDataSources] = useState<
DataSourceType[]
>([]);
@@ -150,13 +161,19 @@ export default function DataSource(): JSX.Element {
className={cx(
'supported-language',
selectedDataSource?.name === dataSource.name ? 'selected' : '',
isKafkaAPM &&
!messagingQueueKakfaSupportedDataSources.includes(dataSource?.id || '')
? 'disabled'
: '',
)}
key={dataSource.name}
onClick={(): void => {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
if (!isKafkaAPM) {
updateSelectedFramework(null);
updateSelectedEnvironment(null);
updateSelectedDataSource(dataSource);
form.setFieldsValue({ selectFramework: null });
}
}}
>
<div>

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import { QueryParams } from 'constants/query';
import { ApmDocFilePaths } from 'container/OnboardingContainer/constants/apmDocFilePaths';
import { AwsMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/awsMonitoringDocFilePaths';
import { AzureMonitoringDocFilePaths } from 'container/OnboardingContainer/constants/azureMonitoringDocFilePaths';
@@ -10,6 +11,7 @@ import {
useOnboardingContext,
} from 'container/OnboardingContainer/context/OnboardingContext';
import { ModulesMap } from 'container/OnboardingContainer/OnboardingContainer';
import useUrlQuery from 'hooks/useUrlQuery';
import { useEffect, useState } from 'react';
export interface IngestionInfoProps {
@@ -31,6 +33,12 @@ export default function MarkdownStep(): JSX.Element {
const [markdownContent, setMarkdownContent] = useState('');
const urlQuery = useUrlQuery();
const getStartedSource = urlQuery.get(QueryParams.getStartedSource);
const getStartedSourceService = urlQuery.get(
QueryParams.getStartedSourceService,
);
const { step } = activeStep;
const getFilePath = (): any => {
@@ -54,6 +62,12 @@ export default function MarkdownStep(): JSX.Element {
path += `_${step?.id}`;
if (
getStartedSource === 'kafka' &&
path === 'APM_java_springBoot_kubernetes_recommendedSteps_runApplication' // todo: Sagar - Make this generic logic in followup PRs
) {
path += `_${getStartedSourceService}`;
}
return path;
};

View File

@@ -252,6 +252,8 @@ import APM_java_springBoot_docker_recommendedSteps_runApplication from '../Modul
import APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-installOtelCollector.md';
import APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-instrumentApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-consumers.md';
import APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers from '../Modules/APM/Java/md-docs/SpringBoot/Kubernetes/springBoot-kubernetes-runApplication-producers.md';
// SpringBoot-LinuxAMD64-quickstart
import APM_java_springBoot_linuxAMD64_quickStart_instrumentApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-instrumentApplication.md';
import APM_java_springBoot_linuxAMD64_quickStart_runApplication from '../Modules/APM/Java/md-docs/SpringBoot/LinuxAMD64/QuickStart/springBoot-linuxamd64-quickStart-runApplication.md';
@@ -1053,6 +1055,8 @@ export const ApmDocFilePaths = {
APM_java_springBoot_kubernetes_recommendedSteps_setupOtelCollector,
APM_java_springBoot_kubernetes_recommendedSteps_instrumentApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_producers,
APM_java_springBoot_kubernetes_recommendedSteps_runApplication_consumers,
// SpringBoot-LinuxAMD64-recommended
APM_java_springBoot_linuxAMD64_recommendedSteps_setupOtelCollector,

View File

@@ -399,3 +399,5 @@ export const moduleRouteMap = {
[ModulesMap.AwsMonitoring]: ROUTES.GET_STARTED_AWS_MONITORING,
[ModulesMap.AzureMonitoring]: ROUTES.GET_STARTED_AZURE_MONITORING,
};
export const messagingQueueKakfaSupportedDataSources = ['java'];

View File

@@ -0,0 +1,29 @@
import getOnboardingStatus, {
OnboardingStatusResponse,
} from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
type UseOnboardingStatus = (
options?: UseQueryOptions<
SuccessResponse<OnboardingStatusResponse> | ErrorResponse
>,
endpointService?: string,
queryKey?: string,
) => UseQueryResult<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>;
export const useOnboardingStatus: UseOnboardingStatus = (
options,
endpointService,
queryKey,
) =>
useQuery<SuccessResponse<OnboardingStatusResponse> | ErrorResponse>({
queryKey: [queryKey || `onboardingStatus-${endpointService}`],
queryFn: () =>
getOnboardingStatus({
start: (Date.now() - 15 * 60 * 1000) * 1_000_000,
end: Date.now() * 1_000_000,
endpointService,
}),
...options,
});

View File

@@ -1,26 +1,60 @@
/* eslint-disable no-nested-ternary */
import '../MessagingQueues.styles.scss';
import { Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import useUrlQuery from 'hooks/useUrlQuery';
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 { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
import {
MessagingQueuesViewType,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import DropRateView from '../MQDetails/DropRateView/DropRateView';
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
import MetricPage from '../MQDetails/MetricPage/MetricPage';
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<string>(
MessagingQueuesViewType.consumerLag.value,
);
const [
producerLatencyOption,
setproducerLatencyOption,
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
const mqServiceView = useUrlQuery().get(QueryParams.mqServiceView);
useEffect(() => {
logEvent('Messaging Queues: Detail page visited', {});
}, []);
useEffect(() => {
if (mqServiceView) {
setSelectedView(mqServiceView);
}
}, [mqServiceView]);
const updateUrlQuery = (query: Record<string, string | number>): void => {
const searchParams = new URLSearchParams(history.location.search);
Object.keys(query).forEach((key) => {
searchParams.set(key, query[key].toString());
});
history.push({
search: searchParams.toString(),
});
};
return (
<div className="messaging-queue-container">
<div className="messaging-breadcrumb">
@@ -39,50 +73,62 @@ function MQDetailPage(): JSX.Element {
className="messaging-queue-options"
defaultValue={MessagingQueuesViewType.consumerLag.value}
popupClassName="messaging-queue-options-popup"
onChange={(value): void => {
setSelectedView(value);
updateUrlQuery({ [QueryParams.mqServiceView]: value });
}}
value={mqServiceView}
options={[
{
label: MessagingQueuesViewType.consumerLag.label,
value: MessagingQueuesViewType.consumerLag.value,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.partitionLatency.label}
/>
),
label: MessagingQueuesViewType.partitionLatency.label,
value: MessagingQueuesViewType.partitionLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.producerLatency.label}
/>
),
label: MessagingQueuesViewType.producerLatency.label,
value: MessagingQueuesViewType.producerLatency.value,
disabled: true,
},
{
label: (
<SelectLabelWithComingSoon
label={MessagingQueuesViewType.consumerLatency.label}
/>
),
value: MessagingQueuesViewType.consumerLatency.value,
disabled: true,
label: MessagingQueuesViewType.dropRate.label,
value: MessagingQueuesViewType.dropRate.value,
},
{
label: MessagingQueuesViewType.metricPage.label,
value: MessagingQueuesViewType.metricPage.value,
},
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
</div>
<div className="messaging-queue-details">
<MessagingQueuesDetails />
</div>
{selectedView === MessagingQueuesViewType.consumerLag.value ? (
<div className="messaging-queue-main-graph">
<MessagingQueuesConfigOptions />
<MessagingQueuesGraph />
</div>
) : selectedView === MessagingQueuesViewType.dropRate.value ? (
<DropRateView />
) : selectedView === MessagingQueuesViewType.metricPage.value ? (
<MetricPage />
) : (
<MessagingQueueOverview
selectedView={selectedView}
option={producerLatencyOption}
setOption={setproducerLatencyOption}
/>
)}
{selectedView !== MessagingQueuesViewType.dropRate.value &&
selectedView !== MessagingQueuesViewType.metricPage.value && (
<div className="messaging-queue-details">
<MessagingQueuesDetails
selectedView={selectedView}
producerLatencyOption={producerLatencyOption}
/>
</div>
)}
</div>
);
}

View File

@@ -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;
}

View File

@@ -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<number, number>,
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 (
<div>
<div className="trace-id-list">
{visibleItems.map((item, idx) => {
const shouldShowMore = remainingCount > 0 && idx === visibleCount - 1;
return (
<div key={item} className="traceid-style">
<Typography.Text
key={item}
className="traceid-text"
onClick={(): void => {
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
}}
>
{item}
</Typography.Text>
{shouldShowMore && (
<Typography
onClick={(): void => handleShowMore(index)}
className="remaing-count"
>
+ {remainingCount} more
</Typography>
)}
</div>
);
})}
</div>
</div>
);
}
if (column === 'consumer_service' || column === 'producer_service') {
return (
<Typography.Link
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
window.open(`/services/${encodeURIComponent(text)}`, '_blank');
}}
>
{text}
</Typography.Link>
);
}
if (column === 'breach_percentage' && text) {
if (!isNumber(text))
return <Typography.Text>{text.toString()}</Typography.Text>;
return (
<Typography.Text>
{(typeof text === 'string' ? parseFloat(text) : text).toFixed(2)} %
</Typography.Text>
);
}
return <Typography.Text>{text}</Typography.Text>;
},
}));
return columns;
}
const showPaginationItem = (total: number, range: number[]): JSX.Element => (
<>
<Typography.Text className="numbers">
{range[0]} &#8212; {range[1]}
</Typography.Text>
<Typography.Text className="total"> of {total}</Typography.Text>
</>
);
function DropRateView(): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
const { notifications } = useNotifications();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const [data, setData] = useState<
DropRateAPIResponse['data']['result'][0]['list']
>([]);
const [interval, setInterval] = useState<string>('');
const [visibleCounts, setVisibleCounts] = useState<Record<number, number>>({});
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 (
<div className={cx('mq-overview-container', 'droprate-view')}>
<div className="mq-overview-title">
<div className="drop-rat-title">
{MessagingQueuesViewType.dropRate.label}
</div>
<EvaluationTimeSelector setInterval={setInterval} />
</div>
<Table
className={cx('mq-table', 'pagination-left')}
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
loading={isLoading}
/>
</div>
);
}
export default DropRateView;

View File

@@ -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<HTMLInputElement>) => void;
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
handleAddCustomValue: () => void;
}
function SelectDropdownRender({
menu,
inputValue,
handleInputChange,
handleAddCustomValue,
handleKeyDown,
}: SelectDropdownRenderProps): JSX.Element {
return (
<>
{menu}
<Input
placeholder="Enter custom time (ms)"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
onBlur={handleAddCustomValue}
className="select-dropdown-render"
/>
</>
);
}
function EvaluationTimeSelector({
setInterval,
}: {
setInterval: Dispatch<SetStateAction<string>>;
}): JSX.Element {
const [inputValue, setInputValue] = useState<string>('');
const [selectedInterval, setSelectedInterval] = useState<string | null>('5ms');
const [dropdownOpen, setDropdownOpen] = useState<boolean>(false);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): 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<HTMLInputElement>): void => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
handleAddCustomValue();
}
};
const renderDropdown = (menu: React.ReactNode): JSX.Element => (
<SelectDropdownRender
menu={menu}
inputValue={inputValue}
handleInputChange={handleInputChange}
handleAddCustomValue={handleAddCustomValue}
handleKeyDown={handleKeyDown}
/>
);
useEffect(() => {
if (selectedInterval) {
setInterval(() => selectedInterval);
}
}, [selectedInterval, setInterval]);
return (
<div className="evaluation-time-selector">
<Typography.Text className="eval-title">
Evaluation Interval:
</Typography.Text>
<Select
style={{ width: 220 }}
placeholder="Select time interval (ms)"
value={selectedInterval}
onChange={handleSelectChange}
open={dropdownOpen}
onDropdownVisibleChange={setDropdownOpen}
dropdownRender={renderDropdown}
>
<Option value="1ms">1ms</Option>
<Option value="2ms">2ms</Option>
<Option value="5ms">5ms</Option>
<Option value="10ms">10ms</Option>
<Option value="15ms">15ms</Option>
</Select>
</div>
);
}
export default EvaluationTimeSelector;

View File

@@ -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[];
}[];
};
}

View File

@@ -4,3 +4,115 @@
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 {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
.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 {
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;
}
}
}
.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;
}
}
}

View File

@@ -1,65 +1,310 @@
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 { ErrorResponse, SuccessResponse } from 'types/api';
import { GlobalReducer } from 'types/reducer/globalTime';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
ProducerLatencyOptions,
SelectedTimelineQuery,
} from '../MessagingQueuesUtils';
import { ComingSoon } from '../MQCommon/MQCommon';
import {
getConsumerLagDetails,
MessagingQueueServicePayload,
MessagingQueuesPayloadProps,
} from './MQTables/getConsumerLagDetails';
import { getPartitionLatencyDetails } from './MQTables/getPartitionLatencyDetails';
import { getTopicThroughputDetails } from './MQTables/getTopicThroughputDetails';
import MessagingQueuesTable from './MQTables/MQTables';
const MQServiceDetailTypePerView = (
producerLatencyOption: ProducerLatencyOptions,
): {
[x: 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: string;
producerLatencyOption: ProducerLatencyOptions;
}
function MessagingQueuesOptions({
currentTab,
setCurrentTab,
}: {
currentTab: ConsumerLagDetailType;
setCurrentTab: Dispatch<SetStateAction<ConsumerLagDetailType>>;
}): JSX.Element {
const [option, setOption] = useState<ConsumerLagDetailType>(currentTab);
selectedView,
producerLatencyOption,
}: MessagingQueuesOptionsProps): JSX.Element {
const [option, setOption] = useState<MessagingQueueServiceDetailType>(
currentTab,
);
useEffect(() => {
setOption(currentTab);
}, [currentTab]);
const handleChange = (value: MessagingQueueServiceDetailType): void => {
setOption(value);
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 (
<Radio.Group
onChange={(value): void => {
setOption(value.target.value);
setCurrentTab(value.target.value);
}}
onChange={(e): void => handleChange(e.target.value)}
value={option}
className="mq-details-options"
>
<Radio.Button value={ConsumerLagDetailType.ConsumerDetails} checked>
{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>
{renderRadioButtons()}
</Radio.Group>
);
}
function MessagingQueuesDetails(): JSX.Element {
const [currentTab, setCurrentTab] = useState<ConsumerLagDetailType>(
ConsumerLagDetailType.ConsumerDetails,
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,
},
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,
},
};
};
const checkValidityOfDetailConfigs = (
selectedTimelineQuery: SelectedTimelineQuery,
selectedView: string,
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;
}
return Boolean(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 selectedView === MessagingQueuesViewType.dropRate.value;
};
function MessagingQueuesDetails({
selectedView,
producerLatencyOption,
}: {
selectedView: string;
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 (
<div className="mq-details">
<MessagingQueuesOptions
currentTab={currentTab}
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>
);
}

View File

@@ -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 {

View File

@@ -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,20 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { isEmpty } from 'lodash-es';
import {
ConsumerLagDetailTitle,
ConsumerLagDetailType,
convertToTitleCase,
MessagingQueueServiceDetailType,
MessagingQueuesViewType,
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 +36,6 @@ export function getColumns(
data: MessagingQueuesPayloadProps['payload'],
history: History<unknown>,
): RowData[] {
console.log(data);
if (data?.result?.length === 0) {
return [];
}
@@ -105,10 +107,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: string;
tableApiPayload?: MessagingQueueServicePayload;
tableApi: (
props: MessagingQueueServicePayload,
) => Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
>;
validConfigPresent?: boolean;
type?: 'Detail' | 'Overview';
}): JSX.Element {
const [columns, setColumns] = useState<any[]>([]);
const [tableData, setTableData] = useState<any[]>([]);
@@ -118,15 +135,26 @@ 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 && {
pageSize: 20,
tableData?.length > 10 && {
pageSize: 10,
showTotal: showPaginationItem,
showSizeChanger: false,
hideOnSinglePage: true,
@@ -134,90 +162,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<React.Key>();
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 => {
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 (
<div className="mq-tables-container">
{isEmptyDetails(timelineQueryData) ? (
{!validConfigPresent ? (
<div className="no-data-style">
<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>
<Skeleton />
</div>
) : (
<>
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{`${timelineQueryData?.group || ''} ${
timelineQueryData?.topic || ''
} ${timelineQueryData?.partition || ''}`}</div>
</div>
{currentTab && (
<div className="mq-table-title">
{ConsumerLagDetailTitle[currentTab]}
<div className="mq-table-subtitle">{subtitle}</div>
</div>
)}
<Table
className="mq-table"
className={cx(
'mq-table',
type !== 'Detail' ? 'mq-overview-row-clickable' : 'pagination-left',
)}
pagination={paginationConfig}
size="middle"
columns={columns}
dataSource={tableData}
bordered={false}
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,20 @@ 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: {
variables?: {
partition?: string;
topic?: string;
consumer_group?: string;
service_name?: string;
};
detailType: ConsumerLagDetailType;
detailType?: MessagingQueueServiceDetailType | 'producer' | 'consumer';
evalTime?: number;
}
export interface MessagingQueuesPayloadProps {
@@ -36,7 +38,7 @@ export interface MessagingQueuesPayloadProps {
}
export const getConsumerLagDetails = async (
props: ConsumerLagPayload,
props: MessagingQueueServicePayload,
): Promise<
SuccessResponse<MessagingQueuesPayloadProps['payload']> | ErrorResponse
> => {

View File

@@ -0,0 +1,30 @@
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 { DropRateAPIResponse } from '../DropRateView/dropRateViewUtils';
import { MessagingQueueServicePayload } from './getConsumerLagDetails';
export const getKafkaSpanEval = async (
props: Omit<MessagingQueueServicePayload, 'detailType' | 'variables'>,
): Promise<SuccessResponse<DropRateAPIResponse['data']> | ErrorResponse> => {
const { start, end, evalTime } = props;
try {
const response = await axios.post(`messaging-queues/kafka/span/evaluation`, {
start,
end,
eval_time: evalTime,
});
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,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,109 @@
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,
ProducerLatencyOptions,
} from '../MessagingQueuesUtils';
import { MessagingQueueServicePayload } from './MQTables/getConsumerLagDetails';
import { getKafkaSpanEval } from './MQTables/getKafkaSpanEval';
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: string): any => {
if (selectedView === MessagingQueuesViewType.producerLatency.value) {
return getTopicThroughputOverview;
}
if (selectedView === MessagingQueuesViewType.dropRate.value) {
return getKafkaSpanEval;
}
return getPartitionLatencyOverview;
};
function MessagingQueueOverview({
selectedView,
option,
setOption,
}: {
selectedView: string;
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,
evalTime:
selectedView === MessagingQueuesViewType.dropRate.value
? 2363404
: 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

@@ -0,0 +1,116 @@
import { Typography } from 'antd';
import cx from 'classnames';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Widgets } from 'types/api/dashboard/getAll';
import MetricPageGridGraph from './MetricPageGraph';
import {
averageRequestLatencyWidgetData,
brokerCountWidgetData,
brokerNetworkThroughputWidgetData,
bytesConsumedWidgetData,
consumerFetchRateWidgetData,
consumerGroupMemberWidgetData,
consumerLagByGroupWidgetData,
consumerOffsetWidgetData,
ioWaitTimeWidgetData,
kafkaProducerByteRateWidgetData,
messagesConsumedWidgetData,
producerFetchRequestPurgatoryWidgetData,
requestResponseWidgetData,
requestTimesWidgetData,
} from './MetricPageUtil';
interface MetricSectionProps {
title: string;
description: string;
graphCount: Widgets[];
}
function MetricSection({
title,
description,
graphCount,
}: MetricSectionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className="metric-column-graph">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className={cx('row-panel')}>
<Typography.Text className="section-title">{title}</Typography.Text>
</div>
</CardContainer>
<Typography.Text className="graph-description">
{description}
</Typography.Text>
<div className="metric-page-grid">
{graphCount.map((widgetData) => (
<MetricPageGridGraph
key={`graph-${widgetData.id}`}
widgetData={widgetData}
/>
))}
</div>
</div>
);
}
function MetricColumnGraphs(): JSX.Element {
const metricsData = [
{
title: 'Broker Metrics',
description:
'The Kafka Broker metrics here inform you of data loss/delay through unclean leader elections and network throughputs, as well as request fails through request purgatories and timeouts metrics',
graphCount: [
brokerCountWidgetData,
requestTimesWidgetData,
producerFetchRequestPurgatoryWidgetData,
brokerNetworkThroughputWidgetData,
],
id: 'broker-metrics',
},
{
title: 'Producer Metrics',
description:
'Kafka Producers send messages to brokers for storage and distribution by topic. These metrics inform you of the volume and rate of data sent, and the success rate of message delivery.',
graphCount: [
ioWaitTimeWidgetData,
requestResponseWidgetData,
averageRequestLatencyWidgetData,
kafkaProducerByteRateWidgetData,
bytesConsumedWidgetData,
],
id: 'producer-metrics',
},
{
title: 'Consumer Metrics',
description:
'Kafka Consumer metrics provide insights into lag between message production and consumption, success rates and latency of message delivery, and the volume of data consumed.',
graphCount: [
consumerOffsetWidgetData,
consumerGroupMemberWidgetData,
consumerLagByGroupWidgetData,
consumerFetchRateWidgetData,
messagesConsumedWidgetData,
],
id: 'consumer-metrics',
},
];
return (
<div className="metric-column-graph-container">
{metricsData.map((metric) => (
<MetricSection
key={metric.id}
title={metric.title}
description={metric.description}
graphCount={metric.graphCount}
/>
))}
</div>
);
}
export default MetricColumnGraphs;

View File

@@ -0,0 +1,124 @@
.metric-page {
padding: 20px;
display: flex;
flex-direction: column;
gap: 32px;
.metric-page-container {
display: flex;
flex-direction: column;
.row-panel {
padding-left: 10px;
}
.metric-page-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
align-items: flex-start;
gap: 10px;
.metric-graph {
height: 320px;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
}
@media (max-width: 1440px) {
.metric-page-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.metric-page-grid {
grid-template-columns: 1fr;
}
}
.graph-description {
padding: 16px 10px 16px 10px;
}
}
.row-panel {
border-radius: 4px;
background: rgba(18, 19, 23, 0.4);
padding: 8px;
display: flex;
gap: 6px;
align-items: center;
height: 48px !important;
.ant-typography {
font-size: 14px;
font-weight: 500;
}
.row-panel-section {
display: flex;
gap: 6px;
align-items: center;
.row-icon {
color: var(--bg-vanilla-400);
cursor: pointer;
}
.section-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.07px;
}
}
}
.metric-column-graph-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
.metric-column-graph {
display: flex;
flex-direction: column;
gap: 10px;
.row-panel {
justify-content: center;
}
.metric-page-grid {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
gap: 10px;
.metric-graph {
height: 320px;
padding: 10px;
width: 100%;
box-sizing: border-box;
}
}
}
}
@media (max-width: 1440px) {
.metric-column-graph-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.metric-column-graph-container {
grid-template-columns: 1fr;
}
}
}

View File

@@ -0,0 +1,133 @@
import './MetricPage.styles.scss';
import { Typography } from 'antd';
import cx from 'classnames';
import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { useState } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import MetricColumnGraphs from './MetricColumnGraphs';
import MetricPageGridGraph from './MetricPageGraph';
import {
cpuRecentUtilizationWidgetData,
currentOffsetPartitionWidgetData,
insyncReplicasWidgetData,
jvmGcCollectionsElapsedWidgetData,
jvmGCCountWidgetData,
jvmMemoryHeapWidgetData,
oldestOffsetWidgetData,
partitionCountPerTopicWidgetData,
} from './MetricPageUtil';
interface CollapsibleMetricSectionProps {
title: string;
description: string;
graphCount: Widgets[];
isCollapsed: boolean;
onToggle: () => void;
}
function CollapsibleMetricSection({
title,
description,
graphCount,
isCollapsed,
onToggle,
}: CollapsibleMetricSectionProps): JSX.Element {
const isDarkMode = useIsDarkMode();
return (
<div className="metric-page-container">
<CardContainer className="row-card" isDarkMode={isDarkMode}>
<div className={cx('row-panel')}>
<div className="row-panel-section">
<Typography.Text className="section-title">{title}</Typography.Text>
{isCollapsed ? (
<ChevronDown size={14} onClick={onToggle} className="row-icon" />
) : (
<ChevronUp size={14} onClick={onToggle} className="row-icon" />
)}
</div>
</div>
</CardContainer>
{!isCollapsed && (
<>
<Typography.Text className="graph-description">
{description}
</Typography.Text>
<div className="metric-page-grid">
{graphCount.map((widgetData) => (
<MetricPageGridGraph
key={`graph-${widgetData.id}`}
widgetData={widgetData}
/>
))}
</div>
</>
)}
</div>
);
}
function MetricPage(): JSX.Element {
const [collapsedSections, setCollapsedSections] = useState<{
[key: string]: boolean;
}>({
producerMetrics: false,
consumerMetrics: false,
});
const toggleCollapse = (key: string): void => {
setCollapsedSections((prev) => ({
...prev,
[key]: !prev[key],
}));
};
const metricSections = [
{
key: 'bokerJVMMetrics',
title: 'Broker JVM Metrics',
description:
"Kafka brokers are Java applications that expose JVM metrics to inform on the broker's system health. Garbage collection metrics like those below provide key insights into free memory, broker performance, and heap size. You need to enable new_gc_metrics for this section to populate.",
graphCount: [
jvmGCCountWidgetData,
jvmGcCollectionsElapsedWidgetData,
cpuRecentUtilizationWidgetData,
jvmMemoryHeapWidgetData,
],
},
{
key: 'partitionMetrics',
title: 'Partition Metrics',
description:
'Kafka partitions are the unit of parallelism in Kafka. These metrics inform you of the number of partitions per topic, the current offset of each partition, the oldest offset, and the number of in-sync replicas.',
graphCount: [
partitionCountPerTopicWidgetData,
currentOffsetPartitionWidgetData,
oldestOffsetWidgetData,
insyncReplicasWidgetData,
],
},
];
return (
<div className="metric-page">
<MetricColumnGraphs />
{metricSections.map(({ key, title, description, graphCount }) => (
<CollapsibleMetricSection
key={key}
title={title}
description={description}
graphCount={graphCount}
isCollapsed={collapsedSections[key]}
onToggle={(): void => toggleCollapse(key)}
/>
))}
</div>
);
}
export default MetricPage;

View File

@@ -0,0 +1,59 @@
import './MetricPage.styles.scss';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
import GridCard from 'container/GridCardLayout/GridCard';
import { Card } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { UpdateTimeInterval } from 'store/actions';
import { Widgets } from 'types/api/dashboard/getAll';
function MetricPageGridGraph({
widgetData,
}: {
widgetData: Widgets;
}): JSX.Element {
const history = useHistory();
const { pathname } = useLocation();
const dispatch = useDispatch();
const urlQuery = useUrlQuery();
const isDarkMode = useIsDarkMode();
const onDragSelect = useCallback(
(start: number, end: number) => {
const startTimestamp = Math.trunc(start);
const endTimestamp = Math.trunc(end);
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
history.push(generatedUrl);
if (startTimestamp !== endTimestamp) {
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
}
},
[dispatch, history, pathname, urlQuery],
);
return (
<Card
isDarkMode={isDarkMode}
$panelType={PANEL_TYPES.TIME_SERIES}
className="metric-graph"
>
<GridCard
widget={widgetData}
headerMenuList={[...ViewMenuAction]}
onDragSelect={onDragSelect}
/>
</Card>
);
}
export default MetricPageGridGraph;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,272 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './MessagingQueueHealthCheck.styles.scss';
import { CaretDownOutlined, LoadingOutlined } from '@ant-design/icons';
import {
Modal,
Select,
Spin,
Tooltip,
Tree,
TreeDataNode,
Typography,
} from 'antd';
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { History } from 'history';
import { Bolt, Check, OctagonAlert, X } from 'lucide-react';
import { ReactNode, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import { v4 as uuid } from 'uuid';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from '../MessagingQueuesUtils';
interface AttributeCheckListProps {
visible: boolean;
onClose: () => void;
onboardingStatusResponses: {
title: string;
data: OnboardingStatusResponse['data'];
errorMsg?: string;
}[];
loading: boolean;
}
export enum AttributesFilters {
ALL = 'all',
SUCCESS = 'success',
ERROR = 'error',
}
function ErrorTitleAndKey({
title,
parentTitle,
history,
isCloudUserVal,
errorMsg,
isLeaf,
}: {
title: string;
parentTitle: string;
isCloudUserVal: boolean;
history: History<unknown>;
errorMsg?: string;
isLeaf?: boolean;
}): TreeDataNode {
const handleRedirection = (): void => {
console.log('Redirect to the error page', parentTitle);
let link = '';
switch (parentTitle) {
case 'Consumers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`;
break;
case 'Producers':
link = `${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`;
break;
case 'Kafka':
link = `${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`;
break;
default:
link = '';
}
if (isCloudUserVal && !!link) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
}
};
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-error-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
<Tooltip title={errorMsg}>
<div
className="attribute-error-warning"
onClick={(e): void => {
e.preventDefault();
handleRedirection();
}}
>
<OctagonAlert size={14} />
Fix
</div>
</Tooltip>
</div>
),
isLeaf,
};
}
function AttributeLabels({ title }: { title: ReactNode }): JSX.Element {
return (
<div className="attribute-label">
<Bolt size={14} />
{title}
</div>
);
}
function treeTitleAndKey({
title,
isLeaf,
}: {
title: string;
isLeaf?: boolean;
}): TreeDataNode {
return {
key: `${title}-key-${uuid()}`,
title: (
<div className="attribute-success-title">
<Typography.Text className="tree-text" ellipsis={{ tooltip: title }}>
{title}
</Typography.Text>
{isLeaf && (
<div className="success-attribute-icon">
<Tooltip title="Success">
<Check size={14} />
</Tooltip>
</div>
)}
</div>
),
isLeaf,
};
}
function generateTreeDataNodes(
response: OnboardingStatusResponse['data'],
parentTitle: string,
isCloudUserVal: boolean,
history: History<unknown>,
): TreeDataNode[] {
return response
.map((item) => {
if (item.attribute) {
if (item.status === '1') {
return treeTitleAndKey({ title: item.attribute, isLeaf: true });
}
if (item.status === '0') {
return ErrorTitleAndKey({
title: item.attribute,
errorMsg: item.error_message || '',
parentTitle,
history,
isCloudUserVal,
});
}
}
return null;
})
.filter(Boolean) as TreeDataNode[];
}
function AttributeCheckList({
visible,
onClose,
onboardingStatusResponses,
loading,
}: AttributeCheckListProps): JSX.Element {
const [filter, setFilter] = useState<AttributesFilters>(AttributesFilters.ALL);
const [treeData, setTreeData] = useState<TreeDataNode[]>([]);
const handleFilterChange = (value: AttributesFilters): void => {
setFilter(value);
};
const isCloudUserVal = isCloudUser();
const history = useHistory();
useEffect(() => {
const filteredData = onboardingStatusResponses.map((response) => {
if (response.errorMsg) {
return ErrorTitleAndKey({
title: response.title,
errorMsg: response.errorMsg,
isLeaf: true,
parentTitle: response.title,
history,
isCloudUserVal,
});
}
let filteredData = response.data;
if (filter === AttributesFilters.SUCCESS) {
filteredData = response.data.filter((item) => item.status === '1');
} else if (filter === AttributesFilters.ERROR) {
filteredData = response.data.filter((item) => item.status === '0');
}
return {
...treeTitleAndKey({ title: response.title }),
children: generateTreeDataNodes(
filteredData,
response.title,
isCloudUserVal,
history,
),
};
});
setTreeData(filteredData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, onboardingStatusResponses]);
return (
<Modal
title="Kafka Service Attributes"
open={visible}
onCancel={onClose}
footer={false}
className="mq-health-check-modal"
closeIcon={<X size={14} />}
>
{loading ? (
<div className="loader-container">
<Spin indicator={<LoadingOutlined spin />} size="large" />
</div>
) : (
<div className="modal-content">
<Select
defaultValue={AttributesFilters.ALL}
className="attribute-select"
onChange={handleFilterChange}
options={[
{
value: AttributesFilters.ALL,
label: AttributeLabels({ title: 'Attributes: All' }),
},
{
value: AttributesFilters.SUCCESS,
label: AttributeLabels({ title: 'Attributes: Success' }),
},
{
value: AttributesFilters.ERROR,
label: AttributeLabels({ title: 'Attributes: Error' }),
},
]}
/>
<Tree
showLine
switcherIcon={<CaretDownOutlined />}
treeData={treeData}
height={450}
className="attribute-tree"
/>
</div>
)}
</Modal>
);
}
export default AttributeCheckList;

View File

@@ -0,0 +1,168 @@
.mq-health-check-modal {
.ant-modal-content {
border-radius: 4px;
border: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
.ant-modal-close {
margin-top: 4px;
}
.ant-modal-header {
border-bottom: 1px solid var(--bg-slate-500);
background: var(--bg-ink-400);
margin-bottom: 16px;
padding-bottom: 4px;
.ant-modal-title {
color: var(--bg-vanilla-100);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 22px;
letter-spacing: 0.52px;
}
}
.modal-content {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: var(--bg-ink-300);
.attribute-select {
align-items: center;
display: flex;
gap: 8px;
width: 170px;
.ant-select-selector {
display: flex;
height: 28px !important;
padding: 8px;
align-items: center;
gap: 4px;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
}
}
.attribute-tree {
padding: 8px;
}
.tree-text {
color: var(--bg-vanilla-400);
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
width: 328px;
}
.ant-tree {
.ant-tree-title {
cursor: default;
.attribute-error-title {
display: flex;
align-items: center;
color: var(--bg-amber-400);
height: 24px;
.tree-text {
color: var(--bg-amber-400);
}
.attribute-error-warning {
display: flex;
align-items: center;
gap: 6px;
font-family: 'Geist Mono';
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
cursor: pointer;
}
}
.attribute-success-title {
display: flex;
align-items: center;
height: 24px;
.success-attribute-icon {
width: 44px;
color: var(--bg-vanilla-400);
display: flex;
> svg {
margin-left: auto;
}
}
}
}
.ant-tree-treenode {
width: 100%;
.ant-tree-node-content-wrapper {
width: 100%;
max-width: 380px;
}
}
}
}
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
padding: 8px;
background: var(--bg-ink-300);
height: 156px;
}
}
}
.attribute-label {
display: flex;
align-items: center;
gap: 8px;
}
.config-btn {
display: flex;
align-items: center;
height: 28px;
border-radius: 2px;
border: none;
box-shadow: none;
background: var(--bg-slate-500);
&.missing-config-btn {
background: rgba(255, 205, 86, 0.1);
color: var(--bg-amber-400);
&:hover {
color: var(--bg-amber-300) !important;
}
}
.config-btn-content {
display: flex;
align-items: center;
margin-right: 8px;
border-right: 1px solid rgba(255, 215, 120, 0.1);
padding-right: 8px;
}
}

View File

@@ -0,0 +1,133 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueueHealthCheck.styles.scss';
import { Button } from 'antd';
import cx from 'classnames';
import { useOnboardingStatus } from 'hooks/messagingQueue / onboarding/useOnboardingStatus';
import { Bolt, FolderTree } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { MessagingQueueHealthCheckService } from '../MessagingQueuesUtils';
import AttributeCheckList from './AttributeCheckList';
interface MessagingQueueHealthCheckProps {
serviceToInclude: string[];
}
function MessagingQueueHealthCheck({
serviceToInclude,
}: MessagingQueueHealthCheckProps): JSX.Element {
const [loading, setLoading] = useState(false);
const [checkListOpen, setCheckListOpen] = useState(false);
// Consumer Data
const {
data: consumerData,
error: consumerError,
isFetching: consumerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Consumers,
).length,
},
MessagingQueueHealthCheckService.Consumers,
);
// Producer Data
const {
data: producerData,
error: producerError,
isFetching: producerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Producers,
).length,
},
MessagingQueueHealthCheckService.Producers,
);
// Kafka Data
const {
data: kafkaData,
error: kafkaError,
isFetching: kafkaLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Kafka,
).length,
},
MessagingQueueHealthCheckService.Kafka,
);
// combined loading and update state
useEffect(() => {
setLoading(consumerLoading || producerLoading || kafkaLoading);
}, [consumerLoading, producerLoading, kafkaLoading]);
const missingConfiguration = useMemo(() => {
const consumerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Consumers) &&
consumerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const producerMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Producers) &&
producerData?.payload?.data?.filter((item) => item.status === '0')
.length) ||
0;
const kafkaMissing =
(serviceToInclude.includes(MessagingQueueHealthCheckService.Kafka) &&
kafkaData?.payload?.data?.filter((item) => item.status === '0').length) ||
0;
return consumerMissing + producerMissing + kafkaMissing;
}, [consumerData, producerData, kafkaData, serviceToInclude]);
return (
<div>
<Button
onClick={(): void => setCheckListOpen(true)}
loading={loading}
className={cx(
'config-btn',
missingConfiguration ? 'missing-config-btn' : '',
)}
icon={<Bolt size={12} />}
>
<div className="config-btn-content">
{missingConfiguration
? `Missing Configuration (${missingConfiguration})`
: 'Configuration'}
</div>
<FolderTree size={14} />
</Button>
<AttributeCheckList
visible={checkListOpen}
onClose={(): void => setCheckListOpen(false)}
onboardingStatusResponses={[
{
title: 'Consumers',
data: consumerData?.payload?.data || [],
errorMsg: (consumerError || consumerData?.error) as string,
},
{
title: 'Producers',
data: producerData?.payload?.data || [],
errorMsg: (producerError || producerData?.error) as string,
},
{
title: 'Kafka',
data: kafkaData?.payload?.data || [],
errorMsg: (kafkaError || kafkaData?.error) as string,
},
].filter((item) => serviceToInclude.includes(item.title.toLowerCase()))}
loading={loading}
/>
</div>
);
}
export default MessagingQueueHealthCheck;

View File

@@ -47,7 +47,7 @@
.header-config {
display: flex;
gap: 10px;
gap: 12px;
align-items: center;
.messaging-queue-options {
@@ -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);
@@ -219,6 +221,23 @@
}
}
}
&.summary-section {
.overview-info-card {
min-height: 144px;
.card-title {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 13px;
font-style: normal;
font-weight: 500;
line-height: 22px;
letter-spacing: 0.52px;
text-transform: uppercase;
}
}
}
}
.summary-section {

View File

@@ -1,47 +1,38 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './MessagingQueues.styles.scss';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { Color } from '@signozhq/design-tokens';
import { Button, Modal } from 'antd';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { Calendar, ListMinus } from 'lucide-react';
import { ListMinus } from 'lucide-react';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router-dom';
import { isCloudUser } from 'utils/app';
import MessagingQueueHealthCheck from './MessagingQueueHealthCheck/MessagingQueueHealthCheck';
import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
MessagingQueuesViewType,
} from './MessagingQueuesUtils';
import { ComingSoon } from './MQCommon/MQCommon';
function MessagingQueues(): JSX.Element {
const history = useHistory();
const { t } = useTranslation('messagingQueuesKafkaOverview');
const { confirm } = Modal;
const showConfirm = (): void => {
const redirectToDetailsPage = (callerView?: string): void => {
logEvent('Messaging Queues: View details clicked', {
page: 'Messaging Queues Overview',
source: 'Consumer Latency view',
source: callerView,
});
confirm({
icon: <ExclamationCircleFilled />,
content: t('confirmModal.content'),
className: 'overview-confirm-modal',
onOk() {
logEvent('Messaging Queues: Proceed button clicked', {
page: 'Messaging Queues Overview',
});
history.push(ROUTES.MESSAGING_QUEUES_DETAIL);
},
okText: t('confirmModal.okText'),
});
history.push(
`${ROUTES.MESSAGING_QUEUES_DETAIL}?${QueryParams.mqServiceView}=${callerView}`,
);
};
const isCloudUserVal = isCloudUser();
@@ -69,7 +60,16 @@ function MessagingQueues(): JSX.Element {
{t('breadcrumb')}
</div>
<div className="messaging-header">
<div className="header-config">{t('header')}</div>
<div className="header-config">
{t('header')} /
<MessagingQueueHealthCheck
serviceToInclude={[
MessagingQueueHealthCheckService.Consumers,
MessagingQueueHealthCheckService.Producers,
MessagingQueueHealthCheckService.Kafka,
]}
/>
</div>
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
</div>
<div className="messaging-overview">
@@ -86,7 +86,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Consumers}`,
'Configure Consumer',
)
}
@@ -105,7 +105,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_APPLICATION_MONITORING,
`${ROUTES.GET_STARTED_APPLICATION_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Producers}`,
'Configure Producer',
)
}
@@ -124,7 +124,7 @@ function MessagingQueues(): JSX.Element {
type="default"
onClick={(): void =>
getStartedRedirect(
ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING,
`${ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING}?${QueryParams.getStartedSource}=kafka&${QueryParams.getStartedSourceService}=${MessagingQueueHealthCheckService.Kafka}`,
'Monitor kafka',
)
}
@@ -134,55 +134,98 @@ function MessagingQueues(): JSX.Element {
</div>
</div>
</div>
<div className="summary-section">
<div className="summary-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLag.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<p className="overview-text">{t('overviewSummarySection.title')}</p>
<p className="overview-subtext">{t('overviewSummarySection.subtitle')}</p>
<div className={cx('overview-doc-area', 'summary-section')}>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.consumer.title')}</p>
<p className="card-info-text">
{t('summarySection.consumer.description')}
</p>
</div>
<div className="view-detail-btn">
<Button type="primary" onClick={showConfirm}>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.partitionLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<div className="overview-info-card middle-card">
<div>
<p className="card-title">{t('summarySection.producer.title')}</p>
<p className="card-info-text">
{t('summarySection.producer.description')}
</p>
</div>
<div className="view-detail-btn">
<ComingSoon />
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.producerLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<div className="overview-info-card middle-card">
<div>
<p className="card-title">{t('summarySection.partition.title')}</p>
<p className="card-info-text">
{t('summarySection.partition.description')}
</p>
</div>
<div className="view-detail-btn">
<ComingSoon />
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="summary-card coming-soon-card">
<div className="summary-title">
<p>{MessagingQueuesViewType.consumerLatency.label}</p>
<div className="time-value">
<Calendar size={14} color={Color.BG_SLATE_200} />
<p className="time-value">1D</p>
</div>
<div className="overview-info-card middle-card">
<div>
<p className="card-title">{t('summarySection.dropRate.title')}</p>
<p className="card-info-text">
{t('summarySection.dropRate.description')}
</p>
</div>
<div className="view-detail-btn">
<ComingSoon />
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
<div className="overview-info-card">
<div>
<p className="card-title">{t('summarySection.metricPage.title')}</p>
<p className="card-info-text">
{t('summarySection.metricPage.description')}
</p>
</div>
<div className="button-grp">
<Button
type="default"
onClick={(): void =>
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
}
>
{t('summarySection.viewDetailsButton')}
</Button>
</div>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
@@ -24,14 +25,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<ConsumerLagDetailType, string> = {
export const ConsumerLagDetailTitle: Record<
MessagingQueueServiceDetailType,
string
> = {
'consumer-details': 'Consumer Groups Details',
'producer-details': 'Producer Details',
'network-latency': 'Network Latency',
@@ -218,8 +222,85 @@ export const MessagingQueuesViewType = {
label: 'Producer Latency view',
value: 'producerLatency',
},
consumerLatency: {
label: 'Consumer latency view',
value: 'consumerLatency',
dropRate: {
label: 'Drop Rate view',
value: 'dropRate',
},
metricPage: {
label: 'Metric Page',
value: 'metricPage',
},
};
interface OnboardingStatusAttributeData {
overallStatus: string;
allAvailableAttributes: string[];
attributeDataWithError: { attributeName: string; errorMsg: string }[];
}
export const getAttributeDataFromOnboardingStatus = (
onboardingStatus?: OnboardingStatusResponse | null,
): OnboardingStatusAttributeData => {
const allAvailableAttributes: string[] = [];
const attributeDataWithError: {
attributeName: string;
errorMsg: string;
}[] = [];
if (onboardingStatus?.data && !isEmpty(onboardingStatus?.data)) {
onboardingStatus.data.forEach((status) => {
if (status.attribute) {
allAvailableAttributes.push(status.attribute);
if (status.status === '0') {
attributeDataWithError.push({
attributeName: status.attribute,
errorMsg: status.error_message || '',
});
}
}
});
}
return {
overallStatus: attributeDataWithError.length ? 'error' : 'success',
allAvailableAttributes,
attributeDataWithError,
};
};
export enum MessagingQueueHealthCheckService {
Consumers = 'consumers',
Producers = 'producers',
Kafka = 'kafka',
}
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',
}