mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 21:39:05 +08:00
feat: implement inspect feature for metrics explorer (#7549)
This commit is contained in:
parent
36886135d1
commit
f01d21cbf2
54
frontend/src/api/metricsExplorer/getInspectMetricsDetails.ts
Normal file
54
frontend/src/api/metricsExplorer/getInspectMetricsDetails.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export interface InspectMetricsRequest {
|
||||||
|
metricName: string;
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
filters: TagFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectMetricsResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
series: InspectMetricsSeries[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectMetricsSeries {
|
||||||
|
title?: string;
|
||||||
|
strokeColor?: string;
|
||||||
|
labels: Record<string, string>;
|
||||||
|
labelsArray: Array<Record<string, string>>;
|
||||||
|
values: InspectMetricsTimestampValue[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InspectMetricsTimestampValue {
|
||||||
|
timestamp: number;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getInspectMetricsDetails = async (
|
||||||
|
request: InspectMetricsRequest,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<InspectMetricsResponse> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/metrics/inspect`, request, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: 'Success',
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
@ -1,6 +1,9 @@
|
|||||||
|
import React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
export const SpanStyle = styled.span`
|
type SpanProps = React.HTMLAttributes<HTMLSpanElement>;
|
||||||
|
|
||||||
|
export const SpanStyle = styled.span<SpanProps>`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -0.313rem;
|
right: -0.313rem;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -12,7 +15,7 @@ export const SpanStyle = styled.span`
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DragSpanStyle = styled.span`
|
export const DragSpanStyle = styled.span<SpanProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: -1rem;
|
margin: -1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
@ -51,6 +51,7 @@ export const REACT_QUERY_KEY = {
|
|||||||
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
||||||
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
|
GET_METRIC_DETAILS: 'GET_METRIC_DETAILS',
|
||||||
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
|
GET_RELATED_METRICS: 'GET_RELATED_METRICS',
|
||||||
|
GET_INSPECT_METRICS_DETAILS: 'GET_INSPECT_METRICS_DETAILS',
|
||||||
|
|
||||||
// API Monitoring Query Keys
|
// API Monitoring Query Keys
|
||||||
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
|
GET_DOMAINS_LIST: 'GET_DOMAINS_LIST',
|
||||||
|
350
frontend/src/container/MetricsExplorer/Inspect/ExpandedView.tsx
Normal file
350
frontend/src/container/MetricsExplorer/Inspect/ExpandedView.tsx
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Card, Tooltip, Typography } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ResizeTable from 'components/ResizeTable/ResizeTable';
|
||||||
|
import { DataType } from 'container/LogDetailedView/TableView';
|
||||||
|
import { ArrowDownCircle, ArrowRightCircle, Focus } from 'lucide-react';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
|
||||||
|
TIME_AGGREGATION_OPTIONS,
|
||||||
|
} from './constants';
|
||||||
|
import {
|
||||||
|
ExpandedViewProps,
|
||||||
|
InspectionStep,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
|
formatTimestampToFullDateTime,
|
||||||
|
getRawDataFromTimeSeries,
|
||||||
|
getSpaceAggregatedDataFromTimeSeries,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function ExpandedView({
|
||||||
|
options,
|
||||||
|
spaceAggregationSeriesMap,
|
||||||
|
step,
|
||||||
|
metricInspectionOptions,
|
||||||
|
timeAggregatedSeriesMap,
|
||||||
|
}: ExpandedViewProps): JSX.Element {
|
||||||
|
const [
|
||||||
|
selectedTimeSeries,
|
||||||
|
setSelectedTimeSeries,
|
||||||
|
] = useState<InspectMetricsSeries | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (step !== InspectionStep.COMPLETED) {
|
||||||
|
setSelectedTimeSeries(options?.timeSeries ?? null);
|
||||||
|
} else {
|
||||||
|
setSelectedTimeSeries(null);
|
||||||
|
}
|
||||||
|
}, [step, options?.timeSeries]);
|
||||||
|
|
||||||
|
const spaceAggregatedData = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!options?.timeSeries ||
|
||||||
|
!options?.timestamp ||
|
||||||
|
step !== InspectionStep.COMPLETED
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getSpaceAggregatedDataFromTimeSeries(
|
||||||
|
options?.timeSeries,
|
||||||
|
spaceAggregationSeriesMap,
|
||||||
|
options?.timestamp,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}, [options?.timeSeries, options?.timestamp, spaceAggregationSeriesMap, step]);
|
||||||
|
|
||||||
|
const rawData = useMemo(() => {
|
||||||
|
if (!selectedTimeSeries || !options?.timestamp) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return getRawDataFromTimeSeries(selectedTimeSeries, options?.timestamp, true);
|
||||||
|
}, [selectedTimeSeries, options?.timestamp]);
|
||||||
|
|
||||||
|
const absoluteValue = useMemo(
|
||||||
|
() =>
|
||||||
|
options?.timeSeries?.values.find(
|
||||||
|
(value) => value.timestamp >= options?.timestamp,
|
||||||
|
)?.value ?? options?.value,
|
||||||
|
[options],
|
||||||
|
);
|
||||||
|
|
||||||
|
const timeAggregatedData = useMemo(() => {
|
||||||
|
if (step !== InspectionStep.SPACE_AGGREGATION || !options?.timestamp) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
timeAggregatedSeriesMap
|
||||||
|
.get(options?.timestamp)
|
||||||
|
?.filter(
|
||||||
|
(popoverData) =>
|
||||||
|
popoverData.title && popoverData.title === options.timeSeries?.title,
|
||||||
|
) ?? []
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
step,
|
||||||
|
options?.timestamp,
|
||||||
|
options?.timeSeries?.title,
|
||||||
|
timeAggregatedSeriesMap,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tableData = useMemo(() => {
|
||||||
|
if (!selectedTimeSeries) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return Object.entries(selectedTimeSeries.labels).map(([key, value]) => ({
|
||||||
|
label: key,
|
||||||
|
value,
|
||||||
|
}));
|
||||||
|
}, [selectedTimeSeries]);
|
||||||
|
|
||||||
|
const columns: ColumnsType<DataType> = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
title: 'Label',
|
||||||
|
dataIndex: 'label',
|
||||||
|
key: 'label',
|
||||||
|
width: 50,
|
||||||
|
align: 'left',
|
||||||
|
className: 'labels-key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Value',
|
||||||
|
dataIndex: 'value',
|
||||||
|
key: 'value',
|
||||||
|
width: 50,
|
||||||
|
align: 'left',
|
||||||
|
ellipsis: true,
|
||||||
|
className: 'labels-value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="expanded-view">
|
||||||
|
<div className="expanded-view-header">
|
||||||
|
<Typography.Title level={5}>
|
||||||
|
<Focus size={16} color={Color.BG_VANILLA_100} />
|
||||||
|
<div>POINT INSPECTOR</div>
|
||||||
|
</Typography.Title>
|
||||||
|
</div>
|
||||||
|
{/* Show only when space aggregation is completed */}
|
||||||
|
{step === InspectionStep.COMPLETED && (
|
||||||
|
<div className="graph-popover">
|
||||||
|
<Card className="graph-popover-card" size="small">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-header-text">
|
||||||
|
{formatTimestampToFullDateTime(options?.timestamp ?? 0)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text strong>
|
||||||
|
{`${absoluteValue} is the ${
|
||||||
|
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW[
|
||||||
|
metricInspectionOptions.spaceAggregationOption ??
|
||||||
|
SpaceAggregationOptions.SUM_BY
|
||||||
|
]
|
||||||
|
} of`}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="graph-popover-section">
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-row-label">
|
||||||
|
VALUES
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="graph-popover-inner-row">
|
||||||
|
{spaceAggregatedData?.map(({ value, title, timestamp }) => (
|
||||||
|
<Tooltip key={`${title}-${timestamp}-${value}`} title={value}>
|
||||||
|
<div className="graph-popover-cell" data-testid="graph-popover-cell">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-row-label">
|
||||||
|
TIME SERIES
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="graph-popover-inner-row">
|
||||||
|
{spaceAggregatedData?.map(({ title, timeSeries }) => (
|
||||||
|
<Tooltip key={title} title={title}>
|
||||||
|
<div
|
||||||
|
data-testid="graph-popover-cell"
|
||||||
|
className={classNames('graph-popover-cell', 'timeseries-cell', {
|
||||||
|
selected: title === selectedTimeSeries?.title,
|
||||||
|
})}
|
||||||
|
onClick={(): void => {
|
||||||
|
setSelectedTimeSeries(timeSeries ?? null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
{selectedTimeSeries?.title === title ? (
|
||||||
|
<ArrowDownCircle color={Color.BG_FOREST_300} size={12} />
|
||||||
|
) : (
|
||||||
|
<ArrowRightCircle size={12} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Show only for space aggregated or raw data */}
|
||||||
|
{selectedTimeSeries && step !== InspectionStep.SPACE_AGGREGATION && (
|
||||||
|
<div className="graph-popover">
|
||||||
|
<Card className="graph-popover-card" size="small">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
{step !== InspectionStep.COMPLETED && (
|
||||||
|
<Typography.Text className="graph-popover-header-text">
|
||||||
|
{formatTimestampToFullDateTime(options?.timestamp ?? 0)}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Typography.Text strong>
|
||||||
|
{step === InspectionStep.COMPLETED
|
||||||
|
? `${
|
||||||
|
selectedTimeSeries?.values.find(
|
||||||
|
(value) => value?.timestamp >= (options?.timestamp || 0),
|
||||||
|
)?.value ?? options?.value
|
||||||
|
} is the ${
|
||||||
|
TIME_AGGREGATION_OPTIONS[
|
||||||
|
metricInspectionOptions.timeAggregationOption ??
|
||||||
|
TimeAggregationOptions.SUM
|
||||||
|
]
|
||||||
|
} of`
|
||||||
|
: selectedTimeSeries?.values.find(
|
||||||
|
(value) => value?.timestamp >= (options?.timestamp || 0),
|
||||||
|
)?.value ?? options?.value}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="graph-popover-section">
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-row-label">
|
||||||
|
RAW VALUES
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="graph-popover-inner-row">
|
||||||
|
{rawData?.map(({ value: rawValue, timestamp, title }) => (
|
||||||
|
<Tooltip key={`${title}-${timestamp}-${rawValue}`} title={rawValue}>
|
||||||
|
<div className="graph-popover-cell" data-testid="graph-popover-cell">
|
||||||
|
{rawValue}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-row-label">
|
||||||
|
TIMESTAMPS
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="graph-popover-inner-row">
|
||||||
|
{rawData?.map(({ timestamp }) => (
|
||||||
|
<Tooltip
|
||||||
|
key={timestamp}
|
||||||
|
title={formatTimestampToFullDateTime(timestamp ?? '', true)}
|
||||||
|
>
|
||||||
|
<div className="graph-popover-cell" data-testid="graph-popover-cell">
|
||||||
|
{formatTimestampToFullDateTime(timestamp ?? '', true)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Show raw values breakdown only for time aggregated data */}
|
||||||
|
{selectedTimeSeries && step === InspectionStep.SPACE_AGGREGATION && (
|
||||||
|
<div className="graph-popover">
|
||||||
|
<Card className="graph-popover-card" size="small">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-header-text">
|
||||||
|
{formatTimestampToFullDateTime(options?.timestamp ?? 0)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text strong>
|
||||||
|
{`${absoluteValue} is the ${
|
||||||
|
TIME_AGGREGATION_OPTIONS[
|
||||||
|
metricInspectionOptions.timeAggregationOption ??
|
||||||
|
TimeAggregationOptions.SUM
|
||||||
|
]
|
||||||
|
} of`}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="graph-popover-section">
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-row-label">
|
||||||
|
RAW VALUES
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="graph-popover-inner-row">
|
||||||
|
{timeAggregatedData?.map(({ value, title, timestamp }) => (
|
||||||
|
<Tooltip key={`${title}-${timestamp}-${value}`} title={value}>
|
||||||
|
<div className="graph-popover-cell" data-testid="graph-popover-cell">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="graph-popover-row">
|
||||||
|
<Typography.Text className="graph-popover-row-label">
|
||||||
|
TIMESTAMPS
|
||||||
|
</Typography.Text>
|
||||||
|
<div className="graph-popover-inner-row">
|
||||||
|
{timeAggregatedData?.map(({ timestamp }) => (
|
||||||
|
<Tooltip
|
||||||
|
key={timestamp}
|
||||||
|
title={formatTimestampToFullDateTime(timestamp ?? '', true)}
|
||||||
|
>
|
||||||
|
<div className="graph-popover-cell" data-testid="graph-popover-cell">
|
||||||
|
{formatTimestampToFullDateTime(timestamp ?? '', true)}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Labels */}
|
||||||
|
{selectedTimeSeries && (
|
||||||
|
<>
|
||||||
|
<Typography.Title
|
||||||
|
level={5}
|
||||||
|
>{`${selectedTimeSeries?.title} Labels`}</Typography.Title>
|
||||||
|
<ResizeTable
|
||||||
|
columns={columns}
|
||||||
|
tableLayout="fixed"
|
||||||
|
dataSource={tableData}
|
||||||
|
pagination={false}
|
||||||
|
showHeader={false}
|
||||||
|
scroll={{ y: 600 }}
|
||||||
|
className="labels-table"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExpandedView;
|
@ -0,0 +1,71 @@
|
|||||||
|
import { Button, Card, Typography } from 'antd';
|
||||||
|
import { ArrowRight } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { GraphPopoverProps } from './types';
|
||||||
|
import { formatTimestampToFullDateTime } from './utils';
|
||||||
|
|
||||||
|
function GraphPopover({
|
||||||
|
options,
|
||||||
|
popoverRef,
|
||||||
|
openInExpandedView,
|
||||||
|
}: GraphPopoverProps): JSX.Element | null {
|
||||||
|
const { x, y, value, timestamp, timeSeries } = options || {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
value: 0,
|
||||||
|
timestamp: 0,
|
||||||
|
timeSeries: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const closestTimestamp = useMemo(() => {
|
||||||
|
if (!timeSeries) {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
return timeSeries?.values.reduce((prev, curr) => {
|
||||||
|
const prevDiff = Math.abs(prev.timestamp - timestamp);
|
||||||
|
const currDiff = Math.abs(curr.timestamp - timestamp);
|
||||||
|
return prevDiff < currDiff ? prev : curr;
|
||||||
|
}).timestamp;
|
||||||
|
}, [timeSeries, timestamp]);
|
||||||
|
|
||||||
|
const closestValue = useMemo(() => {
|
||||||
|
if (!timeSeries) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const index = timeSeries.values.findIndex(
|
||||||
|
(value) => value.timestamp === closestTimestamp,
|
||||||
|
);
|
||||||
|
return index !== undefined && index >= 0
|
||||||
|
? timeSeries?.values[index].value
|
||||||
|
: null;
|
||||||
|
}, [timeSeries, closestTimestamp, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
top: y + 10,
|
||||||
|
left: x + 10,
|
||||||
|
}}
|
||||||
|
ref={popoverRef}
|
||||||
|
className="inspect-graph-popover"
|
||||||
|
>
|
||||||
|
<Card className="inspect-graph-popover-content" size="small">
|
||||||
|
<div className="inspect-graph-popover-row">
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{formatTimestampToFullDateTime(closestTimestamp)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text>{Number(closestValue).toFixed(2)}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="inspect-graph-popover-button-row">
|
||||||
|
<Button size="small" type="primary" onClick={openInExpandedView}>
|
||||||
|
<Typography.Text>View details</Typography.Text>
|
||||||
|
<ArrowRight size={10} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphPopover;
|
256
frontend/src/container/MetricsExplorer/Inspect/GraphView.tsx
Normal file
256
frontend/src/container/MetricsExplorer/Inspect/GraphView.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Skeleton, Switch, Typography } from 'antd';
|
||||||
|
import Uplot from 'components/Uplot';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||||
|
import { METRIC_TYPE_TO_COLOR_MAP, METRIC_TYPE_TO_ICON_MAP } from './constants';
|
||||||
|
import GraphPopover from './GraphPopover';
|
||||||
|
import TableView from './TableView';
|
||||||
|
import { GraphPopoverOptions, GraphViewProps } from './types';
|
||||||
|
import { HoverPopover, onGraphClick, onGraphHover } from './utils';
|
||||||
|
|
||||||
|
function GraphView({
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
metricUnit,
|
||||||
|
metricName,
|
||||||
|
metricType,
|
||||||
|
spaceAggregationSeriesMap,
|
||||||
|
inspectionStep,
|
||||||
|
setPopoverOptions,
|
||||||
|
popoverOptions,
|
||||||
|
setShowExpandedView,
|
||||||
|
setExpandedViewOptions,
|
||||||
|
metricInspectionOptions,
|
||||||
|
isInspectMetricsRefetching,
|
||||||
|
}: GraphViewProps): JSX.Element {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
const start = useMemo(() => Math.floor(Number(minTime) / 1000000000), [
|
||||||
|
minTime,
|
||||||
|
]);
|
||||||
|
const end = useMemo(() => Math.floor(Number(maxTime) / 1000000000), [maxTime]);
|
||||||
|
const [showGraphPopover, setShowGraphPopover] = useState(false);
|
||||||
|
const [showHoverPopover, setShowHoverPopover] = useState(false);
|
||||||
|
const [
|
||||||
|
hoverPopoverOptions,
|
||||||
|
setHoverPopoverOptions,
|
||||||
|
] = useState<GraphPopoverOptions | null>(null);
|
||||||
|
const [viewType, setViewType] = useState<'graph' | 'table'>('graph');
|
||||||
|
|
||||||
|
const popoverRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent): void {
|
||||||
|
if (
|
||||||
|
popoverRef.current &&
|
||||||
|
!popoverRef.current.contains(event.target as Node) &&
|
||||||
|
graphRef.current &&
|
||||||
|
!graphRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setShowGraphPopover(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return (): void => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [popoverRef, graphRef]);
|
||||||
|
|
||||||
|
const options: uPlot.Options = useMemo(
|
||||||
|
() => ({
|
||||||
|
width: dimensions.width,
|
||||||
|
height: 500,
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
axes: [
|
||||||
|
{
|
||||||
|
stroke: isDarkMode ? Color.TEXT_VANILLA_400 : Color.BG_SLATE_400,
|
||||||
|
grid: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
values: (_, vals): string[] =>
|
||||||
|
vals.map((v) => {
|
||||||
|
const d = new Date(v);
|
||||||
|
const date = `${String(d.getDate()).padStart(2, '0')}/${String(
|
||||||
|
d.getMonth() + 1,
|
||||||
|
).padStart(2, '0')}`;
|
||||||
|
const time = `${String(d.getHours()).padStart(2, '0')}:${String(
|
||||||
|
d.getMinutes(),
|
||||||
|
).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
|
||||||
|
return `${date}\n${time}`; // two-line label
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: metricUnit || '',
|
||||||
|
stroke: isDarkMode ? Color.TEXT_VANILLA_400 : Color.BG_SLATE_400,
|
||||||
|
grid: {
|
||||||
|
show: true,
|
||||||
|
stroke: isDarkMode ? Color.BG_SLATE_500 : Color.BG_SLATE_200,
|
||||||
|
},
|
||||||
|
values: (_, vals): string[] =>
|
||||||
|
vals.map((v) => formatNumberIntoHumanReadableFormat(v, false)),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{ label: 'Time' }, // This config is required as a placeholder for x-axis,
|
||||||
|
...formattedInspectMetricsTimeSeries.slice(1).map((_, index) => ({
|
||||||
|
drawStyle: 'line',
|
||||||
|
lineInterpolation: 'spline',
|
||||||
|
show: true,
|
||||||
|
label: String.fromCharCode(65 + (index % 26)),
|
||||||
|
stroke: inspectMetricsTimeSeries[index]?.strokeColor,
|
||||||
|
width: 2,
|
||||||
|
spanGaps: true,
|
||||||
|
points: {
|
||||||
|
size: 5,
|
||||||
|
show: false,
|
||||||
|
stroke: inspectMetricsTimeSeries[index]?.strokeColor,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
min: start,
|
||||||
|
max: end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
hooks: {
|
||||||
|
ready: [
|
||||||
|
(u: uPlot): void => {
|
||||||
|
u.over.addEventListener('click', (e) => {
|
||||||
|
onGraphClick(
|
||||||
|
e,
|
||||||
|
u,
|
||||||
|
popoverRef,
|
||||||
|
setPopoverOptions,
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
showGraphPopover,
|
||||||
|
setShowGraphPopover,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
u.over.addEventListener('mousemove', (e) => {
|
||||||
|
onGraphHover(
|
||||||
|
e,
|
||||||
|
u,
|
||||||
|
setHoverPopoverOptions,
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
u.over.addEventListener('mouseenter', () => {
|
||||||
|
setShowHoverPopover(true);
|
||||||
|
});
|
||||||
|
u.over.addEventListener('mouseleave', () => {
|
||||||
|
setShowHoverPopover(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
dimensions.width,
|
||||||
|
isDarkMode,
|
||||||
|
metricUnit,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
setPopoverOptions,
|
||||||
|
showGraphPopover,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const MetricTypeIcon = metricType ? METRIC_TYPE_TO_ICON_MAP[metricType] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inspect-metrics-graph-view" ref={graphRef}>
|
||||||
|
<div className="inspect-metrics-graph-view-header">
|
||||||
|
<Button.Group>
|
||||||
|
<Button
|
||||||
|
className="metric-name-button-label"
|
||||||
|
size="middle"
|
||||||
|
icon={
|
||||||
|
MetricTypeIcon && metricType ? (
|
||||||
|
<MetricTypeIcon
|
||||||
|
size={14}
|
||||||
|
color={METRIC_TYPE_TO_COLOR_MAP[metricType]}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{metricName}
|
||||||
|
</Button>
|
||||||
|
<Button className="time-series-button-label" size="middle" disabled>
|
||||||
|
{/* First time series in that of timestamps. Hence -1 */}
|
||||||
|
{`${formattedInspectMetricsTimeSeries.length - 1} time series`}
|
||||||
|
</Button>
|
||||||
|
</Button.Group>
|
||||||
|
<div className="view-toggle-button">
|
||||||
|
<Switch
|
||||||
|
checked={viewType === 'graph'}
|
||||||
|
onChange={(checked): void => setViewType(checked ? 'graph' : 'table')}
|
||||||
|
/>
|
||||||
|
<Typography.Text>
|
||||||
|
{viewType === 'graph' ? 'Graph View' : 'Table View'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="graph-view-container">
|
||||||
|
{viewType === 'graph' &&
|
||||||
|
(isInspectMetricsRefetching ? (
|
||||||
|
<Skeleton active />
|
||||||
|
) : (
|
||||||
|
<Uplot data={formattedInspectMetricsTimeSeries} options={options} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
{viewType === 'table' && (
|
||||||
|
<TableView
|
||||||
|
inspectionStep={inspectionStep}
|
||||||
|
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
|
||||||
|
setShowExpandedView={setShowExpandedView}
|
||||||
|
setExpandedViewOptions={setExpandedViewOptions}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
isInspectMetricsRefetching={isInspectMetricsRefetching}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showGraphPopover && (
|
||||||
|
<GraphPopover
|
||||||
|
options={popoverOptions}
|
||||||
|
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
|
||||||
|
popoverRef={popoverRef}
|
||||||
|
step={inspectionStep}
|
||||||
|
openInExpandedView={(): void => {
|
||||||
|
setShowGraphPopover(false);
|
||||||
|
setShowExpandedView(true);
|
||||||
|
setExpandedViewOptions(popoverOptions);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showHoverPopover && !showGraphPopover && hoverPopoverOptions && (
|
||||||
|
<HoverPopover
|
||||||
|
options={hoverPopoverOptions}
|
||||||
|
step={inspectionStep}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GraphView;
|
@ -1,4 +1,14 @@
|
|||||||
.inspect-metrics-modal {
|
.inspect-metrics-modal {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.inspect-metrics-fallback {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.inspect-metrics-title {
|
.inspect-metrics-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -13,4 +23,567 @@
|
|||||||
color: var(--text-vanilla-500);
|
color: var(--text-vanilla-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.inspect-metrics-content-first-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 2;
|
||||||
|
gap: 16px;
|
||||||
|
padding-right: 24px;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
width: 60%;
|
||||||
|
|
||||||
|
.inspect-metrics-graph-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
.inspect-metrics-graph-view-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.ant-btn-group {
|
||||||
|
.time-series-button-label,
|
||||||
|
.metric-name-button-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle-button {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-view-container {
|
||||||
|
min-height: 520px;
|
||||||
|
|
||||||
|
.inspect-metrics-table-view {
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
.ant-spin-nested-loading {
|
||||||
|
.ant-spin-container {
|
||||||
|
.ant-table {
|
||||||
|
height: 450px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ccc transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-view-title-header,
|
||||||
|
.table-view-values-header {
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ccc transparent;
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100px;
|
||||||
|
max-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-query-builder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.inspect-metrics-query-builder-header {
|
||||||
|
.query-builder-button-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-query-builder-content {
|
||||||
|
.ant-card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.selected-step {
|
||||||
|
color: var(--bg-sakura-500);
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-sakura-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
min-width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-arrows-input input[type='number']::-webkit-inner-spin-button,
|
||||||
|
.no-arrows-input input[type='number']::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide number input arrows (Firefox) */
|
||||||
|
.no-arrows-input input[type='number'] {
|
||||||
|
appearance: none;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-time-aggregation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.metric-time-aggregation-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-time-aggregation-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.inspect-metrics-input-group {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-space-aggregation {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.metric-space-aggregation-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-space-aggregation-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.metric-space-aggregation-content-left {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-filters {
|
||||||
|
.query-builder-search-container {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
background-color: var(--bg-ink-400);
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
border-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-content-second-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.home-checklist-container {
|
||||||
|
padding-left: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding-bottom: 32px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.home-checklist-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-checklist-container {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-message-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100px;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding-left: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-graph-popover {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
.inspect-graph-popover-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 350px;
|
||||||
|
|
||||||
|
.inspect-graph-popover-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-graph-popover-button-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
.graph-popover-card {
|
||||||
|
width: 550px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.graph-popover-row-label {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-inner-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
width: 400px;
|
||||||
|
margin-top: 4px;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ccc transparent;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-header-text {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-row-label {
|
||||||
|
color: var(--bg-slate-50);
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-cell {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background-color: #1f1f1f;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
min-width: 60px;
|
||||||
|
max-width: 60px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-row {
|
||||||
|
margin-top: 12px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-divider {
|
||||||
|
flex: 1;
|
||||||
|
border-top: 1px dashed #ccc;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-view {
|
||||||
|
.expanded-view-header {
|
||||||
|
.ant-typography {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover {
|
||||||
|
z-index: 2;
|
||||||
|
position: initial;
|
||||||
|
|
||||||
|
.graph-popover-card {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.timeseries-cell {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
&:hover {
|
||||||
|
opacity: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
opacity: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-section {
|
||||||
|
width: 500px;
|
||||||
|
overflow-x: scroll;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #ccc transparent;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.graph-popover-row {
|
||||||
|
.graph-popover-row-label {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-inner-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-table {
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.labels-key {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
background-color: var(--bg-slate-500);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-value {
|
||||||
|
background-color: var(--bg-slate-500);
|
||||||
|
opacity: 80%;
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
|
||||||
|
.field-renderer-container {
|
||||||
|
.label {
|
||||||
|
color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-popover-card {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 500;
|
||||||
|
max-width: 700px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.hover-popover-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.inspect-metrics-modal {
|
||||||
|
.inspect-metrics-title {
|
||||||
|
.inspect-metrics-button {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-content {
|
||||||
|
.inspect-metrics-content-first-col {
|
||||||
|
.inspect-metrics-graph-view {
|
||||||
|
.inspect-metrics-graph-view-header {
|
||||||
|
.ant-btn-group {
|
||||||
|
.time-series-button-label,
|
||||||
|
.metric-name-button-label {
|
||||||
|
span {
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspect-metrics-query-builder {
|
||||||
|
.inspect-metrics-query-builder-header {
|
||||||
|
.query-builder-button-label {
|
||||||
|
span {
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-filters {
|
||||||
|
.query-builder-search-v2 {
|
||||||
|
.ant-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
border: 0.5px solid var(--bg-slate-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover {
|
||||||
|
.graph-popover-card {
|
||||||
|
.graph-popover-header-text {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-row-label {
|
||||||
|
color: var(--bg-slate-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-popover-cell {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-row {
|
||||||
|
.footer-divider {
|
||||||
|
border-top: 1px dashed var(--bg-slate-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded-view {
|
||||||
|
.labels-table {
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
|
||||||
|
.labels-key {
|
||||||
|
color: var(--bg-slate-400);
|
||||||
|
background-color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-value {
|
||||||
|
background-color: var(--bg-vanilla-400);
|
||||||
|
.field-renderer-container {
|
||||||
|
.label {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,15 +2,236 @@ import './Inspect.styles.scss';
|
|||||||
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Drawer, Typography } from 'antd';
|
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
|
||||||
|
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { Compass } from 'lucide-react';
|
import { Compass } from 'lucide-react';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { InspectProps } from './types';
|
import ExpandedView from './ExpandedView';
|
||||||
|
import GraphView from './GraphView';
|
||||||
|
import QueryBuilder from './QueryBuilder';
|
||||||
|
import Stepper from './Stepper';
|
||||||
|
import { GraphPopoverOptions, InspectProps } from './types';
|
||||||
|
import { useInspectMetrics } from './useInspectMetrics';
|
||||||
|
|
||||||
function Inspect({ metricName, isOpen, onClose }: InspectProps): JSX.Element {
|
function Inspect({
|
||||||
|
metricName: defaultMetricName,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: InspectProps): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const [metricName, setMetricName] = useState<string | null>(defaultMetricName);
|
||||||
|
const [
|
||||||
|
popoverOptions,
|
||||||
|
setPopoverOptions,
|
||||||
|
] = useState<GraphPopoverOptions | null>(null);
|
||||||
|
const [
|
||||||
|
expandedViewOptions,
|
||||||
|
setExpandedViewOptions,
|
||||||
|
] = useState<GraphPopoverOptions | null>(null);
|
||||||
|
const [showExpandedView, setShowExpandedView] = useState(false);
|
||||||
|
|
||||||
|
const { data: metricDetailsData } = useGetMetricDetails(metricName ?? '', {
|
||||||
|
enabled: !!metricName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: currentQuery.builder.queryData[0],
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedCurrentQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...currentQuery.builder.queryData[0],
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: {
|
||||||
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchQuery = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleChangeQueryData('filters', {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
inspectMetricsStatusCode,
|
||||||
|
isInspectMetricsLoading,
|
||||||
|
isInspectMetricsError,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
spaceAggregationLabels,
|
||||||
|
metricInspectionOptions,
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
inspectionStep,
|
||||||
|
isInspectMetricsRefetching,
|
||||||
|
spaceAggregatedSeriesMap: spaceAggregationSeriesMap,
|
||||||
|
aggregatedTimeSeries,
|
||||||
|
timeAggregatedSeriesMap,
|
||||||
|
reset,
|
||||||
|
} = useInspectMetrics(metricName);
|
||||||
|
|
||||||
|
const selectedMetricType = useMemo(
|
||||||
|
() => metricDetailsData?.payload?.data?.metadata?.metric_type,
|
||||||
|
[metricDetailsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedMetricUnit = useMemo(
|
||||||
|
() => metricDetailsData?.payload?.data?.metadata?.unit,
|
||||||
|
[metricDetailsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetInspection = useCallback(() => {
|
||||||
|
setShowExpandedView(false);
|
||||||
|
setPopoverOptions(null);
|
||||||
|
setExpandedViewOptions(null);
|
||||||
|
reset();
|
||||||
|
}, [reset]);
|
||||||
|
|
||||||
|
// Reset inspection when the selected metric changes
|
||||||
|
useEffect(() => {
|
||||||
|
resetInspection();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [metricName]);
|
||||||
|
|
||||||
|
// Hide expanded view whenever inspection step changes
|
||||||
|
useEffect(() => {
|
||||||
|
setShowExpandedView(false);
|
||||||
|
setExpandedViewOptions(null);
|
||||||
|
}, [inspectionStep]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (isInspectMetricsLoading && !isInspectMetricsRefetching) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="inspect-metrics-loading"
|
||||||
|
className="inspect-metrics-fallback"
|
||||||
|
>
|
||||||
|
<Skeleton active />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInspectMetricsError || inspectMetricsStatusCode !== 200) {
|
||||||
|
const errorMessage =
|
||||||
|
inspectMetricsStatusCode === 400
|
||||||
|
? 'The time range is too large. Please modify it to be within 30 minutes.'
|
||||||
|
: 'Error loading inspect metrics.';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="inspect-metrics-error"
|
||||||
|
className="inspect-metrics-fallback"
|
||||||
|
>
|
||||||
|
<Empty description={errorMessage} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inspectMetricsTimeSeries.length) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="inspect-metrics-empty"
|
||||||
|
className="inspect-metrics-fallback"
|
||||||
|
>
|
||||||
|
<Empty description="No time series found for this metric to inspect." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inspect-metrics-content">
|
||||||
|
<div className="inspect-metrics-content-first-col">
|
||||||
|
<GraphView
|
||||||
|
inspectMetricsTimeSeries={aggregatedTimeSeries}
|
||||||
|
formattedInspectMetricsTimeSeries={formattedInspectMetricsTimeSeries}
|
||||||
|
resetInspection={resetInspection}
|
||||||
|
metricName={metricName}
|
||||||
|
metricUnit={selectedMetricUnit}
|
||||||
|
metricType={selectedMetricType}
|
||||||
|
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
|
||||||
|
inspectionStep={inspectionStep}
|
||||||
|
setPopoverOptions={setPopoverOptions}
|
||||||
|
setShowExpandedView={setShowExpandedView}
|
||||||
|
showExpandedView={showExpandedView}
|
||||||
|
setExpandedViewOptions={setExpandedViewOptions}
|
||||||
|
popoverOptions={popoverOptions}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
isInspectMetricsRefetching={isInspectMetricsRefetching}
|
||||||
|
/>
|
||||||
|
<QueryBuilder
|
||||||
|
metricName={metricName}
|
||||||
|
metricType={selectedMetricType}
|
||||||
|
setMetricName={setMetricName}
|
||||||
|
spaceAggregationLabels={spaceAggregationLabels}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
|
||||||
|
inspectionStep={inspectionStep}
|
||||||
|
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="inspect-metrics-content-second-col">
|
||||||
|
<Stepper
|
||||||
|
inspectionStep={inspectionStep}
|
||||||
|
resetInspection={resetInspection}
|
||||||
|
/>
|
||||||
|
{showExpandedView && (
|
||||||
|
<ExpandedView
|
||||||
|
options={expandedViewOptions}
|
||||||
|
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
|
||||||
|
step={inspectionStep}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
timeAggregatedSeriesMap={timeAggregatedSeriesMap}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
isInspectMetricsLoading,
|
||||||
|
isInspectMetricsRefetching,
|
||||||
|
isInspectMetricsError,
|
||||||
|
inspectMetricsStatusCode,
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
aggregatedTimeSeries,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
resetInspection,
|
||||||
|
metricName,
|
||||||
|
selectedMetricUnit,
|
||||||
|
selectedMetricType,
|
||||||
|
spaceAggregationSeriesMap,
|
||||||
|
inspectionStep,
|
||||||
|
showExpandedView,
|
||||||
|
popoverOptions,
|
||||||
|
metricInspectionOptions,
|
||||||
|
spaceAggregationLabels,
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
searchQuery,
|
||||||
|
expandedViewOptions,
|
||||||
|
timeAggregatedSeriesMap,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
@ -38,8 +259,7 @@ function Inspect({ metricName, isOpen, onClose }: InspectProps): JSX.Element {
|
|||||||
className="inspect-metrics-modal"
|
className="inspect-metrics-modal"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<div>Inspect</div>
|
{content}
|
||||||
<div>{metricName}</div>
|
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
import { Button, Card } from 'antd';
|
||||||
|
import { Atom } from 'lucide-react';
|
||||||
|
|
||||||
|
import { QueryBuilderProps } from './types';
|
||||||
|
import {
|
||||||
|
MetricFilters,
|
||||||
|
MetricNameSearch,
|
||||||
|
MetricSpaceAggregation,
|
||||||
|
MetricTimeAggregation,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function QueryBuilder({
|
||||||
|
metricName,
|
||||||
|
setMetricName,
|
||||||
|
spaceAggregationLabels,
|
||||||
|
metricInspectionOptions,
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
inspectionStep,
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
searchQuery,
|
||||||
|
metricType,
|
||||||
|
}: QueryBuilderProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="inspect-metrics-query-builder">
|
||||||
|
<div className="inspect-metrics-query-builder-header">
|
||||||
|
<Button
|
||||||
|
className="query-builder-button-label"
|
||||||
|
size="middle"
|
||||||
|
icon={<Atom size={14} />}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
Query Builder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Card className="inspect-metrics-query-builder-content">
|
||||||
|
<MetricNameSearch metricName={metricName} setMetricName={setMetricName} />
|
||||||
|
<MetricFilters
|
||||||
|
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
metricName={metricName}
|
||||||
|
metricType={metricType || null}
|
||||||
|
/>
|
||||||
|
<MetricTimeAggregation
|
||||||
|
inspectionStep={inspectionStep}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
|
||||||
|
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
|
||||||
|
/>
|
||||||
|
<MetricSpaceAggregation
|
||||||
|
inspectionStep={inspectionStep}
|
||||||
|
spaceAggregationLabels={spaceAggregationLabels}
|
||||||
|
metricInspectionOptions={metricInspectionOptions}
|
||||||
|
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueryBuilder;
|
92
frontend/src/container/MetricsExplorer/Inspect/Stepper.tsx
Normal file
92
frontend/src/container/MetricsExplorer/Inspect/Stepper.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import '../../Home/HomeChecklist/HomeChecklist.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Typography } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { ArrowUpRightFromSquare, RefreshCcw } from 'lucide-react';
|
||||||
|
|
||||||
|
import { SPACE_AGGREGATION_LINK, TEMPORAL_AGGREGATION_LINK } from './constants';
|
||||||
|
import { InspectionStep, StepperProps } from './types';
|
||||||
|
|
||||||
|
function Stepper({
|
||||||
|
inspectionStep,
|
||||||
|
resetInspection,
|
||||||
|
}: StepperProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="home-checklist-container">
|
||||||
|
<div className="home-checklist-title">
|
||||||
|
<Typography.Text>
|
||||||
|
👋 Hello, welcome to the Metrics Inspector
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text>Let’s get you started...</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="completed-checklist-container whats-next-checklist-container">
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
'completed-checklist-item':
|
||||||
|
inspectionStep > InspectionStep.TIME_AGGREGATION,
|
||||||
|
'whats-next-checklist-item':
|
||||||
|
inspectionStep <= InspectionStep.TIME_AGGREGATION,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
'completed-checklist-item-title':
|
||||||
|
inspectionStep > InspectionStep.TIME_AGGREGATION,
|
||||||
|
'whats-next-checklist-item-title':
|
||||||
|
inspectionStep <= InspectionStep.TIME_AGGREGATION,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
First, align the data by selecting a{' '}
|
||||||
|
<Typography.Link href={TEMPORAL_AGGREGATION_LINK} target="_blank">
|
||||||
|
Temporal Aggregation{' '}
|
||||||
|
<ArrowUpRightFromSquare color={Color.BG_ROBIN_500} size={10} />
|
||||||
|
</Typography.Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
'completed-checklist-item':
|
||||||
|
inspectionStep > InspectionStep.SPACE_AGGREGATION,
|
||||||
|
'whats-next-checklist-item':
|
||||||
|
inspectionStep <= InspectionStep.SPACE_AGGREGATION,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
'completed-checklist-item-title':
|
||||||
|
inspectionStep > InspectionStep.SPACE_AGGREGATION,
|
||||||
|
'whats-next-checklist-item-title':
|
||||||
|
inspectionStep <= InspectionStep.SPACE_AGGREGATION,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Add a{' '}
|
||||||
|
<Typography.Link href={SPACE_AGGREGATION_LINK} target="_blank">
|
||||||
|
Spatial Aggregation{' '}
|
||||||
|
<ArrowUpRightFromSquare color={Color.BG_ROBIN_500} size={10} />
|
||||||
|
</Typography.Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="completed-message-container">
|
||||||
|
{inspectionStep === InspectionStep.COMPLETED && (
|
||||||
|
<>
|
||||||
|
<Typography.Text>
|
||||||
|
🎉 Ta-da! You have completed your metric query tutorial.
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text>
|
||||||
|
You can inspect a new metric or reset the query builder.
|
||||||
|
</Typography.Text>
|
||||||
|
<Button icon={<RefreshCcw size={12} />} onClick={resetInspection}>
|
||||||
|
Reset query
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Stepper;
|
136
frontend/src/container/MetricsExplorer/Inspect/TableView.tsx
Normal file
136
frontend/src/container/MetricsExplorer/Inspect/TableView.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { Card, Flex, Table, Typography } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { TableViewProps } from './types';
|
||||||
|
import { formatTimestampToFullDateTime } from './utils';
|
||||||
|
|
||||||
|
function TableView({
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
setShowExpandedView,
|
||||||
|
setExpandedViewOptions,
|
||||||
|
isInspectMetricsRefetching,
|
||||||
|
metricInspectionOptions,
|
||||||
|
}: TableViewProps): JSX.Element {
|
||||||
|
const isSpaceAggregatedWithoutLabel = useMemo(
|
||||||
|
() =>
|
||||||
|
!!metricInspectionOptions.spaceAggregationOption &&
|
||||||
|
metricInspectionOptions.spaceAggregationLabels.length === 0,
|
||||||
|
[metricInspectionOptions],
|
||||||
|
);
|
||||||
|
const labelKeys = useMemo(() => {
|
||||||
|
if (isSpaceAggregatedWithoutLabel) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (inspectMetricsTimeSeries.length > 0) {
|
||||||
|
return Object.keys(inspectMetricsTimeSeries[0].labels);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [inspectMetricsTimeSeries, isSpaceAggregatedWithoutLabel]);
|
||||||
|
|
||||||
|
const getDynamicColumnStyle = (strokeColor?: string): React.CSSProperties => {
|
||||||
|
const style: React.CSSProperties = {
|
||||||
|
maxWidth: '200px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
};
|
||||||
|
if (strokeColor) {
|
||||||
|
style.color = strokeColor;
|
||||||
|
}
|
||||||
|
return style;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
...labelKeys.map((label) => ({
|
||||||
|
title: label,
|
||||||
|
dataIndex: label,
|
||||||
|
align: 'left',
|
||||||
|
render: (text: string): JSX.Element => (
|
||||||
|
<div style={getDynamicColumnStyle()}>{text}</div>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
title: 'Values',
|
||||||
|
dataIndex: 'values',
|
||||||
|
align: 'left',
|
||||||
|
sticky: 'right',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[labelKeys],
|
||||||
|
);
|
||||||
|
const openExpandedView = useCallback(
|
||||||
|
(series: InspectMetricsSeries, value: string, timestamp: number): void => {
|
||||||
|
setShowExpandedView(true);
|
||||||
|
setExpandedViewOptions({
|
||||||
|
x: timestamp,
|
||||||
|
y: Number(value),
|
||||||
|
value: Number(value),
|
||||||
|
timestamp,
|
||||||
|
timeSeries: series,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setShowExpandedView, setExpandedViewOptions],
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataSource = useMemo(
|
||||||
|
() =>
|
||||||
|
inspectMetricsTimeSeries.map((series, index) => {
|
||||||
|
const labelData = labelKeys.reduce((acc, label) => {
|
||||||
|
acc[label] = (
|
||||||
|
<div style={getDynamicColumnStyle(series.strokeColor)}>
|
||||||
|
{series.labels[label]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, JSX.Element>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: index,
|
||||||
|
...labelData,
|
||||||
|
values: (
|
||||||
|
<div className="table-view-values-header">
|
||||||
|
<Flex gap={8}>
|
||||||
|
{series.values.map((value) => {
|
||||||
|
const formattedValue = `(${formatTimestampToFullDateTime(
|
||||||
|
value.timestamp,
|
||||||
|
true,
|
||||||
|
)}, ${value.value})`;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={formattedValue}
|
||||||
|
onClick={(): void =>
|
||||||
|
openExpandedView(series, value.value, value.timestamp)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Typography.Text>{formattedValue}</Typography.Text>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[inspectMetricsTimeSeries, labelKeys, openExpandedView],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
className="inspect-metrics-table-view"
|
||||||
|
dataSource={dataSource}
|
||||||
|
columns={
|
||||||
|
columns as ColumnsType<{
|
||||||
|
values: JSX.Element;
|
||||||
|
key: number;
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
scroll={{ x: '100%' }}
|
||||||
|
loading={isInspectMetricsRefetching}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TableView;
|
@ -0,0 +1,166 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
|
||||||
|
TIME_AGGREGATION_OPTIONS,
|
||||||
|
} from '../constants';
|
||||||
|
import ExpandedView from '../ExpandedView';
|
||||||
|
import {
|
||||||
|
GraphPopoverData,
|
||||||
|
InspectionStep,
|
||||||
|
MetricInspectionOptions,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
describe('ExpandedView', () => {
|
||||||
|
const mockTimeSeries: InspectMetricsSeries = {
|
||||||
|
values: [
|
||||||
|
{ timestamp: 1672531200000, value: '42.123' },
|
||||||
|
{ timestamp: 1672531260000, value: '43.456' },
|
||||||
|
{ timestamp: 1672531320000, value: '44.789' },
|
||||||
|
{ timestamp: 1672531380000, value: '45.012' },
|
||||||
|
],
|
||||||
|
labels: {
|
||||||
|
host_id: 'test-id',
|
||||||
|
},
|
||||||
|
labelsArray: [],
|
||||||
|
title: 'TS1',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockOptions = {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
value: 42.123,
|
||||||
|
timestamp: 1672531200000,
|
||||||
|
timeSeries: mockTimeSeries,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSpaceAggregationSeriesMap = new Map<string, InspectMetricsSeries[]>([
|
||||||
|
['host_id:test-id', [mockTimeSeries]],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mockTimeAggregatedSeriesMap = new Map<number, GraphPopoverData[]>([
|
||||||
|
[
|
||||||
|
1672531200000,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value: '42.123',
|
||||||
|
type: 'instance',
|
||||||
|
timestamp: 1672531200000,
|
||||||
|
title: 'TS1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '43.456',
|
||||||
|
type: 'instance',
|
||||||
|
timestamp: 1672531260000,
|
||||||
|
title: 'TS1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mockMetricInspectionOptions: MetricInspectionOptions = {
|
||||||
|
timeAggregationOption: TimeAggregationOptions.MAX,
|
||||||
|
timeAggregationInterval: 60,
|
||||||
|
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
|
||||||
|
spaceAggregationLabels: ['host_name'],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders entire time series for a raw data inspection', () => {
|
||||||
|
render(
|
||||||
|
<ExpandedView
|
||||||
|
options={mockOptions}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={InspectionStep.TIME_AGGREGATION}
|
||||||
|
metricInspectionOptions={mockMetricInspectionOptions}
|
||||||
|
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const graphPopoverCells = screen.getAllByTestId('graph-popover-cell');
|
||||||
|
expect(graphPopoverCells).toHaveLength(mockTimeSeries.values.length * 2);
|
||||||
|
|
||||||
|
expect(screen.getAllByText('42.123')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correct split data for a time aggregation inspection', () => {
|
||||||
|
const TIME_AGGREGATION_INTERVAL = 120;
|
||||||
|
render(
|
||||||
|
<ExpandedView
|
||||||
|
options={mockOptions}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={InspectionStep.SPACE_AGGREGATION}
|
||||||
|
metricInspectionOptions={{
|
||||||
|
...mockMetricInspectionOptions,
|
||||||
|
timeAggregationInterval: TIME_AGGREGATION_INTERVAL,
|
||||||
|
}}
|
||||||
|
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// time series by default has values at 60 seconds
|
||||||
|
// by doing time aggregation at 120 seconds, we should have 2 values
|
||||||
|
const graphPopoverCells = screen.getAllByTestId('graph-popover-cell');
|
||||||
|
expect(graphPopoverCells).toHaveLength((TIME_AGGREGATION_INTERVAL / 60) * 2);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
`42.123 is the ${
|
||||||
|
TIME_AGGREGATION_OPTIONS[
|
||||||
|
mockMetricInspectionOptions.timeAggregationOption as TimeAggregationOptions
|
||||||
|
]
|
||||||
|
} of`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(screen.getByText('42.123')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('43.456')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all child time series for a space aggregation inspection', () => {
|
||||||
|
render(
|
||||||
|
<ExpandedView
|
||||||
|
options={mockOptions}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={InspectionStep.COMPLETED}
|
||||||
|
metricInspectionOptions={mockMetricInspectionOptions}
|
||||||
|
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const graphPopoverCells = screen.getAllByTestId('graph-popover-cell');
|
||||||
|
expect(graphPopoverCells).toHaveLength(
|
||||||
|
mockSpaceAggregationSeriesMap.size * 2,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText(
|
||||||
|
`42.123 is the ${
|
||||||
|
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW[
|
||||||
|
mockMetricInspectionOptions.spaceAggregationOption as SpaceAggregationOptions
|
||||||
|
]
|
||||||
|
} of`,
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('TS1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all labels for the selected time series', () => {
|
||||||
|
render(
|
||||||
|
<ExpandedView
|
||||||
|
options={mockOptions}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={InspectionStep.TIME_AGGREGATION}
|
||||||
|
metricInspectionOptions={mockMetricInspectionOptions}
|
||||||
|
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText(`${mockTimeSeries.title} Labels`),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('host_id')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('test-id')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,82 @@
|
|||||||
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
|
||||||
|
import GraphPopover from '../GraphPopover';
|
||||||
|
import { GraphPopoverOptions, InspectionStep } from '../types';
|
||||||
|
|
||||||
|
describe('GraphPopover', () => {
|
||||||
|
const mockOptions: GraphPopoverOptions = {
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
value: 42.123,
|
||||||
|
timestamp: 1672531200000,
|
||||||
|
timeSeries: {
|
||||||
|
values: [
|
||||||
|
{ timestamp: 1672531200000, value: '42.123' },
|
||||||
|
{ timestamp: 1672531260000, value: '43.456' },
|
||||||
|
],
|
||||||
|
labels: {},
|
||||||
|
labelsArray: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockSpaceAggregationSeriesMap: Map<
|
||||||
|
string,
|
||||||
|
InspectMetricsSeries[]
|
||||||
|
> = new Map();
|
||||||
|
|
||||||
|
const mockOpenInExpandedView = jest.fn();
|
||||||
|
const mockStep = InspectionStep.TIME_AGGREGATION;
|
||||||
|
|
||||||
|
it('renders with correct values', () => {
|
||||||
|
render(
|
||||||
|
<GraphPopover
|
||||||
|
options={mockOptions}
|
||||||
|
popoverRef={{ current: null }}
|
||||||
|
openInExpandedView={mockOpenInExpandedView}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={mockStep}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check value is rendered with 2 decimal places
|
||||||
|
expect(screen.getByText('42.12')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the expanded view when button is clicked', () => {
|
||||||
|
render(
|
||||||
|
<GraphPopover
|
||||||
|
options={mockOptions}
|
||||||
|
popoverRef={{ current: null }}
|
||||||
|
openInExpandedView={mockOpenInExpandedView}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={mockStep}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = screen.getByText('View details');
|
||||||
|
fireEvent.click(button);
|
||||||
|
|
||||||
|
expect(mockOpenInExpandedView).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds closest timestamp and value from timeSeries', () => {
|
||||||
|
const optionsWithOffset: GraphPopoverOptions = {
|
||||||
|
...mockOptions,
|
||||||
|
timestamp: 1672531230000,
|
||||||
|
value: 42.24,
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<GraphPopover
|
||||||
|
options={optionsWithOffset}
|
||||||
|
popoverRef={{ current: null }}
|
||||||
|
openInExpandedView={mockOpenInExpandedView}
|
||||||
|
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
|
||||||
|
step={mockStep}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should show the closest value
|
||||||
|
expect(screen.getByText('43.46')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,113 @@
|
|||||||
|
/* eslint-disable sonarjs/no-duplicate-string */
|
||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import store from 'store';
|
||||||
|
import { AlignedData } from 'uplot';
|
||||||
|
|
||||||
|
import GraphView from '../GraphView';
|
||||||
|
import {
|
||||||
|
InspectionStep,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
jest.mock('uplot', () =>
|
||||||
|
jest.fn().mockImplementation(() => ({
|
||||||
|
destroy: jest.fn(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
const mockResizeObserver = jest.fn();
|
||||||
|
mockResizeObserver.mockImplementation(() => ({
|
||||||
|
observe: (): void => undefined,
|
||||||
|
unobserve: (): void => undefined,
|
||||||
|
disconnect: (): void => undefined,
|
||||||
|
}));
|
||||||
|
window.ResizeObserver = mockResizeObserver;
|
||||||
|
|
||||||
|
describe('GraphView', () => {
|
||||||
|
const mockTimeSeries: InspectMetricsSeries[] = [
|
||||||
|
{
|
||||||
|
strokeColor: '#000',
|
||||||
|
title: 'Series 1',
|
||||||
|
values: [
|
||||||
|
{ timestamp: 1234567890000, value: '10' },
|
||||||
|
{ timestamp: 1234567891000, value: '20' },
|
||||||
|
],
|
||||||
|
labels: { label1: 'value1' },
|
||||||
|
labelsArray: [{ label: 'label1', value: 'value1' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
inspectMetricsTimeSeries: mockTimeSeries,
|
||||||
|
formattedInspectMetricsTimeSeries: [
|
||||||
|
[1, 2],
|
||||||
|
[1, 2],
|
||||||
|
] as AlignedData,
|
||||||
|
metricUnit: '',
|
||||||
|
metricName: 'test_metric',
|
||||||
|
metricType: MetricType.GAUGE,
|
||||||
|
spaceAggregationSeriesMap: new Map(),
|
||||||
|
inspectionStep: InspectionStep.COMPLETED,
|
||||||
|
setPopoverOptions: jest.fn(),
|
||||||
|
popoverOptions: null,
|
||||||
|
setShowExpandedView: jest.fn(),
|
||||||
|
setExpandedViewOptions: jest.fn(),
|
||||||
|
resetInspection: jest.fn(),
|
||||||
|
showExpandedView: false,
|
||||||
|
metricInspectionOptions: {
|
||||||
|
timeAggregationInterval: 60,
|
||||||
|
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
|
||||||
|
spaceAggregationLabels: ['host_name'],
|
||||||
|
timeAggregationOption: TimeAggregationOptions.MAX,
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isInspectMetricsRefetching: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders graph view by default', () => {
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<GraphView {...defaultProps} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByRole('switch')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Graph View')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switches between graph and table view', async () => {
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<GraphView {...defaultProps} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const switchButton = screen.getByRole('switch');
|
||||||
|
expect(screen.getByText('Graph View')).toBeInTheDocument();
|
||||||
|
|
||||||
|
await userEvent.click(switchButton);
|
||||||
|
expect(screen.getByText('Table View')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders metric name and number of series', () => {
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<GraphView {...defaultProps} />
|
||||||
|
</Provider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('test_metric')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('1 time series')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,198 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import * as useInspectMetricsHooks from 'hooks/metricsExplorer/useGetInspectMetricsDetails';
|
||||||
|
import * as useGetMetricDetailsHooks from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
import ROUTES from '../../../../constants/routes';
|
||||||
|
import Inspect from '../Inspect';
|
||||||
|
import { InspectionStep } from '../types';
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
const mockTimeSeries: InspectMetricsSeries[] = [
|
||||||
|
{
|
||||||
|
strokeColor: '#000',
|
||||||
|
title: 'Series 1',
|
||||||
|
values: [
|
||||||
|
{ timestamp: 1234567890000, value: '10' },
|
||||||
|
{ timestamp: 1234567891000, value: '20' },
|
||||||
|
],
|
||||||
|
labels: { label1: 'value1' },
|
||||||
|
labelsArray: [{ label: 'label1', value: 'value1' }],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
jest.spyOn(useGetMetricDetailsHooks, 'useGetMetricDetails').mockReturnValue({
|
||||||
|
data: {
|
||||||
|
metricDetails: {
|
||||||
|
metricName: 'test_metric',
|
||||||
|
metricType: MetricType.GAUGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
|
||||||
|
.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
series: mockTimeSeries,
|
||||||
|
},
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
jest.mock('uplot', () =>
|
||||||
|
jest.fn().mockImplementation(() => ({
|
||||||
|
destroy: jest.fn(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: (): { pathname: string } => ({
|
||||||
|
pathname: `${ROUTES.METRICS_EXPLORER_BASE}`,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockResizeObserver = jest.fn();
|
||||||
|
mockResizeObserver.mockImplementation(() => ({
|
||||||
|
observe: (): void => undefined,
|
||||||
|
unobserve: (): void => undefined,
|
||||||
|
disconnect: (): void => undefined,
|
||||||
|
}));
|
||||||
|
window.ResizeObserver = mockResizeObserver;
|
||||||
|
|
||||||
|
describe('Inspect', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
inspectMetricsTimeSeries: mockTimeSeries,
|
||||||
|
formattedInspectMetricsTimeSeries: [],
|
||||||
|
metricUnit: '',
|
||||||
|
metricName: 'test_metric',
|
||||||
|
metricType: MetricType.GAUGE,
|
||||||
|
spaceAggregationSeriesMap: new Map(),
|
||||||
|
inspectionStep: InspectionStep.COMPLETED,
|
||||||
|
resetInspection: jest.fn(),
|
||||||
|
isOpen: true,
|
||||||
|
onClose: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all components', () => {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Inspect {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('test_metric')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('switch')).toBeInTheDocument(); // Graph/Table view switch
|
||||||
|
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders loading state', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
|
||||||
|
.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: true,
|
||||||
|
} as any);
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Inspect {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('inspect-metrics-loading')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty state', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
|
||||||
|
.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
} as any);
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Inspect {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('inspect-metrics-empty')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders error state', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
|
||||||
|
.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
series: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
isError: true,
|
||||||
|
} as any);
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Inspect {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('inspect-metrics-error')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders error state with 400 status code', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(useInspectMetricsHooks, 'useGetInspectMetricsDetails')
|
||||||
|
.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
statusCode: 400,
|
||||||
|
},
|
||||||
|
isError: false,
|
||||||
|
} as any);
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Inspect {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('inspect-metrics-error')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,110 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import store from 'store';
|
||||||
|
|
||||||
|
import ROUTES from '../../../../constants/routes';
|
||||||
|
import QueryBuilder from '../QueryBuilder';
|
||||||
|
import {
|
||||||
|
InspectionStep,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: (): { pathname: string } => ({
|
||||||
|
pathname: `${ROUTES.METRICS_EXPLORER_BASE}`,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
describe('QueryBuilder', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
metricName: 'test_metric',
|
||||||
|
setMetricName: jest.fn(),
|
||||||
|
spaceAggregationLabels: ['label1', 'label2'],
|
||||||
|
metricInspectionOptions: {
|
||||||
|
timeAggregationInterval: 60,
|
||||||
|
timeAggregationOption: TimeAggregationOptions.AVG,
|
||||||
|
spaceAggregationLabels: [],
|
||||||
|
spaceAggregationOption: SpaceAggregationOptions.AVG_BY,
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dispatchMetricInspectionOptions: jest.fn(),
|
||||||
|
metricType: MetricType.SUM,
|
||||||
|
inspectionStep: InspectionStep.TIME_AGGREGATION,
|
||||||
|
inspectMetricsTimeSeries: [],
|
||||||
|
searchQuery: {
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders query builder header', () => {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryBuilder {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders metric name search component', () => {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryBuilder {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('metric-name-search')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders metric filters component', () => {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryBuilder {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('metric-filters')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders time aggregation component', () => {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryBuilder {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('metric-time-aggregation')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders space aggregation component', () => {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<QueryBuilder {...defaultProps} />
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('metric-space-aggregation')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,64 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import Stepper from '../Stepper';
|
||||||
|
import { InspectionStep } from '../types';
|
||||||
|
|
||||||
|
describe('Stepper', () => {
|
||||||
|
const mockResetInspection = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders welcome message', () => {
|
||||||
|
render(
|
||||||
|
<Stepper
|
||||||
|
inspectionStep={InspectionStep.TIME_AGGREGATION}
|
||||||
|
resetInspection={mockResetInspection}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText('👋 Hello, welcome to the Metrics Inspector'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows temporal aggregation step as active when on first step', () => {
|
||||||
|
render(
|
||||||
|
<Stepper
|
||||||
|
inspectionStep={InspectionStep.TIME_AGGREGATION}
|
||||||
|
resetInspection={mockResetInspection}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const temporalStep = screen.getByText(/First, align the data by selecting a/);
|
||||||
|
expect(temporalStep.parentElement).toHaveClass('whats-next-checklist-item');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows temporal aggregation step as completed when on later steps', () => {
|
||||||
|
render(
|
||||||
|
<Stepper
|
||||||
|
inspectionStep={InspectionStep.SPACE_AGGREGATION}
|
||||||
|
resetInspection={mockResetInspection}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const temporalStep = screen.getByText(/First, align the data by selecting a/);
|
||||||
|
expect(temporalStep.parentElement).toHaveClass('completed-checklist-item');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls resetInspection when reset button is clicked', async () => {
|
||||||
|
render(
|
||||||
|
<Stepper
|
||||||
|
inspectionStep={InspectionStep.COMPLETED}
|
||||||
|
resetInspection={mockResetInspection}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole('button');
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
expect(mockResetInspection).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,100 @@
|
|||||||
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
|
||||||
|
import TableView from '../TableView';
|
||||||
|
import {
|
||||||
|
InspectionStep,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from '../types';
|
||||||
|
import { formatTimestampToFullDateTime } from '../utils';
|
||||||
|
|
||||||
|
describe('TableView', () => {
|
||||||
|
const mockTimeSeries: InspectMetricsSeries[] = [
|
||||||
|
{
|
||||||
|
strokeColor: '#000',
|
||||||
|
title: 'Series 1',
|
||||||
|
values: [
|
||||||
|
{ timestamp: 1234567890000, value: '10' },
|
||||||
|
{ timestamp: 1234567891000, value: '20' },
|
||||||
|
],
|
||||||
|
labels: { label1: 'value1' },
|
||||||
|
labelsArray: [
|
||||||
|
{
|
||||||
|
label: 'label1',
|
||||||
|
value: 'value1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
strokeColor: '#fff',
|
||||||
|
title: 'Series 2',
|
||||||
|
values: [
|
||||||
|
{ timestamp: 1234567890000, value: '30' },
|
||||||
|
{ timestamp: 1234567891000, value: '40' },
|
||||||
|
],
|
||||||
|
labels: { label2: 'value2' },
|
||||||
|
labelsArray: [
|
||||||
|
{
|
||||||
|
label: 'label2',
|
||||||
|
value: 'value2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
inspectionStep: InspectionStep.COMPLETED,
|
||||||
|
inspectMetricsTimeSeries: mockTimeSeries,
|
||||||
|
setShowExpandedView: jest.fn(),
|
||||||
|
setExpandedViewOptions: jest.fn(),
|
||||||
|
metricInspectionOptions: {
|
||||||
|
timeAggregationInterval: 60,
|
||||||
|
timeAggregationOption: TimeAggregationOptions.MAX,
|
||||||
|
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
|
||||||
|
spaceAggregationLabels: ['host_name'],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isInspectMetricsRefetching: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders table with correct columns', () => {
|
||||||
|
render(<TableView {...defaultProps} />);
|
||||||
|
expect(screen.getByText('label1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Values')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders time series titles correctly when inspection is completed', () => {
|
||||||
|
render(<TableView {...defaultProps} />);
|
||||||
|
expect(screen.getByText('label1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders time series values in correct format', () => {
|
||||||
|
render(<TableView {...defaultProps} />);
|
||||||
|
const formattedValues = mockTimeSeries.map(
|
||||||
|
(series) =>
|
||||||
|
series.values.map(
|
||||||
|
(v) => `(${formatTimestampToFullDateTime(v.timestamp, true)}, ${v.value})`,
|
||||||
|
)[0],
|
||||||
|
);
|
||||||
|
formattedValues.forEach((value) => {
|
||||||
|
expect(screen.getByText(value, { exact: false })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies correct styling to time series titles', () => {
|
||||||
|
render(<TableView {...defaultProps} />);
|
||||||
|
const titles = screen.getByText('value1');
|
||||||
|
expect(titles).toHaveStyle({ color: mockTimeSeries[0].strokeColor });
|
||||||
|
});
|
||||||
|
});
|
@ -1 +1,91 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
BarChart2,
|
||||||
|
BarChartHorizontal,
|
||||||
|
Diff,
|
||||||
|
Gauge,
|
||||||
|
LucideProps,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { ForwardRefExoticComponent, RefAttributes } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MetricInspectionOptions,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export const INSPECT_FEATURE_FLAG_KEY = 'metrics-explorer-inspect-feature-flag';
|
export const INSPECT_FEATURE_FLAG_KEY = 'metrics-explorer-inspect-feature-flag';
|
||||||
|
|
||||||
|
export const METRIC_TYPE_TO_COLOR_MAP: Record<MetricType, string> = {
|
||||||
|
[MetricType.GAUGE]: Color.BG_SAKURA_500,
|
||||||
|
[MetricType.HISTOGRAM]: Color.BG_SIENNA_500,
|
||||||
|
[MetricType.SUM]: Color.BG_ROBIN_500,
|
||||||
|
[MetricType.SUMMARY]: Color.BG_FOREST_500,
|
||||||
|
[MetricType.EXPONENTIAL_HISTOGRAM]: Color.BG_AQUA_500,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const METRIC_TYPE_TO_ICON_MAP: Record<
|
||||||
|
MetricType,
|
||||||
|
ForwardRefExoticComponent<
|
||||||
|
Omit<LucideProps, 'ref'> & RefAttributes<SVGSVGElement>
|
||||||
|
>
|
||||||
|
> = {
|
||||||
|
[MetricType.GAUGE]: Gauge,
|
||||||
|
[MetricType.HISTOGRAM]: BarChart2,
|
||||||
|
[MetricType.SUM]: Diff,
|
||||||
|
[MetricType.SUMMARY]: BarChartHorizontal,
|
||||||
|
[MetricType.EXPONENTIAL_HISTOGRAM]: BarChart,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TIME_AGGREGATION_OPTIONS: Record<
|
||||||
|
TimeAggregationOptions,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
[TimeAggregationOptions.LATEST]: 'Latest',
|
||||||
|
[TimeAggregationOptions.SUM]: 'Sum',
|
||||||
|
[TimeAggregationOptions.AVG]: 'Avg',
|
||||||
|
[TimeAggregationOptions.MIN]: 'Min',
|
||||||
|
[TimeAggregationOptions.MAX]: 'Max',
|
||||||
|
[TimeAggregationOptions.COUNT]: 'Count',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SPACE_AGGREGATION_OPTIONS: Record<
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
[SpaceAggregationOptions.SUM_BY]: 'Sum by',
|
||||||
|
[SpaceAggregationOptions.MIN_BY]: 'Min by',
|
||||||
|
[SpaceAggregationOptions.MAX_BY]: 'Max by',
|
||||||
|
[SpaceAggregationOptions.AVG_BY]: 'Avg by',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW: Record<
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
string
|
||||||
|
> = {
|
||||||
|
[SpaceAggregationOptions.SUM_BY]: 'Sum',
|
||||||
|
[SpaceAggregationOptions.MIN_BY]: 'Min',
|
||||||
|
[SpaceAggregationOptions.MAX_BY]: 'Max',
|
||||||
|
[SpaceAggregationOptions.AVG_BY]: 'Avg',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionOptions = {
|
||||||
|
timeAggregationOption: undefined,
|
||||||
|
timeAggregationInterval: undefined,
|
||||||
|
spaceAggregationOption: undefined,
|
||||||
|
spaceAggregationLabels: [],
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEMPORAL_AGGREGATION_LINK =
|
||||||
|
'https://signoz.io/docs/metrics-management/types-and-aggregation/#step-2-temporal-aggregation';
|
||||||
|
|
||||||
|
export const SPACE_AGGREGATION_LINK =
|
||||||
|
'https://signoz.io/docs/metrics-management/types-and-aggregation/#step-3-spatial-aggregation';
|
||||||
|
|
||||||
|
export const GRAPH_CLICK_PIXEL_TOLERANCE = 10;
|
||||||
|
@ -1,5 +1,176 @@
|
|||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
TagFilter,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { AlignedData } from 'uplot';
|
||||||
|
|
||||||
export type InspectProps = {
|
export type InspectProps = {
|
||||||
metricName: string | null;
|
metricName: string | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface UseInspectMetricsReturnData {
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[];
|
||||||
|
inspectMetricsStatusCode: number;
|
||||||
|
isInspectMetricsLoading: boolean;
|
||||||
|
isInspectMetricsError: boolean;
|
||||||
|
formattedInspectMetricsTimeSeries: AlignedData;
|
||||||
|
spaceAggregationLabels: string[];
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
isInspectMetricsRefetching: boolean;
|
||||||
|
spaceAggregatedSeriesMap: Map<string, InspectMetricsSeries[]>;
|
||||||
|
aggregatedTimeSeries: InspectMetricsSeries[];
|
||||||
|
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphViewProps {
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[];
|
||||||
|
metricUnit: string | undefined;
|
||||||
|
metricName: string | null;
|
||||||
|
metricType?: MetricType | undefined;
|
||||||
|
formattedInspectMetricsTimeSeries: AlignedData;
|
||||||
|
resetInspection: () => void;
|
||||||
|
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
setPopoverOptions: (options: GraphPopoverOptions | null) => void;
|
||||||
|
popoverOptions: GraphPopoverOptions | null;
|
||||||
|
showExpandedView: boolean;
|
||||||
|
setShowExpandedView: (showExpandedView: boolean) => void;
|
||||||
|
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
isInspectMetricsRefetching: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryBuilderProps {
|
||||||
|
metricName: string | null;
|
||||||
|
setMetricName: (metricName: string) => void;
|
||||||
|
metricType: MetricType | undefined;
|
||||||
|
spaceAggregationLabels: string[];
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[];
|
||||||
|
searchQuery: IBuilderQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricNameSearchProps {
|
||||||
|
metricName: string | null;
|
||||||
|
setMetricName: (metricName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricFiltersProps {
|
||||||
|
searchQuery: IBuilderQuery;
|
||||||
|
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
|
||||||
|
metricName: string | null;
|
||||||
|
metricType: MetricType | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricTimeAggregationProps {
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricSpaceAggregationProps {
|
||||||
|
spaceAggregationLabels: string[];
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TimeAggregationOptions {
|
||||||
|
LATEST = 'latest',
|
||||||
|
SUM = 'sum',
|
||||||
|
AVG = 'avg',
|
||||||
|
MIN = 'min',
|
||||||
|
MAX = 'max',
|
||||||
|
COUNT = 'count',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpaceAggregationOptions {
|
||||||
|
SUM_BY = 'sum_by',
|
||||||
|
MIN_BY = 'min_by',
|
||||||
|
MAX_BY = 'max_by',
|
||||||
|
AVG_BY = 'avg_by',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricInspectionOptions {
|
||||||
|
timeAggregationOption: TimeAggregationOptions | undefined;
|
||||||
|
timeAggregationInterval: number | undefined;
|
||||||
|
spaceAggregationOption: SpaceAggregationOptions | undefined;
|
||||||
|
spaceAggregationLabels: string[];
|
||||||
|
filters: TagFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetricInspectionAction =
|
||||||
|
| { type: 'SET_TIME_AGGREGATION_OPTION'; payload: TimeAggregationOptions }
|
||||||
|
| { type: 'SET_TIME_AGGREGATION_INTERVAL'; payload: number }
|
||||||
|
| { type: 'SET_SPACE_AGGREGATION_OPTION'; payload: SpaceAggregationOptions }
|
||||||
|
| { type: 'SET_SPACE_AGGREGATION_LABELS'; payload: string[] }
|
||||||
|
| { type: 'SET_FILTERS'; payload: TagFilter }
|
||||||
|
| { type: 'RESET_INSPECTION' };
|
||||||
|
|
||||||
|
export enum InspectionStep {
|
||||||
|
TIME_AGGREGATION = 1,
|
||||||
|
SPACE_AGGREGATION = 2,
|
||||||
|
COMPLETED = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepperProps {
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
resetInspection: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphPopoverOptions {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
value: number;
|
||||||
|
timestamp: number;
|
||||||
|
timeSeries: InspectMetricsSeries | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphPopoverProps {
|
||||||
|
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
|
||||||
|
options: GraphPopoverOptions | null;
|
||||||
|
popoverRef: React.RefObject<HTMLDivElement>;
|
||||||
|
step: InspectionStep;
|
||||||
|
openInExpandedView: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphPopoverData {
|
||||||
|
timestamp?: number;
|
||||||
|
value: string;
|
||||||
|
title?: string;
|
||||||
|
type: 'instance' | 'aggregated';
|
||||||
|
timeSeries?: InspectMetricsSeries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExpandedViewProps {
|
||||||
|
options: GraphPopoverOptions | null;
|
||||||
|
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
|
||||||
|
step: InspectionStep;
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableViewProps {
|
||||||
|
inspectionStep: InspectionStep;
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[];
|
||||||
|
setShowExpandedView: (showExpandedView: boolean) => void;
|
||||||
|
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
isInspectMetricsRefetching: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TableViewDataItem {
|
||||||
|
title: JSX.Element;
|
||||||
|
values: JSX.Element;
|
||||||
|
key: number;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,226 @@
|
|||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import { useGetInspectMetricsDetails } from 'hooks/metricsExplorer/useGetInspectMetricsDetails';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||||
|
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||||
|
|
||||||
|
import { INITIAL_INSPECT_METRICS_OPTIONS } from './constants';
|
||||||
|
import {
|
||||||
|
GraphPopoverData,
|
||||||
|
InspectionStep,
|
||||||
|
MetricInspectionAction,
|
||||||
|
MetricInspectionOptions,
|
||||||
|
UseInspectMetricsReturnData,
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
|
applySpaceAggregation,
|
||||||
|
applyTimeAggregation,
|
||||||
|
getAllTimestampsOfMetrics,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
const metricInspectionReducer = (
|
||||||
|
state: MetricInspectionOptions,
|
||||||
|
action: MetricInspectionAction,
|
||||||
|
): MetricInspectionOptions => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_TIME_AGGREGATION_OPTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
timeAggregationOption: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_TIME_AGGREGATION_INTERVAL':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
timeAggregationInterval: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_SPACE_AGGREGATION_OPTION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
spaceAggregationOption: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_SPACE_AGGREGATION_LABELS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
spaceAggregationLabels: action.payload,
|
||||||
|
};
|
||||||
|
case 'SET_FILTERS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: action.payload,
|
||||||
|
};
|
||||||
|
case 'RESET_INSPECTION':
|
||||||
|
return { ...INITIAL_INSPECT_METRICS_OPTIONS };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useInspectMetrics(
|
||||||
|
metricName: string | null,
|
||||||
|
): UseInspectMetricsReturnData {
|
||||||
|
// Inspect Metrics API Call and data formatting
|
||||||
|
const { start, end } = useMemo(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
return {
|
||||||
|
start: now - 30 * 60 * 1000, // 30 minutes ago
|
||||||
|
end: now, // now
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Inspect metrics data selection
|
||||||
|
const [metricInspectionOptions, dispatchMetricInspectionOptions] = useReducer(
|
||||||
|
metricInspectionReducer,
|
||||||
|
INITIAL_INSPECT_METRICS_OPTIONS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: inspectMetricsData,
|
||||||
|
isLoading: isInspectMetricsLoading,
|
||||||
|
isError: isInspectMetricsError,
|
||||||
|
isRefetching: isInspectMetricsRefetching,
|
||||||
|
} = useGetInspectMetricsDetails(
|
||||||
|
{
|
||||||
|
metricName: metricName ?? '',
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
filters: metricInspectionOptions.filters,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!metricName,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const inspectMetricsTimeSeries = useMemo(() => {
|
||||||
|
const series = inspectMetricsData?.payload?.data?.series ?? [];
|
||||||
|
|
||||||
|
return series.map((series, index) => {
|
||||||
|
const title = `TS${index + 1}`;
|
||||||
|
const strokeColor = generateColor(
|
||||||
|
title,
|
||||||
|
isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...series,
|
||||||
|
values: [...series.values].sort((a, b) => a.timestamp - b.timestamp),
|
||||||
|
title,
|
||||||
|
strokeColor,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [inspectMetricsData, isDarkMode]);
|
||||||
|
|
||||||
|
const inspectMetricsStatusCode = useMemo(
|
||||||
|
() => inspectMetricsData?.statusCode || 200,
|
||||||
|
[inspectMetricsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Evaluate inspection step
|
||||||
|
const inspectionStep = useMemo(() => {
|
||||||
|
if (metricInspectionOptions.spaceAggregationOption) {
|
||||||
|
return InspectionStep.COMPLETED;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
metricInspectionOptions.timeAggregationOption &&
|
||||||
|
metricInspectionOptions.timeAggregationInterval
|
||||||
|
) {
|
||||||
|
return InspectionStep.SPACE_AGGREGATION;
|
||||||
|
}
|
||||||
|
return InspectionStep.TIME_AGGREGATION;
|
||||||
|
}, [metricInspectionOptions]);
|
||||||
|
|
||||||
|
const [spaceAggregatedSeriesMap, setSpaceAggregatedSeriesMap] = useState<
|
||||||
|
Map<string, InspectMetricsSeries[]>
|
||||||
|
>(new Map());
|
||||||
|
const [timeAggregatedSeriesMap, setTimeAggregatedSeriesMap] = useState<
|
||||||
|
Map<number, GraphPopoverData[]>
|
||||||
|
>(new Map());
|
||||||
|
const [aggregatedTimeSeries, setAggregatedTimeSeries] = useState<
|
||||||
|
InspectMetricsSeries[]
|
||||||
|
>(inspectMetricsTimeSeries);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setAggregatedTimeSeries(inspectMetricsTimeSeries);
|
||||||
|
}, [inspectMetricsTimeSeries]);
|
||||||
|
|
||||||
|
const formattedInspectMetricsTimeSeries = useMemo(() => {
|
||||||
|
let timeSeries: InspectMetricsSeries[] = [...inspectMetricsTimeSeries];
|
||||||
|
|
||||||
|
// Apply time aggregation once required options are set
|
||||||
|
if (
|
||||||
|
inspectionStep >= InspectionStep.SPACE_AGGREGATION &&
|
||||||
|
metricInspectionOptions.timeAggregationOption &&
|
||||||
|
metricInspectionOptions.timeAggregationInterval
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
timeAggregatedSeries,
|
||||||
|
timeAggregatedSeriesMap,
|
||||||
|
} = applyTimeAggregation(inspectMetricsTimeSeries, metricInspectionOptions);
|
||||||
|
timeSeries = timeAggregatedSeries;
|
||||||
|
setTimeAggregatedSeriesMap(timeAggregatedSeriesMap);
|
||||||
|
setAggregatedTimeSeries(timeSeries);
|
||||||
|
}
|
||||||
|
// Apply space aggregation
|
||||||
|
if (inspectionStep === InspectionStep.COMPLETED) {
|
||||||
|
const { aggregatedSeries, spaceAggregatedSeriesMap } = applySpaceAggregation(
|
||||||
|
timeSeries,
|
||||||
|
metricInspectionOptions,
|
||||||
|
);
|
||||||
|
timeSeries = aggregatedSeries;
|
||||||
|
setSpaceAggregatedSeriesMap(spaceAggregatedSeriesMap);
|
||||||
|
setAggregatedTimeSeries(aggregatedSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamps = getAllTimestampsOfMetrics(timeSeries);
|
||||||
|
|
||||||
|
const timeseriesArray = timeSeries.map((series) => {
|
||||||
|
const valuesMap = new Map<number, number>();
|
||||||
|
|
||||||
|
series.values.forEach(({ timestamp, value }) => {
|
||||||
|
valuesMap.set(timestamp, parseFloat(value));
|
||||||
|
});
|
||||||
|
|
||||||
|
return timestamps.map((timestamp) => valuesMap.get(timestamp) ?? NaN);
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawData = [timestamps, ...timeseriesArray];
|
||||||
|
return rawData.map((series) => new Float64Array(series));
|
||||||
|
}, [inspectMetricsTimeSeries, inspectionStep, metricInspectionOptions]);
|
||||||
|
|
||||||
|
const spaceAggregationLabels = useMemo(() => {
|
||||||
|
const labels = new Set<string>();
|
||||||
|
inspectMetricsData?.payload?.data.series.forEach((series) => {
|
||||||
|
Object.keys(series.labels).forEach((label) => {
|
||||||
|
labels.add(label);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return Array.from(labels);
|
||||||
|
}, [inspectMetricsData]);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'RESET_INSPECTION',
|
||||||
|
});
|
||||||
|
setSpaceAggregatedSeriesMap(new Map());
|
||||||
|
setTimeAggregatedSeriesMap(new Map());
|
||||||
|
setAggregatedTimeSeries(inspectMetricsTimeSeries);
|
||||||
|
}, [dispatchMetricInspectionOptions, inspectMetricsTimeSeries]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
inspectMetricsStatusCode,
|
||||||
|
isInspectMetricsLoading,
|
||||||
|
isInspectMetricsError,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
spaceAggregationLabels,
|
||||||
|
metricInspectionOptions,
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
inspectionStep,
|
||||||
|
isInspectMetricsRefetching,
|
||||||
|
spaceAggregatedSeriesMap,
|
||||||
|
aggregatedTimeSeries,
|
||||||
|
timeAggregatedSeriesMap,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
@ -1,11 +1,827 @@
|
|||||||
import { INSPECT_FEATURE_FLAG_KEY } from './constants';
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { Card, Input, Select, Typography } from 'antd';
|
||||||
|
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
|
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
|
import { HardHat } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
BaseAutocompleteData,
|
||||||
|
DataTypes,
|
||||||
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SPACE_AGGREGATION_OPTIONS,
|
||||||
|
TIME_AGGREGATION_OPTIONS,
|
||||||
|
} from './constants';
|
||||||
|
import {
|
||||||
|
GraphPopoverData,
|
||||||
|
GraphPopoverOptions,
|
||||||
|
InspectionStep,
|
||||||
|
MetricFiltersProps,
|
||||||
|
MetricInspectionOptions,
|
||||||
|
MetricNameSearchProps,
|
||||||
|
MetricSpaceAggregationProps,
|
||||||
|
MetricTimeAggregationProps,
|
||||||
|
SpaceAggregationOptions,
|
||||||
|
TimeAggregationOptions,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the inspect feature flag is enabled
|
* Check if the inspect feature flag is enabled
|
||||||
* returns true if the feature flag is enabled, false otherwise
|
* returns true if the feature flag is enabled, false otherwise
|
||||||
* Show the inspect button in metrics explorer if the feature flag is enabled
|
* Show the inspect button in metrics explorer if the feature flag is enabled
|
||||||
*/
|
*/
|
||||||
export function isInspectEnabled(): boolean {
|
export function isInspectEnabled(metricType: MetricType | undefined): boolean {
|
||||||
const featureFlag = localStorage.getItem(INSPECT_FEATURE_FLAG_KEY);
|
return metricType === MetricType.GAUGE;
|
||||||
return featureFlag === 'true';
|
}
|
||||||
|
|
||||||
|
export function getAllTimestampsOfMetrics(
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[],
|
||||||
|
): number[] {
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
inspectMetricsTimeSeries
|
||||||
|
.flatMap((series) => series.values.map((value) => value.timestamp))
|
||||||
|
.sort((a, b) => a - b),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultTimeAggregationInterval(
|
||||||
|
timeSeries: InspectMetricsSeries | undefined,
|
||||||
|
): number {
|
||||||
|
if (!timeSeries) {
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
const reportingInterval =
|
||||||
|
timeSeries.values.length > 1
|
||||||
|
? Math.abs(timeSeries.values[1].timestamp - timeSeries.values[0].timestamp) /
|
||||||
|
1000
|
||||||
|
: 0;
|
||||||
|
return Math.max(60, reportingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricNameSearch({
|
||||||
|
metricName,
|
||||||
|
setMetricName,
|
||||||
|
}: MetricNameSearchProps): JSX.Element {
|
||||||
|
const [searchText, setSearchText] = useState(metricName);
|
||||||
|
|
||||||
|
const handleSetMetricName = (value: BaseAutocompleteData): void => {
|
||||||
|
setMetricName(value.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (value: BaseAutocompleteData): void => {
|
||||||
|
setSearchText(value.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="metric-name-search"
|
||||||
|
className="inspect-metrics-input-group metric-name-search"
|
||||||
|
>
|
||||||
|
<Typography.Text>From</Typography.Text>
|
||||||
|
<AggregatorFilter
|
||||||
|
defaultValue={searchText ?? ''}
|
||||||
|
query={initialQueriesMap[DataSource.METRICS].builder.queryData[0]}
|
||||||
|
onSelect={handleSetMetricName}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricFilters({
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
searchQuery,
|
||||||
|
metricName,
|
||||||
|
metricType,
|
||||||
|
}: MetricFiltersProps): JSX.Element {
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: searchQuery,
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const aggregateAttribute = useMemo(
|
||||||
|
() => ({
|
||||||
|
key: metricName ?? '',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: metricType,
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: `${metricName}--${DataTypes.String}--${metricType}--true`,
|
||||||
|
}),
|
||||||
|
[metricName, metricType],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="metric-filters"
|
||||||
|
className="inspect-metrics-input-group metric-filters"
|
||||||
|
>
|
||||||
|
<Typography.Text>Where</Typography.Text>
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={{
|
||||||
|
...searchQuery,
|
||||||
|
aggregateAttribute,
|
||||||
|
}}
|
||||||
|
onChange={(value): void => {
|
||||||
|
handleChangeQueryData('filters', value);
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'SET_FILTERS',
|
||||||
|
payload: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
suffixIcon={<HardHat size={16} />}
|
||||||
|
disableNavigationShortcuts
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricTimeAggregation({
|
||||||
|
metricInspectionOptions,
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
inspectionStep,
|
||||||
|
inspectMetricsTimeSeries,
|
||||||
|
}: MetricTimeAggregationProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="metric-time-aggregation"
|
||||||
|
className="metric-time-aggregation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames('metric-time-aggregation-header', {
|
||||||
|
'selected-step': inspectionStep === InspectionStep.TIME_AGGREGATION,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Typography.Text>AGGREGATE BY TIME</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="metric-time-aggregation-content">
|
||||||
|
<div className="inspect-metrics-input-group">
|
||||||
|
<Typography.Text>Align with</Typography.Text>
|
||||||
|
<Select
|
||||||
|
value={metricInspectionOptions.timeAggregationOption}
|
||||||
|
onChange={(value): void => {
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'SET_TIME_AGGREGATION_OPTION',
|
||||||
|
payload: value,
|
||||||
|
});
|
||||||
|
// set the time aggregation interval to the default value if it is not set
|
||||||
|
if (!metricInspectionOptions.timeAggregationInterval) {
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'SET_TIME_AGGREGATION_INTERVAL',
|
||||||
|
payload: getDefaultTimeAggregationInterval(
|
||||||
|
inspectMetricsTimeSeries[0],
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
style={{ width: 130 }}
|
||||||
|
placeholder="Select option"
|
||||||
|
>
|
||||||
|
{Object.entries(TIME_AGGREGATION_OPTIONS).map(([key, value]) => (
|
||||||
|
<Select.Option key={key} value={key}>
|
||||||
|
{value}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="inspect-metrics-input-group">
|
||||||
|
<Typography.Text>aggregated every</Typography.Text>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="no-arrows-input"
|
||||||
|
value={metricInspectionOptions.timeAggregationInterval}
|
||||||
|
placeholder="Select interval..."
|
||||||
|
suffix="seconds"
|
||||||
|
onChange={(e): void => {
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'SET_TIME_AGGREGATION_INTERVAL',
|
||||||
|
payload: parseInt(e.target.value, 10),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onWheel={(e): void => (e.target as HTMLInputElement).blur()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetricSpaceAggregation({
|
||||||
|
spaceAggregationLabels,
|
||||||
|
metricInspectionOptions,
|
||||||
|
dispatchMetricInspectionOptions,
|
||||||
|
inspectionStep,
|
||||||
|
}: MetricSpaceAggregationProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="metric-space-aggregation"
|
||||||
|
className="metric-space-aggregation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames('metric-space-aggregation-header', {
|
||||||
|
'selected-step': inspectionStep === InspectionStep.SPACE_AGGREGATION,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Typography.Text>AGGREGATE BY LABELS</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<div className="metric-space-aggregation-content">
|
||||||
|
<div className="metric-space-aggregation-content-left">
|
||||||
|
<Select
|
||||||
|
value={metricInspectionOptions.spaceAggregationOption}
|
||||||
|
placeholder="Select option"
|
||||||
|
onChange={(value): void => {
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'SET_SPACE_AGGREGATION_OPTION',
|
||||||
|
payload: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
style={{ width: 130 }}
|
||||||
|
disabled={inspectionStep === InspectionStep.TIME_AGGREGATION}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line sonarjs/no-identical-functions */}
|
||||||
|
{Object.entries(SPACE_AGGREGATION_OPTIONS).map(([key, value]) => (
|
||||||
|
<Select.Option key={key} value={key}>
|
||||||
|
{value}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Search for attributes..."
|
||||||
|
value={metricInspectionOptions.spaceAggregationLabels}
|
||||||
|
onChange={(value): void => {
|
||||||
|
dispatchMetricInspectionOptions({
|
||||||
|
type: 'SET_SPACE_AGGREGATION_LABELS',
|
||||||
|
payload: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={inspectionStep === InspectionStep.TIME_AGGREGATION}
|
||||||
|
>
|
||||||
|
{spaceAggregationLabels.map((label) => (
|
||||||
|
<Select.Option key={label} value={label}>
|
||||||
|
{label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyFilters(
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[],
|
||||||
|
filters: TagFilter,
|
||||||
|
): InspectMetricsSeries[] {
|
||||||
|
return inspectMetricsTimeSeries.filter((series) =>
|
||||||
|
filters.items.every((filter) => {
|
||||||
|
if ((filter.key?.key || '') in series.labels) {
|
||||||
|
const value = series.labels[filter.key?.key ?? ''];
|
||||||
|
switch (filter.op) {
|
||||||
|
case '=':
|
||||||
|
return value === filter.value;
|
||||||
|
case '!=':
|
||||||
|
return value !== filter.value;
|
||||||
|
case 'in':
|
||||||
|
return (filter.value as string[]).includes(value as string);
|
||||||
|
case 'nin':
|
||||||
|
return !(filter.value as string[]).includes(value as string);
|
||||||
|
case 'like':
|
||||||
|
return value.includes(filter.value as string);
|
||||||
|
case 'nlike':
|
||||||
|
return !value.includes(filter.value as string);
|
||||||
|
case 'contains':
|
||||||
|
return value.includes(filter.value as string);
|
||||||
|
case 'ncontains':
|
||||||
|
return !value.includes(filter.value as string);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTimeAggregation(
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[],
|
||||||
|
metricInspectionOptions: MetricInspectionOptions,
|
||||||
|
): {
|
||||||
|
timeAggregatedSeries: InspectMetricsSeries[];
|
||||||
|
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
|
||||||
|
} {
|
||||||
|
const {
|
||||||
|
timeAggregationOption,
|
||||||
|
timeAggregationInterval,
|
||||||
|
} = metricInspectionOptions;
|
||||||
|
|
||||||
|
if (!timeAggregationInterval) {
|
||||||
|
return {
|
||||||
|
timeAggregatedSeries: inspectMetricsTimeSeries,
|
||||||
|
timeAggregatedSeriesMap: new Map(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group timestamps into intervals and aggregate values for each series independently
|
||||||
|
const timeAggregatedSeriesMap: Map<number, GraphPopoverData[]> = new Map();
|
||||||
|
|
||||||
|
const timeAggregatedSeries: InspectMetricsSeries[] = inspectMetricsTimeSeries.map(
|
||||||
|
(series) => {
|
||||||
|
const groupedTimestamps = new Map<number, number[]>();
|
||||||
|
|
||||||
|
series.values.forEach(({ timestamp, value }) => {
|
||||||
|
const intervalBucket =
|
||||||
|
Math.floor(timestamp / (timeAggregationInterval * 1000)) *
|
||||||
|
(timeAggregationInterval * 1000);
|
||||||
|
|
||||||
|
if (!groupedTimestamps.has(intervalBucket)) {
|
||||||
|
groupedTimestamps.set(intervalBucket, []);
|
||||||
|
}
|
||||||
|
if (!timeAggregatedSeriesMap.has(intervalBucket)) {
|
||||||
|
timeAggregatedSeriesMap.set(intervalBucket, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedTimestamps.get(intervalBucket)?.push(parseFloat(value));
|
||||||
|
timeAggregatedSeriesMap.get(intervalBucket)?.push({
|
||||||
|
timestamp,
|
||||||
|
value,
|
||||||
|
type: 'instance',
|
||||||
|
title: series.title,
|
||||||
|
timeSeries: series,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const aggregatedValues = Array.from(groupedTimestamps.entries()).map(
|
||||||
|
([intervalStart, values]) => {
|
||||||
|
let aggregatedValue: number;
|
||||||
|
|
||||||
|
switch (timeAggregationOption) {
|
||||||
|
case TimeAggregationOptions.LATEST:
|
||||||
|
aggregatedValue = values[values.length - 1];
|
||||||
|
break;
|
||||||
|
case TimeAggregationOptions.SUM:
|
||||||
|
aggregatedValue = values.reduce((sum, val) => sum + val, 0);
|
||||||
|
break;
|
||||||
|
case TimeAggregationOptions.AVG:
|
||||||
|
aggregatedValue =
|
||||||
|
values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||||
|
break;
|
||||||
|
case TimeAggregationOptions.MIN:
|
||||||
|
aggregatedValue = Math.min(...values);
|
||||||
|
break;
|
||||||
|
case TimeAggregationOptions.MAX:
|
||||||
|
aggregatedValue = Math.max(...values);
|
||||||
|
break;
|
||||||
|
case TimeAggregationOptions.COUNT:
|
||||||
|
aggregatedValue = values.length;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
aggregatedValue = values[values.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp: intervalStart,
|
||||||
|
value: aggregatedValue.toString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...series,
|
||||||
|
values: aggregatedValues,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { timeAggregatedSeries, timeAggregatedSeriesMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applySpaceAggregation(
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[],
|
||||||
|
metricInspectionOptions: MetricInspectionOptions,
|
||||||
|
): {
|
||||||
|
aggregatedSeries: InspectMetricsSeries[];
|
||||||
|
spaceAggregatedSeriesMap: Map<string, InspectMetricsSeries[]>;
|
||||||
|
} {
|
||||||
|
// Group series by selected space aggregation labels
|
||||||
|
const groupedSeries = new Map<string, InspectMetricsSeries[]>();
|
||||||
|
|
||||||
|
inspectMetricsTimeSeries.forEach((series) => {
|
||||||
|
// Create composite key from selected labels
|
||||||
|
const key = metricInspectionOptions.spaceAggregationLabels
|
||||||
|
.map((label) => `${label}:${series.labels[label]}`)
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
if (!groupedSeries.has(key)) {
|
||||||
|
groupedSeries.set(key, []);
|
||||||
|
}
|
||||||
|
groupedSeries.get(key)?.push(series);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggregate each group based on space aggregation option
|
||||||
|
const aggregatedSeries: InspectMetricsSeries[] = [];
|
||||||
|
|
||||||
|
groupedSeries.forEach((seriesGroup, key) => {
|
||||||
|
// Get the first series to use as template for labels and timestamps
|
||||||
|
const templateSeries = seriesGroup[0];
|
||||||
|
|
||||||
|
// Create a map of timestamp to array of values across all series in group
|
||||||
|
const timestampValuesMap = new Map<number, number[]>();
|
||||||
|
|
||||||
|
// Collect values for each timestamp across all series
|
||||||
|
seriesGroup.forEach((series) => {
|
||||||
|
series.values.forEach(({ timestamp, value }) => {
|
||||||
|
if (!timestampValuesMap.has(timestamp)) {
|
||||||
|
timestampValuesMap.set(timestamp, []);
|
||||||
|
}
|
||||||
|
timestampValuesMap.get(timestamp)?.push(parseFloat(value));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aggregate values based on selected space aggregation option
|
||||||
|
const aggregatedValues = Array.from(timestampValuesMap.entries()).map(
|
||||||
|
([timestamp, values]) => {
|
||||||
|
let aggregatedValue: number;
|
||||||
|
|
||||||
|
switch (metricInspectionOptions.spaceAggregationOption) {
|
||||||
|
case SpaceAggregationOptions.SUM_BY:
|
||||||
|
aggregatedValue = values.reduce((sum, val) => sum + val, 0);
|
||||||
|
break;
|
||||||
|
case SpaceAggregationOptions.AVG_BY:
|
||||||
|
aggregatedValue =
|
||||||
|
values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||||
|
break;
|
||||||
|
case SpaceAggregationOptions.MIN_BY:
|
||||||
|
aggregatedValue = Math.min(...values);
|
||||||
|
break;
|
||||||
|
case SpaceAggregationOptions.MAX_BY:
|
||||||
|
aggregatedValue = Math.max(...values);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// eslint-disable-next-line prefer-destructuring
|
||||||
|
aggregatedValue = values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timestamp,
|
||||||
|
value: (aggregatedValue || 0).toString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create aggregated series with original labels
|
||||||
|
aggregatedSeries.push({
|
||||||
|
...templateSeries,
|
||||||
|
values: aggregatedValues.sort((a, b) => a.timestamp - b.timestamp),
|
||||||
|
title: key.split(',').join(' '),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
aggregatedSeries,
|
||||||
|
spaceAggregatedSeriesMap: groupedSeries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSeriesIndexFromPixel(
|
||||||
|
e: MouseEvent,
|
||||||
|
u: uPlot,
|
||||||
|
formattedInspectMetricsTimeSeries: uPlot.AlignedData,
|
||||||
|
): number {
|
||||||
|
const bbox = u.over.getBoundingClientRect(); // plot area only
|
||||||
|
const left = e.clientX - bbox.left;
|
||||||
|
const top = e.clientY - bbox.top;
|
||||||
|
|
||||||
|
const timestampIndex = u.posToIdx(left);
|
||||||
|
let seriesIndex = -1;
|
||||||
|
let closestPixelDiff = Infinity;
|
||||||
|
|
||||||
|
for (let i = 1; i < formattedInspectMetricsTimeSeries.length; i++) {
|
||||||
|
const series = formattedInspectMetricsTimeSeries[i];
|
||||||
|
const seriesValue = series[timestampIndex];
|
||||||
|
|
||||||
|
if (
|
||||||
|
seriesValue !== undefined &&
|
||||||
|
seriesValue !== null &&
|
||||||
|
!Number.isNaN(seriesValue)
|
||||||
|
) {
|
||||||
|
const seriesYPx = u.valToPos(seriesValue, 'y');
|
||||||
|
const pixelDiff = Math.abs(seriesYPx - top);
|
||||||
|
|
||||||
|
if (pixelDiff < closestPixelDiff) {
|
||||||
|
closestPixelDiff = pixelDiff;
|
||||||
|
seriesIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seriesIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onGraphClick(
|
||||||
|
e: MouseEvent,
|
||||||
|
u: uPlot,
|
||||||
|
popoverRef: React.RefObject<HTMLDivElement>,
|
||||||
|
setPopoverOptions: (options: GraphPopoverOptions | null) => void,
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[],
|
||||||
|
showPopover: boolean,
|
||||||
|
setShowPopover: (showPopover: boolean) => void,
|
||||||
|
formattedInspectMetricsTimeSeries: uPlot.AlignedData,
|
||||||
|
): void {
|
||||||
|
if (popoverRef.current && popoverRef.current.contains(e.target as Node)) {
|
||||||
|
// Clicked inside the popover, don't close
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// If popover is already open, close it
|
||||||
|
if (showPopover) {
|
||||||
|
setShowPopover(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Get which series the user clicked on
|
||||||
|
// If no series is clicked, return
|
||||||
|
const seriesIndex = getSeriesIndexFromPixel(
|
||||||
|
e,
|
||||||
|
u,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
);
|
||||||
|
if (seriesIndex <= 0) return;
|
||||||
|
|
||||||
|
const series = inspectMetricsTimeSeries[seriesIndex - 1];
|
||||||
|
|
||||||
|
const { left } = u.over.getBoundingClientRect();
|
||||||
|
const x = e.clientX - left;
|
||||||
|
const xVal = u.posToVal(x, 'x'); // Get actual x-axis value
|
||||||
|
|
||||||
|
const closestPoint = series?.values.reduce((prev, curr) => {
|
||||||
|
const prevDiff = Math.abs(prev.timestamp - xVal);
|
||||||
|
const currDiff = Math.abs(curr.timestamp - xVal);
|
||||||
|
return prevDiff < currDiff ? prev : curr;
|
||||||
|
});
|
||||||
|
|
||||||
|
setPopoverOptions({
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
value: parseFloat(closestPoint?.value ?? '0'),
|
||||||
|
timestamp: closestPoint?.timestamp,
|
||||||
|
timeSeries: series,
|
||||||
|
});
|
||||||
|
setShowPopover(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRawDataFromTimeSeries(
|
||||||
|
timeSeries: InspectMetricsSeries,
|
||||||
|
timestamp: number,
|
||||||
|
showAll = false,
|
||||||
|
): GraphPopoverData[] {
|
||||||
|
if (showAll) {
|
||||||
|
return timeSeries.values.map((value) => ({
|
||||||
|
timestamp: value.timestamp,
|
||||||
|
type: 'instance',
|
||||||
|
value: value.value,
|
||||||
|
title: timeSeries.title,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestampIndex = timeSeries.values.findIndex(
|
||||||
|
(value) => value.timestamp >= timestamp,
|
||||||
|
);
|
||||||
|
const timestamps = [];
|
||||||
|
if (timestampIndex !== undefined) {
|
||||||
|
for (
|
||||||
|
let i = Math.max(0, timestampIndex - 2);
|
||||||
|
i <= Math.min((timeSeries?.values?.length ?? 0) - 1, timestampIndex + 2);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
timestamps.push(timeSeries?.values?.[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return timestamps.map((timestamp) => ({
|
||||||
|
timestamp: timestamp.timestamp,
|
||||||
|
type: 'instance',
|
||||||
|
value: timestamp.value,
|
||||||
|
title: timeSeries.title,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSpaceAggregatedDataFromTimeSeries(
|
||||||
|
timeSeries: InspectMetricsSeries,
|
||||||
|
spaceAggregatedSeriesMap: Map<string, InspectMetricsSeries[]>,
|
||||||
|
timestamp: number,
|
||||||
|
showAll = false,
|
||||||
|
): GraphPopoverData[] {
|
||||||
|
if (spaceAggregatedSeriesMap.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedLabels =
|
||||||
|
Array.from(spaceAggregatedSeriesMap.keys())[0]
|
||||||
|
?.split(',')
|
||||||
|
.map((label) => label.split(':')[0]) || [];
|
||||||
|
|
||||||
|
let matchingSeries: InspectMetricsSeries[] = [];
|
||||||
|
spaceAggregatedSeriesMap.forEach((series) => {
|
||||||
|
let isMatching = true;
|
||||||
|
appliedLabels.forEach((label) => {
|
||||||
|
if (timeSeries.labels[label] !== series[0].labels[label]) {
|
||||||
|
isMatching = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isMatching) {
|
||||||
|
matchingSeries = series;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchingSeries
|
||||||
|
.slice(0, showAll ? matchingSeries.length : 5)
|
||||||
|
.map((series) => {
|
||||||
|
const timestampIndex = series.values.findIndex(
|
||||||
|
(value) => value.timestamp >= timestamp,
|
||||||
|
);
|
||||||
|
const value = series.values[timestampIndex]?.value;
|
||||||
|
return {
|
||||||
|
timeseries: Object.entries(series.labels)
|
||||||
|
.map(([key, value]) => `${key}:${value}`)
|
||||||
|
.join(','),
|
||||||
|
type: 'aggregated',
|
||||||
|
value: value ?? '-',
|
||||||
|
title: series.title,
|
||||||
|
timeSeries: series,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatTimestampToFullDateTime = (
|
||||||
|
timestamp: string | number,
|
||||||
|
returnOnlyTime = false,
|
||||||
|
): string => {
|
||||||
|
const date = new Date(Number(timestamp));
|
||||||
|
|
||||||
|
const datePart = date.toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const timePart = date.toLocaleTimeString('en-US', {
|
||||||
|
hour12: false,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (returnOnlyTime) {
|
||||||
|
return timePart;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${datePart} ⎯ ${timePart}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTimeSeriesLabel(
|
||||||
|
timeSeries: InspectMetricsSeries | null,
|
||||||
|
textColor: string | undefined,
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Object.entries(timeSeries?.labels ?? {}).map(([key, value]) => (
|
||||||
|
<span key={key}>
|
||||||
|
<Typography.Text style={{ color: textColor, fontWeight: 600 }}>
|
||||||
|
{key}
|
||||||
|
</Typography.Text>
|
||||||
|
: {value}{' '}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HoverPopover({
|
||||||
|
options,
|
||||||
|
step,
|
||||||
|
metricInspectionOptions,
|
||||||
|
}: {
|
||||||
|
options: GraphPopoverOptions;
|
||||||
|
step: InspectionStep;
|
||||||
|
metricInspectionOptions: MetricInspectionOptions;
|
||||||
|
}): JSX.Element {
|
||||||
|
const closestTimestamp = useMemo(() => {
|
||||||
|
if (!options.timeSeries) {
|
||||||
|
return options.timestamp;
|
||||||
|
}
|
||||||
|
return options.timeSeries?.values.reduce((prev, curr) => {
|
||||||
|
const prevDiff = Math.abs(prev.timestamp - options.timestamp);
|
||||||
|
const currDiff = Math.abs(curr.timestamp - options.timestamp);
|
||||||
|
return prevDiff < currDiff ? prev : curr;
|
||||||
|
}).timestamp;
|
||||||
|
}, [options.timeSeries, options.timestamp]);
|
||||||
|
|
||||||
|
const closestValue = useMemo(() => {
|
||||||
|
if (!options.timeSeries) {
|
||||||
|
return options.value;
|
||||||
|
}
|
||||||
|
const index = options.timeSeries.values.findIndex(
|
||||||
|
(value) => value.timestamp === closestTimestamp,
|
||||||
|
);
|
||||||
|
return index !== undefined && index >= 0
|
||||||
|
? options.timeSeries?.values[index].value
|
||||||
|
: null;
|
||||||
|
}, [options.timeSeries, closestTimestamp, options.value]);
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
if (
|
||||||
|
step === InspectionStep.COMPLETED &&
|
||||||
|
metricInspectionOptions.spaceAggregationLabels.length === 0
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (step === InspectionStep.COMPLETED && options.timeSeries?.title) {
|
||||||
|
return options.timeSeries.title;
|
||||||
|
}
|
||||||
|
if (!options.timeSeries) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return getTimeSeriesLabel(
|
||||||
|
options.timeSeries,
|
||||||
|
options.timeSeries?.strokeColor,
|
||||||
|
);
|
||||||
|
}, [step, options.timeSeries, metricInspectionOptions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className="hover-popover-card"
|
||||||
|
style={{
|
||||||
|
top: options.y + 10,
|
||||||
|
left: options.x + 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="hover-popover-row">
|
||||||
|
<Typography.Text>
|
||||||
|
{formatTimestampToFullDateTime(closestTimestamp ?? 0)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text>{Number(closestValue).toFixed(2)}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
{options.timeSeries && (
|
||||||
|
<Typography.Text
|
||||||
|
style={{
|
||||||
|
color: options.timeSeries?.strokeColor,
|
||||||
|
fontWeight: 200,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onGraphHover(
|
||||||
|
e: MouseEvent,
|
||||||
|
u: uPlot,
|
||||||
|
setPopoverOptions: (options: GraphPopoverOptions | null) => void,
|
||||||
|
inspectMetricsTimeSeries: InspectMetricsSeries[],
|
||||||
|
formattedInspectMetricsTimeSeries: uPlot.AlignedData,
|
||||||
|
): void {
|
||||||
|
const { left, top } = u.over.getBoundingClientRect();
|
||||||
|
const x = e.clientX - left;
|
||||||
|
const y = e.clientY - top;
|
||||||
|
const xVal = u.posToVal(x, 'x'); // Get actual x-axis value
|
||||||
|
const yVal = u.posToVal(y, 'y'); // Get actual y-axis value value (metric value)
|
||||||
|
|
||||||
|
// Get which series the user clicked on
|
||||||
|
const seriesIndex = getSeriesIndexFromPixel(
|
||||||
|
e,
|
||||||
|
u,
|
||||||
|
formattedInspectMetricsTimeSeries,
|
||||||
|
);
|
||||||
|
if (seriesIndex === -1) {
|
||||||
|
setPopoverOptions({
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
value: yVal,
|
||||||
|
timestamp: xVal,
|
||||||
|
timeSeries: undefined,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const series = inspectMetricsTimeSeries[seriesIndex - 1];
|
||||||
|
|
||||||
|
setPopoverOptions({
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
value: yVal,
|
||||||
|
timestamp: xVal,
|
||||||
|
timeSeries: series,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,10 @@ function MetricDetails({
|
|||||||
return formatTimestampToReadableDate(metric.lastReceived);
|
return formatTimestampToReadableDate(metric.lastReceived);
|
||||||
}, [metric]);
|
}, [metric]);
|
||||||
|
|
||||||
const showInspectFeature = useMemo(() => isInspectEnabled(), []);
|
const showInspectFeature = useMemo(
|
||||||
|
() => isInspectEnabled(metric?.metadata?.metric_type),
|
||||||
|
[metric],
|
||||||
|
);
|
||||||
|
|
||||||
const isMetricDetailsLoading = isLoading || isFetching;
|
const isMetricDetailsLoading = isLoading || isFetching;
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ function Summary(): JSX.Element {
|
|||||||
isFetching: isMetricsFetching,
|
isFetching: isMetricsFetching,
|
||||||
isError: isMetricsError,
|
isError: isMetricsError,
|
||||||
} = useGetMetricsList(metricsListQuery, {
|
} = useGetMetricsList(metricsListQuery, {
|
||||||
enabled: !!metricsListQuery,
|
enabled: !!metricsListQuery && !isInspectModalOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -129,7 +129,7 @@ function Summary(): JSX.Element {
|
|||||||
isFetching: isTreeMapFetching,
|
isFetching: isTreeMapFetching,
|
||||||
isError: isTreeMapError,
|
isError: isTreeMapError,
|
||||||
} = useGetMetricsTreeMap(metricsTreemapQuery, {
|
} = useGetMetricsTreeMap(metricsTreemapQuery, {
|
||||||
enabled: !!metricsTreemapQuery,
|
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
@ -188,6 +188,10 @@ function Summary(): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const closeInspectModal = (): void => {
|
const closeInspectModal = (): void => {
|
||||||
|
handleChangeQueryData('filters', {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
});
|
||||||
setIsInspectModalOpen(false);
|
setIsInspectModalOpen(false);
|
||||||
setSelectedMetricName(null);
|
setSelectedMetricName(null);
|
||||||
};
|
};
|
||||||
|
@ -152,12 +152,17 @@ function ValidateRowValueWrapper({
|
|||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatNumberIntoHumanReadableFormat = (num: number): string => {
|
export const formatNumberIntoHumanReadableFormat = (
|
||||||
|
num: number,
|
||||||
|
addPlusSign = true,
|
||||||
|
): string => {
|
||||||
function format(num: number, divisor: number, suffix: string): string {
|
function format(num: number, divisor: number, suffix: string): string {
|
||||||
const value = num / divisor;
|
const value = num / divisor;
|
||||||
return value % 1 === 0
|
return value % 1 === 0
|
||||||
? `${value}${suffix}+`
|
? `${value}${suffix}${addPlusSign ? '+' : ''}`
|
||||||
: `${value.toFixed(1).replace(/\.0$/, '')}${suffix}+`;
|
: `${value.toFixed(1).replace(/\.0$/, '')}${suffix}${
|
||||||
|
addPlusSign ? '+' : ''
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (num >= 1_000_000_000) {
|
if (num >= 1_000_000_000) {
|
||||||
|
@ -2,7 +2,9 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledLabel = styled.div<Props>`
|
export const StyledLabel = styled.div<Props>`
|
||||||
padding: 0 0.6875rem;
|
padding: 0 0.6875rem;
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
|
@ -5,4 +5,6 @@ import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
onChange: (value: BaseAutocompleteData) => void;
|
onChange: (value: BaseAutocompleteData) => void;
|
||||||
|
defaultValue?: string;
|
||||||
|
onSelect?: (value: BaseAutocompleteData) => void;
|
||||||
};
|
};
|
||||||
|
@ -35,6 +35,8 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
query,
|
query,
|
||||||
disabled,
|
disabled,
|
||||||
onChange,
|
onChange,
|
||||||
|
defaultValue,
|
||||||
|
onSelect,
|
||||||
}: AgregatorFilterProps): JSX.Element {
|
}: AgregatorFilterProps): JSX.Element {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||||
@ -183,6 +185,27 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
[getAttributesData, handleChangeCustomValue, onChange],
|
[getAttributesData, handleChangeCustomValue, onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSelect = useCallback(
|
||||||
|
(_: string, option: ExtendedSelectOption | ExtendedSelectOption[]): void => {
|
||||||
|
const currentOption = option as ExtendedSelectOption;
|
||||||
|
|
||||||
|
const aggregateAttributes = getAttributesData();
|
||||||
|
|
||||||
|
if (currentOption.key) {
|
||||||
|
const attribute = aggregateAttributes.find(
|
||||||
|
(item) => item.id === currentOption.key,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (attribute && onSelect) {
|
||||||
|
onSelect(attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchText('');
|
||||||
|
},
|
||||||
|
[getAttributesData, onSelect],
|
||||||
|
);
|
||||||
|
|
||||||
const value = removePrefix(
|
const value = removePrefix(
|
||||||
transformStringWithPrefix({
|
transformStringWithPrefix({
|
||||||
str: query.aggregateAttribute.key,
|
str: query.aggregateAttribute.key,
|
||||||
@ -203,10 +226,11 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
onSearch={handleSearchText}
|
onSearch={handleSearchText}
|
||||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
options={optionsData}
|
options={optionsData}
|
||||||
value={value}
|
value={defaultValue || value}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
onSelect={handleSelect}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -93,6 +93,8 @@ interface QueryBuilderSearchV2Props {
|
|||||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||||
operatorConfigKey?: OperatorConfigKeys;
|
operatorConfigKey?: OperatorConfigKeys;
|
||||||
hideSpanScopeSelector?: boolean;
|
hideSpanScopeSelector?: boolean;
|
||||||
|
// Determines whether to call onChange when a tag is closed
|
||||||
|
triggerOnChangeOnClose?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
@ -128,6 +130,7 @@ function QueryBuilderSearchV2(
|
|||||||
hardcodedAttributeKeys,
|
hardcodedAttributeKeys,
|
||||||
operatorConfigKey,
|
operatorConfigKey,
|
||||||
hideSpanScopeSelector,
|
hideSpanScopeSelector,
|
||||||
|
triggerOnChangeOnClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
@ -902,6 +905,9 @@ function QueryBuilderSearchV2(
|
|||||||
onClose();
|
onClose();
|
||||||
setSearchValue('');
|
setSearchValue('');
|
||||||
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||||
|
if (triggerOnChangeOnClose) {
|
||||||
|
onChange(query.filters);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagEditHandler = (value: string): void => {
|
const tagEditHandler = (value: string): void => {
|
||||||
@ -1035,6 +1041,7 @@ QueryBuilderSearchV2.defaultProps = {
|
|||||||
hardcodedAttributeKeys: undefined,
|
hardcodedAttributeKeys: undefined,
|
||||||
operatorConfigKey: undefined,
|
operatorConfigKey: undefined,
|
||||||
hideSpanScopeSelector: true,
|
hideSpanScopeSelector: true,
|
||||||
|
triggerOnChangeOnClose: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueryBuilderSearchV2;
|
export default QueryBuilderSearchV2;
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
getInspectMetricsDetails,
|
||||||
|
InspectMetricsRequest,
|
||||||
|
InspectMetricsResponse,
|
||||||
|
} from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
type UseGetInspectMetricsDetails = (
|
||||||
|
requestData: InspectMetricsRequest,
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
) => UseQueryResult<
|
||||||
|
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useGetInspectMetricsDetails: UseGetInspectMetricsDetails = (
|
||||||
|
requestData,
|
||||||
|
options,
|
||||||
|
headers,
|
||||||
|
) => {
|
||||||
|
const queryKey = useMemo(() => {
|
||||||
|
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||||
|
return [...options.queryKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||||
|
return options.queryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
REACT_QUERY_KEY.GET_INSPECT_METRICS_DETAILS,
|
||||||
|
requestData.metricName,
|
||||||
|
requestData.start,
|
||||||
|
requestData.end,
|
||||||
|
requestData.filters,
|
||||||
|
];
|
||||||
|
}, [options?.queryKey, requestData]);
|
||||||
|
|
||||||
|
return useQuery<
|
||||||
|
SuccessResponse<InspectMetricsResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>({
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
getInspectMetricsDetails(requestData, signal, headers),
|
||||||
|
...options,
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user