From 8844144c01ca95e391900cd80a78f5286283a70d Mon Sep 17 00:00:00 2001 From: Rajat Dabade Date: Wed, 16 Aug 2023 12:18:56 +0530 Subject: [PATCH] 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 Co-authored-by: Srikanth Chekuri --- .../src/api/metrics/ApDex/apDexSettings.ts | 16 + .../src/api/metrics/ApDex/getApDexSettings.ts | 8 + .../src/api/metrics/ApDex/getMetricMeta.ts | 8 + frontend/src/components/Graph/index.tsx | 8 +- frontend/src/components/Graph/types.ts | 4 +- frontend/src/components/TextToolTip/index.tsx | 36 +- frontend/src/constants/apDex.ts | 5 + .../Graph/WidgetGraphComponent.tsx | 2 + .../container/GridGraphLayout/Graph/index.tsx | 10 +- .../container/GridGraphLayout/Graph/types.ts | 4 +- .../WidgetHeader/DisplayThreshold.tsx | 19 + .../GridGraphLayout/WidgetHeader/index.tsx | 14 +- .../GridGraphLayout/WidgetHeader/styles.ts | 33 ++ .../GridGraphLayout/WidgetHeader/types.ts | 4 + .../src/container/GridPanelSwitch/types.ts | 3 +- .../src/container/GridPanelSwitch/utils.ts | 12 + .../container/GridValueComponent/index.tsx | 6 +- .../src/container/GridValueComponent/types.ts | 3 +- .../MetricsPageQueries/OverviewQueries.ts | 367 +++++++++++++++++- .../MetricsApplication/Tabs/Overview.tsx | 74 ++-- .../Tabs/Overview/ApDex/ApDexMetrics.tsx | 102 +++++ .../ApDex/ApDexMetricsApplication.tsx | 36 ++ .../Tabs/Overview/ApDex/ApDexTraces.tsx | 62 +++ .../Tabs/Overview/ApDex/constants.ts | 1 + .../Tabs/Overview/ApDex/index.tsx | 47 +++ .../Tabs/Overview/ApDex/types.ts | 19 + .../Tabs/Overview/ServiceOverview.tsx | 11 + .../MetricsApplication/Tabs/types.ts | 12 + .../container/MetricsApplication/constant.ts | 7 + .../container/MetricsApplication/styles.ts | 8 + .../src/container/MetricsApplication/types.ts | 3 +- .../src/container/MetricsApplication/utils.ts | 11 + frontend/src/container/NewWidget/index.tsx | 4 +- .../src/hooks/apDex/useGetApDexSettings.ts | 12 + frontend/src/hooks/apDex/useGetMetricMeta.ts | 12 + .../src/hooks/apDex/useSetApDexSettings.ts | 21 + .../ApDex/ApDexApplication.test.tsx | 65 ++++ .../ApDex/ApDexApplication.tsx | 56 +++ .../ApDex/ApDexSettings.test.tsx | 62 +++ .../ApDex/ApDexSettings.tsx | 119 ++++++ .../axiosResponseMockThresholdData.ts | 9 + .../ApDex/__mock__/thresholdMockData.ts | 7 + .../pages/MetricsApplication/ApDex/types.ts | 10 + .../src/pages/MetricsApplication/constants.ts | 1 + .../src/pages/MetricsApplication/index.tsx | 2 + .../src/pages/MetricsApplication/styles.ts | 44 +++ .../src/pages/MetricsApplication/types.ts | 20 + .../src/pages/MetricsApplication/utils.ts | 31 +- frontend/src/types/api/dashboard/getAll.ts | 3 +- frontend/src/types/api/metrics/getApDex.ts | 14 + .../api/queryBuilder/queryBuilderData.ts | 2 +- 51 files changed, 1398 insertions(+), 51 deletions(-) create mode 100644 frontend/src/api/metrics/ApDex/apDexSettings.ts create mode 100644 frontend/src/api/metrics/ApDex/getApDexSettings.ts create mode 100644 frontend/src/api/metrics/ApDex/getMetricMeta.ts create mode 100644 frontend/src/constants/apDex.ts create mode 100644 frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx create mode 100644 frontend/src/container/GridPanelSwitch/utils.ts create mode 100644 frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx create mode 100644 frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx create mode 100644 frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx create mode 100644 frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts create mode 100644 frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx create mode 100644 frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts create mode 100644 frontend/src/hooks/apDex/useGetApDexSettings.ts create mode 100644 frontend/src/hooks/apDex/useGetMetricMeta.ts create mode 100644 frontend/src/hooks/apDex/useSetApDexSettings.ts create mode 100644 frontend/src/pages/MetricsApplication/ApDex/ApDexApplication.test.tsx create mode 100644 frontend/src/pages/MetricsApplication/ApDex/ApDexApplication.tsx create mode 100644 frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx create mode 100644 frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx create mode 100644 frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts create mode 100644 frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts create mode 100644 frontend/src/pages/MetricsApplication/ApDex/types.ts create mode 100644 frontend/src/pages/MetricsApplication/constants.ts create mode 100644 frontend/src/pages/MetricsApplication/styles.ts create mode 100644 frontend/src/types/api/metrics/getApDex.ts diff --git a/frontend/src/api/metrics/ApDex/apDexSettings.ts b/frontend/src/api/metrics/ApDex/apDexSettings.ts new file mode 100644 index 0000000000..e3d69c9f15 --- /dev/null +++ b/frontend/src/api/metrics/ApDex/apDexSettings.ts @@ -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 => + axios.post('/settings/apdex', { + servicename, + threshold, + excludeStatusCode, + }); diff --git a/frontend/src/api/metrics/ApDex/getApDexSettings.ts b/frontend/src/api/metrics/ApDex/getApDexSettings.ts new file mode 100644 index 0000000000..4dcb96c760 --- /dev/null +++ b/frontend/src/api/metrics/ApDex/getApDexSettings.ts @@ -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> => + axios.get(`/settings/apdex?services=${servicename}`); diff --git a/frontend/src/api/metrics/ApDex/getMetricMeta.ts b/frontend/src/api/metrics/ApDex/getMetricMeta.ts new file mode 100644 index 0000000000..36466e1e69 --- /dev/null +++ b/frontend/src/api/metrics/ApDex/getMetricMeta.ts @@ -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> => + axios.get(`/metric_meta?metricName=${metricName}`); diff --git a/frontend/src/components/Graph/index.tsx b/frontend/src/components/Graph/index.tsx index d1deb08c4e..0065f6b33c 100644 --- a/frontend/src/components/Graph/index.tsx +++ b/frontend/src/components/Graph/index.tsx @@ -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( const nearestDatasetIndex = useRef(null); const chartRef = useRef(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( const options: CustomChartOptions = getGraphOptions( animate, staticLine, - title, + gridTitle, nearestDatasetIndex, yAxisUnit, onDragSelect, @@ -154,7 +156,7 @@ const Graph = forwardRef( }, [ animate, staticLine, - title, + gridTitle, yAxisUnit, onDragSelect, dragSelectColor, diff --git a/frontend/src/components/Graph/types.ts b/frontend/src/components/Graph/types.ts index dcb9607a0c..b005e24c80 100644 --- a/frontend/src/components/Graph/types.ts +++ b/frontend/src/components/Graph/types.ts @@ -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; diff --git a/frontend/src/components/TextToolTip/index.tsx b/frontend/src/components/TextToolTip/index.tsx index f01a623988..1bcae8454d 100644 --- a/frontend/src/components/TextToolTip/index.tsx +++ b/frontend/src/components/TextToolTip/index.tsx @@ -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 && ( - here + {urlText || 'here'} )} ), - [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 ( - + {useFilledIcon ? ( + + ) : ( + + )} ); } TextToolTip.defaultProps = { url: '', + urlText: '', + useFilledIcon: true, }; interface TextToolTipProps { url?: string; text: string; + useFilledIcon?: boolean; + urlText?: string; } export default TextToolTip; diff --git a/frontend/src/constants/apDex.ts b/frontend/src/constants/apDex.ts new file mode 100644 index 0000000000..a49b8b9694 --- /dev/null +++ b/frontend/src/constants/apDex.ts @@ -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.'; diff --git a/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx b/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx index 75a69f129b..4d3d6867ca 100644 --- a/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/WidgetGraphComponent.tsx @@ -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} /> diff --git a/frontend/src/container/GridGraphLayout/Graph/index.tsx b/frontend/src/container/GridGraphLayout/Graph/index.tsx index f363e02356..1256a29f7b 100644 --- a/frontend/src/container/GridGraphLayout/Graph/index.tsx +++ b/frontend/src/container/GridGraphLayout/Graph/index.tsx @@ -29,6 +29,7 @@ function GridCardGraph({ onClickHandler, headerMenuList = [MenuItemKeys.View], isQueryEnabled, + threshold, }: GridCardGraphProps): JSX.Element { const { isAddWidget } = useSelector( (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 ; } - if (queryResponse.isError && !isEmptyLayout) { + if ((queryResponse.isError && !isEmptyLayout) || !isQueryEnabled) { return ( {!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], }; diff --git a/frontend/src/container/GridGraphLayout/Graph/types.ts b/frontend/src/container/GridGraphLayout/Graph/types.ts index cbcd9d8718..49b637a178 100644 --- a/frontend/src/container/GridGraphLayout/Graph/types.ts +++ b/frontend/src/container/GridGraphLayout/Graph/types.ts @@ -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>; onDragSelect?: (start: number, end: number) => void; onClickHandler?: GraphOnClickHandler; + threshold?: ReactNode; headerMenuList: MenuItemKeys[]; } @@ -50,6 +51,7 @@ export interface GridCardGraphProps { setLayout?: Dispatch>; onDragSelect?: (start: number, end: number) => void; onClickHandler?: GraphOnClickHandler; + threshold?: ReactNode; headerMenuList?: WidgetGraphComponentProps['headerMenuList']; isQueryEnabled: boolean; } diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx new file mode 100644 index 0000000000..e3f8ad7d98 --- /dev/null +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/DisplayThreshold.tsx @@ -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 ( + + Threshold + {threshold || } + + ); +} + +export default DisplayThreshold; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx index 98766ab54b..0e0829ecce 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx @@ -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 | 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 ( -
+ + {threshold} {queryResponse.isFetching && !queryResponse.isError && ( )} @@ -204,13 +209,14 @@ function WidgetHeader({ )} -
+ ); } WidgetHeader.defaultProps = { onDelete: undefined, onClone: undefined, + threshold: undefined, headerMenuList: [MenuItemKeys.View], }; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts b/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts index 7f48b37681..cb2e95fcc5 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/styles.ts @@ -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]}; + } +`; diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/types.ts b/frontend/src/container/GridGraphLayout/WidgetHeader/types.ts index 0891c6634c..65e355574c 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/types.ts +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/types.ts @@ -10,3 +10,7 @@ export interface MenuItem { disabled: boolean; danger?: boolean; } + +export interface DisplayThresholdProps { + threshold: ReactNode; +} diff --git a/frontend/src/container/GridPanelSwitch/types.ts b/frontend/src/container/GridPanelSwitch/types.ts index 2b2483f671..c7703aee56 100644 --- a/frontend/src/container/GridPanelSwitch/types.ts +++ b/frontend/src/container/GridPanelSwitch/types.ts @@ -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; diff --git a/frontend/src/container/GridPanelSwitch/utils.ts b/frontend/src/container/GridPanelSwitch/utils.ts new file mode 100644 index 0000000000..dc9d3a863f --- /dev/null +++ b/frontend/src/container/GridPanelSwitch/utils.ts @@ -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() || ''; +}; diff --git a/frontend/src/container/GridValueComponent/index.tsx b/frontend/src/container/GridValueComponent/index.tsx index 73275bee90..be9a3890ba 100644 --- a/frontend/src/container/GridValueComponent/index.tsx +++ b/frontend/src/container/GridValueComponent/index.tsx @@ -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 ( <> - {title} + {gridTitle} { + 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, diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index e18afc7683..b7ab171ccd 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -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} /> @@ -221,28 +229,48 @@ function Application(): JSX.Element { - + + + + + + - + + diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx new file mode 100644 index 0000000000..b20613bae7 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetrics.tsx @@ -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(); + + 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: ( + + {GraphTitle.APDEX} + + + ), + panelTypes: PANEL_TYPES.TIME_SERIES, + }), + [ + delta, + metricsBuckets, + servicename, + tagFilterItems, + thresholdValue, + topLevelOperationsRoute, + ], + ); + + const threshold: ReactNode = useMemo(() => { + if (thresholdValue) return ; + return null; + }, [thresholdValue]); + + const isQueryEnabled = + topLevelOperationsRoute.length > 0 && + metricsBuckets && + metricsBuckets?.length > 0 && + delta !== undefined; + + return ( + + ); +} + +ApDexMetrics.defaultProps = { + delta: undefined, + le: undefined, +}; + +export default ApDexMetrics; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx new file mode 100644 index 0000000000..0e1486b852 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexMetricsApplication.tsx @@ -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 ; + } + + return ( + + ); +} + +export default ApDexMetricsApplication; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx new file mode 100644 index 0000000000..bf6297785f --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/ApDexTraces.tsx @@ -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(); + + 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 ( + + ); +} + +export default ApDexTraces; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts new file mode 100644 index 0000000000..91467b372f --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/constants.ts @@ -0,0 +1 @@ +export const metricMeta = 'signoz_latency_bucket'; diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx new file mode 100644 index 0000000000..f69e871bb6 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/index.tsx @@ -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(); + const { data, isLoading, error, isRefetching } = useGetApDexSettings( + servicename, + ); + useErrorNotification(error); + + if (isLoading || isRefetching) { + return ( + + + + ); + } + + return ( + + + + + + ); +} + +export default memo(ApDexApplication); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts new file mode 100644 index 0000000000..e3046261f0 --- /dev/null +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ApDex/types.ts @@ -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[]; +} diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx index c8ea330fb4..52014ac48b 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview/ServiceOverview.tsx @@ -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(); @@ -63,6 +65,14 @@ function ServiceOverview({ const isQueryEnabled = topLevelOperationsRoute.length > 0; + if (topLevelOperationsLoading) { + return ( + + + + ); + } + return ( <> + + ); +} + +export default ApDexApplication; diff --git a/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx new file mode 100644 index 0000000000..daa95d1d56 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.test.tsx @@ -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( + , + ); + + expect(screen.getByText('Application Settings')).toBeInTheDocument(); + }); + + it('should render the spinner when the data is loading', () => { + render( + , + ); + + expect(screen.getByText('Loading...')).toBeInTheDocument(); + }); + + it('should close the popover when the cancel button is clicked', async () => { + const mockHandlePopOverClose = jest.fn(); + render( + , + ); + + const button = screen.getByText('Cancel'); + fireEvent.click(button); + await waitFor(() => { + expect(mockHandlePopOverClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx new file mode 100644 index 0000000000..b0d5cdba02 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/ApDexSettings.tsx @@ -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 ( + + + + ); + } + + return ( + } + actions={[ + + + + Save + + , + ]} + > + + + Apdex threshold (in seconds){' '} + + + + + {/* TODO: Add this feature later when backend is ready to support it. */} + {/* + + Exclude following error codes from error rate calculation + + + */} + + ); +} + +ApDexSettings.defaultProps = { + isLoading: undefined, + data: undefined, + refetchGetApDexSetting: undefined, +}; + +export default ApDexSettings; diff --git a/frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts b/frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts new file mode 100644 index 0000000000..c0028a91c2 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/__mock__/axiosResponseMockThresholdData.ts @@ -0,0 +1,9 @@ +import { AxiosResponse } from 'axios'; + +export const axiosResponseThresholdData = { + data: [ + { + threshold: 0.5, + }, + ], +} as AxiosResponse; diff --git a/frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts b/frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts new file mode 100644 index 0000000000..ac681612a1 --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/__mock__/thresholdMockData.ts @@ -0,0 +1,7 @@ +export const thresholdMockData = { + data: [ + { + threshold: 0.5, + }, + ], +}; diff --git a/frontend/src/pages/MetricsApplication/ApDex/types.ts b/frontend/src/pages/MetricsApplication/ApDex/types.ts new file mode 100644 index 0000000000..37a931886c --- /dev/null +++ b/frontend/src/pages/MetricsApplication/ApDex/types.ts @@ -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; + refetchGetApDexSetting?: () => void; +} diff --git a/frontend/src/pages/MetricsApplication/constants.ts b/frontend/src/pages/MetricsApplication/constants.ts new file mode 100644 index 0000000000..de7f19143e --- /dev/null +++ b/frontend/src/pages/MetricsApplication/constants.ts @@ -0,0 +1 @@ +export const APPLICATION_SETTINGS = 'Application Settings'; diff --git a/frontend/src/pages/MetricsApplication/index.tsx b/frontend/src/pages/MetricsApplication/index.tsx index afd5fc4881..94cbd5d99e 100644 --- a/frontend/src/pages/MetricsApplication/index.tsx +++ b/frontend/src/pages/MetricsApplication/index.tsx @@ -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 ( <> + ); diff --git a/frontend/src/pages/MetricsApplication/styles.ts b/frontend/src/pages/MetricsApplication/styles.ts new file mode 100644 index 0000000000..8d3bb451bc --- /dev/null +++ b/frontend/src/pages/MetricsApplication/styles.ts @@ -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; +`; diff --git a/frontend/src/pages/MetricsApplication/types.ts b/frontend/src/pages/MetricsApplication/types.ts index 0bd7166eaa..b5f6d49731 100644 --- a/frontend/src/pages/MetricsApplication/types.ts +++ b/frontend/src/pages/MetricsApplication/types.ts @@ -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; +} diff --git a/frontend/src/pages/MetricsApplication/utils.ts b/frontend/src/pages/MetricsApplication/utils.ts index ec47b3ca62..5d785e629a 100644 --- a/frontend/src/pages/MetricsApplication/utils.ts +++ b/frontend/src/pages/MetricsApplication/utils.ts @@ -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 => { + 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(); + } +}; diff --git a/frontend/src/types/api/dashboard/getAll.ts b/frontend/src/types/api/dashboard/getAll.ts index 69a3dc6401..886de6ec07 100644 --- a/frontend/src/types/api/dashboard/getAll.ts +++ b/frontend/src/types/api/dashboard/getAll.ts @@ -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; diff --git a/frontend/src/types/api/metrics/getApDex.ts b/frontend/src/types/api/metrics/getApDex.ts new file mode 100644 index 0000000000..051f3994ff --- /dev/null +++ b/frontend/src/types/api/metrics/getApDex.ts @@ -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[]; +} diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index 926785e072..caf2d0554d 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -21,7 +21,7 @@ export interface TagFilterItem { id: string; key?: BaseAutocompleteData; op: string; - value: string[] | string; + value: string[] | string | number | boolean; } export interface TagFilter {