chore: improve error handling and loading states in summary view of metrics explorer (#7862)

This commit is contained in:
Amlan Kumar Nandy 2025-05-09 11:41:41 +07:00 committed by GitHub
parent 727a039eb9
commit c03541cd6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 385 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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

View File

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