mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 16:49:01 +08:00
chore: improve error handling and loading states in summary view of metrics explorer (#7862)
This commit is contained in:
parent
727a039eb9
commit
c03541cd6c
@ -50,27 +50,39 @@ function MetricsTable({
|
||||
|
||||
return (
|
||||
<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>
|
||||
{!isError && !isLoading && (
|
||||
<div className="metrics-table-title" data-testid="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
|
||||
loading={{
|
||||
spinning: isLoading,
|
||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||
indicator: (
|
||||
<Spin
|
||||
data-testid="metrics-table-loading-state"
|
||||
indicator={<LoadingOutlined size={14} spin />}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
dataSource={data}
|
||||
columns={metricsTableColumns}
|
||||
locale={{
|
||||
emptyText: isLoading ? null : (
|
||||
<div className="no-metrics-message-container">
|
||||
<div
|
||||
className="no-metrics-message-container"
|
||||
data-testid={
|
||||
isError ? 'metrics-table-error-state' : 'metrics-table-empty-state'
|
||||
}
|
||||
>
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
|
@ -54,7 +54,9 @@ function MetricsTreemap({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Skeleton style={{ width: treemapWidth, height: TREEMAP_HEIGHT }} active />
|
||||
<div data-testid="metrics-treemap-loading-state">
|
||||
<Skeleton style={{ width: treemapWidth, height: TREEMAP_HEIGHT }} active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -66,6 +68,7 @@ function MetricsTreemap({
|
||||
return (
|
||||
<Empty
|
||||
description="No metrics found"
|
||||
data-testid="metrics-treemap-empty-state"
|
||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||
/>
|
||||
);
|
||||
@ -75,13 +78,17 @@ function MetricsTreemap({
|
||||
return (
|
||||
<Empty
|
||||
description="Error fetching metrics. If the problem persists, please contact support."
|
||||
data-testid="metrics-treemap-error-state"
|
||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="metrics-treemap-container">
|
||||
<div
|
||||
className="metrics-treemap-container"
|
||||
data-testid="metrics-treemap-container"
|
||||
>
|
||||
<div className="metrics-treemap-title">
|
||||
<Typography.Title level={4}>Proportion View</Typography.Title>
|
||||
<Tooltip
|
||||
|
@ -123,6 +123,11 @@ function Summary(): JSX.Element {
|
||||
enabled: !!metricsListQuery && !isInspectModalOpen,
|
||||
});
|
||||
|
||||
const isListViewError = useMemo(
|
||||
() => isMetricsError || (metricsData && metricsData.statusCode !== 200),
|
||||
[isMetricsError, metricsData],
|
||||
);
|
||||
|
||||
const {
|
||||
data: treeMapData,
|
||||
isLoading: isTreeMapLoading,
|
||||
@ -132,6 +137,11 @@ function Summary(): JSX.Element {
|
||||
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
|
||||
});
|
||||
|
||||
const isProportionViewError = useMemo(
|
||||
() => isTreeMapError || treeMapData?.statusCode !== 200,
|
||||
[isTreeMapError, treeMapData],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(value: TagFilter) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
@ -208,13 +218,13 @@ function Summary(): JSX.Element {
|
||||
<MetricsTreemap
|
||||
data={treeMapData?.payload}
|
||||
isLoading={isTreeMapLoading || isTreeMapFetching}
|
||||
isError={isTreeMapError}
|
||||
isError={isProportionViewError}
|
||||
viewType={heatmapView}
|
||||
openMetricDetails={openMetricDetails}
|
||||
/>
|
||||
<MetricsTable
|
||||
isLoading={isMetricsLoading || isMetricsFetching}
|
||||
isError={isMetricsError}
|
||||
isError={isListViewError}
|
||||
data={formattedMetricsData}
|
||||
pageSize={pageSize}
|
||||
currentPage={currentPage}
|
||||
|
@ -0,0 +1,200 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as useGetMetricsListFilterValues from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
|
||||
import MetricsTable from '../MetricsTable';
|
||||
import { MetricsListItemRowData } from '../types';
|
||||
|
||||
const mockData: MetricsListItemRowData[] = [
|
||||
{
|
||||
key: 'metric1',
|
||||
metric_name: 'Metric 1',
|
||||
description: 'Test metric 1',
|
||||
metric_type: 'gauge',
|
||||
unit: 'bytes',
|
||||
samples: 100,
|
||||
timeseries: 10,
|
||||
},
|
||||
{
|
||||
key: 'metric2',
|
||||
metric_name: 'Metric 2',
|
||||
description: 'Test metric 2',
|
||||
metric_type: 'counter',
|
||||
unit: 'count',
|
||||
samples: 200,
|
||||
timeseries: 20,
|
||||
},
|
||||
];
|
||||
|
||||
describe('MetricsTable', () => {
|
||||
jest
|
||||
.spyOn(useGetMetricsListFilterValues, 'useGetMetricsListFilterValues')
|
||||
.mockReturnValue({
|
||||
data: {
|
||||
statusCode: 200,
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
filterValues: ['metric1', 'metric2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any);
|
||||
|
||||
it('renders table with data correctly', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
data={mockData}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
onPaginationChange={jest.fn()}
|
||||
setOrderBy={jest.fn()}
|
||||
totalCount={2}
|
||||
openMetricDetails={jest.fn()}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('List View')).toBeInTheDocument();
|
||||
expect(screen.getByText('Metric 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Metric 2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isError={false}
|
||||
data={mockData}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
onPaginationChange={jest.fn()}
|
||||
setOrderBy={jest.fn()}
|
||||
totalCount={2}
|
||||
openMetricDetails={jest.fn()}
|
||||
isLoading
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metrics-table-loading-state')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error state', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isLoading={false}
|
||||
isError
|
||||
data={[]}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
onPaginationChange={jest.fn()}
|
||||
setOrderBy={jest.fn()}
|
||||
totalCount={2}
|
||||
openMetricDetails={jest.fn()}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metrics-table-error-state')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Error fetching metrics. If the problem persists, please contact support.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no data', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
data={[]}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
onPaginationChange={jest.fn()}
|
||||
setOrderBy={jest.fn()}
|
||||
totalCount={2}
|
||||
openMetricDetails={jest.fn()}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metrics-table-empty-state')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'This query had no results. Edit your query and try again!',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls openMetricDetails when row is clicked', () => {
|
||||
const mockOpenMetricDetails = jest.fn();
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
data={mockData}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
onPaginationChange={jest.fn()}
|
||||
setOrderBy={jest.fn()}
|
||||
totalCount={2}
|
||||
openMetricDetails={mockOpenMetricDetails}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Metric 1'));
|
||||
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1');
|
||||
});
|
||||
|
||||
it('calls setOrderBy when column header is clicked', () => {
|
||||
const mockSetOrderBy = jest.fn();
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTable
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
data={mockData}
|
||||
pageSize={10}
|
||||
currentPage={1}
|
||||
onPaginationChange={jest.fn()}
|
||||
setOrderBy={mockSetOrderBy}
|
||||
totalCount={2}
|
||||
openMetricDetails={jest.fn()}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const samplesHeader = screen.getByText('SAMPLES');
|
||||
fireEvent.click(samplesHeader);
|
||||
|
||||
expect(mockSetOrderBy).toHaveBeenCalledWith({
|
||||
columnName: 'samples',
|
||||
order: 'asc',
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,139 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
|
||||
import MetricsTreemap from '../MetricsTreemap';
|
||||
import { TreemapViewType } from '../types';
|
||||
|
||||
jest.mock('d3-hierarchy', () => ({
|
||||
stratify: jest.fn().mockReturnValue({
|
||||
id: jest.fn().mockReturnValue({
|
||||
parentId: jest.fn().mockReturnValue(
|
||||
jest.fn().mockReturnValue({
|
||||
sum: jest.fn().mockReturnValue({
|
||||
descendants: jest.fn().mockReturnValue([]),
|
||||
eachBefore: jest.fn().mockReturnValue([]),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
treemapBinary: jest.fn(),
|
||||
}));
|
||||
jest.mock('react-use', () => ({
|
||||
useWindowSize: jest.fn().mockReturnValue({ width: 1000, height: 1000 }),
|
||||
}));
|
||||
|
||||
const mockData = [
|
||||
{
|
||||
metric_name: 'Metric 1',
|
||||
percentage: 0.5,
|
||||
total_value: 15,
|
||||
},
|
||||
{
|
||||
metric_name: 'Metric 2',
|
||||
percentage: 0.6,
|
||||
total_value: 10,
|
||||
},
|
||||
];
|
||||
|
||||
describe('MetricsTreemap', () => {
|
||||
it('renders treemap with data correctly', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTreemap
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
data={{
|
||||
status: 'success',
|
||||
data: {
|
||||
timeseries: [mockData[0]],
|
||||
samples: [mockData[1]],
|
||||
},
|
||||
}}
|
||||
openMetricDetails={jest.fn()}
|
||||
viewType={TreemapViewType.SAMPLES}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Proportion View')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows loading state', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTreemap
|
||||
isLoading
|
||||
isError={false}
|
||||
data={{
|
||||
status: 'success',
|
||||
data: {
|
||||
timeseries: [mockData[0]],
|
||||
samples: [mockData[1]],
|
||||
},
|
||||
}}
|
||||
openMetricDetails={jest.fn()}
|
||||
viewType={TreemapViewType.SAMPLES}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('metrics-treemap-loading-state'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows error state', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTreemap
|
||||
isLoading={false}
|
||||
isError
|
||||
data={{
|
||||
status: 'success',
|
||||
data: {
|
||||
timeseries: [mockData[0]],
|
||||
samples: [mockData[1]],
|
||||
},
|
||||
}}
|
||||
openMetricDetails={jest.fn()}
|
||||
viewType={TreemapViewType.SAMPLES}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metrics-treemap-error-state')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Error fetching metrics. If the problem persists, please contact support.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty state when no data', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MetricsTreemap
|
||||
isLoading={false}
|
||||
isError={false}
|
||||
data={null}
|
||||
openMetricDetails={jest.fn()}
|
||||
viewType={TreemapViewType.SAMPLES}
|
||||
/>
|
||||
</Provider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metrics-treemap-empty-state')).toBeInTheDocument();
|
||||
expect(screen.getByText('No metrics found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user