chore: metrics explorer fixes and improvements (#7334)

This commit is contained in:
Amlan Kumar Nandy 2025-03-18 16:27:14 +05:30 committed by GitHub
parent 5b6b5bf359
commit b3fa1ad60e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 226 additions and 107 deletions

View File

@ -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 {

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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,
}); });

View File

@ -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.',
}), }),
}, },
); );

View File

@ -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;
} }

View File

@ -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}

View File

@ -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',

View File

@ -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>
); );
} }

View File

@ -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>

View File

@ -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}

View File

@ -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;

View File

@ -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);

View File

@ -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',
};

View File

@ -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 {

View File

@ -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,
); );

View File

@ -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',