Compare commits
23 Commits
main
...
v0.58.0-ka
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57d9713454 | ||
|
|
9bce03e2e8 | ||
|
|
f1c6a3de2c | ||
|
|
8c2dbe2534 | ||
|
|
c21b706b55 | ||
|
|
8990f09fca | ||
|
|
7e7f37fc26 | ||
|
|
57eeeb8046 | ||
|
|
ad3e19065b | ||
|
|
b16d81dde0 | ||
|
|
1042ef3414 | ||
|
|
7a5cbf1267 | ||
|
|
c0e3c1ee2a | ||
|
|
32e4ac1c85 | ||
|
|
f6f196aee3 | ||
|
|
87a0bf7565 | ||
|
|
e04867cd14 | ||
|
|
dd9ba2d9c8 | ||
|
|
e1e033b477 | ||
|
|
07af634db4 | ||
|
|
b60ca11ce0 | ||
|
|
967490499d | ||
|
|
167c22652f |
@@ -1,30 +1,54 @@
|
||||
{
|
||||
"breadcrumb": "Messaging Queues",
|
||||
"header": "Kafka / Overview",
|
||||
"overview": {
|
||||
"title": "Start sending data in as little as 20 minutes",
|
||||
"subtitle": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"configureConsumer": {
|
||||
"title": "Configure Consumer",
|
||||
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||
"button": "Get Started"
|
||||
},
|
||||
"configureProducer": {
|
||||
"title": "Configure Producer",
|
||||
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||
"button": "Get Started"
|
||||
},
|
||||
"monitorKafka": {
|
||||
"title": "Monitor kafka",
|
||||
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||
"button": "Get Started"
|
||||
},
|
||||
"summarySection": {
|
||||
"viewDetailsButton": "View Details"
|
||||
},
|
||||
"confirmModal": {
|
||||
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||
"okText": "Proceed"
|
||||
}
|
||||
}
|
||||
"breadcrumb": "Messaging Queues",
|
||||
"header": "Kafka / Overview",
|
||||
"overview": {
|
||||
"title": "Start sending data in as little as 20 minutes",
|
||||
"subtitle": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"configureConsumer": {
|
||||
"title": "Configure Consumer",
|
||||
"description": "Add consumer data sources to gain insights and enhance monitoring.",
|
||||
"button": "Get Started"
|
||||
},
|
||||
"configureProducer": {
|
||||
"title": "Configure Producer",
|
||||
"description": "Add producer data sources to gain insights and enhance monitoring.",
|
||||
"button": "Get Started"
|
||||
},
|
||||
"monitorKafka": {
|
||||
"title": "Monitor kafka",
|
||||
"description": "Add your Kafka source to gain insights and enhance activity tracking.",
|
||||
"button": "Get Started"
|
||||
},
|
||||
"summarySection": {
|
||||
"viewDetailsButton": "View Details",
|
||||
"consumer": {
|
||||
"title": "Consumer lag view",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"producer": {
|
||||
"title": "Producer latency view",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"partition": {
|
||||
"title": "Partition Latency view",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"dropRate": {
|
||||
"title": "Drop Rate view",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
},
|
||||
"metricPage": {
|
||||
"title": "Metric View",
|
||||
"description": "Connect and Monitor Your Data Streams"
|
||||
}
|
||||
},
|
||||
"confirmModal": {
|
||||
"content": "Before navigating to the details page, please make sure you have configured all the required setup to ensure correct data monitoring.",
|
||||
"okText": "Proceed"
|
||||
},
|
||||
"overviewSummarySection": {
|
||||
"title": "Monitor Your Data Streams",
|
||||
"subtitle": "Monitor key Kafka metrics like consumer lag and latency to ensure efficient data flow and troubleshoot in real time."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,60 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import '../MessagingQueues.styles.scss';
|
||||
|
||||
import { Select, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { ListMinus } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { MessagingQueuesViewType } from '../MessagingQueuesUtils';
|
||||
import { SelectLabelWithComingSoon } from '../MQCommon/MQCommon';
|
||||
import {
|
||||
MessagingQueuesViewType,
|
||||
ProducerLatencyOptions,
|
||||
} from '../MessagingQueuesUtils';
|
||||
import DropRateView from '../MQDetails/DropRateView/DropRateView';
|
||||
import MessagingQueueOverview from '../MQDetails/MessagingQueueOverview';
|
||||
import MetricPage from '../MQDetails/MetricPage/MetricPage';
|
||||
import MessagingQueuesDetails from '../MQDetails/MQDetails';
|
||||
import MessagingQueuesConfigOptions from '../MQGraph/MQConfigOptions';
|
||||
import MessagingQueuesGraph from '../MQGraph/MQGraph';
|
||||
|
||||
function MQDetailPage(): JSX.Element {
|
||||
const history = useHistory();
|
||||
const [selectedView, setSelectedView] = useState<string>(
|
||||
MessagingQueuesViewType.consumerLag.value,
|
||||
);
|
||||
|
||||
const [
|
||||
producerLatencyOption,
|
||||
setproducerLatencyOption,
|
||||
] = useState<ProducerLatencyOptions>(ProducerLatencyOptions.Producers);
|
||||
|
||||
const mqServiceView = useUrlQuery().get(QueryParams.mqServiceView);
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('Messaging Queues: Detail page visited', {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (mqServiceView) {
|
||||
setSelectedView(mqServiceView);
|
||||
}
|
||||
}, [mqServiceView]);
|
||||
|
||||
const updateUrlQuery = (query: Record<string, string | number>): void => {
|
||||
const searchParams = new URLSearchParams(history.location.search);
|
||||
Object.keys(query).forEach((key) => {
|
||||
searchParams.set(key, query[key].toString());
|
||||
});
|
||||
history.push({
|
||||
search: searchParams.toString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="messaging-queue-container">
|
||||
<div className="messaging-breadcrumb">
|
||||
@@ -39,50 +73,62 @@ function MQDetailPage(): JSX.Element {
|
||||
className="messaging-queue-options"
|
||||
defaultValue={MessagingQueuesViewType.consumerLag.value}
|
||||
popupClassName="messaging-queue-options-popup"
|
||||
onChange={(value): void => {
|
||||
setSelectedView(value);
|
||||
updateUrlQuery({ [QueryParams.mqServiceView]: value });
|
||||
}}
|
||||
value={mqServiceView}
|
||||
options={[
|
||||
{
|
||||
label: MessagingQueuesViewType.consumerLag.label,
|
||||
value: MessagingQueuesViewType.consumerLag.value,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<SelectLabelWithComingSoon
|
||||
label={MessagingQueuesViewType.partitionLatency.label}
|
||||
/>
|
||||
),
|
||||
label: MessagingQueuesViewType.partitionLatency.label,
|
||||
value: MessagingQueuesViewType.partitionLatency.value,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<SelectLabelWithComingSoon
|
||||
label={MessagingQueuesViewType.producerLatency.label}
|
||||
/>
|
||||
),
|
||||
label: MessagingQueuesViewType.producerLatency.label,
|
||||
value: MessagingQueuesViewType.producerLatency.value,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<SelectLabelWithComingSoon
|
||||
label={MessagingQueuesViewType.consumerLatency.label}
|
||||
/>
|
||||
),
|
||||
value: MessagingQueuesViewType.consumerLatency.value,
|
||||
disabled: true,
|
||||
label: MessagingQueuesViewType.dropRate.label,
|
||||
value: MessagingQueuesViewType.dropRate.value,
|
||||
},
|
||||
{
|
||||
label: MessagingQueuesViewType.metricPage.label,
|
||||
value: MessagingQueuesViewType.metricPage.value,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<DateTimeSelectionV2 showAutoRefresh={false} hideShareModal />
|
||||
</div>
|
||||
<div className="messaging-queue-main-graph">
|
||||
<MessagingQueuesConfigOptions />
|
||||
<MessagingQueuesGraph />
|
||||
</div>
|
||||
<div className="messaging-queue-details">
|
||||
<MessagingQueuesDetails />
|
||||
</div>
|
||||
{selectedView === MessagingQueuesViewType.consumerLag.value ? (
|
||||
<div className="messaging-queue-main-graph">
|
||||
<MessagingQueuesConfigOptions />
|
||||
<MessagingQueuesGraph />
|
||||
</div>
|
||||
) : selectedView === MessagingQueuesViewType.dropRate.value ? (
|
||||
<DropRateView />
|
||||
) : selectedView === MessagingQueuesViewType.metricPage.value ? (
|
||||
<MetricPage />
|
||||
) : (
|
||||
<MessagingQueueOverview
|
||||
selectedView={selectedView}
|
||||
option={producerLatencyOption}
|
||||
setOption={setproducerLatencyOption}
|
||||
/>
|
||||
)}
|
||||
{selectedView !== MessagingQueuesViewType.dropRate.value &&
|
||||
selectedView !== MessagingQueuesViewType.metricPage.value && (
|
||||
<div className="messaging-queue-details">
|
||||
<MessagingQueuesDetails
|
||||
selectedView={selectedView}
|
||||
producerLatencyOption={producerLatencyOption}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,116 @@
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import MetricPageGridGraph from './MetricPageGraph';
|
||||
import {
|
||||
averageRequestLatencyWidgetData,
|
||||
brokerCountWidgetData,
|
||||
brokerNetworkThroughputWidgetData,
|
||||
bytesConsumedWidgetData,
|
||||
consumerFetchRateWidgetData,
|
||||
consumerGroupMemberWidgetData,
|
||||
consumerLagByGroupWidgetData,
|
||||
consumerOffsetWidgetData,
|
||||
ioWaitTimeWidgetData,
|
||||
kafkaProducerByteRateWidgetData,
|
||||
messagesConsumedWidgetData,
|
||||
producerFetchRequestPurgatoryWidgetData,
|
||||
requestResponseWidgetData,
|
||||
requestTimesWidgetData,
|
||||
} from './MetricPageUtil';
|
||||
|
||||
interface MetricSectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
graphCount: Widgets[];
|
||||
}
|
||||
|
||||
function MetricSection({
|
||||
title,
|
||||
description,
|
||||
graphCount,
|
||||
}: MetricSectionProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<div className="metric-column-graph">
|
||||
<CardContainer className="row-card" isDarkMode={isDarkMode}>
|
||||
<div className={cx('row-panel')}>
|
||||
<Typography.Text className="section-title">{title}</Typography.Text>
|
||||
</div>
|
||||
</CardContainer>
|
||||
<Typography.Text className="graph-description">
|
||||
{description}
|
||||
</Typography.Text>
|
||||
<div className="metric-page-grid">
|
||||
{graphCount.map((widgetData) => (
|
||||
<MetricPageGridGraph
|
||||
key={`graph-${widgetData.id}`}
|
||||
widgetData={widgetData}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricColumnGraphs(): JSX.Element {
|
||||
const metricsData = [
|
||||
{
|
||||
title: 'Broker Metrics',
|
||||
description:
|
||||
'The Kafka Broker metrics here inform you of data loss/delay through unclean leader elections and network throughputs, as well as request fails through request purgatories and timeouts metrics',
|
||||
graphCount: [
|
||||
brokerCountWidgetData,
|
||||
requestTimesWidgetData,
|
||||
producerFetchRequestPurgatoryWidgetData,
|
||||
brokerNetworkThroughputWidgetData,
|
||||
],
|
||||
id: 'broker-metrics',
|
||||
},
|
||||
{
|
||||
title: 'Producer Metrics',
|
||||
description:
|
||||
'Kafka Producers send messages to brokers for storage and distribution by topic. These metrics inform you of the volume and rate of data sent, and the success rate of message delivery.',
|
||||
graphCount: [
|
||||
ioWaitTimeWidgetData,
|
||||
requestResponseWidgetData,
|
||||
averageRequestLatencyWidgetData,
|
||||
kafkaProducerByteRateWidgetData,
|
||||
bytesConsumedWidgetData,
|
||||
],
|
||||
id: 'producer-metrics',
|
||||
},
|
||||
{
|
||||
title: 'Consumer Metrics',
|
||||
description:
|
||||
'Kafka Consumer metrics provide insights into lag between message production and consumption, success rates and latency of message delivery, and the volume of data consumed.',
|
||||
graphCount: [
|
||||
consumerOffsetWidgetData,
|
||||
consumerGroupMemberWidgetData,
|
||||
consumerLagByGroupWidgetData,
|
||||
consumerFetchRateWidgetData,
|
||||
messagesConsumedWidgetData,
|
||||
],
|
||||
id: 'consumer-metrics',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="metric-column-graph-container">
|
||||
{metricsData.map((metric) => (
|
||||
<MetricSection
|
||||
key={metric.id}
|
||||
title={metric.title}
|
||||
description={metric.description}
|
||||
graphCount={metric.graphCount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricColumnGraphs;
|
||||
@@ -0,0 +1,124 @@
|
||||
.metric-page {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
|
||||
.metric-page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.row-panel {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.metric-page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
|
||||
.metric-graph {
|
||||
height: 320px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.metric-page-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metric-page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-description {
|
||||
padding: 16px 10px 16px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.row-panel {
|
||||
border-radius: 4px;
|
||||
background: rgba(18, 19, 23, 0.4);
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
height: 48px !important;
|
||||
|
||||
.ant-typography {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.row-panel-section {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
|
||||
.row-icon {
|
||||
color: var(--bg-vanilla-400);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-column-graph-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
|
||||
.metric-column-graph {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
.row-panel {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metric-page-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 10px;
|
||||
|
||||
.metric-graph {
|
||||
height: 320px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.metric-column-graph-container {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.metric-column-graph-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import './MetricPage.styles.scss';
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
import MetricColumnGraphs from './MetricColumnGraphs';
|
||||
import MetricPageGridGraph from './MetricPageGraph';
|
||||
import {
|
||||
cpuRecentUtilizationWidgetData,
|
||||
currentOffsetPartitionWidgetData,
|
||||
insyncReplicasWidgetData,
|
||||
jvmGcCollectionsElapsedWidgetData,
|
||||
jvmGCCountWidgetData,
|
||||
jvmMemoryHeapWidgetData,
|
||||
oldestOffsetWidgetData,
|
||||
partitionCountPerTopicWidgetData,
|
||||
} from './MetricPageUtil';
|
||||
|
||||
interface CollapsibleMetricSectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
graphCount: Widgets[];
|
||||
isCollapsed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
function CollapsibleMetricSection({
|
||||
title,
|
||||
description,
|
||||
graphCount,
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
}: CollapsibleMetricSectionProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<div className="metric-page-container">
|
||||
<CardContainer className="row-card" isDarkMode={isDarkMode}>
|
||||
<div className={cx('row-panel')}>
|
||||
<div className="row-panel-section">
|
||||
<Typography.Text className="section-title">{title}</Typography.Text>
|
||||
{isCollapsed ? (
|
||||
<ChevronDown size={14} onClick={onToggle} className="row-icon" />
|
||||
) : (
|
||||
<ChevronUp size={14} onClick={onToggle} className="row-icon" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContainer>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
<Typography.Text className="graph-description">
|
||||
{description}
|
||||
</Typography.Text>
|
||||
<div className="metric-page-grid">
|
||||
{graphCount.map((widgetData) => (
|
||||
<MetricPageGridGraph
|
||||
key={`graph-${widgetData.id}`}
|
||||
widgetData={widgetData}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MetricPage(): JSX.Element {
|
||||
const [collapsedSections, setCollapsedSections] = useState<{
|
||||
[key: string]: boolean;
|
||||
}>({
|
||||
producerMetrics: false,
|
||||
consumerMetrics: false,
|
||||
});
|
||||
|
||||
const toggleCollapse = (key: string): void => {
|
||||
setCollapsedSections((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key],
|
||||
}));
|
||||
};
|
||||
|
||||
const metricSections = [
|
||||
{
|
||||
key: 'bokerJVMMetrics',
|
||||
title: 'Broker JVM Metrics',
|
||||
description:
|
||||
"Kafka brokers are Java applications that expose JVM metrics to inform on the broker's system health. Garbage collection metrics like those below provide key insights into free memory, broker performance, and heap size. You need to enable new_gc_metrics for this section to populate.",
|
||||
graphCount: [
|
||||
jvmGCCountWidgetData,
|
||||
jvmGcCollectionsElapsedWidgetData,
|
||||
cpuRecentUtilizationWidgetData,
|
||||
jvmMemoryHeapWidgetData,
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'partitionMetrics',
|
||||
title: 'Partition Metrics',
|
||||
description:
|
||||
'Kafka partitions are the unit of parallelism in Kafka. These metrics inform you of the number of partitions per topic, the current offset of each partition, the oldest offset, and the number of in-sync replicas.',
|
||||
graphCount: [
|
||||
partitionCountPerTopicWidgetData,
|
||||
currentOffsetPartitionWidgetData,
|
||||
oldestOffsetWidgetData,
|
||||
insyncReplicasWidgetData,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="metric-page">
|
||||
<MetricColumnGraphs />
|
||||
{metricSections.map(({ key, title, description, graphCount }) => (
|
||||
<CollapsibleMetricSection
|
||||
key={key}
|
||||
title={title}
|
||||
description={description}
|
||||
graphCount={graphCount}
|
||||
isCollapsed={collapsedSections[key]}
|
||||
onToggle={(): void => toggleCollapse(key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricPage;
|
||||
@@ -0,0 +1,59 @@
|
||||
import './MetricPage.styles.scss';
|
||||
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
function MetricPageGridGraph({
|
||||
widgetData,
|
||||
}: {
|
||||
widgetData: Widgets;
|
||||
}): JSX.Element {
|
||||
const history = useHistory();
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const urlQuery = useUrlQuery();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number) => {
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
urlQuery.set(QueryParams.startTime, startTimestamp.toString());
|
||||
urlQuery.set(QueryParams.endTime, endTimestamp.toString());
|
||||
const generatedUrl = `${pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
|
||||
if (startTimestamp !== endTimestamp) {
|
||||
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
||||
}
|
||||
},
|
||||
[dispatch, history, pathname, urlQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
isDarkMode={isDarkMode}
|
||||
$panelType={PANEL_TYPES.TIME_SERIES}
|
||||
className="metric-graph"
|
||||
>
|
||||
<GridCard
|
||||
widget={widgetData}
|
||||
headerMenuList={[...ViewMenuAction]}
|
||||
onDragSelect={onDragSelect}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricPageGridGraph;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,98 @@ function MessagingQueues(): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-section">
|
||||
<div className="summary-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.consumerLag.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
|
||||
<p className="overview-text">{t('overviewSummarySection.title')}</p>
|
||||
<p className="overview-subtext">{t('overviewSummarySection.subtitle')}</p>
|
||||
<div className={cx('overview-doc-area', 'summary-section')}>
|
||||
<div className="overview-info-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.consumer.title')}</p>
|
||||
<p className="card-info-text">
|
||||
{t('summarySection.consumer.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<Button type="primary" onClick={showConfirm}>
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.consumerLag.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card coming-soon-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.partitionLatency.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
<div className="overview-info-card middle-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.producer.title')}</p>
|
||||
<p className="card-info-text">
|
||||
{t('summarySection.producer.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<ComingSoon />
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.producerLatency.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card coming-soon-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.producerLatency.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
<div className="overview-info-card middle-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.partition.title')}</p>
|
||||
<p className="card-info-text">
|
||||
{t('summarySection.partition.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<ComingSoon />
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.partitionLatency.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="summary-card coming-soon-card">
|
||||
<div className="summary-title">
|
||||
<p>{MessagingQueuesViewType.consumerLatency.label}</p>
|
||||
<div className="time-value">
|
||||
<Calendar size={14} color={Color.BG_SLATE_200} />
|
||||
<p className="time-value">1D</p>
|
||||
</div>
|
||||
<div className="overview-info-card middle-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.dropRate.title')}</p>
|
||||
<p className="card-info-text">
|
||||
{t('summarySection.dropRate.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="view-detail-btn">
|
||||
<ComingSoon />
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.dropRate.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="overview-info-card">
|
||||
<div>
|
||||
<p className="card-title">{t('summarySection.metricPage.title')}</p>
|
||||
<p className="card-info-text">
|
||||
{t('summarySection.metricPage.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="button-grp">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={(): void =>
|
||||
redirectToDetailsPage(MessagingQueuesViewType.metricPage.value)
|
||||
}
|
||||
>
|
||||
{t('summarySection.viewDetailsButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OnboardingStatusResponse } from 'api/messagingQueues/onboarding/getOnboardingStatus';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
|
||||
@@ -24,14 +25,17 @@ export type RowData = {
|
||||
[key: string]: string | number;
|
||||
};
|
||||
|
||||
export enum ConsumerLagDetailType {
|
||||
export enum MessagingQueueServiceDetailType {
|
||||
ConsumerDetails = 'consumer-details',
|
||||
ProducerDetails = 'producer-details',
|
||||
NetworkLatency = 'network-latency',
|
||||
PartitionHostMetrics = 'partition-host-metric',
|
||||
}
|
||||
|
||||
export const ConsumerLagDetailTitle: Record<ConsumerLagDetailType, string> = {
|
||||
export const ConsumerLagDetailTitle: Record<
|
||||
MessagingQueueServiceDetailType,
|
||||
string
|
||||
> = {
|
||||
'consumer-details': 'Consumer Groups Details',
|
||||
'producer-details': 'Producer Details',
|
||||
'network-latency': 'Network Latency',
|
||||
@@ -218,8 +222,85 @@ export const MessagingQueuesViewType = {
|
||||
label: 'Producer Latency view',
|
||||
value: 'producerLatency',
|
||||
},
|
||||
consumerLatency: {
|
||||
label: 'Consumer latency view',
|
||||
value: 'consumerLatency',
|
||||
dropRate: {
|
||||
label: 'Drop Rate view',
|
||||
value: 'dropRate',
|
||||
},
|
||||
metricPage: {
|
||||
label: 'Metric Page',
|
||||
value: 'metricPage',
|
||||
},
|
||||
};
|
||||
|
||||
interface OnboardingStatusAttributeData {
|
||||
overallStatus: string;
|
||||
allAvailableAttributes: string[];
|
||||
attributeDataWithError: { attributeName: string; errorMsg: string }[];
|
||||
}
|
||||
|
||||
export const getAttributeDataFromOnboardingStatus = (
|
||||
onboardingStatus?: OnboardingStatusResponse | null,
|
||||
): OnboardingStatusAttributeData => {
|
||||
const allAvailableAttributes: string[] = [];
|
||||
const attributeDataWithError: {
|
||||
attributeName: string;
|
||||
errorMsg: string;
|
||||
}[] = [];
|
||||
|
||||
if (onboardingStatus?.data && !isEmpty(onboardingStatus?.data)) {
|
||||
onboardingStatus.data.forEach((status) => {
|
||||
if (status.attribute) {
|
||||
allAvailableAttributes.push(status.attribute);
|
||||
if (status.status === '0') {
|
||||
attributeDataWithError.push({
|
||||
attributeName: status.attribute,
|
||||
errorMsg: status.error_message || '',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
overallStatus: attributeDataWithError.length ? 'error' : 'success',
|
||||
allAvailableAttributes,
|
||||
attributeDataWithError,
|
||||
};
|
||||
};
|
||||
|
||||
export enum MessagingQueueHealthCheckService {
|
||||
Consumers = 'consumers',
|
||||
Producers = 'producers',
|
||||
Kafka = 'kafka',
|
||||
}
|
||||
|
||||
export function setConfigDetail(
|
||||
urlQuery: URLSearchParams,
|
||||
location: Location<unknown>,
|
||||
history: History<unknown>,
|
||||
paramsToSet?: {
|
||||
[key: string]: string;
|
||||
},
|
||||
): void {
|
||||
// remove "key" and its value from the paramsToSet object
|
||||
const { key, ...restParamsToSet } = paramsToSet || {};
|
||||
|
||||
if (!isEmpty(restParamsToSet)) {
|
||||
const configDetail = {
|
||||
...restParamsToSet,
|
||||
};
|
||||
urlQuery.set(
|
||||
QueryParams.configDetail,
|
||||
encodeURIComponent(JSON.stringify(configDetail)),
|
||||
);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.configDetail);
|
||||
}
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.replace(generatedUrl);
|
||||
}
|
||||
|
||||
export enum ProducerLatencyOptions {
|
||||
Producers = 'Producers',
|
||||
Consumers = 'Consumers',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user