mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 19:49:06 +08:00
chore: metrics explorer fixes and improvements (#7334)
This commit is contained in:
parent
5b6b5bf359
commit
b3fa1ad60e
@ -27,8 +27,8 @@ export interface MetricDetails {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum Temporality {
|
export enum Temporality {
|
||||||
CUMULATIVE = 'cumulative',
|
CUMULATIVE = 'Cumulative',
|
||||||
DELTA = 'delta',
|
DELTA = 'Delta',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricDetailsAttribute {
|
export interface MetricDetailsAttribute {
|
||||||
|
@ -30,8 +30,8 @@ export interface MetricsListItemData {
|
|||||||
description: string;
|
description: string;
|
||||||
type: MetricType;
|
type: MetricType;
|
||||||
unit: string;
|
unit: string;
|
||||||
[TreemapViewType.CARDINALITY]: number;
|
[TreemapViewType.TIMESERIES]: number;
|
||||||
[TreemapViewType.DATAPOINTS]: number;
|
[TreemapViewType.SAMPLES]: number;
|
||||||
lastReceived: string;
|
lastReceived: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,18 +14,18 @@ export interface MetricsTreeMapPayload {
|
|||||||
export interface MetricsTreeMapResponse {
|
export interface MetricsTreeMapResponse {
|
||||||
status: string;
|
status: string;
|
||||||
data: {
|
data: {
|
||||||
[TreemapViewType.CARDINALITY]: CardinalityData[];
|
[TreemapViewType.TIMESERIES]: TimeseriesData[];
|
||||||
[TreemapViewType.DATAPOINTS]: DatapointsData[];
|
[TreemapViewType.SAMPLES]: SamplesData[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardinalityData {
|
export interface TimeseriesData {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
total_value: number;
|
total_value: number;
|
||||||
metric_name: string;
|
metric_name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DatapointsData {
|
export interface SamplesData {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
metric_name: string;
|
metric_name: string;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import { MetricType } from './getMetricsList';
|
|||||||
|
|
||||||
export interface UpdateMetricMetadataProps {
|
export interface UpdateMetricMetadataProps {
|
||||||
description: string;
|
description: string;
|
||||||
unit: string;
|
|
||||||
metricType: MetricType;
|
metricType: MetricType;
|
||||||
temporality: Temporality;
|
temporality: Temporality;
|
||||||
isMonotonic?: boolean;
|
isMonotonic?: boolean;
|
||||||
@ -21,7 +20,7 @@ const updateMetricMetadata = async (
|
|||||||
metricName: string,
|
metricName: string,
|
||||||
props: UpdateMetricMetadataProps,
|
props: UpdateMetricMetadataProps,
|
||||||
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
|
): Promise<SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse> => {
|
||||||
const response = await axios.put(`/metrics/${metricName}/metadata`, {
|
const response = await axios.post(`/metrics/${metricName}/metadata`, {
|
||||||
...props,
|
...props,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,7 +11,10 @@ import { useNotifications } from 'hooks/useNotifications';
|
|||||||
import { Edit2, Save } from 'lucide-react';
|
import { Edit2, Save } from 'lucide-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { METRIC_TYPE_LABEL_MAP } from '../Summary/constants';
|
import {
|
||||||
|
METRIC_TYPE_LABEL_MAP,
|
||||||
|
METRIC_TYPE_VALUES_MAP,
|
||||||
|
} from '../Summary/constants';
|
||||||
import { MetricTypeRenderer } from '../Summary/utils';
|
import { MetricTypeRenderer } from '../Summary/utils';
|
||||||
import { METRIC_METADATA_KEYS } from './constants';
|
import { METRIC_METADATA_KEYS } from './constants';
|
||||||
import { MetadataProps } from './types';
|
import { MetadataProps } from './types';
|
||||||
@ -29,7 +32,6 @@ function Metadata({
|
|||||||
] = useState<UpdateMetricMetadataProps>({
|
] = useState<UpdateMetricMetadataProps>({
|
||||||
metricType: metadata?.metric_type || MetricType.SUM,
|
metricType: metadata?.metric_type || MetricType.SUM,
|
||||||
description: metadata?.description || '',
|
description: metadata?.description || '',
|
||||||
unit: metadata?.unit || '',
|
|
||||||
temporality: metadata?.temporality || Temporality.CUMULATIVE,
|
temporality: metadata?.temporality || Temporality.CUMULATIVE,
|
||||||
});
|
});
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
@ -82,7 +84,7 @@ function Metadata({
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
className: 'metric-metadata-value',
|
className: 'metric-metadata-value',
|
||||||
render: (field: { value: string; key: string }): JSX.Element => {
|
render: (field: { value: string; key: string }): JSX.Element => {
|
||||||
if (!isEditing) {
|
if (!isEditing || field.key === 'unit') {
|
||||||
if (field.key === 'metric_type') {
|
if (field.key === 'metric_type') {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -95,9 +97,9 @@ function Metadata({
|
|||||||
if (field.key === 'metric_type') {
|
if (field.key === 'metric_type') {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
options={Object.entries(METRIC_TYPE_LABEL_MAP).map(([key, value]) => ({
|
options={Object.entries(METRIC_TYPE_VALUES_MAP).map(([key]) => ({
|
||||||
value: key,
|
value: key,
|
||||||
label: value,
|
label: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
||||||
}))}
|
}))}
|
||||||
value={metricMetadata.metricType}
|
value={metricMetadata.metricType}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
@ -167,13 +169,15 @@ function Metadata({
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
} else {
|
} else {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Failed to update metadata',
|
message:
|
||||||
|
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (): void =>
|
onError: (): void =>
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'Failed to update metadata',
|
message:
|
||||||
|
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
.labels-row,
|
.labels-row,
|
||||||
.values-row {
|
.values-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr 2fr 2fr;
|
grid-template-columns: 1fr 1fr 2fr;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,10 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
|||||||
import { Compass, X } from 'lucide-react';
|
import { Compass, X } from 'lucide-react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||||
import AllAttributes from './AllAttributes';
|
import AllAttributes from './AllAttributes';
|
||||||
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
|
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
|
||||||
import Metadata from './Metadata';
|
import Metadata from './Metadata';
|
||||||
import TopAttributes from './TopAttributes';
|
|
||||||
import { MetricDetailsProps } from './types';
|
import { MetricDetailsProps } from './types';
|
||||||
import {
|
import {
|
||||||
formatNumberToCompactFormat,
|
formatNumberToCompactFormat,
|
||||||
@ -60,7 +60,16 @@ function MetricDetails({
|
|||||||
if (!metric) return null;
|
if (!metric) return null;
|
||||||
const timeSeriesActive = formatNumberToCompactFormat(metric.timeSeriesActive);
|
const timeSeriesActive = formatNumberToCompactFormat(metric.timeSeriesActive);
|
||||||
const timeSeriesTotal = formatNumberToCompactFormat(metric.timeSeriesTotal);
|
const timeSeriesTotal = formatNumberToCompactFormat(metric.timeSeriesTotal);
|
||||||
return `${timeSeriesActive} ⎯ ${timeSeriesTotal} active`;
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
title="Active time series are those that have received data points in the last 1
|
||||||
|
hour."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<span>{`${timeSeriesTotal} ⎯ ${timeSeriesActive} active`}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
}, [metric]);
|
}, [metric]);
|
||||||
|
|
||||||
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
||||||
@ -73,17 +82,6 @@ function MetricDetails({
|
|||||||
}
|
}
|
||||||
}, [metricName, safeNavigate]);
|
}, [metricName, safeNavigate]);
|
||||||
|
|
||||||
const top5Attributes = useMemo(() => {
|
|
||||||
if (!metric) return [];
|
|
||||||
const totalSum =
|
|
||||||
metric?.attributes.reduce((acc, curr) => acc + curr.valueCount, 0) || 0;
|
|
||||||
return metric?.attributes.slice(0, 5).map((attr) => ({
|
|
||||||
key: attr.key,
|
|
||||||
count: attr.valueCount,
|
|
||||||
percentage: totalSum === 0 ? 0 : (attr.valueCount / totalSum) * 100,
|
|
||||||
}));
|
|
||||||
}, [metric]);
|
|
||||||
|
|
||||||
const isMetricDetailsError = metricDetailsError || !metric;
|
const isMetricDetailsError = metricDetailsError || !metric;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -99,6 +97,8 @@ function MetricDetails({
|
|||||||
onClick={goToMetricsExplorerwithSelectedMetric}
|
onClick={goToMetricsExplorerwithSelectedMetric}
|
||||||
icon={<Compass size={16} />}
|
icon={<Compass size={16} />}
|
||||||
disabled={!metricName}
|
disabled={!metricName}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
Open in Explorer
|
Open in Explorer
|
||||||
</Button>
|
</Button>
|
||||||
@ -124,7 +124,7 @@ function MetricDetails({
|
|||||||
<div className="metric-details-content-grid">
|
<div className="metric-details-content-grid">
|
||||||
<div className="labels-row">
|
<div className="labels-row">
|
||||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||||
DATAPOINTS
|
SAMPLES
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||||
TIME SERIES
|
TIME SERIES
|
||||||
@ -135,8 +135,8 @@ function MetricDetails({
|
|||||||
</div>
|
</div>
|
||||||
<div className="values-row">
|
<div className="values-row">
|
||||||
<Typography.Text className="metric-details-grid-value">
|
<Typography.Text className="metric-details-grid-value">
|
||||||
<Tooltip title={metric?.samples}>
|
<Tooltip title={metric?.samples.toLocaleString()}>
|
||||||
{metric?.samples.toLocaleString()}
|
{formatNumberIntoHumanReadableFormat(metric?.samples)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text className="metric-details-grid-value">
|
<Typography.Text className="metric-details-grid-value">
|
||||||
@ -151,7 +151,6 @@ function MetricDetails({
|
|||||||
dashboards={metric.dashboards}
|
dashboards={metric.dashboards}
|
||||||
alerts={metric.alerts}
|
alerts={metric.alerts}
|
||||||
/>
|
/>
|
||||||
<TopAttributes items={top5Attributes} title="Top 5 Attributes" />
|
|
||||||
<Metadata
|
<Metadata
|
||||||
metricName={metric?.name}
|
metricName={metric?.name}
|
||||||
metadata={metric.metadata}
|
metadata={metric.metadata}
|
||||||
|
@ -7,17 +7,29 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
|
|
||||||
export function formatTimestampToReadableDate(timestamp: string): string {
|
export function formatTimestampToReadableDate(timestamp: string): string {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
// Extracting date components
|
const now = new Date();
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are 0-based
|
|
||||||
const year = String(date.getFullYear()).slice(-2); // Get last two digits of year
|
|
||||||
|
|
||||||
// Extracting time components
|
if (diffInSeconds < 60) return 'Few seconds ago';
|
||||||
const hours = String(date.getHours()).padStart(2, '0');
|
|
||||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
||||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
||||||
|
|
||||||
return `${day}.${month}.${year} ⎯ ${hours}:${minutes}:${seconds}`;
|
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
||||||
|
if (diffInMinutes < 60)
|
||||||
|
return `${diffInMinutes} minute${diffInMinutes > 1 ? 's' : ''} ago`;
|
||||||
|
|
||||||
|
const diffInHours = Math.floor(diffInMinutes / 60);
|
||||||
|
if (diffInHours < 24)
|
||||||
|
return `${diffInHours} hour${diffInHours > 1 ? 's' : ''} ago`;
|
||||||
|
|
||||||
|
const diffInDays = Math.floor(diffInHours / 24);
|
||||||
|
if (diffInDays === 1) {
|
||||||
|
return `Yesterday at ${date
|
||||||
|
.getHours()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
if (diffInDays < 7) return `${diffInDays} days ago`;
|
||||||
|
|
||||||
|
return date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumberToCompactFormat(num: number): string {
|
export function formatNumberToCompactFormat(num: number): string {
|
||||||
@ -31,7 +43,10 @@ export function determineIsMonotonic(
|
|||||||
metricType: MetricType,
|
metricType: MetricType,
|
||||||
temporality: Temporality,
|
temporality: Temporality,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (metricType === MetricType.HISTOGRAM) {
|
if (
|
||||||
|
metricType === MetricType.HISTOGRAM ||
|
||||||
|
metricType === MetricType.EXPONENTIAL_HISTOGRAM
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
|
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
|
||||||
@ -55,7 +70,7 @@ export function getMetricDetailsQuery(
|
|||||||
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||||
aggregateAttribute: {
|
aggregateAttribute: {
|
||||||
key: metricName,
|
key: metricName,
|
||||||
type: DataTypes.String,
|
type: '',
|
||||||
id: `${metricName}----string--`,
|
id: `${metricName}----string--`,
|
||||||
},
|
},
|
||||||
timeAggregation: 'rate',
|
timeAggregation: 'rate',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Select } from 'antd';
|
import { Select, Tooltip } from 'antd';
|
||||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
import { HardHat } from 'lucide-react';
|
import { HardHat, Info } from 'lucide-react';
|
||||||
|
|
||||||
import { TREEMAP_VIEW_OPTIONS } from './constants';
|
import { TREEMAP_VIEW_OPTIONS } from './constants';
|
||||||
import { MetricsSearchProps } from './types';
|
import { MetricsSearchProps } from './types';
|
||||||
@ -27,12 +27,20 @@ function MetricsSearch({
|
|||||||
hideShareModal
|
hideShareModal
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<QueryBuilderSearch
|
<div className="qb-search-container">
|
||||||
query={query}
|
<Tooltip
|
||||||
onChange={onChange}
|
title="Use filters to refine metrics based on attributes. Example: service_name=api - Shows all metrics associated with the API service"
|
||||||
suffixIcon={<HardHat size={16} />}
|
placement="right"
|
||||||
isMetricsExplorer
|
>
|
||||||
/>
|
<Info size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
suffixIcon={<HardHat size={16} />}
|
||||||
|
isMetricsExplorer
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ import {
|
|||||||
Table,
|
Table,
|
||||||
TablePaginationConfig,
|
TablePaginationConfig,
|
||||||
TableProps,
|
TableProps,
|
||||||
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { SorterResult } from 'antd/es/table/interface';
|
import { SorterResult } from 'antd/es/table/interface';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
||||||
@ -37,8 +39,8 @@ function MetricsTable({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setOrderBy({
|
setOrderBy({
|
||||||
columnName: 'type',
|
columnName: 'samples',
|
||||||
order: 'asc',
|
order: 'desc',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -47,6 +49,17 @@ function MetricsTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-table-container">
|
<div className="metrics-table-container">
|
||||||
|
<div className="metrics-table-title">
|
||||||
|
<Typography.Title level={4} className="metrics-table-title">
|
||||||
|
List View
|
||||||
|
</Typography.Title>
|
||||||
|
<Tooltip
|
||||||
|
title="The table displays all metrics in the selected time range. Each row represents a unique metric, and its metric name, and metadata like description, type, unit, and samples/timeseries cardinality observed in the selected time range."
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<Info size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
<Table
|
<Table
|
||||||
loading={{
|
loading={{
|
||||||
spinning: isLoading,
|
spinning: isLoading,
|
||||||
@ -62,7 +75,6 @@ function MetricsTable({
|
|||||||
alt="thinking-emoji"
|
alt="thinking-emoji"
|
||||||
className="empty-state-svg"
|
className="empty-state-svg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Typography.Text className="no-metrics-message">
|
<Typography.Text className="no-metrics-message">
|
||||||
This query had no results. Edit your query and try again!
|
This query had no results. Edit your query and try again!
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Group } from '@visx/group';
|
import { Group } from '@visx/group';
|
||||||
import { Treemap } from '@visx/hierarchy';
|
import { Treemap } from '@visx/hierarchy';
|
||||||
import { Empty, Skeleton, Tooltip } from 'antd';
|
import { Empty, Skeleton, Tooltip, Typography } from 'antd';
|
||||||
import { stratify, treemapBinary } from 'd3-hierarchy';
|
import { stratify, treemapBinary } from 'd3-hierarchy';
|
||||||
|
import { Info } from 'lucide-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useWindowSize } from 'react-use';
|
import { useWindowSize } from 'react-use';
|
||||||
|
|
||||||
@ -36,9 +37,9 @@ function MetricsTreemap({
|
|||||||
|
|
||||||
const treemapData = useMemo(() => {
|
const treemapData = useMemo(() => {
|
||||||
const extracedTreemapData =
|
const extracedTreemapData =
|
||||||
(viewType === TreemapViewType.CARDINALITY
|
(viewType === TreemapViewType.TIMESERIES
|
||||||
? data?.data?.[TreemapViewType.CARDINALITY]
|
? data?.data?.[TreemapViewType.TIMESERIES]
|
||||||
: data?.data?.[TreemapViewType.DATAPOINTS]) || [];
|
: data?.data?.[TreemapViewType.SAMPLES]) || [];
|
||||||
return transformTreemapData(extracedTreemapData, viewType);
|
return transformTreemapData(extracedTreemapData, viewType);
|
||||||
}, [data, viewType]);
|
}, [data, viewType]);
|
||||||
|
|
||||||
@ -71,8 +72,21 @@ function MetricsTreemap({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-treemap">
|
<div className="metrics-treemap-container">
|
||||||
<svg width={treemapWidth} height={TREEMAP_HEIGHT}>
|
<div className="metrics-treemap-title">
|
||||||
|
<Typography.Title level={4}>Proportion View</Typography.Title>
|
||||||
|
<Tooltip
|
||||||
|
title="The treemap displays the proportion of samples/timeseries in the selected time range. Each tile represents a unique metric, and its size indicates the percentage of samples/timeseries it contributes to the total."
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<Info size={16} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
width={treemapWidth}
|
||||||
|
height={TREEMAP_HEIGHT}
|
||||||
|
className="metrics-treemap"
|
||||||
|
>
|
||||||
<rect
|
<rect
|
||||||
width={treemapWidth}
|
width={treemapWidth}
|
||||||
height={TREEMAP_HEIGHT}
|
height={TREEMAP_HEIGHT}
|
||||||
|
@ -4,6 +4,23 @@
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.metrics-table-title,
|
||||||
|
.metrics-treemap-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucide-info {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.metrics-search-container {
|
.metrics-search-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -13,13 +30,27 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qb-search-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.lucide-info {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-builder-search-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.metrics-table-container {
|
.metrics-table-container {
|
||||||
margin-left: -16px;
|
|
||||||
margin-right: -16px;
|
|
||||||
|
|
||||||
.ant-table {
|
.ant-table {
|
||||||
|
margin-left: -16px;
|
||||||
|
margin-right: -16px;
|
||||||
|
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead > tr > th {
|
||||||
@ -61,6 +92,12 @@
|
|||||||
background: var(--bg-ink-400);
|
background: var(--bg-ink-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metric-description-column-value {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-name-column-value {
|
.metric-name-column-value {
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
font-family: 'Geist Mono';
|
font-family: 'Geist Mono';
|
||||||
@ -150,6 +187,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.metrics-treemap {
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
}
|
||||||
|
|
||||||
.no-metrics-message-container {
|
.no-metrics-message-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -7,7 +7,7 @@ import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap
|
|||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@ -28,11 +28,11 @@ function Summary(): JSX.Element {
|
|||||||
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
|
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [orderBy, setOrderBy] = useState<OrderByPayload>({
|
const [orderBy, setOrderBy] = useState<OrderByPayload>({
|
||||||
columnName: 'type',
|
columnName: 'samples',
|
||||||
order: 'asc',
|
order: 'desc',
|
||||||
});
|
});
|
||||||
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
|
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
|
||||||
TreemapViewType.CARDINALITY,
|
TreemapViewType.TIMESERIES,
|
||||||
);
|
);
|
||||||
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
|
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
|
||||||
const [selectedMetricName, setSelectedMetricName] = useState<string | null>(
|
const [selectedMetricName, setSelectedMetricName] = useState<string | null>(
|
||||||
@ -99,15 +99,6 @@ function Summary(): JSX.Element {
|
|||||||
enabled: !!metricsTreemapQuery,
|
enabled: !!metricsTreemapQuery,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset the filters when the component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
handleChangeQueryData('filters', {
|
|
||||||
op: 'AND',
|
|
||||||
items: [],
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(value: TagFilter) => {
|
(value: TagFilter) => {
|
||||||
handleChangeQueryData('filters', value);
|
handleChangeQueryData('filters', value);
|
||||||
|
@ -8,11 +8,11 @@ export const TREEMAP_VIEW_OPTIONS: {
|
|||||||
value: TreemapViewType;
|
value: TreemapViewType;
|
||||||
label: string;
|
label: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{ value: TreemapViewType.CARDINALITY, label: 'Cardinality' },
|
{ value: TreemapViewType.TIMESERIES, label: 'Time Series' },
|
||||||
{ value: TreemapViewType.DATAPOINTS, label: 'Datapoints' },
|
{ value: TreemapViewType.SAMPLES, label: 'Samples' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const TREEMAP_HEIGHT = 300;
|
export const TREEMAP_HEIGHT = 200;
|
||||||
export const TREEMAP_SQUARE_PADDING = 5;
|
export const TREEMAP_SQUARE_PADDING = 5;
|
||||||
|
|
||||||
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
||||||
@ -24,3 +24,11 @@ export const METRIC_TYPE_LABEL_MAP = {
|
|||||||
[MetricType.SUMMARY]: 'Summary',
|
[MetricType.SUMMARY]: 'Summary',
|
||||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const METRIC_TYPE_VALUES_MAP = {
|
||||||
|
[MetricType.SUM]: 'Sum',
|
||||||
|
[MetricType.GAUGE]: 'Gauge',
|
||||||
|
[MetricType.HISTOGRAM]: 'Histogram',
|
||||||
|
[MetricType.SUMMARY]: 'Summary',
|
||||||
|
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
|
||||||
|
};
|
||||||
|
@ -46,8 +46,8 @@ export interface MetricsListItemRowData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum TreemapViewType {
|
export enum TreemapViewType {
|
||||||
CARDINALITY = 'timeseries',
|
TIMESERIES = 'timeseries',
|
||||||
DATAPOINTS = 'samples',
|
SAMPLES = 'samples',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TreemapTile {
|
export interface TreemapTile {
|
||||||
|
@ -7,8 +7,8 @@ import {
|
|||||||
MetricType,
|
MetricType,
|
||||||
} from 'api/metricsExplorer/getMetricsList';
|
} from 'api/metricsExplorer/getMetricsList';
|
||||||
import {
|
import {
|
||||||
CardinalityData,
|
SamplesData,
|
||||||
DatapointsData,
|
TimeseriesData,
|
||||||
} from 'api/metricsExplorer/getMetricsTreeMap';
|
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||||
import {
|
import {
|
||||||
BarChart,
|
BarChart,
|
||||||
@ -37,6 +37,11 @@ export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
|
|||||||
title: 'DESCRIPTION',
|
title: 'DESCRIPTION',
|
||||||
dataIndex: 'description',
|
dataIndex: 'description',
|
||||||
width: 400,
|
width: 400,
|
||||||
|
render: (value: string): React.ReactNode => (
|
||||||
|
<Tooltip title={value}>
|
||||||
|
<div className="metric-description-column-value">{value}</div>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'TYPE',
|
title: 'TYPE',
|
||||||
@ -50,14 +55,14 @@ export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
|
|||||||
width: 150,
|
width: 150,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'DATAPOINTS',
|
title: 'SAMPLES',
|
||||||
dataIndex: TreemapViewType.DATAPOINTS,
|
dataIndex: TreemapViewType.SAMPLES,
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'CARDINALITY',
|
title: 'TIME SERIES',
|
||||||
dataIndex: TreemapViewType.CARDINALITY,
|
dataIndex: TreemapViewType.TIMESERIES,
|
||||||
width: 150,
|
width: 150,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
},
|
},
|
||||||
@ -138,6 +143,26 @@ function ValidateRowValueWrapper({
|
|||||||
return <div>{children}</div>;
|
return <div>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const formatNumberIntoHumanReadableFormat = (num: number): string => {
|
||||||
|
function format(num: number, divisor: number, suffix: string): string {
|
||||||
|
const value = num / divisor;
|
||||||
|
return value % 1 === 0
|
||||||
|
? `${value}${suffix}+`
|
||||||
|
: `${value.toFixed(1).replace(/\.0$/, '')}${suffix}+`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num >= 1_000_000_000) {
|
||||||
|
return format(num, 1_000_000_000, 'B');
|
||||||
|
}
|
||||||
|
if (num >= 1_000_000) {
|
||||||
|
return format(num, 1_000_000, 'M');
|
||||||
|
}
|
||||||
|
if (num >= 1_000) {
|
||||||
|
return format(num, 1_000, 'K');
|
||||||
|
}
|
||||||
|
return num.toString();
|
||||||
|
};
|
||||||
|
|
||||||
export const formatDataForMetricsTable = (
|
export const formatDataForMetricsTable = (
|
||||||
data: MetricsListItemData[],
|
data: MetricsListItemData[],
|
||||||
): MetricsListItemRowData[] =>
|
): MetricsListItemRowData[] =>
|
||||||
@ -159,25 +184,28 @@ export const formatDataForMetricsTable = (
|
|||||||
{metric.unit}
|
{metric.unit}
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
[TreemapViewType.DATAPOINTS]: (
|
[TreemapViewType.SAMPLES]: (
|
||||||
<ValidateRowValueWrapper value={metric[TreemapViewType.DATAPOINTS]}>
|
<ValidateRowValueWrapper value={metric[TreemapViewType.SAMPLES]}>
|
||||||
{metric[TreemapViewType.DATAPOINTS].toLocaleString()}
|
<Tooltip title={metric[TreemapViewType.SAMPLES].toLocaleString()}>
|
||||||
|
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.SAMPLES])}
|
||||||
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
[TreemapViewType.CARDINALITY]: (
|
[TreemapViewType.TIMESERIES]: (
|
||||||
<ValidateRowValueWrapper value={metric[TreemapViewType.CARDINALITY]}>
|
<ValidateRowValueWrapper value={metric[TreemapViewType.TIMESERIES]}>
|
||||||
{metric[TreemapViewType.CARDINALITY]}
|
<Tooltip title={metric[TreemapViewType.TIMESERIES].toLocaleString()}>
|
||||||
|
{formatNumberIntoHumanReadableFormat(metric[TreemapViewType.TIMESERIES])}
|
||||||
|
</Tooltip>
|
||||||
</ValidateRowValueWrapper>
|
</ValidateRowValueWrapper>
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const transformTreemapData = (
|
export const transformTreemapData = (
|
||||||
data: CardinalityData[] | DatapointsData[],
|
data: TimeseriesData[] | SamplesData[],
|
||||||
viewType: TreemapViewType,
|
viewType: TreemapViewType,
|
||||||
): TreemapTile[] => {
|
): TreemapTile[] => {
|
||||||
const totalSize = (data as (CardinalityData | DatapointsData)[]).reduce(
|
const totalSize = (data as (TimeseriesData | SamplesData)[]).reduce(
|
||||||
(acc: number, item: CardinalityData | DatapointsData) =>
|
(acc: number, item: TimeseriesData | SamplesData) => acc + item.percentage,
|
||||||
acc + item.percentage,
|
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -100,13 +100,12 @@ const menuItems: SidebarItem[] = [
|
|||||||
label: 'Logs',
|
label: 'Logs',
|
||||||
icon: <ScrollText size={16} />,
|
icon: <ScrollText size={16} />,
|
||||||
},
|
},
|
||||||
// TODO - Enable this when the metrics explorer feature is read for release
|
{
|
||||||
// {
|
key: ROUTES.METRICS_EXPLORER,
|
||||||
// key: ROUTES.METRICS_EXPLORER,
|
label: 'Metrics',
|
||||||
// label: 'Metrics',
|
icon: <BarChart2 size={16} />,
|
||||||
// icon: <BarChart2 size={16} />,
|
isNew: true,
|
||||||
// isNew: true,
|
},
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
key: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||||
label: 'Infra Monitoring',
|
label: 'Infra Monitoring',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user