mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 06:39:03 +08:00
Introduce new Resource Attribute FIlter in exceptions tab (#7589)
* chore: resource attr filter init * chore: resource attr filter api integration * chore: operator config updated * chore: fliter show hide logic and styles * chore: add support for custom operator list to qb * chore: minor refactor * chore: minor code refactor * test: quick filters test suite added * test: quick filters test suite added * test: all errors test suite added * chore: style fix * test: all errors mock fix * chore: test case fix and mixpanel update * chore: color update * chore: minor refactor * chore: style fix * chore: set default query in exceptions tab * chore: style fix * chore: minor refactor * chore: minor refactor * chore: minor refactor * chore: test update * chore: fix filter header with no query name --------- Co-authored-by: Aditya Singh <adityasingh@Adityas-MacBook-Pro.local>
This commit is contained in:
parent
87922e9577
commit
f11b9644cf
@ -6,6 +6,7 @@ import {
|
||||
VerticalAlignTopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { cloneDeep, isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@ -68,10 +69,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<section className="header">
|
||||
<section className="left-actions">
|
||||
<FilterOutlined />
|
||||
<Typography.Text className="text">Filters for</Typography.Text>
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
<Typography.Text className="text">
|
||||
{lastQueryName ? 'Filters for' : 'Filters'}
|
||||
</Typography.Text>
|
||||
{lastQueryName && (
|
||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="right-actions">
|
||||
@ -89,31 +94,33 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
switch (filter.type) {
|
||||
case FiltersType.CHECKBOX:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider filter={filter} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</section>
|
||||
<TypicalOverlayScrollbar>
|
||||
<section className="filters">
|
||||
{config.map((filter) => {
|
||||
switch (filter.type) {
|
||||
case FiltersType.CHECKBOX:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
case FiltersType.SLIDER:
|
||||
return <Slider filter={filter} />;
|
||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||
default:
|
||||
return (
|
||||
<Checkbox
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</section>
|
||||
</TypicalOverlayScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
111
frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx
Normal file
111
frontend/src/components/QuickFilters/tests/QuickFilters.test.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersConfig } from './constants';
|
||||
|
||||
// Mock the useQueryBuilder hook
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
// Mock the useGetAggregateValues hook
|
||||
jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
|
||||
useGetAggregateValues: jest.fn(),
|
||||
}));
|
||||
|
||||
const handleFilterVisibilityChange = jest.fn();
|
||||
const redirectWithQueryBuilderData = jest.fn();
|
||||
|
||||
function TestQuickFilters(): JSX.Element {
|
||||
return (
|
||||
<MockQueryClientProvider>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={QuickFiltersConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</MockQueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Quick Filters', () => {
|
||||
beforeEach(() => {
|
||||
// Provide a mock implementation for useQueryBuilder
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'Test Query',
|
||||
filters: { items: [{ key: 'test', value: 'value' }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
lastUsedQuery: 0,
|
||||
redirectWithQueryBuilderData,
|
||||
});
|
||||
|
||||
// Provide a mock implementation for useGetAggregateValues
|
||||
(useGetAggregateValues as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: 'success',
|
||||
payload: {
|
||||
stringAttributeValues: [
|
||||
'mq-kafka',
|
||||
'otel-demo',
|
||||
'otlp-python',
|
||||
'sample-flask',
|
||||
],
|
||||
numberAttributeValues: null,
|
||||
boolAttributeValues: null,
|
||||
},
|
||||
}, // Mocked API response
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders correctly with default props', () => {
|
||||
const { container } = render(<TestQuickFilters />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays the correct query name in the header', () => {
|
||||
render(<TestQuickFilters />);
|
||||
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Query')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should add filter data to query when checkbox is clicked', () => {
|
||||
render(<TestQuickFilters />);
|
||||
const checkbox = screen.getByText('mq-kafka');
|
||||
fireEvent.click(checkbox);
|
||||
expect(redirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
builder: {
|
||||
queryData: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
filters: expect.objectContaining({
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({
|
||||
key: 'deployment.environment',
|
||||
}),
|
||||
value: 'mq-kafka',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
}),
|
||||
); // sets composite query param
|
||||
});
|
||||
});
|
@ -0,0 +1,382 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Quick Filters renders correctly with default props 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="quick-filters"
|
||||
>
|
||||
<section
|
||||
class="header"
|
||||
>
|
||||
<section
|
||||
class="left-actions"
|
||||
>
|
||||
<span
|
||||
aria-label="filter"
|
||||
class="anticon anticon-filter"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="filter"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M880.1 154H143.9c-24.5 0-39.8 26.7-27.5 48L349 597.4V838c0 17.7 14.2 32 31.8 32h262.4c17.6 0 31.8-14.3 31.8-32V597.4L907.7 202c12.2-21.3-3.1-48-27.6-48zM603.4 798H420.6V642h182.9v156zm9.6-236.6l-9.5 16.6h-183l-9.5-16.6L212.7 226h598.6L613 561.4z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="ant-typography text css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Filters for
|
||||
</span>
|
||||
<span
|
||||
class="ant-typography sync-tag css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Test Query
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
class="right-actions"
|
||||
>
|
||||
<span
|
||||
aria-label="sync"
|
||||
class="anticon anticon-sync sync-icon"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="sync"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M168 504.2c1-43.7 10-86.1 26.9-126 17.3-41 42.1-77.7 73.7-109.4S337 212.3 378 195c42.4-17.9 87.4-27 133.9-27s91.5 9.1 133.8 27A341.5 341.5 0 01755 268.8c9.9 9.9 19.2 20.4 27.8 31.4l-60.2 47a8 8 0 003 14.1l175.7 43c5 1.2 9.9-2.6 9.9-7.7l.8-180.9c0-6.7-7.7-10.5-12.9-6.3l-56.4 44.1C765.8 155.1 646.2 92 511.8 92 282.7 92 96.3 275.6 92 503.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8zm756 7.8h-60c-4.4 0-7.9 3.5-8 7.8-1 43.7-10 86.1-26.9 126-17.3 41-42.1 77.8-73.7 109.4A342.45 342.45 0 01512.1 856a342.24 342.24 0 01-243.2-100.8c-9.9-9.9-19.2-20.4-27.8-31.4l60.2-47a8 8 0 00-3-14.1l-175.7-43c-5-1.2-9.9 2.6-9.9 7.7l-.7 181c0 6.7 7.7 10.5 12.9 6.3l56.4-44.1C258.2 868.9 377.8 932 512.2 932c229.2 0 415.5-183.7 419.8-411.8a8 8 0 00-8-8.2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="divider-filter"
|
||||
/>
|
||||
<span
|
||||
aria-label="vertical-align-top"
|
||||
class="anticon anticon-vertical-align-top"
|
||||
role="img"
|
||||
tabindex="-1"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="vertical-align-top"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
style="transform: rotate(270deg);"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M859.9 168H164.1c-4.5 0-8.1 3.6-8.1 8v60c0 4.4 3.6 8 8.1 8h695.8c4.5 0 8.1-3.6 8.1-8v-60c0-4.4-3.6-8-8.1-8zM518.3 355a8 8 0 00-12.6 0l-112 141.7a7.98 7.98 0 006.3 12.9h73.9V848c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V509.7H624c6.7 0 10.4-7.7 6.3-12.9L518.3 355z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</section>
|
||||
</section>
|
||||
<div
|
||||
class="overlay-scrollbar"
|
||||
data-overlayscrollbars-initialize="true"
|
||||
>
|
||||
<div
|
||||
data-overlayscrollbars-contents=""
|
||||
>
|
||||
<section
|
||||
class="filters"
|
||||
>
|
||||
<div
|
||||
class="checkbox-filter"
|
||||
>
|
||||
<section
|
||||
class="filter-header-checkbox"
|
||||
>
|
||||
<section
|
||||
class="left-action"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-down"
|
||||
cursor="pointer"
|
||||
fill="none"
|
||||
height="13"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m6 9 6 6 6-6"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-typography title css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Environment
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
class="right-action"
|
||||
>
|
||||
<span
|
||||
class="ant-typography clear-all css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Clear All
|
||||
</span>
|
||||
</section>
|
||||
</section>
|
||||
<section
|
||||
class="search"
|
||||
>
|
||||
<input
|
||||
class="ant-input css-dev-only-do-not-override-2i2tap"
|
||||
placeholder="Filter values"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
</section>
|
||||
<section
|
||||
class="values"
|
||||
>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
mq-kafka
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
otel-demo
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
otlp-python
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
<label
|
||||
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked check-box css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
<span
|
||||
class="ant-checkbox ant-wave-target css-dev-only-do-not-override-2i2tap ant-checkbox-checked"
|
||||
>
|
||||
<input
|
||||
checked=""
|
||||
class="ant-checkbox-input"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span
|
||||
class="ant-checkbox-inner"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
class="checkbox-value-section"
|
||||
>
|
||||
<span
|
||||
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line value-string css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
sample-flask
|
||||
</span>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text only-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Only
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="ant-btn css-dev-only-do-not-override-2i2tap ant-btn-text toggle-btn"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
Toggle
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div
|
||||
class="checkbox-filter"
|
||||
>
|
||||
<section
|
||||
class="filter-header-checkbox"
|
||||
>
|
||||
<section
|
||||
class="left-action"
|
||||
>
|
||||
<svg
|
||||
class="lucide lucide-chevron-right"
|
||||
cursor="pointer"
|
||||
fill="none"
|
||||
height="13"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="13"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m9 18 6-6-6-6"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="ant-typography title css-dev-only-do-not-override-2i2tap"
|
||||
>
|
||||
Service Name
|
||||
</span>
|
||||
</section>
|
||||
<section
|
||||
class="right-action"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
30
frontend/src/components/QuickFilters/tests/constants.ts
Normal file
30
frontend/src/components/QuickFilters/tests/constants.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import { FiltersType } from '../types';
|
||||
|
||||
export const QuickFiltersConfig = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Service Name',
|
||||
attributeKey: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
@ -40,4 +40,5 @@ export enum QuickFiltersSource {
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
}
|
||||
|
@ -27,4 +27,5 @@ export enum LOCALSTORAGE {
|
||||
CELERY_OVERVIEW_COLUMNS = 'CELERY_OVERVIEW_COLUMNS',
|
||||
DONT_SHOW_SLOW_API_WARNING = 'DONT_SHOW_SLOW_API_WARNING',
|
||||
METRICS_LIST_OPTIONS = 'METRICS_LIST_OPTIONS',
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
}
|
||||
|
@ -398,6 +398,23 @@ export const QUERY_BUILDER_OPERATORS_BY_TYPES = {
|
||||
],
|
||||
};
|
||||
|
||||
export enum OperatorConfigKeys {
|
||||
'EXCEPTIONS' = 'EXCEPTIONS',
|
||||
}
|
||||
|
||||
export const OPERATORS_CONFIG = {
|
||||
[OperatorConfigKeys.EXCEPTIONS]: [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
OPERATORS.IN,
|
||||
OPERATORS.NIN,
|
||||
OPERATORS.EXISTS,
|
||||
OPERATORS.NOT_EXISTS,
|
||||
OPERATORS.CONTAINS,
|
||||
OPERATORS.NOT_CONTAINS,
|
||||
],
|
||||
};
|
||||
|
||||
export const HAVING_OPERATORS: string[] = [
|
||||
OPERATORS['='],
|
||||
OPERATORS['!='],
|
||||
|
@ -16,3 +16,51 @@ export const OperatorConversions: Array<{
|
||||
traceValue: 'NotIn',
|
||||
},
|
||||
];
|
||||
|
||||
// mapping from qb to exceptions
|
||||
export const CompositeQueryOperatorsConfig: Array<{
|
||||
label: string;
|
||||
metricValue: string;
|
||||
traceValue: OperatorValues;
|
||||
}> = [
|
||||
{
|
||||
label: 'in',
|
||||
metricValue: '=~',
|
||||
traceValue: 'In',
|
||||
},
|
||||
{
|
||||
label: 'nin',
|
||||
metricValue: '!~',
|
||||
traceValue: 'NotIn',
|
||||
},
|
||||
{
|
||||
label: '=',
|
||||
metricValue: '=',
|
||||
traceValue: 'Equals',
|
||||
},
|
||||
{
|
||||
label: '!=',
|
||||
metricValue: '!=',
|
||||
traceValue: 'NotEquals',
|
||||
},
|
||||
{
|
||||
label: 'exists',
|
||||
metricValue: '=~',
|
||||
traceValue: 'Exists',
|
||||
},
|
||||
{
|
||||
label: 'nexists',
|
||||
metricValue: '!~',
|
||||
traceValue: 'NotExists',
|
||||
},
|
||||
{
|
||||
label: 'contains',
|
||||
metricValue: '=~',
|
||||
traceValue: 'Contains',
|
||||
},
|
||||
{
|
||||
label: 'ncontains',
|
||||
metricValue: '!~',
|
||||
traceValue: 'NotContains',
|
||||
},
|
||||
];
|
||||
|
@ -18,16 +18,17 @@ import getErrorCounts from 'api/errors/getErrorCounts';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { convertCompositeQueryToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import createQueryParams from 'lib/createQueryParams';
|
||||
import history from 'lib/history';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@ -109,10 +110,11 @@ function AllErrors(): JSX.Element {
|
||||
);
|
||||
|
||||
const { queries } = useResourceAttribute();
|
||||
const compositeData = useGetCompositeQueryParam();
|
||||
|
||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||
{
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
|
||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
|
||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||
getAll({
|
||||
end: maxTime,
|
||||
@ -123,7 +125,9 @@ function AllErrors(): JSX.Element {
|
||||
orderParam: getUpdatedParams,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@ -134,7 +138,7 @@ function AllErrors(): JSX.Element {
|
||||
minTime,
|
||||
getUpdatedExceptionType,
|
||||
getUpdatedServiceName,
|
||||
queries,
|
||||
compositeData,
|
||||
],
|
||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||
getErrorCounts({
|
||||
@ -142,7 +146,9 @@ function AllErrors(): JSX.Element {
|
||||
start: minTime,
|
||||
exceptionType: getUpdatedExceptionType,
|
||||
serviceName: getUpdatedServiceName,
|
||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
}),
|
||||
enabled: !loading,
|
||||
},
|
||||
@ -429,12 +435,8 @@ function AllErrors(): JSX.Element {
|
||||
[pathname],
|
||||
);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (
|
||||
!logEventCalledRef.current &&
|
||||
!isUndefined(errorCountResponse.data?.payload)
|
||||
) {
|
||||
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||
const selectedEnvironments = queries.find(
|
||||
(val) => val.tagKey === 'resource_deployment_environment',
|
||||
)?.tagValue;
|
||||
@ -442,9 +444,12 @@ function AllErrors(): JSX.Element {
|
||||
logEvent('Exception: List page visited', {
|
||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||
selectedEnvironments,
|
||||
resourceAttributeUsed: !!queries?.length,
|
||||
resourceAttributeUsed: !!compositeData?.builder.queryData?.[0]?.filters
|
||||
.items?.length,
|
||||
tags: convertCompositeQueryToTraceSelectedTags(
|
||||
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||
),
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [errorCountResponse.data?.payload]);
|
||||
|
114
frontend/src/container/AllError/tests/AllError.test.tsx
Normal file
114
frontend/src/container/AllError/tests/AllError.test.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
import { Provider, useSelector } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
|
||||
import AllErrors from '../index';
|
||||
import {
|
||||
INIT_URL_WITH_COMMON_QUERY,
|
||||
MOCK_ERROR_LIST,
|
||||
TAG_FROM_QUERY,
|
||||
} from './constants';
|
||||
|
||||
jest.mock('hooks/useResourceAttribute', () =>
|
||||
jest.fn(() => ({
|
||||
queries: [],
|
||||
})),
|
||||
);
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
function Exceptions({ initUrl }: { initUrl?: string[] }): JSX.Element {
|
||||
return (
|
||||
<MemoryRouter initialEntries={initUrl ?? ['/exceptions']}>
|
||||
<TimezoneProvider>
|
||||
<Provider store={store}>
|
||||
<MockQueryClientProvider>
|
||||
<AllErrors />
|
||||
</MockQueryClientProvider>
|
||||
</Provider>
|
||||
</TimezoneProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
Exceptions.defaultProps = {
|
||||
initUrl: ['/exceptions'],
|
||||
};
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const listErrorsURL = `${BASE_URL}/api/v1/listErrors`;
|
||||
const countErrorsURL = `${BASE_URL}/api/v1/countErrors`;
|
||||
|
||||
const postListErrorsSpy = jest.fn();
|
||||
|
||||
describe('Exceptions - All Errors', () => {
|
||||
beforeEach(() => {
|
||||
(useSelector as jest.Mock).mockReturnValue({
|
||||
maxTime: 1000,
|
||||
minTime: 0,
|
||||
loading: false,
|
||||
});
|
||||
server.use(
|
||||
rest.post(listErrorsURL, async (req, res, ctx) => {
|
||||
const body = await req.json();
|
||||
postListErrorsSpy(body);
|
||||
return res(ctx.status(200), ctx.json(MOCK_ERROR_LIST));
|
||||
}),
|
||||
);
|
||||
server.use(
|
||||
rest.post(countErrorsURL, (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(540)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders correctly with default props', async () => {
|
||||
render(<Exceptions />);
|
||||
const item = await screen.findByText(/redis timeout/i);
|
||||
expect(item).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should sort Error Message appropriately', async () => {
|
||||
render(<Exceptions />);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
|
||||
const caretIconUp = screen.getAllByLabelText('caret-up')[0];
|
||||
const caretIconDown = screen.getAllByLabelText('caret-down')[0];
|
||||
|
||||
// sort by ascending
|
||||
expect(caretIconUp.className).not.toContain('active');
|
||||
fireEvent.click(caretIconUp);
|
||||
expect(caretIconUp.className).toContain('active');
|
||||
let queryParams = new URLSearchParams(window.location.search);
|
||||
expect(queryParams.get('order')).toBe('ascending');
|
||||
expect(queryParams.get('orderParam')).toBe('exceptionType');
|
||||
|
||||
// sort by descending
|
||||
expect(caretIconDown.className).not.toContain('active');
|
||||
fireEvent.click(caretIconDown);
|
||||
expect(caretIconDown.className).toContain('active');
|
||||
queryParams = new URLSearchParams(window.location.search);
|
||||
expect(queryParams.get('order')).toBe('descending');
|
||||
});
|
||||
|
||||
it('should call useQueries with exact composite query object', async () => {
|
||||
render(<Exceptions initUrl={[INIT_URL_WITH_COMMON_QUERY]} />);
|
||||
await screen.findByText(/redis timeout/i);
|
||||
expect(postListErrorsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
tags: TAG_FROM_QUERY,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
94
frontend/src/container/AllError/tests/constants.ts
Normal file
94
frontend/src/container/AllError/tests/constants.ts
Normal file
@ -0,0 +1,94 @@
|
||||
export const MOCK_USE_QUERIES_DATA = [
|
||||
{
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
payload: [
|
||||
{
|
||||
exceptionType: '*errors.errorString',
|
||||
exceptionMessage: 'redis timeout',
|
||||
exceptionCount: 2510,
|
||||
lastSeen: '2025-04-14T18:27:57.797616374Z',
|
||||
firstSeen: '2025-04-14T17:58:00.262775497Z',
|
||||
serviceName: 'redis-manual',
|
||||
groupID: '511b9c91a92b9c5166ecb77235f5743b',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
status: 'success',
|
||||
isLoading: false,
|
||||
isSuccess: true,
|
||||
isError: false,
|
||||
isIdle: false,
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
payload: 525,
|
||||
},
|
||||
dataUpdatedAt: 1744661020341,
|
||||
error: null,
|
||||
errorUpdatedAt: 0,
|
||||
failureCount: 0,
|
||||
errorUpdateCount: 0,
|
||||
isFetched: true,
|
||||
isFetchedAfterMount: true,
|
||||
isFetching: false,
|
||||
isRefetching: false,
|
||||
isLoadingError: false,
|
||||
isPlaceholderData: false,
|
||||
isPreviousData: false,
|
||||
isRefetchError: false,
|
||||
isStale: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const INIT_URL_WITH_COMMON_QUERY =
|
||||
'/exceptions?compositeQuery=%257B%2522queryType%2522%253A%2522builder%2522%252C%2522builder%2522%253A%257B%2522queryData%2522%253A%255B%257B%2522dataSource%2522%253A%2522traces%2522%252C%2522queryName%2522%253A%2522A%2522%252C%2522aggregateOperator%2522%253A%2522noop%2522%252C%2522aggregateAttribute%2522%253A%257B%2522id%2522%253A%2522----resource--false%2522%252C%2522dataType%2522%253A%2522%2522%252C%2522key%2522%253A%2522%2522%252C%2522isColumn%2522%253Afalse%252C%2522type%2522%253A%2522resource%2522%252C%2522isJSON%2522%253Afalse%257D%252C%2522timeAggregation%2522%253A%2522rate%2522%252C%2522spaceAggregation%2522%253A%2522sum%2522%252C%2522functions%2522%253A%255B%255D%252C%2522filters%2522%253A%257B%2522items%2522%253A%255B%257B%2522id%2522%253A%2522db118ac7-9313-4adb-963f-f31b5b32c496%2522%252C%2522op%2522%253A%2522in%2522%252C%2522key%2522%253A%257B%2522key%2522%253A%2522deployment.environment%2522%252C%2522dataType%2522%253A%2522string%2522%252C%2522type%2522%253A%2522resource%2522%252C%2522isColumn%2522%253Afalse%252C%2522isJSON%2522%253Afalse%257D%252C%2522value%2522%253A%2522mq-kafka%2522%257D%255D%252C%2522op%2522%253A%2522AND%2522%257D%252C%2522expression%2522%253A%2522A%2522%252C%2522disabled%2522%253Afalse%252C%2522stepInterval%2522%253A60%252C%2522having%2522%253A%255B%255D%252C%2522limit%2522%253Anull%252C%2522orderBy%2522%253A%255B%255D%252C%2522groupBy%2522%253A%255B%255D%252C%2522legend%2522%253A%2522%2522%252C%2522reduceTo%2522%253A%2522avg%2522%257D%255D%252C%2522queryFormulas%2522%253A%255B%255D%257D%252C%2522promql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522query%2522%253A%2522%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%257D%255D%252C%2522clickhouse_sql%2522%253A%255B%257B%2522name%2522%253A%2522A%2522%252C%2522legend%2522%253A%2522%2522%252C%2522disabled%2522%253Afalse%252C%2522query%2522%253A%2522%2522%257D%255D%252C%2522id%2522%253A%2522dd576d04-0822-476d-b0c2-807a7af2e5e7%2522%257D';
|
||||
|
||||
export const extractCompositeQueryObject = (
|
||||
url: string,
|
||||
): Record<string, unknown> | null => {
|
||||
try {
|
||||
const urlObj = new URL(`http://dummy-base${url}`); // Add dummy base to parse relative URL
|
||||
const encodedParam = urlObj.searchParams.get('compositeQuery');
|
||||
|
||||
if (!encodedParam) return null;
|
||||
|
||||
// Decode twice
|
||||
const firstDecode = decodeURIComponent(encodedParam);
|
||||
const secondDecode = decodeURIComponent(firstDecode);
|
||||
|
||||
// Parse JSON
|
||||
return JSON.parse(secondDecode);
|
||||
} catch (err) {
|
||||
console.error('Failed to extract compositeQuery:', err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const TAG_FROM_QUERY = [
|
||||
{
|
||||
BoolValues: [],
|
||||
Key: 'deployment.environment',
|
||||
NumberValues: [],
|
||||
Operator: 'In',
|
||||
StringValues: ['mq-kafka'],
|
||||
TagType: 'ResourceAttribute',
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_ERROR_LIST = [
|
||||
{
|
||||
exceptionType: '*errors.errorString',
|
||||
exceptionMessage: 'redis timeout',
|
||||
exceptionCount: 2510,
|
||||
lastSeen: '2025-04-14T18:27:57.797616374Z',
|
||||
firstSeen: '2025-04-14T17:58:00.262775497Z',
|
||||
serviceName: 'redis-manual',
|
||||
groupID: '511b9c91a92b9c5166ecb77235f5743b',
|
||||
},
|
||||
];
|
@ -339,6 +339,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||
|
||||
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
|
||||
|
||||
const isTracesView = (): boolean =>
|
||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||
|
||||
@ -661,7 +663,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isMessagingQueues() ||
|
||||
isCloudIntegrationPage() ||
|
||||
isInfraMonitoring() ||
|
||||
isApiMonitoringView()
|
||||
isApiMonitoringView() ||
|
||||
isExceptionsView()
|
||||
? 0
|
||||
: '0 1rem',
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
||||
OperatorConfigKeys,
|
||||
OPERATORS,
|
||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||
QUERY_BUILDER_SEARCH_VALUES,
|
||||
@ -62,6 +63,7 @@ import {
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from '../QueryBuilderSearch/utils';
|
||||
import { filterByOperatorConfig } from '../utils';
|
||||
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
||||
import Suggestions from './Suggestions';
|
||||
|
||||
@ -88,6 +90,7 @@ interface QueryBuilderSearchV2Props {
|
||||
className?: string;
|
||||
suffixIcon?: React.ReactNode;
|
||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||
operatorConfigKey?: OperatorConfigKeys;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@ -121,6 +124,7 @@ function QueryBuilderSearchV2(
|
||||
suffixIcon,
|
||||
whereClauseConfig,
|
||||
hardcodedAttributeKeys,
|
||||
operatorConfigKey,
|
||||
} = props;
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@ -717,15 +721,11 @@ function QueryBuilderSearchV2(
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||
label: operator,
|
||||
value: operator,
|
||||
}));
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
} else {
|
||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||
(operator) => ({
|
||||
@ -739,9 +739,12 @@ function QueryBuilderSearchV2(
|
||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||
);
|
||||
}
|
||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
||||
setDropdownOptions(operatorOptions);
|
||||
}
|
||||
const filterOperatorOptions = filterByOperatorConfig(
|
||||
operatorOptions,
|
||||
operatorConfigKey,
|
||||
);
|
||||
setDropdownOptions([{ label: '', value: '' }, ...filterOperatorOptions]);
|
||||
}
|
||||
|
||||
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||
@ -774,6 +777,7 @@ function QueryBuilderSearchV2(
|
||||
isLogsDataSource,
|
||||
searchValue,
|
||||
suggestionsData?.payload?.attributes,
|
||||
operatorConfigKey,
|
||||
]);
|
||||
|
||||
// keep the query in sync with the selected tags in logs explorer page
|
||||
@ -1000,6 +1004,7 @@ QueryBuilderSearchV2.defaultProps = {
|
||||
suffixIcon: null,
|
||||
whereClauseConfig: {},
|
||||
hardcodedAttributeKeys: undefined,
|
||||
operatorConfigKey: undefined,
|
||||
};
|
||||
|
||||
export default QueryBuilderSearchV2;
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
||||
import { OperatorConfigKeys, OPERATORS_CONFIG } from 'constants/queryBuilder';
|
||||
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
||||
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||
import uniqWith from 'lodash-es/unionWith';
|
||||
@ -110,3 +111,13 @@ export const transformKeyValuesToAttributeValuesMap = (
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
export const filterByOperatorConfig = (
|
||||
options: IOption[],
|
||||
key?: OperatorConfigKeys,
|
||||
): IOption[] => {
|
||||
if (!key || !OPERATORS_CONFIG[key]) return options;
|
||||
return options.filter((option) =>
|
||||
OPERATORS_CONFIG[key].includes(option.label),
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,17 @@
|
||||
.resourceAttributesFilter-container-v2 {
|
||||
margin: 8px;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-tag .ant-typography {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import './ResourceAttributesFilter.styles.scss';
|
||||
|
||||
import { initialQueriesMap, OperatorConfigKeys } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useCallback } from 'react';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
function ResourceAttributesFilter(): JSX.Element | null {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const query = currentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
const { handleChangeQueryData } = useQueryOperations({
|
||||
index: 0,
|
||||
query,
|
||||
entityVersion: '',
|
||||
});
|
||||
|
||||
// initialise tab with default query.
|
||||
useShareBuilderUrl({
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueriesMap.traces.builder.queryData[0],
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...initialQueriesMap.traces.builder.queryData[0].aggregateAttribute,
|
||||
type: 'resource',
|
||||
},
|
||||
queryName: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const handleChangeTagFilters = useCallback(
|
||||
(value: IBuilderQuery['filters']) => {
|
||||
handleChangeQueryData('filters', value);
|
||||
},
|
||||
[handleChangeQueryData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="resourceAttributesFilter-container-v2">
|
||||
<QueryBuilderSearchV2
|
||||
query={query}
|
||||
onChange={handleChangeTagFilters}
|
||||
operatorConfigKey={OperatorConfigKeys.EXCEPTIONS}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ResourceAttributesFilter;
|
@ -230,6 +230,7 @@ export const routesToSkip = [
|
||||
ROUTES.CHANNELS_NEW,
|
||||
ROUTES.CHANNELS_EDIT,
|
||||
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
||||
ROUTES.ALL_ERROR,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
@ -2,7 +2,10 @@ import {
|
||||
getResourceAttributesTagKeys,
|
||||
getResourceAttributesTagValues,
|
||||
} from 'api/metrics/getResourceAttributes';
|
||||
import { OperatorConversions } from 'constants/resourceAttributes';
|
||||
import {
|
||||
CompositeQueryOperatorsConfig,
|
||||
OperatorConversions,
|
||||
} from 'constants/resourceAttributes';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import {
|
||||
@ -49,6 +52,32 @@ export const convertOperatorLabelToTraceOperator = (
|
||||
OperatorConversions.find((operator) => operator.label === label)
|
||||
?.traceValue as OperatorValues;
|
||||
|
||||
export function convertOperatorLabelForExceptions(
|
||||
label: string,
|
||||
): OperatorValues {
|
||||
return CompositeQueryOperatorsConfig.find(
|
||||
(operator) => operator.label === label,
|
||||
)?.traceValue as OperatorValues;
|
||||
}
|
||||
|
||||
export function formatStringValuesForTrace(
|
||||
val: TagFilterItem['value'] = [],
|
||||
): string[] {
|
||||
return !Array.isArray(val) ? [String(val)] : val;
|
||||
}
|
||||
|
||||
export const convertCompositeQueryToTraceSelectedTags = (
|
||||
filterItems: TagFilterItem[] = [],
|
||||
): Tags[] =>
|
||||
filterItems.map((item) => ({
|
||||
Key: item?.key?.key,
|
||||
Operator: convertOperatorLabelForExceptions(item.op),
|
||||
StringValues: formatStringValuesForTrace(item?.value),
|
||||
NumberValues: [],
|
||||
BoolValues: [],
|
||||
TagType: 'ResourceAttribute',
|
||||
})) as Tags[];
|
||||
|
||||
export const convertRawQueriesToTraceSelectedTags = (
|
||||
queries: IResourceAttribute[],
|
||||
tagType = 'ResourceAttribute',
|
||||
|
27
frontend/src/pages/AllErrors/AllErrors.styles.scss
Normal file
27
frontend/src/pages/AllErrors/AllErrors.styles.scss
Normal file
@ -0,0 +1,27 @@
|
||||
.all-errors-page {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
.all-errors-quick-filter-section {
|
||||
width: 0%;
|
||||
flex-shrink: 0;
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.ant-tabs {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
&.filter-visible {
|
||||
.all-errors-quick-filter-section {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.all-errors-right-section {
|
||||
width: calc(100% - 260px);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,82 @@
|
||||
import './AllErrors.styles.scss';
|
||||
|
||||
import { FilterOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import cx from 'classnames';
|
||||
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
import ResourceAttributesFilterV2 from 'container/ResourceAttributeFilterV2/ResourceAttributesFilterV2';
|
||||
import Toolbar from 'container/Toolbar/Toolbar';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import history from 'lib/history';
|
||||
import { isNull } from 'lodash-es';
|
||||
import { useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { routes } from './config';
|
||||
import { ExceptionsQuickFiltersConfig } from './utils';
|
||||
|
||||
function AllErrors(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(() => {
|
||||
const localStorageValue = getLocalStorageKey(
|
||||
LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS,
|
||||
);
|
||||
if (!isNull(localStorageValue)) {
|
||||
return localStorageValue === 'true';
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const handleFilterVisibilityChange = (): void => {
|
||||
setLocalStorageApi(
|
||||
LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS,
|
||||
String(!showFilters),
|
||||
);
|
||||
setShowFilters((prev) => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResourceAttributesFilter />
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</>
|
||||
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
|
||||
{showFilters && (
|
||||
<section className={cx('all-errors-quick-filter-section')}>
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={ExceptionsQuickFiltersConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
<section
|
||||
className={cx(
|
||||
'all-errors-right-section',
|
||||
showFilters ? 'filter-visible' : '',
|
||||
)}
|
||||
>
|
||||
<Toolbar
|
||||
showAutoRefresh={false}
|
||||
leftActions={
|
||||
!showFilters ? (
|
||||
<Tooltip title="Show Filters">
|
||||
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||
<FilterOutlined />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : undefined
|
||||
}
|
||||
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
|
||||
/>
|
||||
<ResourceAttributesFilterV2 />
|
||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
92
frontend/src/pages/AllErrors/utils.tsx
Normal file
92
frontend/src/pages/AllErrors/utils.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const ExceptionsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
attributeKey: {
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Service Name',
|
||||
attributeKey: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Hostname',
|
||||
attributeKey: {
|
||||
key: 'host.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Cluster Name',
|
||||
attributeKey: {
|
||||
key: 'k8s.cluster.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Deployment Name',
|
||||
attributeKey: {
|
||||
key: 'k8s.deployment.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Namespace Name',
|
||||
attributeKey: {
|
||||
key: 'k8s.namespace.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Pod Name',
|
||||
attributeKey: {
|
||||
key: 'k8s.pod.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
Loading…
x
Reference in New Issue
Block a user