feat: created a component for apdex (#3283)

* feat: created a component for apdex

* refactor: added get and set apDexSetting API support

* refactor: done with ApDexSetting

* feat: done with the traces graph for ApDex

* refactor: separated traces using feature flag

* refactor: restucture the component level

* feat: done with metrics ApDex application

* refactor: removed unwanted logs

* fix: some css part

* refactor: handle error state

* refactor: made use of constants and handleGraphClick for apDex

* refactor: shifted type to type.ts

* chore: css fixes

* refactor: handle error and loading state

* refactor: one on one mapping

* refactor: according to review comments

* refactor: removed unwanted color from local theme colors

* refactor: one on one mapping for queryKey

* refactor: updated css for view traces button issue

* chore: commented out the ExcludeStatusCode feature

* refactor: updated some css part of ApDexSetting popover

* test: added test case for ApDexApplication and ApDexSettings

* refactor: test cases

* refactor: review comments

* refactor: remove the checked for threshold size upto 1

* refactor: changed some text part of ApDex

* refactor: only ApDexMetrics inuse

* refactor: changes due to merge conflicts

* fix: build pipeline

* chore: change the type of the threshold

* feat: widget header as ReactNode

* chore: error for the title is updated

* refactor: widget header as Reactnode

* refactor: show tooltip when hover over the question icon

* refactor: review changes

* refactor: convert threadhold to ReactNode

* refactor: updated test cases

* refactor: move allow threshold a level up

* fix: build pipeline

* fix: input number issue for value 0

---------

Co-authored-by: Palash Gupta <palashgdev@gmail.com>
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
This commit is contained in:
Rajat Dabade 2023-08-16 12:18:56 +05:30 committed by GitHub
parent f8ec850670
commit 8844144c01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1398 additions and 51 deletions

View File

@ -0,0 +1,16 @@
import axios from 'api';
import {
ApDexPayloadAndSettingsProps,
SetApDexPayloadProps,
} from 'types/api/metrics/getApDex';
export const setApDexSettings = async ({
servicename,
threshold,
excludeStatusCode,
}: ApDexPayloadAndSettingsProps): Promise<SetApDexPayloadProps> =>
axios.post('/settings/apdex', {
servicename,
threshold,
excludeStatusCode,
});

View File

@ -0,0 +1,8 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { ApDexPayloadAndSettingsProps } from 'types/api/metrics/getApDex';
export const getApDexSettings = (
servicename: string,
): Promise<AxiosResponse<ApDexPayloadAndSettingsProps[]>> =>
axios.get(`/settings/apdex?services=${servicename}`);

View File

@ -0,0 +1,8 @@
import axios from 'api';
import { AxiosResponse } from 'axios';
import { MetricMetaProps } from 'types/api/metrics/getApDex';
export const getMetricMeta = (
metricName: string,
): Promise<AxiosResponse<MetricMetaProps>> =>
axios.get(`/metric_meta?metricName=${metricName}`);

View File

@ -3,7 +3,6 @@ import {
BarElement,
CategoryScale,
Chart,
ChartType,
Decimation,
Filler,
Legend,
@ -18,6 +17,7 @@ import {
Tooltip,
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';
import {
@ -26,6 +26,7 @@ import {
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from 'react';
@ -83,6 +84,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
@ -119,7 +121,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
const options: CustomChartOptions = getGraphOptions(
animate,
staticLine,
title,
gridTitle,
nearestDatasetIndex,
yAxisUnit,
onDragSelect,
@ -154,7 +156,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
}, [
animate,
staticLine,
title,
gridTitle,
yAxisUnit,
onDragSelect,
dragSelectColor,

View File

@ -7,7 +7,7 @@ import {
ChartType,
TimeUnit,
} from 'chart.js';
import { ForwardedRef } from 'react';
import { ForwardedRef, ReactNode } from 'react';
import {
dragSelectPluginId,
@ -49,7 +49,7 @@ export interface GraphProps {
animate?: boolean;
type: ChartType;
data: Chart['data'];
title?: string;
title?: ReactNode;
isStacked?: boolean;
onClickHandler?: GraphOnClickHandler;
name: string;

View File

@ -1,5 +1,8 @@
import { grey } from '@ant-design/colors';
import { QuestionCircleFilled } from '@ant-design/icons';
import { blue, grey } from '@ant-design/colors';
import {
QuestionCircleFilled,
QuestionCircleOutlined,
} from '@ant-design/icons';
import { Tooltip } from 'antd';
import { themeColors } from 'constants/theme';
import { useIsDarkMode } from 'hooks/useDarkMode';
@ -7,7 +10,12 @@ import { useMemo } from 'react';
import { style } from './styles';
function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
function TextToolTip({
text,
url,
useFilledIcon = true,
urlText,
}: TextToolTipProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const overlay = useMemo(
@ -16,12 +24,12 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
{`${text} `}
{url && (
<a href={url} rel="noopener noreferrer" target="_blank">
here
{urlText || 'here'}
</a>
)}
</div>
),
[text, url],
[text, url, urlText],
);
const iconStyle = useMemo(
@ -32,19 +40,35 @@ function TextToolTip({ text, url }: TextToolTipProps): JSX.Element {
[isDarkMode],
);
const iconOutlinedStyle = useMemo(
() => ({
...style,
color: isDarkMode ? themeColors.navyBlue : blue[0],
}),
[isDarkMode],
);
return (
<Tooltip overlay={overlay}>
<QuestionCircleFilled style={iconStyle} />
{useFilledIcon ? (
<QuestionCircleFilled style={iconStyle} />
) : (
<QuestionCircleOutlined style={iconOutlinedStyle} />
)}
</Tooltip>
);
}
TextToolTip.defaultProps = {
url: '',
urlText: '',
useFilledIcon: true,
};
interface TextToolTipProps {
url?: string;
text: string;
useFilledIcon?: boolean;
urlText?: string;
}
export default TextToolTip;

View File

@ -0,0 +1,5 @@
export const apDexToolTipText =
"Apdex is a way to measure your users' satisfaction with the response time of your web service. It's represented as a score from 0-1.";
export const apDexToolTipUrl =
'https://signoz.io/docs/userguide/metrics/#apdex?utm_source=product&utm_medium=frontend&utm_campaign=apdex';
export const apDexToolTipUrlText = 'Learn more about Apdex.';

View File

@ -53,6 +53,7 @@ function WidgetGraphComponent({
setLayout,
onDragSelect,
onClickHandler,
threshold,
headerMenuList,
}: WidgetGraphComponentProps): JSX.Element {
const [deleteModal, setDeleteModal] = useState(false);
@ -279,6 +280,7 @@ function WidgetGraphComponent({
onClone={onCloneHandler}
queryResponse={queryResponse}
errorMessage={errorMessage}
threshold={threshold}
headerMenuList={headerMenuList}
/>
</div>

View File

@ -29,6 +29,7 @@ function GridCardGraph({
onClickHandler,
headerMenuList = [MenuItemKeys.View],
isQueryEnabled,
threshold,
}: GridCardGraphProps): JSX.Element {
const { isAddWidget } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
@ -70,11 +71,12 @@ function GridCardGraph({
{
queryKey: [
`GetMetricsQueryRange-${widget?.timePreferance}-${globalSelectedInterval}-${widget?.id}`,
widget,
maxTime,
minTime,
globalSelectedInterval,
variables,
widget?.query,
widget?.panelTypes,
],
keepPreviousData: true,
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled && !isAddWidget,
@ -105,7 +107,7 @@ function GridCardGraph({
return <Spinner height="20vh" tip="Loading..." />;
}
if (queryResponse.isError && !isEmptyLayout) {
if ((queryResponse.isError && !isEmptyLayout) || !isQueryEnabled) {
return (
<span ref={graphRef}>
{!isEmpty(widget) && prevChartDataSetRef && (
@ -120,6 +122,7 @@ function GridCardGraph({
yAxisUnit={yAxisUnit}
layout={layout}
setLayout={setLayout}
threshold={threshold}
headerMenuList={headerMenuList}
/>
)}
@ -141,6 +144,7 @@ function GridCardGraph({
yAxisUnit={yAxisUnit}
layout={layout}
setLayout={setLayout}
threshold={threshold}
headerMenuList={headerMenuList}
onClickHandler={onClickHandler}
/>
@ -161,6 +165,7 @@ function GridCardGraph({
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
threshold={threshold}
headerMenuList={headerMenuList}
onClickHandler={onClickHandler}
/>
@ -175,6 +180,7 @@ GridCardGraph.defaultProps = {
onDragSelect: undefined,
onClickHandler: undefined,
isQueryEnabled: true,
threshold: undefined,
headerMenuList: [MenuItemKeys.View],
};

View File

@ -1,6 +1,6 @@
import { ChartData } from 'chart.js';
import { GraphOnClickHandler, ToggleGraphProps } from 'components/Graph/types';
import { Dispatch, MutableRefObject, SetStateAction } from 'react';
import { Dispatch, MutableRefObject, ReactNode, SetStateAction } from 'react';
import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query';
import { DeleteWidgetProps } from 'store/actions/dashboard/deleteWidget';
@ -39,6 +39,7 @@ export interface WidgetGraphComponentProps extends DispatchProps {
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
onDragSelect?: (start: number, end: number) => void;
onClickHandler?: GraphOnClickHandler;
threshold?: ReactNode;
headerMenuList: MenuItemKeys[];
}
@ -50,6 +51,7 @@ export interface GridCardGraphProps {
setLayout?: Dispatch<SetStateAction<LayoutProps[]>>;
onDragSelect?: (start: number, end: number) => void;
onClickHandler?: GraphOnClickHandler;
threshold?: ReactNode;
headerMenuList?: WidgetGraphComponentProps['headerMenuList'];
isQueryEnabled: boolean;
}

View File

@ -0,0 +1,19 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import {
DisplayThresholdContainer,
TypographHeading,
Typography,
} from './styles';
import { DisplayThresholdProps } from './types';
function DisplayThreshold({ threshold }: DisplayThresholdProps): JSX.Element {
return (
<DisplayThresholdContainer>
<TypographHeading>Threshold </TypographHeading>
<Typography>{threshold || <InfoCircleOutlined />}</Typography>
</DisplayThresholdContainer>
);
}
export default DisplayThreshold;

View File

@ -12,7 +12,7 @@ import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes';
import useComponentPermission from 'hooks/useComponentPermission';
import history from 'lib/history';
import { useCallback, useMemo, useState } from 'react';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -32,12 +32,14 @@ import {
ArrowContainer,
HeaderContainer,
HeaderContentContainer,
ThesholdContainer,
WidgetHeaderContainer,
} from './styles';
import { MenuItem } from './types';
import { generateMenuList, isTWidgetOptions } from './utils';
interface IWidgetHeaderProps {
title: string;
title: ReactNode;
widget: Widgets;
onView: VoidFunction;
onDelete?: VoidFunction;
@ -47,6 +49,7 @@ interface IWidgetHeaderProps {
SuccessResponse<MetricRangePayloadProps> | ErrorResponse
>;
errorMessage: string | undefined;
threshold?: ReactNode;
headerMenuList?: MenuItemKeys[];
}
@ -59,6 +62,7 @@ function WidgetHeader({
parentHover,
queryResponse,
errorMessage,
threshold,
headerMenuList,
}: IWidgetHeaderProps): JSX.Element {
const [localHover, setLocalHover] = useState(false);
@ -171,7 +175,7 @@ function WidgetHeader({
);
return (
<div>
<WidgetHeaderContainer>
<Dropdown
destroyPopupOnHide
open={isOpen}
@ -196,6 +200,7 @@ function WidgetHeader({
</HeaderContentContainer>
</HeaderContainer>
</Dropdown>
<ThesholdContainer>{threshold}</ThesholdContainer>
{queryResponse.isFetching && !queryResponse.isError && (
<Spinner height="5vh" style={spinnerStyles} />
)}
@ -204,13 +209,14 @@ function WidgetHeader({
<ExclamationCircleOutlined style={tooltipStyles} />
</Tooltip>
)}
</div>
</WidgetHeaderContainer>
);
}
WidgetHeader.defaultProps = {
onDelete: undefined,
onClone: undefined,
threshold: undefined,
headerMenuList: [MenuItemKeys.View],
};

View File

@ -1,4 +1,6 @@
import { grey } from '@ant-design/colors';
import { Typography as TypographyComponent } from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const HeaderContainer = styled.div<{ hover: boolean }>`
@ -24,3 +26,34 @@ export const ArrowContainer = styled.span<{ hover: boolean }>`
position: absolute;
right: -1rem;
`;
export const ThesholdContainer = styled.span`
margin-top: -0.3rem;
`;
export const DisplayThresholdContainer = styled.div`
display: flex;
align-items: center;
width: auto;
justify-content: space-between;
`;
export const WidgetHeaderContainer = styled.div`
display: flex;
flex-direction: row-reverse;
align-items: center;
`;
export const Typography = styled(TypographyComponent)`
&&& {
color: ${themeColors.white};
width: auto;
margin-left: 0.2rem;
}
`;
export const TypographHeading = styled(TypographyComponent)`
&&& {
color: ${grey[2]};
}
`;

View File

@ -10,3 +10,7 @@ export interface MenuItem {
disabled: boolean;
danger?: boolean;
}
export interface DisplayThresholdProps {
threshold: ReactNode;
}

View File

@ -6,6 +6,7 @@ import {
} from 'components/Graph/types';
import { GridTableComponentProps } from 'container/GridTableComponent/types';
import { GridValueComponentProps } from 'container/GridValueComponent/types';
import { Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
@ -14,7 +15,7 @@ import { PANEL_TYPES } from '../../constants/queryBuilder';
export type GridPanelSwitchProps = {
panelType: PANEL_TYPES;
data: ChartData;
title?: string;
title?: Widgets['title'];
opacity?: string;
isStacked?: boolean;
onClickHandler?: GraphOnClickHandler;

View File

@ -0,0 +1,12 @@
import React, { ReactNode } from 'react';
export const generateGridTitle = (title: ReactNode): string => {
if (React.isValidElement(title)) {
return Array.isArray(title.props.children)
? title.props.children
.map((child: ReactNode) => (typeof child === 'string' ? child : ''))
.join(' ')
: title.props.children;
}
return title?.toString() || '';
};

View File

@ -1,7 +1,8 @@
import { Typography } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ValueGraph from 'components/ValueGraph';
import { memo } from 'react';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import { memo, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { TitleContainer, ValueContainer } from './styles';
@ -15,6 +16,7 @@ function GridValueComponent({
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
const location = useLocation();
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const isDashboardPage = location.pathname.split('/').length === 3;
@ -29,7 +31,7 @@ function GridValueComponent({
return (
<>
<TitleContainer isDashboardPage={isDashboardPage}>
<Typography>{title}</Typography>
<Typography>{gridTitle}</Typography>
</TitleContainer>
<ValueContainer isDashboardPage={isDashboardPage}>
<ValueGraph

View File

@ -1,7 +1,8 @@
import { ChartData } from 'chart.js';
import { ReactNode } from 'react';
export type GridValueComponentProps = {
data: ChartData;
title?: string;
title?: ReactNode;
yAxisUnit?: string;
};

View File

@ -18,7 +18,13 @@ import {
QUERYNAME_AND_EXPRESSION,
WidgetKeys,
} from '../constant';
import { LatencyProps, OperationPerSecProps } from '../Tabs/types';
import {
ApDexMetricsQueryBuilderQueriesProps,
ApDexProps,
LatencyProps,
OperationPerSecProps,
} from '../Tabs/types';
import { convertMilSecToNanoSec, getNearestHighestBucketValue } from '../utils';
import {
getQueryBuilderQueries,
getQueryBuilderQuerieswithFormula,
@ -85,6 +91,365 @@ export const latency = ({
});
};
export const apDexTracesQueryBuilderQueries = ({
servicename,
tagFilterItems,
topLevelOperationsRoute,
threashold,
}: ApDexProps): QueryBuilderData => {
const autoCompleteDataA: BaseAutocompleteData = {
dataType: DataType.FLOAT64,
isColumn: true,
key: '',
type: null,
};
const autoCompleteDataB: BaseAutocompleteData = {
dataType: DataType.FLOAT64,
isColumn: true,
key: '',
type: null,
};
const autoCompleteDataC: BaseAutocompleteData = {
dataType: DataType.FLOAT64,
isColumn: true,
key: '',
type: null,
};
const filterItemA: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.ServiceName,
dataType: DataType.STRING,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: servicename,
},
{
id: '',
key: {
key: WidgetKeys.Name,
dataType: DataType.STRING,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperationsRoute],
},
...tagFilterItems,
];
const filterItemB: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.HasError,
dataType: DataType.BOOL,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: false,
},
{
id: '',
key: {
key: WidgetKeys.DurationNano,
dataType: DataType.FLOAT64,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['<='],
value: convertMilSecToNanoSec(threashold),
},
{
id: '',
key: {
key: WidgetKeys.ServiceName,
dataType: DataType.STRING,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: servicename,
},
{
id: '',
key: {
key: WidgetKeys.Name,
dataType: DataType.STRING,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperationsRoute],
},
];
const filterItemC: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.DurationNano,
dataType: DataType.FLOAT64,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['<='],
value: convertMilSecToNanoSec(threashold * 4),
},
{
id: '',
key: {
key: WidgetKeys.HasError,
dataType: DataType.BOOL,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: false,
},
{
id: '',
key: {
key: WidgetKeys.ServiceName,
dataType: DataType.STRING,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: servicename,
},
{
id: '',
key: {
key: WidgetKeys.Name,
dataType: DataType.STRING,
isColumn: true,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperationsRoute],
},
];
const autocompleteData = [
autoCompleteDataA,
autoCompleteDataB,
autoCompleteDataC,
];
const additionalItems = [filterItemA, filterItemB, filterItemC];
const legends = [GraphTitle.APDEX];
const disabled = Array(3).fill(true);
const expressions = [FORMULA.APDEX_TRACES];
const legendFormulas = [GraphTitle.APDEX];
const aggregateOperators = [
MetricAggregateOperator.COUNT,
MetricAggregateOperator.COUNT,
MetricAggregateOperator.COUNT,
];
const dataSource = DataSource.TRACES;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
disabled,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
});
};
export const apDexMetricsQueryBuilderQueries = ({
servicename,
tagFilterItems,
topLevelOperationsRoute,
threashold,
delta,
metricsBuckets,
}: ApDexMetricsQueryBuilderQueriesProps): QueryBuilderData => {
const autoCompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyCount,
dataType: DataType.FLOAT64,
isColumn: true,
type: null,
};
const autoCompleteDataB: BaseAutocompleteData = {
key: WidgetKeys.Signoz_latency_bucket,
dataType: DataType.FLOAT64,
isColumn: true,
type: null,
};
const autoCompleteDataC: BaseAutocompleteData = {
key: WidgetKeys.Signoz_latency_bucket,
dataType: DataType.FLOAT64,
isColumn: true,
type: null,
};
const filterItemA: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.Service_name,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: servicename,
},
{
id: '',
key: {
key: WidgetKeys.Operation,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperationsRoute],
},
...tagFilterItems,
];
const filterItemB: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.StatusCode,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: 'STATUS_CODE_UNSET',
},
{
id: '',
key: {
key: WidgetKeys.Le,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: getNearestHighestBucketValue(threashold * 1000, metricsBuckets),
},
{
id: '',
key: {
key: WidgetKeys.Service_name,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: servicename,
},
{
id: '',
key: {
key: WidgetKeys.Operation,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperationsRoute],
},
...tagFilterItems,
];
const filterItemC: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.Le,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: getNearestHighestBucketValue(threashold * 1000 * 4, metricsBuckets),
},
{
id: '',
key: {
key: WidgetKeys.StatusCode,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: 'STATUS_CODE_UNSET',
},
{
id: '',
key: {
key: WidgetKeys.Service_name,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS['='],
value: servicename,
},
{
id: '',
key: {
key: WidgetKeys.Operation,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: [...topLevelOperationsRoute],
},
...tagFilterItems,
];
const autocompleteData = [
autoCompleteDataA,
autoCompleteDataB,
autoCompleteDataC,
];
const additionalItems = [filterItemA, filterItemB, filterItemC];
const legends = [GraphTitle.APDEX];
const disabled = Array(3).fill(true);
const expressions = delta
? [FORMULA.APDEX_DELTA_SPAN_METRICS]
: [FORMULA.APDEX_CUMULATIVE_SPAN_METRICS];
const legendFormulas = [GraphTitle.APDEX];
const aggregateOperators = [
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
disabled,
expressions,
legendFormulas,
aggregateOperators,
dataSource,
});
};
export const operationPerSec = ({
servicename,
tagFilterItems,

View File

@ -32,7 +32,14 @@ import {
errorPercentage,
operationPerSec,
} from '../MetricsPageQueries/OverviewQueries';
import { Card, Col, Row } from '../styles';
import {
Card,
Col,
ColApDexContainer,
ColErrorContainer,
Row,
} from '../styles';
import ApDex from './Overview/ApDex';
import ServiceOverview from './Overview/ServiceOverview';
import TopLevelOperation from './Overview/TopLevelOperations';
import TopOperation from './Overview/TopOperation';
@ -160,7 +167,7 @@ function Application(): JSX.Element {
[dispatch],
);
const onErrorTrackHandler = (timestamp: number): void => {
const onErrorTrackHandler = (timestamp: number): (() => void) => (): void => {
const currentTime = timestamp;
const tPlusOne = timestamp + 60 * 1000;
@ -190,6 +197,7 @@ function Application(): JSX.Element {
selectedTimeStamp={selectedTimeStamp}
selectedTraceTags={selectedTraceTags}
topLevelOperationsRoute={topLevelOperationsRoute}
topLevelOperationsLoading={topLevelOperationsLoading}
/>
</Col>
@ -221,28 +229,48 @@ function Application(): JSX.Element {
</Row>
<Row gutter={24}>
<Col span={12}>
<Button
type="default"
size="small"
id="Error_button"
onClick={(): void => {
onErrorTrackHandler(selectedTimeStamp);
}}
>
View Traces
</Button>
<ColApDexContainer>
<Button
type="default"
size="small"
id="ApDex_button"
onClick={onViewTracePopupClick({
servicename,
selectedTraceTags,
timestamp: selectedTimeStamp,
})}
>
View Traces
</Button>
<ApDex
handleGraphClick={handleGraphClick}
onDragSelect={onDragSelect}
topLevelOperationsRoute={topLevelOperationsRoute}
tagFilterItems={tagFilterItems}
/>
</ColApDexContainer>
<ColErrorContainer>
<Button
type="default"
size="small"
id="Error_button"
onClick={onErrorTrackHandler(selectedTimeStamp)}
>
View Traces
</Button>
<TopLevelOperation
handleGraphClick={handleGraphClick}
onDragSelect={onDragSelect}
topLevelOperationsError={topLevelOperationsError}
topLevelOperationsLoading={topLevelOperationsLoading}
topLevelOperationsIsError={topLevelOperationsIsError}
name="error_percentage_%"
widget={errorPercentageWidget}
yAxisUnit="%"
opName="Error"
/>
<TopLevelOperation
handleGraphClick={handleGraphClick}
onDragSelect={onDragSelect}
topLevelOperationsError={topLevelOperationsError}
topLevelOperationsLoading={topLevelOperationsLoading}
topLevelOperationsIsError={topLevelOperationsIsError}
name="error_percentage_%"
widget={errorPercentageWidget}
yAxisUnit="%"
opName="Error"
/>
</ColErrorContainer>
</Col>
<Col span={12}>

View File

@ -0,0 +1,102 @@
import { Space, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import {
apDexToolTipText,
apDexToolTipUrl,
apDexToolTipUrlText,
} from 'constants/apDex';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridGraphLayout/Graph';
import DisplayThreshold from 'container/GridGraphLayout/WidgetHeader/DisplayThreshold';
import { GraphTitle } from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
import { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { IServiceName } from '../../types';
import { ApDexMetricsProps } from './types';
function ApDexMetrics({
delta,
metricsBuckets,
thresholdValue,
onDragSelect,
tagFilterItems,
topLevelOperationsRoute,
handleGraphClick,
}: ApDexMetricsProps): JSX.Element {
const { servicename } = useParams<IServiceName>();
const apDexMetricsWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: apDexMetricsQueryBuilderQueries({
servicename,
tagFilterItems,
topLevelOperationsRoute,
threashold: thresholdValue || 0,
delta: delta || false,
metricsBuckets: metricsBuckets || [],
}),
clickhouse_sql: [],
id: uuid(),
},
title: (
<Space>
<Typography>{GraphTitle.APDEX}</Typography>
<TextToolTip
text={apDexToolTipText}
url={apDexToolTipUrl}
useFilledIcon={false}
urlText={apDexToolTipUrlText}
/>
</Space>
),
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[
delta,
metricsBuckets,
servicename,
tagFilterItems,
thresholdValue,
topLevelOperationsRoute,
],
);
const threshold: ReactNode = useMemo(() => {
if (thresholdValue) return <DisplayThreshold threshold={thresholdValue} />;
return null;
}, [thresholdValue]);
const isQueryEnabled =
topLevelOperationsRoute.length > 0 &&
metricsBuckets &&
metricsBuckets?.length > 0 &&
delta !== undefined;
return (
<Graph
name="apdex"
widget={apDexMetricsWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}
yAxisUnit=""
threshold={threshold}
isQueryEnabled={isQueryEnabled}
/>
);
}
ApDexMetrics.defaultProps = {
delta: undefined,
le: undefined,
};
export default ApDexMetrics;

View File

@ -0,0 +1,36 @@
import Spinner from 'components/Spinner';
import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
import useErrorNotification from 'hooks/useErrorNotification';
import ApDexMetrics from './ApDexMetrics';
import { metricMeta } from './constants';
import { ApDexDataSwitcherProps } from './types';
function ApDexMetricsApplication({
handleGraphClick,
onDragSelect,
tagFilterItems,
topLevelOperationsRoute,
thresholdValue,
}: ApDexDataSwitcherProps): JSX.Element {
const { data, isLoading, error } = useGetMetricMeta(metricMeta);
useErrorNotification(error);
if (isLoading) {
return <Spinner height="40vh" tip="Loading..." />;
}
return (
<ApDexMetrics
handleGraphClick={handleGraphClick}
delta={data?.data.delta}
metricsBuckets={data?.data.le}
onDragSelect={onDragSelect}
topLevelOperationsRoute={topLevelOperationsRoute}
tagFilterItems={tagFilterItems}
thresholdValue={thresholdValue}
/>
);
}
export default ApDexMetricsApplication;

View File

@ -0,0 +1,62 @@
// This component is not been used in the application as we support only metrics for ApDex as of now.
// This component is been kept for future reference.
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridGraphLayout/Graph';
import { GraphTitle } from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { apDexTracesQueryBuilderQueries } from 'container/MetricsApplication/MetricsPageQueries/OverviewQueries';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { IServiceName } from '../../types';
import { ApDexDataSwitcherProps } from './types';
function ApDexTraces({
handleGraphClick,
onDragSelect,
topLevelOperationsRoute,
tagFilterItems,
thresholdValue,
}: ApDexDataSwitcherProps): JSX.Element {
const { servicename } = useParams<IServiceName>();
const apDexTracesWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: apDexTracesQueryBuilderQueries({
servicename,
tagFilterItems,
topLevelOperationsRoute,
threashold: thresholdValue || 0,
}),
clickhouse_sql: [],
id: uuid(),
},
title: GraphTitle.APDEX,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems, thresholdValue, topLevelOperationsRoute],
);
const isQueryEnabled =
topLevelOperationsRoute.length > 0 && thresholdValue !== undefined;
return (
<Graph
name="apdex"
widget={apDexTracesWidget}
onDragSelect={onDragSelect}
onClickHandler={handleGraphClick('ApDex')}
yAxisUnit=""
threshold={thresholdValue}
isQueryEnabled={isQueryEnabled}
/>
);
}
export default ApDexTraces;

View File

@ -0,0 +1 @@
export const metricMeta = 'signoz_latency_bucket';

View File

@ -0,0 +1,47 @@
import Spinner from 'components/Spinner';
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
import { useGetApDexSettings } from 'hooks/apDex/useGetApDexSettings';
import useErrorNotification from 'hooks/useErrorNotification';
import { memo } from 'react';
import { useParams } from 'react-router-dom';
import { IServiceName } from '../../types';
import ApDexMetricsApplication from './ApDexMetricsApplication';
import { ApDexApplicationProps } from './types';
function ApDexApplication({
handleGraphClick,
onDragSelect,
topLevelOperationsRoute,
tagFilterItems,
}: ApDexApplicationProps): JSX.Element {
const { servicename } = useParams<IServiceName>();
const { data, isLoading, error, isRefetching } = useGetApDexSettings(
servicename,
);
useErrorNotification(error);
if (isLoading || isRefetching) {
return (
<Card>
<Spinner height="40vh" tip="Loading..." />
</Card>
);
}
return (
<Card>
<GraphContainer>
<ApDexMetricsApplication
handleGraphClick={handleGraphClick}
onDragSelect={onDragSelect}
topLevelOperationsRoute={topLevelOperationsRoute}
tagFilterItems={tagFilterItems}
thresholdValue={data?.data[0].threshold}
/>
</GraphContainer>
</Card>
);
}
export default memo(ApDexApplication);

View File

@ -0,0 +1,19 @@
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { ClickHandlerType } from '../../Overview';
export interface ApDexApplicationProps {
handleGraphClick: (type: string) => ClickHandlerType;
onDragSelect: (start: number, end: number) => void;
topLevelOperationsRoute: string[];
tagFilterItems: TagFilterItem[];
}
export interface ApDexDataSwitcherProps extends ApDexApplicationProps {
thresholdValue?: number;
}
export interface ApDexMetricsProps extends ApDexDataSwitcherProps {
delta?: boolean;
metricsBuckets?: number[];
}

View File

@ -1,3 +1,4 @@
import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridGraphLayout/Graph/';
@ -24,6 +25,7 @@ function ServiceOverview({
selectedTraceTags,
selectedTimeStamp,
topLevelOperationsRoute,
topLevelOperationsLoading,
}: ServiceOverviewProps): JSX.Element {
const { servicename } = useParams<IServiceName>();
@ -63,6 +65,14 @@ function ServiceOverview({
const isQueryEnabled = topLevelOperationsRoute.length > 0;
if (topLevelOperationsLoading) {
return (
<Card>
<Spinner height="40vh" tip="Loading..." />
</Card>
);
}
return (
<>
<Button
@ -99,6 +109,7 @@ interface ServiceOverviewProps {
onDragSelect: (start: number, end: number) => void;
handleGraphClick: (type: string) => ClickHandlerType;
topLevelOperationsRoute: string[];
topLevelOperationsLoading: boolean;
}
export default ServiceOverview;

View File

@ -56,7 +56,19 @@ export interface LatencyProps {
topLevelOperationsRoute: string[];
}
export interface ApDexProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
topLevelOperationsRoute: string[];
threashold: number;
}
export interface TableRendererProps {
columnName: string;
renderFunction: (record: RowData) => ReactNode;
}
export interface ApDexMetricsQueryBuilderQueriesProps extends ApDexProps {
delta: boolean;
metricsBuckets: number[];
}

View File

@ -14,9 +14,13 @@ export const OPERATION_LEGENDS = ['Operations'];
export enum FORMULA {
ERROR_PERCENTAGE = 'A*100/B',
DATABASE_CALLS_AVG_DURATION = 'A/B',
APDEX_TRACES = '((B + C)/2)/A',
APDEX_DELTA_SPAN_METRICS = '(B + C/2)/A',
APDEX_CUMULATIVE_SPAN_METRICS = '((B + C)/2)/A',
}
export enum GraphTitle {
APDEX = 'Apdex',
LATENCY = 'Latency',
RATE_PER_OPS = 'Rate (ops/s)',
ERROR_PERCENTAGE = 'Error Percentage',
@ -41,6 +45,7 @@ export enum DataType {
STRING = 'string',
FLOAT64 = 'float64',
INT64 = 'int64',
BOOL = 'bool',
}
export enum MetricsType {
@ -49,7 +54,9 @@ export enum MetricsType {
}
export enum WidgetKeys {
Le = 'le',
Name = 'name',
HasError = 'hasError',
Address = 'address',
DurationNano = 'durationNano',
StatusCode = 'status_code',

View File

@ -29,6 +29,14 @@ export const Col = styled(ColComponent)`
}
`;
export const ColApDexContainer = styled(ColComponent)`
padding: 0 !important;
`;
export const ColErrorContainer = styled(ColComponent)`
padding: 2rem 0 !important;
`;
export const GraphContainer = styled.div`
height: 40vh;
`;

View File

@ -1,3 +1,4 @@
import { ReactNode } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
@ -5,7 +6,7 @@ import { IServiceName } from './Tabs/types';
export interface GetWidgetQueryBuilderProps {
query: Widgets['query'];
title?: string;
title?: ReactNode;
panelTypes: Widgets['panelTypes'];
}

View File

@ -24,3 +24,14 @@ export const navigateToTrace = ({
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1`,
);
};
export const getNearestHighestBucketValue = (
value: number,
buckets: number[],
): string => {
const nearestBucket = buckets.find((bucket) => bucket >= value);
return nearestBucket?.toString() || '+Inf';
};
export const convertMilSecToNanoSec = (value: number): number =>
value * 1000000000;

View File

@ -67,7 +67,9 @@ function NewWidget({ selectedGraph, saveSettingOfPanel }: Props): JSX.Element {
const selectedWidget = getWidget();
const [title, setTitle] = useState<string>(selectedWidget?.title || '');
const [title, setTitle] = useState<string>(
selectedWidget?.title?.toString() || '',
);
const [description, setDescription] = useState<string>(
selectedWidget?.description || '',
);

View File

@ -0,0 +1,12 @@
import { getApDexSettings } from 'api/metrics/ApDex/getApDexSettings';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import { ApDexPayloadAndSettingsProps } from 'types/api/metrics/getApDex';
export const useGetApDexSettings = (
servicename: string,
): UseQueryResult<AxiosResponse<ApDexPayloadAndSettingsProps[]>, AxiosError> =>
useQuery<AxiosResponse<ApDexPayloadAndSettingsProps[]>, AxiosError>({
queryKey: [{ servicename }],
queryFn: async () => getApDexSettings(servicename),
});

View File

@ -0,0 +1,12 @@
import { getMetricMeta } from 'api/metrics/ApDex/getMetricMeta';
import { AxiosError, AxiosResponse } from 'axios';
import { useQuery, UseQueryResult } from 'react-query';
import { MetricMetaProps } from 'types/api/metrics/getApDex';
export const useGetMetricMeta = (
metricName: string,
): UseQueryResult<AxiosResponse<MetricMetaProps>, AxiosError> =>
useQuery<AxiosResponse<MetricMetaProps>, AxiosError>({
queryKey: [{ metricName }],
queryFn: async () => getMetricMeta(metricName),
});

View File

@ -0,0 +1,21 @@
import { setApDexSettings } from 'api/metrics/ApDex/apDexSettings';
import { useMutation, UseMutationResult } from 'react-query';
import {
ApDexPayloadAndSettingsProps,
SetApDexPayloadProps,
} from 'types/api/metrics/getApDex';
export const useSetApDexSettings = ({
servicename,
threshold,
excludeStatusCode,
}: ApDexPayloadAndSettingsProps): UseMutationResult<
SetApDexPayloadProps,
Error,
ApDexPayloadAndSettingsProps
> =>
useMutation<SetApDexPayloadProps, Error, ApDexPayloadAndSettingsProps>({
mutationKey: [servicename, threshold.toString(), excludeStatusCode],
mutationFn: async () =>
setApDexSettings({ servicename, threshold, excludeStatusCode }),
});

View File

@ -0,0 +1,65 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { APPLICATION_SETTINGS } from '../constants';
import { thresholdMockData } from './__mock__/thresholdMockData';
import ApDexApplication from './ApDexApplication';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: (): {
servicename: string;
} => ({ servicename: 'mockServiceName' }),
}));
jest.mock('hooks/apDex/useGetApDexSettings', () => ({
__esModule: true,
useGetApDexSettings: jest.fn().mockReturnValue({
data: thresholdMockData,
isLoading: false,
error: null,
refetch: jest.fn(),
}),
}));
jest.mock('hooks/apDex/useSetApDexSettings', () => ({
__esModule: true,
useSetApDexSettings: jest.fn().mockReturnValue({
mutateAsync: jest.fn(),
isLoading: false,
error: null,
}),
}));
describe('ApDexApplication', () => {
it('should render the component', () => {
render(<ApDexApplication />);
expect(screen.getByText('Settings')).toBeInTheDocument();
});
it('should open the popover when the settings button is clicked', async () => {
render(<ApDexApplication />);
const button = screen.getByText('Settings');
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText(APPLICATION_SETTINGS)).toBeInTheDocument();
});
});
it('should close the popover when the close button is clicked', async () => {
render(<ApDexApplication />);
const button = screen.getByText('Settings');
fireEvent.click(button);
await waitFor(() => {
expect(screen.getByText(APPLICATION_SETTINGS)).toBeInTheDocument();
});
const closeButton = screen.getByText('Cancel');
fireEvent.click(closeButton);
await waitFor(() => {
expect(screen.queryByText(APPLICATION_SETTINGS)).not.toBeInTheDocument();
});
});
});

View File

@ -0,0 +1,56 @@
import { SettingOutlined } from '@ant-design/icons';
import { Popover } from 'antd';
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
import { useGetApDexSettings } from 'hooks/apDex/useGetApDexSettings';
import useErrorNotification from 'hooks/useErrorNotification';
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { Button } from '../styles';
import ApDexSettings from './ApDexSettings';
function ApDexApplication(): JSX.Element {
const { servicename } = useParams<IServiceName>();
const {
data,
isLoading,
error,
refetch: refetchGetApDexSetting,
} = useGetApDexSettings(servicename);
const [isOpen, setIsOpen] = useState<boolean>(false);
useErrorNotification(error);
const handlePopOverClose = (): void => {
setIsOpen(false);
};
const handleOpenChange = (newOpen: boolean): void => {
setIsOpen(newOpen);
};
return (
<Popover
placement="bottomRight"
destroyTooltipOnHide
trigger={['click']}
showArrow={false}
open={isOpen}
onOpenChange={handleOpenChange}
content={
<ApDexSettings
servicename={servicename}
handlePopOverClose={handlePopOverClose}
isLoading={isLoading}
data={data}
refetchGetApDexSetting={refetchGetApDexSetting}
/>
}
>
<Button size="middle" icon={<SettingOutlined />}>
Settings
</Button>
</Popover>
);
}
export default ApDexApplication;

View File

@ -0,0 +1,62 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { axiosResponseThresholdData } from './__mock__/axiosResponseMockThresholdData';
import ApDexSettings from './ApDexSettings';
jest.mock('hooks/apDex/useSetApDexSettings', () => ({
__esModule: true,
useSetApDexSettings: jest.fn().mockReturnValue({
mutateAsync: jest.fn(),
isLoading: false,
error: null,
}),
}));
describe('ApDexSettings', () => {
it('should render the component', () => {
render(
<ApDexSettings
servicename="mockServiceName"
handlePopOverClose={jest.fn()}
isLoading={false}
data={axiosResponseThresholdData}
refetchGetApDexSetting={jest.fn()}
/>,
);
expect(screen.getByText('Application Settings')).toBeInTheDocument();
});
it('should render the spinner when the data is loading', () => {
render(
<ApDexSettings
servicename="mockServiceName"
handlePopOverClose={jest.fn()}
isLoading
data={axiosResponseThresholdData}
refetchGetApDexSetting={jest.fn()}
/>,
);
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('should close the popover when the cancel button is clicked', async () => {
const mockHandlePopOverClose = jest.fn();
render(
<ApDexSettings
servicename="mockServiceName"
handlePopOverClose={mockHandlePopOverClose}
isLoading={false}
data={axiosResponseThresholdData}
refetchGetApDexSetting={jest.fn()}
/>,
);
const button = screen.getByText('Cancel');
fireEvent.click(button);
await waitFor(() => {
expect(mockHandlePopOverClose).toHaveBeenCalled();
});
});
});

View File

@ -0,0 +1,119 @@
import { CloseOutlined } from '@ant-design/icons';
import { Card, InputNumber } from 'antd';
import Spinner from 'components/Spinner';
import TextToolTip from 'components/TextToolTip';
import {
apDexToolTipText,
apDexToolTipUrl,
apDexToolTipUrlText,
} from 'constants/apDex';
import { themeColors } from 'constants/theme';
import { useSetApDexSettings } from 'hooks/apDex/useSetApDexSettings';
import { useNotifications } from 'hooks/useNotifications';
import { useState } from 'react';
import { APPLICATION_SETTINGS } from '../constants';
import {
AppDexThresholdContainer,
Button,
SaveAndCancelContainer,
SaveButton,
Typography,
} from '../styles';
import { onSaveApDexSettings } from '../utils';
import { ApDexSettingsProps } from './types';
function ApDexSettings({
servicename,
handlePopOverClose,
isLoading,
data,
refetchGetApDexSetting,
}: ApDexSettingsProps): JSX.Element {
const [thresholdValue, setThresholdValue] = useState(() => {
if (data) {
return data.data[0].threshold;
}
return 0;
});
const { notifications } = useNotifications();
const { isLoading: isApDexLoading, mutateAsync } = useSetApDexSettings({
servicename,
threshold: thresholdValue,
excludeStatusCode: '',
});
const handleThreadholdChange = (value: number | null): void => {
if (value !== null) {
setThresholdValue(value);
}
};
if (isLoading) {
return (
<Typography.Text style={{ color: themeColors.white }}>
<Spinner height="5vh" tip="Loading..." />
</Typography.Text>
);
}
return (
<Card
title={APPLICATION_SETTINGS}
extra={<CloseOutlined width={10} height={10} onClick={handlePopOverClose} />}
actions={[
<SaveAndCancelContainer key="SaveAndCancelContainer">
<Button onClick={handlePopOverClose}>Cancel</Button>
<SaveButton
onClick={onSaveApDexSettings({
handlePopOverClose,
mutateAsync,
notifications,
refetchGetApDexSetting,
servicename,
thresholdValue,
})}
type="primary"
loading={isApDexLoading}
>
Save
</SaveButton>
</SaveAndCancelContainer>,
]}
>
<AppDexThresholdContainer>
<Typography>
Apdex threshold (in seconds){' '}
<TextToolTip
text={apDexToolTipText}
url={apDexToolTipUrl}
useFilledIcon={false}
urlText={apDexToolTipUrlText}
/>
</Typography>
<InputNumber
value={thresholdValue}
onChange={handleThreadholdChange}
min={0}
step={0.1}
/>
</AppDexThresholdContainer>
{/* TODO: Add this feature later when backend is ready to support it. */}
{/* <ExcludeErrorCodeContainer>
<Typography.Text>
Exclude following error codes from error rate calculation
</Typography.Text>
<Input placeholder="e.g. 406, 418" />
</ExcludeErrorCodeContainer> */}
</Card>
);
}
ApDexSettings.defaultProps = {
isLoading: undefined,
data: undefined,
refetchGetApDexSetting: undefined,
};
export default ApDexSettings;

View File

@ -0,0 +1,9 @@
import { AxiosResponse } from 'axios';
export const axiosResponseThresholdData = {
data: [
{
threshold: 0.5,
},
],
} as AxiosResponse;

View File

@ -0,0 +1,7 @@
export const thresholdMockData = {
data: [
{
threshold: 0.5,
},
],
};

View File

@ -0,0 +1,10 @@
import { AxiosResponse } from 'axios';
import { ApDexPayloadAndSettingsProps } from 'types/api/metrics/getApDex';
export interface ApDexSettingsProps {
servicename: string;
handlePopOverClose: () => void;
isLoading?: boolean;
data?: AxiosResponse<ApDexPayloadAndSettingsProps[]>;
refetchGetApDexSetting?: () => void;
}

View File

@ -0,0 +1 @@
export const APPLICATION_SETTINGS = 'Application Settings';

View File

@ -8,6 +8,7 @@ import history from 'lib/history';
import { useMemo } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import ApDexApplication from './ApDex/ApDexApplication';
import { MetricsApplicationTab, TAB_KEY_VS_LABEL } from './types';
import useMetricsApplicationTabKey from './useMetricsApplicationTabKey';
@ -49,6 +50,7 @@ function MetricsApplication(): JSX.Element {
return (
<>
<ResourceAttributesFilter />
<ApDexApplication />
<RouteTab routes={routes} history={history} activeKey={activeKey} />
</>
);

View File

@ -0,0 +1,44 @@
import {
Button as ButtonComponent,
Typography as TypographyComponent,
} from 'antd';
import { themeColors } from 'constants/theme';
import styled from 'styled-components';
export const Button = styled(ButtonComponent)`
&&& {
width: min-content;
align-self: flex-end;
}
`;
export const AppDexThresholdContainer = styled.div`
display: flex;
align-items: center;
`;
export const Typography = styled(TypographyComponent)`
&&& {
width: 24rem;
margin: 0.5rem 0;
color: ${themeColors.white};
}
`;
export const SaveAndCancelContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-right: 1rem;
`;
export const SaveButton = styled(ButtonComponent)`
&&& {
margin: 0 0.5rem;
display: flex;
align-items: center;
}
`;
export const ExcludeErrorCodeContainer = styled.div`
margin: 1rem 0;
`;

View File

@ -1,3 +1,10 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import { UseMutateAsyncFunction } from 'react-query';
import {
ApDexPayloadAndSettingsProps,
SetApDexPayloadProps,
} from 'types/api/metrics/getApDex';
export enum MetricsApplicationTab {
OVER_METRICS = 'OVER_METRICS',
DB_CALL_METRICS = 'DB_CALL_METRICS',
@ -9,3 +16,16 @@ export const TAB_KEY_VS_LABEL = {
[MetricsApplicationTab.DB_CALL_METRICS]: 'DB Call Metrics',
[MetricsApplicationTab.EXTERNAL_METRICS]: 'External Metrics',
};
export interface OnSaveApDexSettingsProps {
thresholdValue: number;
servicename: string;
notifications: NotificationInstance;
refetchGetApDexSetting?: VoidFunction;
mutateAsync: UseMutateAsyncFunction<
SetApDexPayloadProps,
Error,
ApDexPayloadAndSettingsProps
>;
handlePopOverClose: VoidFunction;
}

View File

@ -1,5 +1,8 @@
import axios from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { TAB_KEYS_VS_METRICS_APPLICATION_KEY } from './config';
import { MetricsApplicationTab } from './types';
import { MetricsApplicationTab, OnSaveApDexSettingsProps } from './types';
export const isMetricsApplicationTab = (
tab: string,
@ -15,3 +18,29 @@ export const getMetricsApplicationKey = (
return MetricsApplicationTab.OVER_METRICS;
};
export const onSaveApDexSettings = ({
thresholdValue,
refetchGetApDexSetting,
mutateAsync,
notifications,
handlePopOverClose,
servicename,
}: OnSaveApDexSettingsProps) => async (): Promise<void> => {
if (!refetchGetApDexSetting) return;
try {
await mutateAsync({
servicename,
threshold: thresholdValue,
excludeStatusCode: '',
});
await refetchGetApDexSetting();
} catch (err) {
notifications.error({
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
});
} finally {
handlePopOverClose();
}
};

View File

@ -1,5 +1,6 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { ReactNode } from 'react';
import { Layout } from 'react-grid-layout';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@ -58,7 +59,7 @@ export interface IBaseWidget {
isStacked: boolean;
id: string;
panelTypes: PANEL_TYPES;
title: string;
title: ReactNode;
description: string;
opacity: string;
nullZeroValues: string;

View File

@ -0,0 +1,14 @@
export interface ApDexPayloadAndSettingsProps {
servicename: string;
threshold: number;
excludeStatusCode: string;
}
export interface SetApDexPayloadProps {
data: string;
}
export interface MetricMetaProps {
delta: boolean;
le: number[];
}

View File

@ -21,7 +21,7 @@ export interface TagFilterItem {
id: string;
key?: BaseAutocompleteData;
op: string;
value: string[] | string;
value: string[] | string | number | boolean;
}
export interface TagFilter {