From 1118c56356814444f9663482da4a3cf942aaf646 Mon Sep 17 00:00:00 2001 From: sawhil Date: Tue, 22 Apr 2025 17:34:47 +0530 Subject: [PATCH] feat: new dep. services table added --- .../DomainDetails/DomainDetails.styles.scss | 155 ++++++-- .../components/DependentServices.tsx | 103 +++--- .../src/container/ApiMonitoring/utils.tsx | 350 +++++++++++++++++- 3 files changed, 522 insertions(+), 86 deletions(-) diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss index b4dc5657e7..55f685d6dd 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/DomainDetails.styles.scss @@ -690,30 +690,140 @@ border-radius: 3px; border: 1px solid var(--bg-slate-500); - .top-services-title { - border-bottom: 1px solid var(--bg-slate-500); - padding: 10px 12px; - border-radius: 3px 3px 0px 0px; - background: rgba(171, 189, 255, 0.04); + .title-wrapper { + display: inline-flex; + padding: 1px 2px; + align-items: center; + border-radius: 2px; + background: rgba(113, 144, 249, 0.08); - .title-wrapper { - display: inline-flex; - padding: 1px 2px; - align-items: center; - border-radius: 2px; - background: rgba(113, 144, 249, 0.08); - - color: var(--bg-robin-400); - font-family: Inter; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 18px; - letter-spacing: -0.07px; - } + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; + letter-spacing: -0.07px; } .dependent-services-container { - padding: 10px 12px; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + .ant-table { + .ant-table-thead > tr > th { + padding: 12px; + font-weight: 500; + font-size: 12px; + line-height: 18px; + border-bottom: none; + + color: var(--text-vanilla-400); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 600; + line-height: 18px; + /* 163.636% */ + letter-spacing: 0.44px; + text-transform: uppercase; + background: none; + + &::before { + background-color: transparent; + } + } + + .ant-table-thead > tr > th:has(.status-code-header) { + background: var(--bg-ink-300); + opacity: 0.6; + } + + .ant-table-cell { + padding: 12px; + font-size: 13px; + line-height: 20px; + color: var(--bg-vanilla-100); + border-bottom: none; + background: var(--bg-ink-400); + } + + .ant-table-cell:has(.col-title) { + background: rgba(171, 189, 255, 0.04); + } + + .ant-table-cell:has(.top-services-item-latency) { + text-align: center; + opacity: 0.8; + background: rgba(171, 189, 255, 0.04); + } + + .ant-table-cell:has(.top-services-item-latency-title) { + text-align: center; + opacity: 0.8; + background: rgba(171, 189, 255, 0.04); + } + + .ant-table-tbody > tr:hover > td { + background: rgba(255, 255, 255, 0.04); + } + + .ant-table-cell:first-child { + text-align: justify; + } + + .ant-table-cell:nth-child(2) { + padding-left: 16px; + padding-right: 16px; + } + + .ant-table-cell:nth-child(n + 3) { + padding-right: 24px; + } + + .ant-table-tbody > tr > td { + border-bottom: none; + } + + .ant-table-thead + > tr + > th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before { + background-color: transparent; + } + + .ant-empty-normal { + visibility: hidden; + } + + .table-row-dark { + background: var(--bg-ink-300); + } + + .ant-table-content { + margin-bottom: 0px; + } + } + + .no-status-code-data-message-container { + height: 30vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .no-status-code-data-message-content { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + + width: fit-content; + padding: 24px; + } + + .no-status-code-data-message { + margin-top: 8px; + } + } + .top-services-item { display: flex; justify-content: space-between; @@ -743,6 +853,7 @@ .top-services-item-progress-bar { background-color: var(--bg-slate-400); + border-radius: 2px; height: 100%; position: absolute; top: 0; @@ -758,7 +869,7 @@ .top-services-load-more { border-top: 1px solid var(--bg-slate-500); - padding-top: 10px; + padding: 10px; color: var(--text-vanilla-400); font-family: Inter; diff --git a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx index f6b31787b4..c07396ecab 100644 --- a/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx +++ b/frontend/src/container/ApiMonitoring/Explorer/Domains/DomainDetails/components/DependentServices.tsx @@ -1,6 +1,12 @@ -import { Typography } from 'antd'; +import '../DomainDetails.styles.scss'; + +import { Table, TablePaginationConfig, Typography } from 'antd'; import Skeleton from 'antd/lib/skeleton'; -import { getFormattedDependentServicesData } from 'container/ApiMonitoring/utils'; +import { + dependentServicesColumns, + DependentServicesData, + getFormattedDependentServicesData, +} from 'container/ApiMonitoring/utils'; import { UnfoldVertical } from 'lucide-react'; import { useMemo, useState } from 'react'; import { UseQueryResult } from 'react-query'; @@ -23,19 +29,25 @@ function DependentServices({ isRefetching, } = dependentServicesQuery; - const [currentRenderCount, setCurrentRenderCount] = useState(0); + const [isExpanded, setIsExpanded] = useState(false); - const dependentServicesData = useMemo(() => { - const formattedDependentServicesData = getFormattedDependentServicesData( - data?.payload?.data?.result[0].table.rows, - ); - setCurrentRenderCount(Math.min(formattedDependentServicesData.length, 5)); - return formattedDependentServicesData; - }, [data]); + const handleShowMoreClick = (): void => { + setIsExpanded((prev) => !prev); + }; - const renderItems = useMemo( - () => dependentServicesData.slice(0, currentRenderCount), - [currentRenderCount, dependentServicesData], + const dependentServicesData = useMemo( + (): DependentServicesData[] => + getFormattedDependentServicesData(data?.payload?.data?.result[0].table.rows), + [data], + ); + + const paginationConfig = useMemo( + (): TablePaginationConfig => ({ + pageSize: isExpanded ? dependentServicesData.length : 5, + hideOnSinglePage: true, + position: ['none', 'none'], + }), + [isExpanded, dependentServicesData.length], ); if (isLoading || isRefetching) { @@ -48,56 +60,47 @@ function DependentServices({ return (
-
- Dependent Services -
- {renderItems.length === 0 ? ( -
-
- thinking-emoji + +
+ thinking-emoji - - This query had no results. Edit your query and try again! - -
- - ) : ( - renderItems.map((item) => ( -
-
-
{item.serviceName}
-
{item.count}
-
-
-
- {item.percentage.toFixed(2)}% -
-
- )) - )} + + This query had no results. Edit your query and try again! + +
+ + ), + }} + /> - {currentRenderCount < dependentServicesData.length && ( + {dependentServicesData.length > 5 && (
setCurrentRenderCount(dependentServicesData.length)} + onClick={handleShowMoreClick} onKeyDown={(e): void => { if (e.key === 'Enter') { - setCurrentRenderCount(dependentServicesData.length); + handleShowMoreClick(); } }} role="button" tabIndex={0} > - Show more... + {isExpanded ? 'Show less...' : 'Show more...'}
)} diff --git a/frontend/src/container/ApiMonitoring/utils.tsx b/frontend/src/container/ApiMonitoring/utils.tsx index 80a6549f50..44769befbc 100644 --- a/frontend/src/container/ApiMonitoring/utils.tsx +++ b/frontend/src/container/ApiMonitoring/utils.tsx @@ -15,6 +15,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import { cloneDeep } from 'lodash-es'; import { ArrowUpDown, ChevronDown, ChevronRight } from 'lucide-react'; import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil'; +import { ReactNode } from 'react'; import { Widgets } from 'types/api/dashboard/getAll'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { @@ -1472,14 +1473,14 @@ export const getEndPointDetailsQueryPayload = ( key: 'span_id', type: '', }, - aggregateOperator: 'count', + aggregateOperator: 'count_distinct', dataSource: DataSource.TRACES, disabled: false, expression: 'A', filters: { items: [ { - id: 'a32988a4', + id: 'bdac4904', key: { dataType: DataTypes.String, id: 'http.url--string--tag--false', @@ -1492,7 +1493,7 @@ export const getEndPointDetailsQueryPayload = ( value: endPointName, }, { - id: '5a15032f', + id: 'b78ff216', key: { dataType: DataTypes.String, id: 'net.peer.name--string--tag--false', @@ -1527,10 +1528,227 @@ export const getEndPointDetailsQueryPayload = ( reduceTo: 'avg', spaceAggregation: 'sum', stepInterval: 60, - timeAggregation: 'count', + timeAggregation: 'count_distinct', + }, + { + aggregateAttribute: { + dataType: DataTypes.Float64, + id: 'duration_nano--float64----true', + isColumn: true, + isJSON: false, + key: 'duration_nano', + type: '', + }, + aggregateOperator: 'p99', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'B', + filters: { + items: [ + { + id: '74f9d185', + key: { + dataType: DataTypes.String, + id: 'http.url--string--tag--false', + isColumn: false, + isJSON: false, + key: 'http.url', + type: 'tag', + }, + op: '=', + value: endPointName, + }, + { + id: 'a9024472', + key: { + dataType: DataTypes.String, + id: 'net.peer.name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'net.peer.name', + type: 'tag', + }, + op: '=', + value: domainName, + }, + ...filters.items, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'service.name--string--resource--true', + isColumn: true, + isJSON: false, + key: 'service.name', + type: 'resource', + }, + ], + having: [], + legend: 'p99 latency', + limit: null, + orderBy: [], + queryName: 'B', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'p99', + }, + { + aggregateAttribute: { + dataType: DataTypes.String, + id: '------false', + isColumn: false, + key: '', + type: '', + }, + aggregateOperator: 'rate', + dataSource: DataSource.TRACES, + disabled: false, + expression: 'C', + filters: { + items: [ + { + id: 'b7e36a72', + key: { + dataType: DataTypes.String, + id: 'http.url--string--tag--false', + isColumn: false, + isJSON: false, + key: 'http.url', + type: 'tag', + }, + op: '=', + value: endPointName, + }, + { + id: '1b6c062d', + key: { + dataType: DataTypes.String, + id: 'net.peer.name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'net.peer.name', + type: 'tag', + }, + op: '=', + value: domainName, + }, + ...filters.items, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'service.name--string--resource--true', + isColumn: true, + isJSON: false, + key: 'service.name', + type: 'resource', + }, + ], + having: [], + legend: 'request per second', + limit: null, + orderBy: [], + queryName: 'C', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'rate', + }, + { + aggregateAttribute: { + dataType: DataTypes.String, + id: 'span_id--string----true', + isColumn: true, + isJSON: false, + key: 'span_id', + type: '', + }, + aggregateOperator: 'count_distinct', + dataSource: DataSource.TRACES, + disabled: true, + expression: 'D', + filters: { + items: [ + { + id: 'ede7cbfe', + key: { + dataType: DataTypes.String, + id: 'http.url--string--tag--false', + isColumn: false, + isJSON: false, + key: 'http.url', + type: 'tag', + }, + op: '=', + value: endPointName, + }, + { + id: 'd14792a8', + key: { + dataType: DataTypes.String, + id: 'net.peer.name--string--tag--false', + isColumn: false, + isJSON: false, + key: 'net.peer.name', + type: 'tag', + }, + op: '=', + value: domainName, + }, + { + id: '3212bf1a', + key: { + dataType: DataTypes.bool, + id: 'has_error--bool----true', + isColumn: true, + isJSON: false, + key: 'has_error', + type: '', + }, + op: '=', + value: 'true', + }, + ...filters.items, + ], + op: 'AND', + }, + functions: [], + groupBy: [ + { + dataType: DataTypes.String, + id: 'service.name--string--resource--true', + isColumn: true, + isJSON: false, + key: 'service.name', + type: 'resource', + }, + ], + having: [], + legend: 'count', + limit: null, + orderBy: [], + queryName: 'D', + reduceTo: 'avg', + spaceAggregation: 'sum', + stepInterval: 60, + timeAggregation: 'count_distinct', + }, + ], + queryFormulas: [ + { + queryName: 'F1', + expression: '(D/A)*100', + disabled: false, + legend: 'error percentage', }, ], - queryFormulas: [], }, clickhouse_sql: [ { @@ -1963,31 +2181,135 @@ export const getFormattedEndPointDropDownData = ( interface DependentServicesResponseRow { data: { ['service.name']: string; - A: number; + A: number | string; + B: number | string; + C: number | string; + F1: number | string; }; } -interface DependentServicesData { - key: string; +export interface ServiceData { serviceName: string; - count: number; - percentage: number; + count: number | string; + percentage: number | string; +} + +export interface DependentServicesData { + key: string; + serviceData: ServiceData; + latency: number | string; + rate: number | string; + errorPercentage: number | string; } // Discuss once about type safety of this function export const getFormattedDependentServicesData = ( data: DependentServicesResponseRow[], + // eslint-disable-next-line sonarjs/cognitive-complexity ): DependentServicesData[] => { if (!data) return []; - const totalCount = data?.reduce((acc, row) => acc + row.data.A, 0); + const totalCount = data?.reduce((acc, row) => acc + Number(row.data.A), 0); return data?.map((row) => ({ key: v4(), - serviceName: row.data['service.name'], - count: row.data.A, - percentage: Number(((row.data.A / totalCount) * 100).toFixed(2)), + serviceData: { + serviceName: row.data['service.name'] || '-', + count: + row.data.A !== undefined && row.data.A !== '-' && row.data.A !== 'n/a' + ? Number(row.data.A) + : '-', + percentage: + totalCount > 0 && + row.data.A !== undefined && + row.data.A !== '-' && + row.data.A !== 'n/a' + ? Number(((Number(row.data.A) / totalCount) * 100).toFixed(2)) + : 0, + }, + latency: + row.data.B !== undefined && row.data.B !== '-' && row.data.B !== 'n/a' + ? Math.round(Number(row.data.B) / 1000000) + : '-', + rate: + row.data.C !== undefined && row.data.C !== '-' && row.data.C !== 'n/a' + ? row.data.C + : '-', + errorPercentage: + row.data.F1 !== undefined && row.data.F1 !== '-' && row.data.F1 !== 'n/a' + ? row.data.F1 + : 0, })); }; +export const dependentServicesColumns: ColumnType[] = [ + { + title: Dependent Services, + dataIndex: 'serviceData', + key: 'serviceData', + render: (serviceData: ServiceData): ReactNode => ( +
+
+
{serviceData.serviceName}
+
{serviceData.count}
+
+
+
+ {typeof serviceData.percentage === 'number' + ? serviceData.percentage.toFixed(2) + : serviceData.percentage} + % +
+
+ ), + sorter: (a: DependentServicesData, b: DependentServicesData): number => + Number(a.serviceData.count) - Number(b.serviceData.count), + }, + { + title: ( + + AVG. LATENCY + + ), + dataIndex: 'latency', + key: 'latency', + render: (latency: number): ReactNode => ( +
{latency || '-'}ms
+ ), + sorter: (a: DependentServicesData, b: DependentServicesData): number => + Number(a.latency) - Number(b.latency), + }, + { + title: ( + + ERROR % + + ), + dataIndex: 'errorPercentage', + key: 'errorPercentage', + align: 'center', + render: (errorPercentage: number): ReactNode => ( +
{errorPercentage}%
+ ), + sorter: (a: DependentServicesData, b: DependentServicesData): number => + Number(a.errorPercentage) - Number(b.errorPercentage), + }, + { + title: ( + AVG. RATE + ), + dataIndex: 'rate', + key: 'rate', + align: 'right', + render: (rate: number): ReactNode => ( +
{rate || '-'} ops/sec
+ ), + sorter: (a: DependentServicesData, b: DependentServicesData): number => + Number(a.rate) - Number(b.rate), + }, +]; + export const getFormattedChartData = ( data: MetricRangePayloadProps, newLegendArray: string[],