mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 03:35:52 +08:00
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:
parent
f8ec850670
commit
8844144c01
16
frontend/src/api/metrics/ApDex/apDexSettings.ts
Normal file
16
frontend/src/api/metrics/ApDex/apDexSettings.ts
Normal 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,
|
||||
});
|
8
frontend/src/api/metrics/ApDex/getApDexSettings.ts
Normal file
8
frontend/src/api/metrics/ApDex/getApDexSettings.ts
Normal 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}`);
|
8
frontend/src/api/metrics/ApDex/getMetricMeta.ts
Normal file
8
frontend/src/api/metrics/ApDex/getMetricMeta.ts
Normal 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}`);
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
5
frontend/src/constants/apDex.ts
Normal file
5
frontend/src/constants/apDex.ts
Normal 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.';
|
@ -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>
|
||||
|
@ -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],
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
@ -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],
|
||||
};
|
||||
|
||||
|
@ -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]};
|
||||
}
|
||||
`;
|
||||
|
@ -10,3 +10,7 @@ export interface MenuItem {
|
||||
disabled: boolean;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayThresholdProps {
|
||||
threshold: ReactNode;
|
||||
}
|
||||
|
@ -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;
|
||||
|
12
frontend/src/container/GridPanelSwitch/utils.ts
Normal file
12
frontend/src/container/GridPanelSwitch/utils.ts
Normal 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() || '';
|
||||
};
|
@ -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
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ChartData } from 'chart.js';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export type GridValueComponentProps = {
|
||||
data: ChartData;
|
||||
title?: string;
|
||||
title?: ReactNode;
|
||||
yAxisUnit?: string;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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}>
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -0,0 +1 @@
|
||||
export const metricMeta = 'signoz_latency_bucket';
|
@ -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);
|
@ -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[];
|
||||
}
|
@ -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;
|
||||
|
@ -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[];
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
`;
|
||||
|
@ -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'];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 || '',
|
||||
);
|
||||
|
12
frontend/src/hooks/apDex/useGetApDexSettings.ts
Normal file
12
frontend/src/hooks/apDex/useGetApDexSettings.ts
Normal 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),
|
||||
});
|
12
frontend/src/hooks/apDex/useGetMetricMeta.ts
Normal file
12
frontend/src/hooks/apDex/useGetMetricMeta.ts
Normal 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),
|
||||
});
|
21
frontend/src/hooks/apDex/useSetApDexSettings.ts
Normal file
21
frontend/src/hooks/apDex/useSetApDexSettings.ts
Normal 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 }),
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
119
frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx
Normal file
119
frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx
Normal 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;
|
@ -0,0 +1,9 @@
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export const axiosResponseThresholdData = {
|
||||
data: [
|
||||
{
|
||||
threshold: 0.5,
|
||||
},
|
||||
],
|
||||
} as AxiosResponse;
|
@ -0,0 +1,7 @@
|
||||
export const thresholdMockData = {
|
||||
data: [
|
||||
{
|
||||
threshold: 0.5,
|
||||
},
|
||||
],
|
||||
};
|
10
frontend/src/pages/MetricsApplication/ApDex/types.ts
Normal file
10
frontend/src/pages/MetricsApplication/ApDex/types.ts
Normal 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;
|
||||
}
|
1
frontend/src/pages/MetricsApplication/constants.ts
Normal file
1
frontend/src/pages/MetricsApplication/constants.ts
Normal file
@ -0,0 +1 @@
|
||||
export const APPLICATION_SETTINGS = 'Application Settings';
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
|
44
frontend/src/pages/MetricsApplication/styles.ts
Normal file
44
frontend/src/pages/MetricsApplication/styles.ts
Normal 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;
|
||||
`;
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
14
frontend/src/types/api/metrics/getApDex.ts
Normal file
14
frontend/src/types/api/metrics/getApDex.ts
Normal 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[];
|
||||
}
|
@ -21,7 +21,7 @@ export interface TagFilterItem {
|
||||
id: string;
|
||||
key?: BaseAutocompleteData;
|
||||
op: string;
|
||||
value: string[] | string;
|
||||
value: string[] | string | number | boolean;
|
||||
}
|
||||
|
||||
export interface TagFilter {
|
||||
|
Loading…
x
Reference in New Issue
Block a user