mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 22:08:59 +08:00
chore: metrics explorer improvements (#7285)
This commit is contained in:
parent
9a3c49bce4
commit
86a888a6a2
@ -16,15 +16,21 @@ export interface MetricDetails {
|
|||||||
timeSeriesActive: number;
|
timeSeriesActive: number;
|
||||||
lastReceived: string;
|
lastReceived: string;
|
||||||
attributes: MetricDetailsAttribute[];
|
attributes: MetricDetailsAttribute[];
|
||||||
metadata: {
|
metadata?: {
|
||||||
metric_type: MetricType;
|
metric_type: MetricType;
|
||||||
description: string;
|
description: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
|
temporality: Temporality;
|
||||||
};
|
};
|
||||||
alerts: MetricDetailsAlert[] | null;
|
alerts: MetricDetailsAlert[] | null;
|
||||||
dashboards: MetricDetailsDashboard[] | null;
|
dashboards: MetricDetailsDashboard[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Temporality {
|
||||||
|
CUMULATIVE = 'cumulative',
|
||||||
|
DELTA = 'delta',
|
||||||
|
}
|
||||||
|
|
||||||
export interface MetricDetailsAttribute {
|
export interface MetricDetailsAttribute {
|
||||||
key: string;
|
key: string;
|
||||||
value: string[];
|
value: string[];
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
import { Temporality } from './getMetricDetails';
|
||||||
import { MetricType } from './getMetricsList';
|
import { MetricType } from './getMetricsList';
|
||||||
|
|
||||||
export interface UpdateMetricMetadataProps {
|
export interface UpdateMetricMetadataProps {
|
||||||
description: string;
|
description: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
type: MetricType;
|
metricType: MetricType;
|
||||||
|
temporality: Temporality;
|
||||||
|
isMonotonic?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateMetricMetadataResponse {
|
export interface UpdateMetricMetadataResponse {
|
||||||
|
@ -26,4 +26,5 @@ export enum LOCALSTORAGE {
|
|||||||
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
UNAUTHENTICATED_ROUTE_HIT = 'UNAUTHENTICATED_ROUTE_HIT',
|
||||||
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
||||||
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
|
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
|
||||||
|
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,6 @@
|
|||||||
.related-metrics-container {
|
.related-metrics-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
max-height: 450px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -102,19 +101,23 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.related-metrics-body {
|
.related-metrics-body {
|
||||||
padding: 10px 0;
|
margin-top: 20px;
|
||||||
|
max-height: 650px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
.related-metrics-card-container {
|
.related-metrics-card-container {
|
||||||
min-height: 300px;
|
margin-bottom: 20px;
|
||||||
margin-bottom: 25px;
|
min-height: 640px;
|
||||||
|
|
||||||
.related-metrics-card {
|
.related-metrics-card {
|
||||||
// height: 400px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
.related-metrics-card-error {
|
.related-metrics-card-error {
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
|
height: fit-content;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,8 +38,8 @@ function Explorer(): JSX.Element {
|
|||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
|
const { mutate: updateDashboard, isLoading } = useUpdateDashboard();
|
||||||
const { options } = useOptionsMenu({
|
const { options } = useOptionsMenu({
|
||||||
storageKey: LOCALSTORAGE.TRACES_LIST_OPTIONS,
|
storageKey: LOCALSTORAGE.METRICS_LIST_OPTIONS,
|
||||||
dataSource: DataSource.TRACES,
|
dataSource: DataSource.METRICS,
|
||||||
aggregateOperator: 'noop',
|
aggregateOperator: 'noop',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Card, Col, Input, Row, Select, Skeleton } from 'antd';
|
import { Card, Col, Empty, Input, Row, Select, Skeleton } from 'antd';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
|
||||||
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
|
|
||||||
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
|
||||||
import { Gauge } from 'lucide-react';
|
import { Gauge } from 'lucide-react';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -15,9 +11,7 @@ import { RelatedMetricsProps, RelatedMetricWithQueryResult } from './types';
|
|||||||
import { useGetRelatedMetricsGraphs } from './useGetRelatedMetricsGraphs';
|
import { useGetRelatedMetricsGraphs } from './useGetRelatedMetricsGraphs';
|
||||||
|
|
||||||
function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
|
function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
|
||||||
const graphRef = useRef<HTMLDivElement>(null);
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
const dimensions = useResizeObserver(graphRef);
|
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
@ -41,13 +35,15 @@ function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [metricNames]);
|
}, [metricNames]);
|
||||||
|
|
||||||
const { relatedMetrics, isRelatedMetricsLoading } = useGetRelatedMetricsGraphs(
|
const {
|
||||||
{
|
relatedMetrics,
|
||||||
selectedMetricName,
|
isRelatedMetricsLoading,
|
||||||
startMs,
|
isRelatedMetricsError,
|
||||||
endMs,
|
} = useGetRelatedMetricsGraphs({
|
||||||
},
|
selectedMetricName,
|
||||||
);
|
startMs,
|
||||||
|
endMs,
|
||||||
|
});
|
||||||
|
|
||||||
const metricNamesSelectOptions = useMemo(
|
const metricNamesSelectOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -91,31 +87,6 @@ function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
|
|||||||
return filteredMetrics;
|
return filteredMetrics;
|
||||||
}, [relatedMetrics, selectedRelatedMetric, searchValue]);
|
}, [relatedMetrics, selectedRelatedMetric, searchValue]);
|
||||||
|
|
||||||
const chartData = useMemo(
|
|
||||||
() =>
|
|
||||||
filteredRelatedMetrics.map(({ queryResult }) =>
|
|
||||||
getUPlotChartData(queryResult.data?.payload),
|
|
||||||
),
|
|
||||||
[filteredRelatedMetrics],
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = useMemo(
|
|
||||||
() =>
|
|
||||||
filteredRelatedMetrics.map(({ queryResult }) =>
|
|
||||||
getUPlotChartOptions({
|
|
||||||
apiResponse: queryResult.data?.payload,
|
|
||||||
isDarkMode,
|
|
||||||
dimensions,
|
|
||||||
yAxisUnit: '',
|
|
||||||
softMax: null,
|
|
||||||
softMin: null,
|
|
||||||
minTimeScale: startMs,
|
|
||||||
maxTimeScale: endMs,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
[filteredRelatedMetrics, isDarkMode, dimensions, startMs, endMs],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="related-metrics-container">
|
<div className="related-metrics-container">
|
||||||
<div className="related-metrics-header">
|
<div className="related-metrics-header">
|
||||||
@ -145,20 +116,34 @@ function RelatedMetrics({ metricNames }: RelatedMetricsProps): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<div className="related-metrics-body">
|
<div className="related-metrics-body">
|
||||||
{isRelatedMetricsLoading && <Skeleton active />}
|
{isRelatedMetricsLoading && <Skeleton active />}
|
||||||
<Row gutter={24}>
|
{isRelatedMetricsError && (
|
||||||
{filteredRelatedMetrics.map((relatedMetricWithQueryResult, index) => (
|
<Empty description="Error fetching related metrics" />
|
||||||
<Col span={8} key={relatedMetricWithQueryResult.name}>
|
)}
|
||||||
<Card bordered ref={graphRef} className="related-metrics-card-container">
|
{!isRelatedMetricsLoading &&
|
||||||
<RelatedMetricsCard
|
!isRelatedMetricsError &&
|
||||||
key={relatedMetricWithQueryResult.name}
|
filteredRelatedMetrics.length === 0 && (
|
||||||
metric={relatedMetricWithQueryResult}
|
<Empty description="No related metrics found" />
|
||||||
options={options[index]}
|
)}
|
||||||
chartData={chartData[index]}
|
{!isRelatedMetricsLoading &&
|
||||||
/>
|
!isRelatedMetricsError &&
|
||||||
</Card>
|
filteredRelatedMetrics.length > 0 && (
|
||||||
</Col>
|
<Row gutter={24}>
|
||||||
))}
|
{filteredRelatedMetrics.map((relatedMetricWithQueryResult) => (
|
||||||
</Row>
|
<Col span={12} key={relatedMetricWithQueryResult.name}>
|
||||||
|
<Card
|
||||||
|
bordered
|
||||||
|
ref={graphRef}
|
||||||
|
className="related-metrics-card-container"
|
||||||
|
>
|
||||||
|
<RelatedMetricsCard
|
||||||
|
key={relatedMetricWithQueryResult.name}
|
||||||
|
metric={relatedMetricWithQueryResult}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
import { Skeleton, Typography } from 'antd';
|
import { Empty, Skeleton, Typography } from 'antd';
|
||||||
import Uplot from 'components/Uplot';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import DashboardsAndAlertsPopover from '../MetricDetails/DashboardsAndAlertsPopover';
|
import DashboardsAndAlertsPopover from '../MetricDetails/DashboardsAndAlertsPopover';
|
||||||
import { RelatedMetricsCardProps } from './types';
|
import { RelatedMetricsCardProps } from './types';
|
||||||
|
|
||||||
function RelatedMetricsCard({
|
function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
|
||||||
metric,
|
|
||||||
options,
|
|
||||||
chartData,
|
|
||||||
}: RelatedMetricsCardProps): JSX.Element {
|
|
||||||
const { queryResult } = metric;
|
const { queryResult } = metric;
|
||||||
|
|
||||||
if (queryResult.isLoading) {
|
if (queryResult.isLoading) {
|
||||||
@ -27,13 +24,20 @@ function RelatedMetricsCard({
|
|||||||
{metric.name}
|
{metric.name}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
{queryResult.isLoading ? <Skeleton /> : null}
|
{queryResult.isLoading ? <Skeleton /> : null}
|
||||||
{queryResult.error ? (
|
{queryResult.isError ? (
|
||||||
<div className="related-metrics-card-error">
|
<div className="related-metrics-card-error">
|
||||||
<Typography.Text>Something went wrong</Typography.Text>
|
<Empty description="Error fetching metric data" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!queryResult.isLoading && !queryResult.error && (
|
{!queryResult.isLoading && !queryResult.error && (
|
||||||
<Uplot options={options} data={chartData} />
|
<TimeSeriesView
|
||||||
|
isFilterApplied={false}
|
||||||
|
isError={queryResult.isError}
|
||||||
|
isLoading={queryResult.isLoading}
|
||||||
|
data={queryResult.data}
|
||||||
|
yAxisUnit="ms"
|
||||||
|
dataSource={DataSource.METRICS}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<DashboardsAndAlertsPopover
|
<DashboardsAndAlertsPopover
|
||||||
dashboards={metric.dashboards}
|
dashboards={metric.dashboards}
|
||||||
|
@ -18,8 +18,6 @@ export interface RelatedMetricsProps {
|
|||||||
|
|
||||||
export interface RelatedMetricsCardProps {
|
export interface RelatedMetricsCardProps {
|
||||||
metric: RelatedMetricWithQueryResult;
|
metric: RelatedMetricWithQueryResult;
|
||||||
options: uPlot.Options;
|
|
||||||
chartData: any[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UseGetRelatedMetricsGraphsProps {
|
export interface UseGetRelatedMetricsGraphsProps {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { Button, Collapse, Input, Typography } from 'antd';
|
import { Button, Collapse, Input, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import { DataType } from 'container/LogDetailedView/TableView';
|
import { DataType } from 'container/LogDetailedView/TableView';
|
||||||
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { AllAttributesProps } from './types';
|
import { AllAttributesProps } from './types';
|
||||||
|
import { getMetricDetailsQuery } from './utils';
|
||||||
|
|
||||||
function AllAttributes({
|
function AllAttributes({
|
||||||
attributes,
|
attributes,
|
||||||
@ -16,12 +19,17 @@ function AllAttributes({
|
|||||||
'all-attributes',
|
'all-attributes',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
const goToMetricsExploreWithAppliedAttribute = useCallback(
|
const goToMetricsExploreWithAppliedAttribute = useCallback(
|
||||||
(attribute: string) => {
|
(key: string, value: string) => {
|
||||||
// TODO: Implement this when explore page is ready
|
const compositeQuery = getMetricDetailsQuery(metricName, { key, value });
|
||||||
console.log(metricName, attribute);
|
const encodedCompositeQuery = JSON.stringify(compositeQuery);
|
||||||
|
safeNavigate(
|
||||||
|
`${ROUTES.METRICS_EXPLORER_EXPLORER}?compositeQuery=${encodedCompositeQuery}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[metricName],
|
[metricName, safeNavigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredAttributes = useMemo(
|
const filteredAttributes = useMemo(
|
||||||
@ -40,7 +48,10 @@ function AllAttributes({
|
|||||||
label: attribute.key,
|
label: attribute.key,
|
||||||
contribution: attribute.valueCount,
|
contribution: attribute.valueCount,
|
||||||
},
|
},
|
||||||
value: attribute.value,
|
value: {
|
||||||
|
key: attribute.key,
|
||||||
|
value: attribute.value,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
: [],
|
: [],
|
||||||
[filteredAttributes],
|
[filteredAttributes],
|
||||||
@ -70,14 +81,14 @@ function AllAttributes({
|
|||||||
align: 'left',
|
align: 'left',
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
className: 'metric-metadata-value',
|
className: 'metric-metadata-value',
|
||||||
render: (attributes: string[]): JSX.Element => (
|
render: (field: { key: string; value: string[] }): JSX.Element => (
|
||||||
<div className="all-attributes-value">
|
<div className="all-attributes-value">
|
||||||
{attributes.map((attribute) => (
|
{field.value.map((attribute) => (
|
||||||
<Button
|
<Button
|
||||||
key={attribute}
|
key={attribute}
|
||||||
type="text"
|
type="text"
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
goToMetricsExploreWithAppliedAttribute(attribute);
|
goToMetricsExploreWithAppliedAttribute(field.key, attribute);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography.Text>{attribute}</Typography.Text>
|
<Typography.Text>{attribute}</Typography.Text>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Button, Collapse, Input, Select, Typography } from 'antd';
|
import { Button, Collapse, Input, Select, Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
@ -14,6 +15,7 @@ import { METRIC_TYPE_LABEL_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';
|
||||||
|
import { determineIsMonotonic } from './utils';
|
||||||
|
|
||||||
function Metadata({
|
function Metadata({
|
||||||
metricName,
|
metricName,
|
||||||
@ -25,9 +27,10 @@ function Metadata({
|
|||||||
metricMetadata,
|
metricMetadata,
|
||||||
setMetricMetadata,
|
setMetricMetadata,
|
||||||
] = useState<UpdateMetricMetadataProps>({
|
] = useState<UpdateMetricMetadataProps>({
|
||||||
type: metadata.metric_type,
|
metricType: metadata?.metric_type || MetricType.SUM,
|
||||||
description: metadata.description,
|
description: metadata?.description || '',
|
||||||
unit: metadata.unit,
|
unit: metadata?.unit || '',
|
||||||
|
temporality: metadata?.temporality || Temporality.CUMULATIVE,
|
||||||
});
|
});
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const {
|
const {
|
||||||
@ -41,13 +44,16 @@ function Metadata({
|
|||||||
const tableData = useMemo(
|
const tableData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
metadata
|
metadata
|
||||||
? Object.keys(metadata).map((key) => ({
|
? Object.keys(metadata)
|
||||||
key,
|
// Filter out isMonotonic as user input is not required
|
||||||
value: {
|
.filter((key) => key !== 'isMonotonic')
|
||||||
value: metadata[key as keyof typeof metadata],
|
.map((key) => ({
|
||||||
key,
|
key,
|
||||||
},
|
value: {
|
||||||
}))
|
value: metadata[key as keyof typeof metadata],
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
}))
|
||||||
: [],
|
: [],
|
||||||
[metadata],
|
[metadata],
|
||||||
);
|
);
|
||||||
@ -93,11 +99,28 @@ function Metadata({
|
|||||||
value: key,
|
value: key,
|
||||||
label: value,
|
label: value,
|
||||||
}))}
|
}))}
|
||||||
value={metricMetadata.type}
|
value={metricMetadata.metricType}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
setMetricMetadata({
|
setMetricMetadata({
|
||||||
...metricMetadata,
|
...metricMetadata,
|
||||||
type: value as MetricType,
|
metricType: value as MetricType,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (field.key === 'temporality') {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={Object.values(Temporality).map((key) => ({
|
||||||
|
value: key,
|
||||||
|
label: key,
|
||||||
|
}))}
|
||||||
|
value={metricMetadata.temporality}
|
||||||
|
onChange={(value): void => {
|
||||||
|
setMetricMetadata({
|
||||||
|
...metricMetadata,
|
||||||
|
temporality: value as Temporality,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -106,7 +129,11 @@ function Metadata({
|
|||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
name={field.key}
|
name={field.key}
|
||||||
value={metricMetadata[field.key as keyof UpdateMetricMetadataProps]}
|
value={
|
||||||
|
metricMetadata[
|
||||||
|
field.key as Exclude<keyof UpdateMetricMetadataProps, 'isMonotonic'>
|
||||||
|
]
|
||||||
|
}
|
||||||
onChange={(e): void => {
|
onChange={(e): void => {
|
||||||
setMetricMetadata({ ...metricMetadata, [field.key]: e.target.value });
|
setMetricMetadata({ ...metricMetadata, [field.key]: e.target.value });
|
||||||
}}
|
}}
|
||||||
@ -122,7 +149,13 @@ function Metadata({
|
|||||||
updateMetricMetadata(
|
updateMetricMetadata(
|
||||||
{
|
{
|
||||||
metricName,
|
metricName,
|
||||||
payload: metricMetadata,
|
payload: {
|
||||||
|
...metricMetadata,
|
||||||
|
isMonotonic: determineIsMonotonic(
|
||||||
|
metricMetadata.metricType,
|
||||||
|
metricMetadata.temporality,
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: (response): void => {
|
onSuccess: (response): void => {
|
||||||
|
@ -2,9 +2,19 @@ import './MetricDetails.styles.scss';
|
|||||||
import '../Summary/Summary.styles.scss';
|
import '../Summary/Summary.styles.scss';
|
||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Divider, Drawer, Skeleton, Tooltip, Typography } from 'antd';
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Drawer,
|
||||||
|
Empty,
|
||||||
|
Skeleton,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
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';
|
||||||
|
|
||||||
@ -16,6 +26,7 @@ import { MetricDetailsProps } from './types';
|
|||||||
import {
|
import {
|
||||||
formatNumberToCompactFormat,
|
formatNumberToCompactFormat,
|
||||||
formatTimestampToReadableDate,
|
formatTimestampToReadableDate,
|
||||||
|
getMetricDetailsQuery,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function MetricDetails({
|
function MetricDetails({
|
||||||
@ -24,10 +35,13 @@ function MetricDetails({
|
|||||||
metricName,
|
metricName,
|
||||||
}: MetricDetailsProps): JSX.Element {
|
}: MetricDetailsProps): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
error: metricDetailsError,
|
||||||
refetch: refetchMetricDetails,
|
refetch: refetchMetricDetails,
|
||||||
} = useGetMetricDetails(metricName ?? '', {
|
} = useGetMetricDetails(metricName ?? '', {
|
||||||
enabled: !!metricName,
|
enabled: !!metricName,
|
||||||
@ -40,7 +54,7 @@ function MetricDetails({
|
|||||||
return formatTimestampToReadableDate(metric.lastReceived);
|
return formatTimestampToReadableDate(metric.lastReceived);
|
||||||
}, [metric]);
|
}, [metric]);
|
||||||
|
|
||||||
const isMetricDetailsLoading = isLoading || isFetching || !metric;
|
const isMetricDetailsLoading = isLoading || isFetching;
|
||||||
|
|
||||||
const timeSeries = useMemo(() => {
|
const timeSeries = useMemo(() => {
|
||||||
if (!metric) return null;
|
if (!metric) return null;
|
||||||
@ -50,21 +64,28 @@ function MetricDetails({
|
|||||||
}, [metric]);
|
}, [metric]);
|
||||||
|
|
||||||
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
||||||
// TODO: Implement this when explore page is ready
|
if (metricName) {
|
||||||
console.log(metricName);
|
const compositeQuery = getMetricDetailsQuery(metricName);
|
||||||
}, [metricName]);
|
const encodedCompositeQuery = JSON.stringify(compositeQuery);
|
||||||
|
safeNavigate(
|
||||||
|
`${ROUTES.METRICS_EXPLORER_EXPLORER}?compositeQuery=${encodedCompositeQuery}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [metricName, safeNavigate]);
|
||||||
|
|
||||||
const top5Attributes = useMemo(() => {
|
const top5Attributes = useMemo(() => {
|
||||||
|
if (!metric) return [];
|
||||||
const totalSum =
|
const totalSum =
|
||||||
metric?.attributes.reduce((acc, curr) => acc + curr.valueCount, 0) || 0;
|
metric?.attributes.reduce((acc, curr) => acc + curr.valueCount, 0) || 0;
|
||||||
if (!metric) return [];
|
return metric?.attributes.slice(0, 5).map((attr) => ({
|
||||||
return metric.attributes.slice(0, 5).map((attr) => ({
|
|
||||||
key: attr.key,
|
key: attr.key,
|
||||||
count: attr.valueCount,
|
count: attr.valueCount,
|
||||||
percentage: totalSum === 0 ? 0 : (attr.valueCount / totalSum) * 100,
|
percentage: totalSum === 0 ? 0 : (attr.valueCount / totalSum) * 100,
|
||||||
}));
|
}));
|
||||||
}, [metric]);
|
}, [metric]);
|
||||||
|
|
||||||
|
const isMetricDetailsError = metricDetailsError || !metric;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width="60%"
|
width="60%"
|
||||||
@ -77,6 +98,7 @@ function MetricDetails({
|
|||||||
<Button
|
<Button
|
||||||
onClick={goToMetricsExplorerwithSelectedMetric}
|
onClick={goToMetricsExplorerwithSelectedMetric}
|
||||||
icon={<Compass size={16} />}
|
icon={<Compass size={16} />}
|
||||||
|
disabled={!metricName}
|
||||||
>
|
>
|
||||||
Open in Explorer
|
Open in Explorer
|
||||||
</Button>
|
</Button>
|
||||||
@ -93,9 +115,11 @@ function MetricDetails({
|
|||||||
destroyOnClose
|
destroyOnClose
|
||||||
closeIcon={<X size={16} />}
|
closeIcon={<X size={16} />}
|
||||||
>
|
>
|
||||||
{isMetricDetailsLoading ? (
|
{isMetricDetailsLoading && <Skeleton active />}
|
||||||
<Skeleton active />
|
{isMetricDetailsError && !isMetricDetailsLoading && (
|
||||||
) : (
|
<Empty description="Error fetching metric details" />
|
||||||
|
)}
|
||||||
|
{!isMetricDetailsLoading && !isMetricDetailsError && (
|
||||||
<div className="metric-details-content">
|
<div className="metric-details-content">
|
||||||
<div className="metric-details-content-grid">
|
<div className="metric-details-content-grid">
|
||||||
<div className="labels-row">
|
<div className="labels-row">
|
||||||
|
@ -2,4 +2,5 @@ export const METRIC_METADATA_KEYS = {
|
|||||||
description: 'Description',
|
description: 'Description',
|
||||||
unit: 'Unit',
|
unit: 'Unit',
|
||||||
metric_type: 'Metric Type',
|
metric_type: 'Metric Type',
|
||||||
|
temporality: 'Temporality',
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ export interface DashboardsAndAlertsPopoverProps {
|
|||||||
|
|
||||||
export interface MetadataProps {
|
export interface MetadataProps {
|
||||||
metricName: string;
|
metricName: string;
|
||||||
metadata: MetricDetails['metadata'];
|
metadata: MetricDetails['metadata'] | undefined;
|
||||||
refetchMetricDetails: () => void;
|
refetchMetricDetails: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
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
|
// Extracting date components
|
||||||
@ -19,3 +26,59 @@ export function formatNumberToCompactFormat(num: number): string {
|
|||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
}).format(num);
|
}).format(num);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function determineIsMonotonic(
|
||||||
|
metricType: MetricType,
|
||||||
|
temporality: Temporality,
|
||||||
|
): boolean {
|
||||||
|
if (metricType === MetricType.HISTOGRAM) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (metricType === MetricType.SUM) {
|
||||||
|
return temporality === Temporality.CUMULATIVE;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetricDetailsQuery(
|
||||||
|
metricName: string,
|
||||||
|
filter?: { key: string; value: string },
|
||||||
|
): Query {
|
||||||
|
return {
|
||||||
|
...initialQueriesMap[DataSource.METRICS],
|
||||||
|
builder: {
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||||
|
aggregateAttribute: {
|
||||||
|
key: metricName,
|
||||||
|
type: DataTypes.String,
|
||||||
|
id: `${metricName}----string--`,
|
||||||
|
},
|
||||||
|
timeAggregation: 'rate',
|
||||||
|
spaceAggregation: 'sum',
|
||||||
|
filters: {
|
||||||
|
op: 'AND',
|
||||||
|
items: filter
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
op: '=',
|
||||||
|
id: filter.key,
|
||||||
|
value: filter.value,
|
||||||
|
key: {
|
||||||
|
key: filter.key,
|
||||||
|
type: DataTypes.String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFormulas: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
.loading-metrics {
|
||||||
|
padding: 24px 0;
|
||||||
|
height: 240px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.loading-metrics-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.loading-gif {
|
||||||
|
height: 72px;
|
||||||
|
margin-left: -24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import './MetricsLoading.styles.scss';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export function MetricsLoading(): JSX.Element {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
return (
|
||||||
|
<div className="loading-metrics">
|
||||||
|
<div className="loading-metrics-content">
|
||||||
|
<img
|
||||||
|
className="loading-gif"
|
||||||
|
src="/Icons/loading-plane.gif"
|
||||||
|
alt="wait-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography>
|
||||||
|
{t('pending_data_placeholder', { dataSource: DataSource.METRICS })}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -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, useMemo, useState } from 'react';
|
import { useCallback, useEffect, 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';
|
||||||
@ -99,6 +99,15 @@ 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);
|
||||||
|
@ -6,6 +6,7 @@ import { QueryParams } from 'constants/query';
|
|||||||
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
import EmptyLogsSearch from 'container/EmptyLogsSearch/EmptyLogsSearch';
|
||||||
import LogsError from 'container/LogsError/LogsError';
|
import LogsError from 'container/LogsError/LogsError';
|
||||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||||
|
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
|
||||||
import NoLogs from 'container/NoLogs/NoLogs';
|
import NoLogs from 'container/NoLogs/NoLogs';
|
||||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||||
@ -131,6 +132,10 @@ function TimeSeriesView({
|
|||||||
logEvent('Logs Explorer: Data present', {
|
logEvent('Logs Explorer: Data present', {
|
||||||
panelType: 'TIME_SERIES',
|
panelType: 'TIME_SERIES',
|
||||||
});
|
});
|
||||||
|
} else if (dataSource === DataSource.METRICS) {
|
||||||
|
logEvent('Metrics Explorer: Data present', {
|
||||||
|
panelType: 'TIME_SERIES',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isLoading, isError, chartData, dataSource]);
|
}, [isLoading, isError, chartData, dataSource]);
|
||||||
@ -164,8 +169,9 @@ function TimeSeriesView({
|
|||||||
ref={graphRef}
|
ref={graphRef}
|
||||||
data-testid="time-series-graph"
|
data-testid="time-series-graph"
|
||||||
>
|
>
|
||||||
{isLoading &&
|
{isLoading && dataSource === DataSource.LOGS && <LogsLoading />}
|
||||||
(dataSource === DataSource.LOGS ? <LogsLoading /> : <TracesLoading />)}
|
{isLoading && dataSource === DataSource.TRACES && <TracesLoading />}
|
||||||
|
{isLoading && dataSource === DataSource.METRICS && <MetricsLoading />}
|
||||||
|
|
||||||
{chartData &&
|
{chartData &&
|
||||||
chartData[0] &&
|
chartData[0] &&
|
||||||
|
@ -5,12 +5,12 @@ import { TabRoutes } from 'components/RouteTab/types';
|
|||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import { Explorer, Summary, Views } from './constants';
|
import { Explorer, Summary } from './constants';
|
||||||
|
|
||||||
function MetricsExplorerPage(): JSX.Element {
|
function MetricsExplorerPage(): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
const routes: TabRoutes[] = [Summary, Explorer, Views];
|
const routes: TabRoutes[] = [Summary, Explorer];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-explorer-page">
|
<div className="metrics-explorer-page">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user