Compare commits
22 Commits
main
...
v0.57.0-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d44d8c4ac4 | ||
|
|
942eb4942f | ||
|
|
7faa4adffd | ||
|
|
2ac707daa9 | ||
|
|
fca16b29b3 | ||
|
|
b6db4d535a | ||
|
|
6d317eb07e | ||
|
|
172c587f9c | ||
|
|
c27bd32b2d | ||
|
|
47d3ade68f | ||
|
|
0d5041c5d6 | ||
|
|
8c82e0db3a | ||
|
|
30806d1e34 | ||
|
|
02c68c6253 | ||
|
|
99eb186748 | ||
|
|
ddda9be1c4 | ||
|
|
ee684be44e | ||
|
|
b0c9eb3b11 | ||
|
|
380269f598 | ||
|
|
2f2ecc3446 | ||
|
|
e9668c500f | ||
|
|
fc5db557d2 |
@@ -1,30 +1,50 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -37,4 +37,8 @@ export enum QueryParams {
|
||||
partition = 'partition',
|
||||
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||
ruleType = 'ruleType',
|
||||
getStartedSource = 'getStartedSource',
|
||||
getStartedSourceService = 'getStartedSourceService',
|
||||
configDetail = 'configDetail',
|
||||
mqServiceView = 'mqServiceView',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
**Note:**
|
||||
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
|
||||
|
||||
|
||||
|
||||
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
**Note:**
|
||||
- In case you're dockerising your application, make sure to dockerise it along with OpenTelemetry instrumentation done in previous step.
|
||||
|
||||
|
||||
|
||||
If you encounter any difficulties, please consult the [troubleshooting section](https://signoz.io/docs/instrumentation/springboot/#troubleshooting-your-installation) for assistance.
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -1,26 +1,59 @@
|
||||
/* 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 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 +72,55 @@ 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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</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 />
|
||||
) : (
|
||||
<MessagingQueueOverview
|
||||
selectedView={selectedView}
|
||||
option={producerLatencyOption}
|
||||
setOption={setproducerLatencyOption}
|
||||
/>
|
||||
)}
|
||||
{selectedView !== MessagingQueuesViewType.dropRate.value && (
|
||||
<div className="messaging-queue-details">
|
||||
<MessagingQueuesDetails
|
||||
selectedView={selectedView}
|
||||
producerLatencyOption={producerLatencyOption}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]} — {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;
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
}[];
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
> => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,80 @@ 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">
|
||||
<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>
|
||||
|
||||
@@ -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,81 @@ export const MessagingQueuesViewType = {
|
||||
label: 'Producer Latency view',
|
||||
value: 'producerLatency',
|
||||
},
|
||||
consumerLatency: {
|
||||
label: 'Consumer latency view',
|
||||
value: 'consumerLatency',
|
||||
dropRate: {
|
||||
label: 'Drop Rate view',
|
||||
value: 'dropRate',
|
||||
},
|
||||
};
|
||||
|
||||
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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user