feat: new dep services top 10 errors localised date picker agrregate domain details etc

This commit is contained in:
sawhil 2025-04-25 16:27:06 +05:30 committed by Sahil Khan
parent 8b30e3cc5c
commit 1123a9a93d
16 changed files with 617 additions and 290 deletions

View File

@ -54,6 +54,7 @@ export const REACT_QUERY_KEY = {
// API Monitoring Query Keys // API Monitoring Query Keys
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST', GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
GET_DOMAIN_METRICS_DATA: 'GET_DOMAIN_METRICS_DATA',
GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN', GET_ENDPOINTS_LIST_BY_DOMAIN: 'GET_ENDPOINTS_LIST_BY_DOMAIN',
GET_TOP_ERRORS_BY_DOMAIN: 'GET_TOP_ERRORS_BY_DOMAIN', GET_TOP_ERRORS_BY_DOMAIN: 'GET_TOP_ERRORS_BY_DOMAIN',
GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST', GET_NESTED_ENDPOINTS_LIST: 'GET_NESTED_ENDPOINTS_LIST',

View File

@ -159,7 +159,7 @@ function AllEndPoints({
const handleRowClick = (record: EndPointsTableRowData): void => { const handleRowClick = (record: EndPointsTableRowData): void => {
if (groupBy.length === 0) { if (groupBy.length === 0) {
setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab setSelectedEndPointName(record.endpointName); // this will open up the endpoint details tab
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS); setSelectedView(VIEW_TYPES.ENDPOINT_STATS);
logEvent('API Monitoring: Endpoint name row clicked', {}); logEvent('API Monitoring: Endpoint name row clicked', {});
} else { } else {
handleGroupByRowClick(record); // this will prepare the nested query payload handleGroupByRowClick(record); // this will prepare the nested query payload

View File

@ -252,6 +252,9 @@
border: 1px solid var(--bg-slate-500); border: 1px solid var(--bg-slate-500);
.endpoints-table-header { .endpoints-table-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px; padding: 12px;
color: var(--Vanilla-100, #fff); color: var(--Vanilla-100, #fff);
font-family: Inter; font-family: Inter;
@ -392,6 +395,21 @@
padding-top: 20px; padding-top: 20px;
} }
.top-errors-dropdown-container {
display: flex;
flex-direction: row;
gap: 10px;
align-items: center;
.endpoint-details-filters-container-dropdown {
width: 100%;
}
.endpoint-details-filters-container-search {
flex: 1;
}
}
.endpoint-details-container { .endpoint-details-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -20,7 +20,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import AllEndPoints from './AllEndPoints'; import AllEndPoints from './AllEndPoints';
import DomainMetrics from './components/DomainMetrics'; import DomainMetrics from './components/DomainMetrics';
import { VIEW_TYPES, VIEWS } from './constants'; import { VIEW_TYPES, VIEWS } from './constants';
import EndPointDetailsWrapper from './EndPointDetailsWrapper'; import EndPointDetails from './EndPointDetails';
import TopErrors from './TopErrors'; import TopErrors from './TopErrors';
const TimeRangeOffset = 1000000000; const TimeRangeOffset = 1000000000;
@ -156,7 +156,10 @@ function DomainDetails({
> >
{domainData && ( {domainData && (
<> <>
<DomainMetrics domainData={domainData} /> <DomainMetrics
domainName={domainData.domainName}
timeRange={modalTimeRange}
/>
<div className="views-tabs-container"> <div className="views-tabs-container">
<Radio.Group <Radio.Group
className="views-tabs" className="views-tabs"
@ -174,13 +177,13 @@ function DomainDetails({
</Radio.Button> </Radio.Button>
<Radio.Button <Radio.Button
className={ className={
selectedView === VIEW_TYPES.ENDPOINT_DETAILS selectedView === VIEW_TYPES.ENDPOINT_STATS
? 'tab selected_view' ? 'tab selected_view'
: 'tab' : 'tab'
} }
value={VIEW_TYPES.ENDPOINT_DETAILS} value={VIEW_TYPES.ENDPOINT_STATS}
> >
<div className="view-title">Endpoint Details</div> <div className="view-title">Endpoint(s) Stats</div>
</Radio.Button> </Radio.Button>
<Radio.Button <Radio.Button
className={ className={
@ -188,7 +191,7 @@ function DomainDetails({
} }
value={VIEW_TYPES.TOP_ERRORS} value={VIEW_TYPES.TOP_ERRORS}
> >
<div className="view-title">Top Errors</div> <div className="view-title">Top 10 Errors</div>
</Radio.Button> </Radio.Button>
</Radio.Group> </Radio.Group>
</div> </div>
@ -203,13 +206,14 @@ function DomainDetails({
/> />
)} )}
{selectedView === VIEW_TYPES.ENDPOINT_DETAILS && ( {selectedView === VIEW_TYPES.ENDPOINT_STATS && (
<EndPointDetailsWrapper <EndPointDetails
domainName={domainData.domainName} domainName={domainData.domainName}
endPointName={selectedEndPointName} endPointName={selectedEndPointName}
setSelectedEndPointName={setSelectedEndPointName} setSelectedEndPointName={setSelectedEndPointName}
domainListFilters={domainListFilters} domainListFilters={domainListFilters}
timeRange={modalTimeRange} timeRange={modalTimeRange}
// handleTimeChange={handleTimeChange}
/> />
)} )}

View File

@ -8,6 +8,10 @@ import {
getRateOverTimeWidgetData, getRateOverTimeWidgetData,
} from 'container/ApiMonitoring/utils'; } from 'container/ApiMonitoring/utils';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2'; import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
// import {
// CustomTimeType,
// Time,
// } from 'container/TopNav/DateTimeSelectionV2/config';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
@ -29,7 +33,8 @@ function EndPointDetails({
setSelectedEndPointName, setSelectedEndPointName,
domainListFilters, domainListFilters,
timeRange, timeRange,
}: { }: // handleTimeChange,
{
domainName: string; domainName: string;
endPointName: string; endPointName: string;
setSelectedEndPointName: (value: string) => void; setSelectedEndPointName: (value: string) => void;
@ -38,6 +43,10 @@ function EndPointDetails({
startTime: number; startTime: number;
endTime: number; endTime: number;
}; };
// handleTimeChange: (
// interval: Time | CustomTimeType,
// dateTimeRange?: [number, number],
// ) => void;
}): JSX.Element { }): JSX.Element {
const { startTime: minTime, endTime: maxTime } = timeRange; const { startTime: minTime, endTime: maxTime } = timeRange;
@ -47,6 +56,7 @@ function EndPointDetails({
op: 'AND', op: 'AND',
items: [], items: [],
}); });
// [TODO] if endPointName is there then add it to the filters under http.url key
// Manually update the query to include the filters // Manually update the query to include the filters
// Because using the hook is causing the global domain // Because using the hook is causing the global domain
@ -78,15 +88,8 @@ function EndPointDetails({
); );
const endPointDetailsQueryPayload = useMemo( const endPointDetailsQueryPayload = useMemo(
() => () => getEndPointDetailsQueryPayload(domainName, minTime, maxTime, filters),
getEndPointDetailsQueryPayload( [domainName, filters, minTime, maxTime],
domainName,
endPointName,
minTime,
maxTime,
filters,
),
[domainName, endPointName, filters, minTime, maxTime],
); );
const endPointDetailsDataQueries = useQueries( const endPointDetailsDataQueries = useQueries(
@ -141,6 +144,20 @@ function EndPointDetails({
[domainName, endPointName, filters, domainListFilters], [domainName, endPointName, filters, domainListFilters],
); );
// // [TODO] Fix this later
// const onDragSelect = useCallback(
// (start: number, end: number) => {
// const startTimestamp = Math.trunc(start);
// const endTimestamp = Math.trunc(end);
// if (startTimestamp !== endTimestamp) {
// // update the value in local time picker
// handleTimeChange('custom', [startTimestamp, endTimestamp]);
// }
// },
// [handleTimeChange],
// );
return ( return (
<div className="endpoint-details-container"> <div className="endpoint-details-container">
<div className="endpoint-details-filters-container"> <div className="endpoint-details-filters-container">
@ -166,7 +183,9 @@ function EndPointDetails({
<div className="endpoint-meta-data"> <div className="endpoint-meta-data">
<div className="endpoint-meta-data-pill"> <div className="endpoint-meta-data-pill">
<div className="endpoint-meta-data-label">Endpoint</div> <div className="endpoint-meta-data-label">Endpoint</div>
<div className="endpoint-meta-data-value">{endpoint || '-'}</div> <div className="endpoint-meta-data-value">
{endpoint || 'All Endpoints'}
</div>
</div> </div>
<div className="endpoint-meta-data-pill"> <div className="endpoint-meta-data-pill">
<div className="endpoint-meta-data-label">Port</div> <div className="endpoint-meta-data-label">Port</div>
@ -177,6 +196,7 @@ function EndPointDetails({
{!isServicesFilterApplied && ( {!isServicesFilterApplied && (
<DependentServices <DependentServices
dependentServicesQuery={endPointDependentServicesDataQuery} dependentServicesQuery={endPointDependentServicesDataQuery}
timeRange={timeRange}
/> />
)} )}
<StatusCodeBarCharts <StatusCodeBarCharts
@ -191,8 +211,16 @@ function EndPointDetails({
timeRange={timeRange} timeRange={timeRange}
/> />
<StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} /> <StatusCodeTable endPointStatusCodeDataQuery={endPointStatusCodeDataQuery} />
<MetricOverTimeGraph widget={rateOverTimeWidget} timeRange={timeRange} /> <MetricOverTimeGraph
<MetricOverTimeGraph widget={latencyOverTimeWidget} timeRange={timeRange} /> widget={rateOverTimeWidget}
timeRange={timeRange}
onDragSelect={(): void => {}}
/>
<MetricOverTimeGraph
widget={latencyOverTimeWidget}
timeRange={timeRange}
onDragSelect={(): void => {}}
/>
</div> </div>
); );
} }

View File

@ -1,19 +1,24 @@
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined } from '@ant-design/icons';
import { Spin, Table, Typography } from 'antd'; import { Spin, Table, Tooltip, Typography } from 'antd';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { import {
END_POINT_DETAILS_QUERY_KEYS_ARRAY,
formatTopErrorsDataForTable, formatTopErrorsDataForTable,
getEndPointDetailsQueryPayload,
getTopErrorsColumnsConfig, getTopErrorsColumnsConfig,
getTopErrorsQueryPayload, getTopErrorsQueryPayload,
TopErrorsResponseRow, TopErrorsResponseRow,
} from 'container/ApiMonitoring/utils'; } from 'container/ApiMonitoring/utils';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo } from 'react'; import { Info } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useQueries } from 'react-query'; import { useQueries } from 'react-query';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import EndPointsDropDown from './components/EndPointsDropDown';
import ErrorState from './components/ErrorState'; import ErrorState from './components/ErrorState';
function TopErrors({ function TopErrors({
@ -28,9 +33,31 @@ function TopErrors({
}): JSX.Element { }): JSX.Element {
const { startTime: minTime, endTime: maxTime } = timeRange; const { startTime: minTime, endTime: maxTime } = timeRange;
const [endPointName, setSelectedEndPointName] = useState<string>('');
const queryPayloads = useMemo( const queryPayloads = useMemo(
() => getTopErrorsQueryPayload(domainName, minTime, maxTime), () =>
[domainName, minTime, maxTime], getTopErrorsQueryPayload(domainName, minTime, maxTime, {
items: endPointName
? [
{
id: '92b8a1c1',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
]
: [],
op: 'AND',
}),
[domainName, endPointName, minTime, maxTime],
); );
// Since only one query here // Since only one query here
@ -67,6 +94,35 @@ function TopErrors({
[topErrorsData], [topErrorsData],
); );
const endPointDropDownQueryPayload = useMemo(
() => [
getEndPointDetailsQueryPayload(domainName, minTime, maxTime, {
items: [],
op: 'AND',
})[2],
],
[domainName, minTime, maxTime],
);
const endPointDropDownDataQueries = useQueries(
endPointDropDownQueryPayload.map((payload) => ({
queryKey: [
END_POINT_DETAILS_QUERY_KEYS_ARRAY[4],
payload,
ENTITY_VERSION_V4,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
})),
);
const [endPointDropDownDataQuery] = useMemo(
() => [endPointDropDownDataQueries[0]],
[endPointDropDownDataQueries],
);
if (isError) { if (isError) {
return ( return (
<div className="all-endpoints-error-state-wrapper"> <div className="all-endpoints-error-state-wrapper">
@ -77,8 +133,27 @@ function TopErrors({
return ( return (
<div className="all-endpoints-container"> <div className="all-endpoints-container">
<div className="top-errors-dropdown-container">
<div className="endpoint-details-filters-container-dropdown">
<EndPointsDropDown
selectedEndPointName={endPointName}
setSelectedEndPointName={setSelectedEndPointName}
endPointDropDownDataQuery={endPointDropDownDataQuery}
parentContainerDiv=".endpoint-details-filters-container"
/>
</div>
<Tooltip title="Optionally select a specific endpoint to see status message if present">
<Info size={16} color="white" />
</Tooltip>
</div>
<div className="endpoints-table-container"> <div className="endpoints-table-container">
<div className="endpoints-table-header">Top Errors</div> <div className="endpoints-table-header">
Top Errors{' '}
<Tooltip title="Shows top 10 errors only when status message is propagated">
<Info size={16} color="white" />
</Tooltip>
</div>
<Table <Table
columns={topErrorsColumnsConfig} columns={topErrorsColumnsConfig}
loading={{ loading={{

View File

@ -2,6 +2,7 @@ import '../DomainDetails.styles.scss';
import { Table, TablePaginationConfig, Typography } from 'antd'; import { Table, TablePaginationConfig, Typography } from 'antd';
import Skeleton from 'antd/lib/skeleton'; import Skeleton from 'antd/lib/skeleton';
import { QueryParams } from 'constants/query';
import { import {
dependentServicesColumns, dependentServicesColumns,
DependentServicesData, DependentServicesData,
@ -16,10 +17,15 @@ import ErrorState from './ErrorState';
interface DependentServicesProps { interface DependentServicesProps {
dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>; dependentServicesQuery: UseQueryResult<SuccessResponse<any>, unknown>;
timeRange: {
startTime: number;
endTime: number;
};
} }
function DependentServices({ function DependentServices({
dependentServicesQuery, dependentServicesQuery,
timeRange,
}: DependentServicesProps): JSX.Element { }: DependentServicesProps): JSX.Element {
const { const {
data, data,
@ -85,6 +91,25 @@ function DependentServices({
</div> </div>
), ),
}} }}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
const url = new URL(
`/services/${
record.serviceData.serviceName &&
record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: ''
}`,
window.location.origin,
);
const urlQuery = new URLSearchParams();
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
},
className: 'clickable-row',
})}
/> />
{dependentServicesData.length > 5 && ( {dependentServicesData.length > 5 && (

View File

@ -1,8 +1,79 @@
import { Color } from '@signozhq/design-tokens'; import { Color } from '@signozhq/design-tokens';
import { Progress, Tooltip, Typography } from 'antd'; import { Progress, Skeleton, Tooltip, Typography } from 'antd';
import { getLastUsedRelativeTime } from 'container/ApiMonitoring/utils'; import { ENTITY_VERSION_V4 } from 'constants/app';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import {
DomainMetricsResponseRow,
formatDomainMetricsDataForTable,
getDomainMetricsQueryPayload,
} from 'container/ApiMonitoring/utils';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import ErrorState from './ErrorState';
function DomainMetrics({
domainName,
timeRange,
}: {
domainName: string;
timeRange: { startTime: number; endTime: number };
}): JSX.Element {
const { startTime: minTime, endTime: maxTime } = timeRange;
const queryPayloads = useMemo(
() => getDomainMetricsQueryPayload(domainName, minTime, maxTime),
[domainName, minTime, maxTime],
);
// Since only one query here
const domainMetricsDataQueries = useQueries(
queryPayloads.map((payload) => ({
queryKey: [
REACT_QUERY_KEY.GET_DOMAIN_METRICS_DATA,
payload,
ENTITY_VERSION_V4,
],
queryFn: (): Promise<SuccessResponse<MetricRangePayloadProps>> =>
GetMetricQueryRange(payload, ENTITY_VERSION_V4),
enabled: !!payload,
staleTime: 60 * 1000, // 1 minute stale time : optimize this part
})),
);
const domainMetricsDataQuery = domainMetricsDataQueries[0];
// [TODO] handle the case where the data is not available
// [TODO] Format the data properly
const {
data: domainMetricsData,
isLoading,
isRefetching,
isError,
refetch,
} = domainMetricsDataQuery;
// [TODO] Fix type error
const formattedDomainMetricsData = useMemo(() => {
// Safely access the data with proper type checking
const rowData = domainMetricsData?.payload?.data?.result[0]?.table?.rows[0];
// Only pass the data if it matches the expected format
return formatDomainMetricsDataForTable(
rowData as DomainMetricsResponseRow | undefined,
);
}, [domainMetricsData]);
if (isError) {
return (
<div className="all-endpoints-error-state-wrapper">
<ErrorState refetch={refetch} />
</div>
);
}
function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
return ( return (
<div className="domain-detail-drawer__endpoint"> <div className="domain-detail-drawer__endpoint">
<div className="domain-details-grid"> <div className="domain-details-grid">
@ -35,28 +106,45 @@ function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
<div className="values-row"> <div className="values-row">
<Typography.Text className="domain-details-metadata-value"> <Typography.Text className="domain-details-metadata-value">
<Tooltip title={domainData.endpointCount}> {isLoading || isRefetching ? (
<span className="round-metric-tag">{domainData.endpointCount}</span> <Skeleton.Button active size="small" />
) : (
<Tooltip title={formattedDomainMetricsData.endpointCount}>
<span className="round-metric-tag">
{formattedDomainMetricsData.endpointCount}
</span>
</Tooltip> </Tooltip>
)}
</Typography.Text> </Typography.Text>
{/* // update the tooltip as well */} {/* // update the tooltip as well */}
<Typography.Text className="domain-details-metadata-value"> <Typography.Text className="domain-details-metadata-value">
<Tooltip title={domainData.latency}> {isLoading || isRefetching ? (
<Skeleton.Button active size="small" />
) : (
<Tooltip title={formattedDomainMetricsData.latency}>
<span className="round-metric-tag"> <span className="round-metric-tag">
{(domainData.latency / 1000).toFixed(3)}s {(Number(formattedDomainMetricsData.latency) / 1000).toFixed(3)}s
</span> </span>
</Tooltip> </Tooltip>
)}
</Typography.Text> </Typography.Text>
{/* // update the tooltip as well */} {/* // update the tooltip as well */}
<Typography.Text className="domain-details-metadata-value error-rate"> <Typography.Text className="domain-details-metadata-value error-rate">
<Tooltip title={domainData.errorRate}> {isLoading || isRefetching ? (
<Skeleton.Button active size="small" />
) : (
<Tooltip title={formattedDomainMetricsData.errorRate}>
<Progress <Progress
status="active" status="active"
percent={Number(domainData.errorRate.toFixed(1))} percent={Number(
Number(formattedDomainMetricsData.errorRate).toFixed(1),
)}
strokeLinecap="butt" strokeLinecap="butt"
size="small" size="small"
strokeColor={((): string => { strokeColor={((): string => {
const errorRatePercent = Number(domainData.errorRate.toFixed(1)); const errorRatePercent = Number(
Number(formattedDomainMetricsData.errorRate).toFixed(1),
);
if (errorRatePercent >= 90) return Color.BG_SAKURA_500; if (errorRatePercent >= 90) return Color.BG_SAKURA_500;
if (errorRatePercent >= 60) return Color.BG_AMBER_500; if (errorRatePercent >= 60) return Color.BG_AMBER_500;
return Color.BG_FOREST_500; return Color.BG_FOREST_500;
@ -64,12 +152,16 @@ function DomainMetrics({ domainData }: { domainData: any }): JSX.Element {
className="progress-bar" className="progress-bar"
/> />
</Tooltip> </Tooltip>
)}
</Typography.Text> </Typography.Text>
{/* // update the tooltip as well */}
<Typography.Text className="domain-details-metadata-value"> <Typography.Text className="domain-details-metadata-value">
<Tooltip title={domainData.lastUsed}> {isLoading || isRefetching ? (
{getLastUsedRelativeTime(domainData.lastUsed)} <Skeleton.Button active size="small" />
) : (
<Tooltip title={formattedDomainMetricsData.lastUsed}>
{formattedDomainMetricsData.lastUsed}
</Tooltip> </Tooltip>
)}
</Typography.Text> </Typography.Text>
</div> </div>
</div> </div>

View File

@ -52,6 +52,10 @@ function EndPointsDropDown({
: (triggerNode): HTMLElement => triggerNode.parentNode as HTMLElement : (triggerNode): HTMLElement => triggerNode.parentNode as HTMLElement
} }
dropdownStyle={dropdownStyle} dropdownStyle={dropdownStyle}
allowClear
onClear={(): void => {
setSelectedEndPointName('');
}}
/> />
); );
} }

View File

@ -118,7 +118,7 @@ function ExpandedRow({
onRow={(record): { onClick: () => void; className: string } => ({ onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => { onClick: (): void => {
setSelectedEndPointName(record.endpointName); setSelectedEndPointName(record.endpointName);
setSelectedView(VIEW_TYPES.ENDPOINT_DETAILS); setSelectedView(VIEW_TYPES.ENDPOINT_STATS);
logEvent('API Monitoring: Endpoint name row clicked', {}); logEvent('API Monitoring: Endpoint name row clicked', {});
}, },
className: 'expanded-clickable-row', className: 'expanded-clickable-row',

View File

@ -5,9 +5,11 @@ import { Widgets } from 'types/api/dashboard/getAll';
function MetricOverTimeGraph({ function MetricOverTimeGraph({
widget, widget,
timeRange, timeRange,
onDragSelect,
}: { }: {
widget: Widgets; widget: Widgets;
timeRange: { startTime: number; endTime: number }; timeRange: { startTime: number; endTime: number };
onDragSelect: (start: number, end: number) => void;
}): JSX.Element { }): JSX.Element {
return ( return (
<div> <div>
@ -16,10 +18,9 @@ function MetricOverTimeGraph({
<GridCard <GridCard
widget={widget} widget={widget}
isQueryEnabled isQueryEnabled
onDragSelect={(): void => {}} onDragSelect={onDragSelect}
customOnDragSelect={(): void => {}} customOnDragSelect={(): void => {}}
start={timeRange.startTime} customTimeRange={timeRange}
end={timeRange.endTime}
/> />
</div> </div>
</Card> </Card>

View File

@ -1,11 +1,11 @@
export enum VIEWS { export enum VIEWS {
ALL_ENDPOINTS = 'all_endpoints', ALL_ENDPOINTS = 'all_endpoints',
ENDPOINT_DETAILS = 'endpoint_details', ENDPOINT_STATS = 'endpoint_stats',
TOP_ERRORS = 'top_errors', TOP_ERRORS = 'top_errors',
} }
export const VIEW_TYPES = { export const VIEW_TYPES = {
ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS, ALL_ENDPOINTS: VIEWS.ALL_ENDPOINTS,
ENDPOINT_DETAILS: VIEWS.ENDPOINT_DETAILS, ENDPOINT_STATS: VIEWS.ENDPOINT_STATS,
TOP_ERRORS: VIEWS.TOP_ERRORS, TOP_ERRORS: VIEWS.TOP_ERRORS,
}; };

View File

@ -60,7 +60,6 @@ function DomainList({ showIP }: { showIP: boolean }): JSX.Element {
aggregateAttribute: { aggregateAttribute: {
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute, ...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
}, },
queryName: '',
}, },
], ],
}, },

View File

@ -333,6 +333,278 @@ export const formatDataForTable = (
).toISOString(), // Convert from nanoseconds to milliseconds ).toISOString(), // Convert from nanoseconds to milliseconds
})); }));
export const getDomainMetricsQueryPayload = (
domainName: string,
start: number,
end: number,
): GetQueryResultsProps[] => [
{
selectedTime: 'GLOBAL_TIME',
graphType: PANEL_TYPES.TABLE,
query: {
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [
{
id: '4c57937c',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: DataSource.TRACES,
queryName: 'B',
aggregateOperator: 'p99',
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'duration_nano--float64----true',
isColumn: true,
isJSON: false,
key: 'duration_nano',
type: '',
},
timeAggregation: 'p99',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [
{
id: '2cf675cd',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
],
op: 'AND',
},
expression: 'B',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: DataSource.TRACES,
queryName: 'C',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: DataTypes.String,
id: '------false',
isColumn: false,
key: '',
type: '',
},
timeAggregation: 'count',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [
{
id: '3db0f605',
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: '6096f745',
key: {
dataType: DataTypes.bool,
id: 'has_error--bool----true',
isColumn: true,
isJSON: false,
key: 'has_error',
type: '',
},
op: '=',
value: 'true',
},
],
op: 'AND',
},
expression: 'C',
disabled: true,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: DataSource.TRACES,
queryName: 'D',
aggregateOperator: 'max',
aggregateAttribute: {
dataType: DataTypes.String,
id: 'timestamp------false',
isColumn: false,
key: 'timestamp',
type: '',
},
timeAggregation: 'max',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [
{
id: '8ff8dea1',
key: {
dataType: DataTypes.String,
id: 'net.peer.name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'net.peer.name',
type: 'tag',
},
op: '=',
value: domainName,
},
],
op: 'AND',
},
expression: 'D',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [
{
queryName: 'F1',
expression: '(C/A)*100',
disabled: false,
legend: '',
},
],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: EQueryType.QUERY_BUILDER,
},
variables: {},
formatForWeb: true,
start,
end,
step: 60,
},
];
export interface DomainMetricsData {
endpointCount: number | string;
latency: number | string;
errorRate: number | string;
lastUsed: number | string;
}
export interface DomainMetricsResponseRow {
data: {
A: number | string;
B: number | string;
D: number | string;
F1: number | string;
};
}
export const formatDomainMetricsDataForTable = (
row: DomainMetricsResponseRow | undefined,
): DomainMetricsData => {
if (!row) {
return {
endpointCount: '-',
latency: '-',
errorRate: 0,
lastUsed: '-',
};
}
return {
endpointCount: row.data.A === 'n/a' || !row.data.A ? '-' : Number(row.data.A),
latency:
row.data.B === 'n/a' || row.data.B === undefined
? '-'
: Math.round(Number(row.data.B) / 1000000),
errorRate: row.data.F1 === 'n/a' || !row.data.F1 ? 0 : Number(row.data.F1),
lastUsed:
row.data.D === 'n/a' || !row.data.D
? '-'
: getLastUsedRelativeTime(Math.floor(Number(row.data.D) / 1000000)),
};
};
// Rename this to a proper name // Rename this to a proper name
const defaultGroupBy = [ const defaultGroupBy = [
{ {
@ -637,6 +909,7 @@ export const getTopErrorsQueryPayload = (
domainName: string, domainName: string,
start: number, start: number,
end: number, end: number,
filters: IBuilderQuery['filters'],
): GetQueryResultsProps[] => [ ): GetQueryResultsProps[] => [
{ {
selectedTime: 'GLOBAL_TIME', selectedTime: 'GLOBAL_TIME',
@ -714,6 +987,7 @@ export const getTopErrorsQueryPayload = (
op: '=', op: '=',
value: domainName, value: domainName,
}, },
...filters.items,
], ],
}, },
expression: 'A', expression: 'A',
@ -1192,7 +1466,6 @@ export const createFiltersForSelectedRowData = (
// Sixth query payload for endpoint response status code latency bar chart // Sixth query payload for endpoint response status code latency bar chart
export const getEndPointDetailsQueryPayload = ( export const getEndPointDetailsQueryPayload = (
domainName: string, domainName: string,
endPointName: string,
start: number, start: number,
end: number, end: number,
filters: IBuilderQuery['filters'], filters: IBuilderQuery['filters'],
@ -1218,19 +1491,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'A', expression: 'A',
filters: { filters: {
items: [ items: [
{
id: '92b8a1c1',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '874562e1', id: '874562e1',
key: { key: {
@ -1288,19 +1548,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'B', expression: 'B',
filters: { filters: {
items: [ items: [
{
id: 'c0c0f76b',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '0c5564e0', id: '0c5564e0',
key: { key: {
@ -1358,19 +1605,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'C', expression: 'C',
filters: { filters: {
items: [ items: [
{
id: '7a3eebed',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '0d656701', id: '0d656701',
key: { key: {
@ -1440,19 +1674,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'D', expression: 'D',
filters: { filters: {
items: [ items: [
{
id: 'e7f12d52',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '918f5b99', id: '918f5b99',
key: { key: {
@ -1510,19 +1731,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'E', expression: 'E',
filters: { filters: {
items: [ items: [
{
id: '5281578a',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: 'b355d1aa', id: 'b355d1aa',
key: { key: {
@ -1634,19 +1842,6 @@ export const getEndPointDetailsQueryPayload = (
op: '=', op: '=',
value: domainName, value: domainName,
}, },
{
id: 'e1b24204',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '212678b9', id: '212678b9',
key: { key: {
@ -1713,19 +1908,6 @@ export const getEndPointDetailsQueryPayload = (
op: '=', op: '=',
value: domainName, value: domainName,
}, },
{
id: '5dbe3518',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '212678b9', id: '212678b9',
key: { key: {
@ -1909,19 +2091,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'A', expression: 'A',
filters: { filters: {
items: [ items: [
{
id: 'bdac4904',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: 'b78ff216', id: 'b78ff216',
key: { key: {
@ -1988,19 +2157,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'B', expression: 'B',
filters: { filters: {
items: [ 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', id: 'a9024472',
key: { key: {
@ -2066,19 +2222,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'C', expression: 'C',
filters: { filters: {
items: [ 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', id: '1b6c062d',
key: { key: {
@ -2145,19 +2288,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'D', expression: 'D',
filters: { filters: {
items: [ 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', id: 'd14792a8',
key: { key: {
@ -2290,19 +2420,6 @@ export const getEndPointDetailsQueryPayload = (
op: '=', op: '=',
value: domainName, value: domainName,
}, },
{
id: '8b1be6f0',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: '212678b9', id: '212678b9',
key: { key: {
@ -2390,19 +2507,6 @@ export const getEndPointDetailsQueryPayload = (
expression: 'A', expression: 'A',
filters: { filters: {
items: [ items: [
{
id: '52aca159',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
{ {
id: 'aae93366', id: 'aae93366',
key: { key: {
@ -2787,7 +2891,7 @@ export const dependentServicesColumns: ColumnType<DependentServicesData>[] = [
<div className="top-services-item"> <div className="top-services-item">
<div className="top-services-item-progress"> <div className="top-services-item-progress">
<div className="top-services-item-key">{serviceData.serviceName}</div> <div className="top-services-item-key">{serviceData.serviceName}</div>
<div className="top-services-item-count">{serviceData.count}</div> <div className="top-services-item-count">{serviceData.count} Calls</div>
<div <div
className="top-services-item-progress-bar" className="top-services-item-progress-bar"
style={{ width: `${serviceData.percentage}%` }} style={{ width: `${serviceData.percentage}%` }}
@ -3177,10 +3281,13 @@ export const getRateOverTimeWidgetData = (
endPointName: string, endPointName: string,
filters: IBuilderQuery['filters'], filters: IBuilderQuery['filters'],
): Widgets => { ): Widgets => {
let legend = domainName;
if (endPointName) {
const { endpoint, port } = extractPortAndEndpoint(endPointName); const { endpoint, port } = extractPortAndEndpoint(endPointName);
const legend = `${ // eslint-disable-next-line sonarjs/no-nested-template-literals
port !== '-' && port !== 'n/a' ? `${port}:` : '' legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
}${endpoint}`; }
return getWidgetQueryBuilder( return getWidgetQueryBuilder(
getWidgetQuery({ getWidgetQuery({
title: 'Rate Over Time', title: 'Rate Over Time',
@ -3213,34 +3320,12 @@ export const getRateOverTimeWidgetData = (
op: '=', op: '=',
value: domainName, value: domainName,
}, },
{
id: '30710f04',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items, ...filters.items,
], ],
op: 'AND', op: 'AND',
}, },
functions: [], functions: [],
groupBy: [ groupBy: [],
{
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
],
having: [], having: [],
legend, legend,
limit: null, limit: null,
@ -3262,8 +3347,13 @@ export const getLatencyOverTimeWidgetData = (
endPointName: string, endPointName: string,
filters: IBuilderQuery['filters'], filters: IBuilderQuery['filters'],
): Widgets => { ): Widgets => {
let legend = domainName;
if (endPointName) {
const { endpoint, port } = extractPortAndEndpoint(endPointName); const { endpoint, port } = extractPortAndEndpoint(endPointName);
const legend = `${port}:${endpoint}`; // eslint-disable-next-line sonarjs/no-nested-template-literals
legend = `${port !== '-' && port !== 'n/a' ? `${port}:` : ''}${endpoint}`;
}
return getWidgetQueryBuilder( return getWidgetQueryBuilder(
getWidgetQuery({ getWidgetQuery({
title: 'Latency Over Time', title: 'Latency Over Time',
@ -3297,34 +3387,12 @@ export const getLatencyOverTimeWidgetData = (
op: '=', op: '=',
value: domainName, value: domainName,
}, },
{
id: '50142500',
key: {
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
op: '=',
value: endPointName,
},
...filters.items, ...filters.items,
], ],
op: 'AND', op: 'AND',
}, },
functions: [], functions: [],
groupBy: [ groupBy: [],
{
dataType: DataTypes.String,
id: 'http.url--string--tag--false',
isColumn: false,
isJSON: false,
key: 'http.url',
type: 'tag',
},
],
having: [], having: [],
legend, legend,
limit: null, limit: null,

View File

@ -47,6 +47,7 @@ function GridCardGraph({
start, start,
end, end,
analyticsEvent, analyticsEvent,
customTimeRange,
}: GridCardGraphProps): JSX.Element { }: GridCardGraphProps): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [errorMessage, setErrorMessage] = useState<string>(); const [errorMessage, setErrorMessage] = useState<string>();
@ -130,6 +131,8 @@ function GridCardGraph({
variables: getDashboardVariables(variables), variables: getDashboardVariables(variables),
fillGaps: widget.fillSpans, fillGaps: widget.fillSpans,
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE, formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
start: customTimeRange?.startTime || start,
end: customTimeRange?.endTime || end,
}; };
} }
updatedQuery.builder.queryData[0].pageSize = 10; updatedQuery.builder.queryData[0].pageSize = 10;
@ -149,6 +152,8 @@ function GridCardGraph({
initialDataSource === DataSource.TRACES && widget.selectedTracesFields, initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
}, },
fillGaps: widget.fillSpans, fillGaps: widget.fillSpans,
start: customTimeRange?.startTime || start,
end: customTimeRange?.endTime || end,
}; };
}); });
@ -187,8 +192,8 @@ function GridCardGraph({
variables: getDashboardVariables(variables), variables: getDashboardVariables(variables),
selectedTime: widget.timePreferance || 'GLOBAL_TIME', selectedTime: widget.timePreferance || 'GLOBAL_TIME',
globalSelectedInterval, globalSelectedInterval,
start, start: customTimeRange?.startTime || start,
end, end: customTimeRange?.endTime || end,
}, },
version || DEFAULT_ENTITY_VERSION, version || DEFAULT_ENTITY_VERSION,
{ {
@ -202,6 +207,9 @@ function GridCardGraph({
widget.timePreferance, widget.timePreferance,
widget.fillSpans, widget.fillSpans,
requestData, requestData,
...(customTimeRange && customTimeRange.startTime && customTimeRange.endTime
? [customTimeRange.startTime, customTimeRange.endTime]
: []),
], ],
retry(failureCount, error): boolean { retry(failureCount, error): boolean {
if ( if (

View File

@ -61,6 +61,10 @@ export interface GridCardGraphProps {
start?: number; start?: number;
end?: number; end?: number;
analyticsEvent?: string; analyticsEvent?: string;
customTimeRange?: {
startTime: number;
endTime: number;
};
} }
export interface GetGraphVisibilityStateOnLegendClickProps { export interface GetGraphVisibilityStateOnLegendClickProps {