mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 06:26:03 +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 (
|
return (
|
||||||
<div className="metrics-table-container">
|
<div className="metrics-table-container">
|
||||||
<div className="metrics-table-title">
|
{!isError && !isLoading && (
|
||||||
<Typography.Title level={4} className="metrics-table-title">
|
<div className="metrics-table-title" data-testid="metrics-table-title">
|
||||||
List View
|
<Typography.Title level={4} className="metrics-table-title">
|
||||||
</Typography.Title>
|
List View
|
||||||
<Tooltip
|
</Typography.Title>
|
||||||
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."
|
<Tooltip
|
||||||
placement="right"
|
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>
|
<Info size={16} />
|
||||||
</div>
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table
|
<Table
|
||||||
loading={{
|
loading={{
|
||||||
spinning: isLoading,
|
spinning: isLoading,
|
||||||
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
indicator: (
|
||||||
|
<Spin
|
||||||
|
data-testid="metrics-table-loading-state"
|
||||||
|
indicator={<LoadingOutlined size={14} spin />}
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
columns={metricsTableColumns}
|
columns={metricsTableColumns}
|
||||||
locale={{
|
locale={{
|
||||||
emptyText: isLoading ? null : (
|
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
|
<img
|
||||||
src="/Icons/emptyState.svg"
|
src="/Icons/emptyState.svg"
|
||||||
alt="thinking-emoji"
|
alt="thinking-emoji"
|
||||||
|
@ -54,7 +54,9 @@ function MetricsTreemap({
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
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 (
|
return (
|
||||||
<Empty
|
<Empty
|
||||||
description="No metrics found"
|
description="No metrics found"
|
||||||
|
data-testid="metrics-treemap-empty-state"
|
||||||
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -75,13 +78,17 @@ function MetricsTreemap({
|
|||||||
return (
|
return (
|
||||||
<Empty
|
<Empty
|
||||||
description="Error fetching metrics. If the problem persists, please contact support."
|
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 }}
|
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="metrics-treemap-container">
|
<div
|
||||||
|
className="metrics-treemap-container"
|
||||||
|
data-testid="metrics-treemap-container"
|
||||||
|
>
|
||||||
<div className="metrics-treemap-title">
|
<div className="metrics-treemap-title">
|
||||||
<Typography.Title level={4}>Proportion View</Typography.Title>
|
<Typography.Title level={4}>Proportion View</Typography.Title>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -123,6 +123,11 @@ function Summary(): JSX.Element {
|
|||||||
enabled: !!metricsListQuery && !isInspectModalOpen,
|
enabled: !!metricsListQuery && !isInspectModalOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isListViewError = useMemo(
|
||||||
|
() => isMetricsError || (metricsData && metricsData.statusCode !== 200),
|
||||||
|
[isMetricsError, metricsData],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: treeMapData,
|
data: treeMapData,
|
||||||
isLoading: isTreeMapLoading,
|
isLoading: isTreeMapLoading,
|
||||||
@ -132,6 +137,11 @@ function Summary(): JSX.Element {
|
|||||||
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
|
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isProportionViewError = useMemo(
|
||||||
|
() => isTreeMapError || treeMapData?.statusCode !== 200,
|
||||||
|
[isTreeMapError, treeMapData],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
(value: TagFilter) => {
|
(value: TagFilter) => {
|
||||||
handleChangeQueryData('filters', value);
|
handleChangeQueryData('filters', value);
|
||||||
@ -208,13 +218,13 @@ function Summary(): JSX.Element {
|
|||||||
<MetricsTreemap
|
<MetricsTreemap
|
||||||
data={treeMapData?.payload}
|
data={treeMapData?.payload}
|
||||||
isLoading={isTreeMapLoading || isTreeMapFetching}
|
isLoading={isTreeMapLoading || isTreeMapFetching}
|
||||||
isError={isTreeMapError}
|
isError={isProportionViewError}
|
||||||
viewType={heatmapView}
|
viewType={heatmapView}
|
||||||
openMetricDetails={openMetricDetails}
|
openMetricDetails={openMetricDetails}
|
||||||
/>
|
/>
|
||||||
<MetricsTable
|
<MetricsTable
|
||||||
isLoading={isMetricsLoading || isMetricsFetching}
|
isLoading={isMetricsLoading || isMetricsFetching}
|
||||||
isError={isMetricsError}
|
isError={isListViewError}
|
||||||
data={formattedMetricsData}
|
data={formattedMetricsData}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
currentPage={currentPage}
|
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