mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:29:04 +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,
|
VerticalAlignTopOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import TypicalOverlayScrollbar from 'components/TypicalOverlayScrollbar/TypicalOverlayScrollbar';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { cloneDeep, isFunction } from 'lodash-es';
|
import { cloneDeep, isFunction } from 'lodash-es';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
@ -68,10 +69,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
<section className="header">
|
<section className="header">
|
||||||
<section className="left-actions">
|
<section className="left-actions">
|
||||||
<FilterOutlined />
|
<FilterOutlined />
|
||||||
<Typography.Text className="text">Filters for</Typography.Text>
|
<Typography.Text className="text">
|
||||||
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
{lastQueryName ? 'Filters for' : 'Filters'}
|
||||||
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
</Typography.Text>
|
||||||
</Tooltip>
|
{lastQueryName && (
|
||||||
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="right-actions">
|
<section className="right-actions">
|
||||||
@ -89,31 +94,33 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="filters">
|
<TypicalOverlayScrollbar>
|
||||||
{config.map((filter) => {
|
<section className="filters">
|
||||||
switch (filter.type) {
|
{config.map((filter) => {
|
||||||
case FiltersType.CHECKBOX:
|
switch (filter.type) {
|
||||||
return (
|
case FiltersType.CHECKBOX:
|
||||||
<Checkbox
|
return (
|
||||||
source={source}
|
<Checkbox
|
||||||
filter={filter}
|
source={source}
|
||||||
onFilterChange={onFilterChange}
|
filter={filter}
|
||||||
/>
|
onFilterChange={onFilterChange}
|
||||||
);
|
/>
|
||||||
case FiltersType.SLIDER:
|
);
|
||||||
return <Slider filter={filter} />;
|
case FiltersType.SLIDER:
|
||||||
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
return <Slider filter={filter} />;
|
||||||
default:
|
// eslint-disable-next-line sonarjs/no-duplicated-branches
|
||||||
return (
|
default:
|
||||||
<Checkbox
|
return (
|
||||||
source={source}
|
<Checkbox
|
||||||
filter={filter}
|
source={source}
|
||||||
onFilterChange={onFilterChange}
|
filter={filter}
|
||||||
/>
|
onFilterChange={onFilterChange}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
})}
|
}
|
||||||
</section>
|
})}
|
||||||
|
</section>
|
||||||
|
</TypicalOverlayScrollbar>
|
||||||
</div>
|
</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',
|
INFRA_MONITORING = 'infra-monitoring',
|
||||||
TRACES_EXPLORER = 'traces-explorer',
|
TRACES_EXPLORER = 'traces-explorer',
|
||||||
API_MONITORING = 'api-monitoring',
|
API_MONITORING = 'api-monitoring',
|
||||||
|
EXCEPTIONS = 'exceptions',
|
||||||
}
|
}
|
||||||
|
@ -27,4 +27,5 @@ export enum LOCALSTORAGE {
|
|||||||
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',
|
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[] = [
|
export const HAVING_OPERATORS: string[] = [
|
||||||
OPERATORS['='],
|
OPERATORS['='],
|
||||||
OPERATORS['!='],
|
OPERATORS['!='],
|
||||||
|
@ -16,3 +16,51 @@ export const OperatorConversions: Array<{
|
|||||||
traceValue: 'NotIn',
|
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 { ResizeTable } from 'components/ResizeTable';
|
||||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useResourceAttribute from 'hooks/useResourceAttribute';
|
import useResourceAttribute from 'hooks/useResourceAttribute';
|
||||||
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
import { convertCompositeQueryToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
|
||||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import createQueryParams from 'lib/createQueryParams';
|
import createQueryParams from 'lib/createQueryParams';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isUndefined } from 'lodash-es';
|
import { isUndefined } from 'lodash-es';
|
||||||
import { useTimezone } from 'providers/Timezone';
|
import { useTimezone } from 'providers/Timezone';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -109,10 +110,11 @@ function AllErrors(): JSX.Element {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { queries } = useResourceAttribute();
|
const { queries } = useResourceAttribute();
|
||||||
|
const compositeData = useGetCompositeQueryParam();
|
||||||
|
|
||||||
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
const [{ isLoading, data }, errorCountResponse] = useQueries([
|
||||||
{
|
{
|
||||||
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, queries],
|
queryKey: ['getAllErrors', updatedPath, maxTime, minTime, compositeData],
|
||||||
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
queryFn: (): Promise<SuccessResponse<PayloadProps> | ErrorResponse> =>
|
||||||
getAll({
|
getAll({
|
||||||
end: maxTime,
|
end: maxTime,
|
||||||
@ -123,7 +125,9 @@ function AllErrors(): JSX.Element {
|
|||||||
orderParam: getUpdatedParams,
|
orderParam: getUpdatedParams,
|
||||||
exceptionType: getUpdatedExceptionType,
|
exceptionType: getUpdatedExceptionType,
|
||||||
serviceName: getUpdatedServiceName,
|
serviceName: getUpdatedServiceName,
|
||||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
tags: convertCompositeQueryToTraceSelectedTags(
|
||||||
|
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
enabled: !loading,
|
enabled: !loading,
|
||||||
},
|
},
|
||||||
@ -134,7 +138,7 @@ function AllErrors(): JSX.Element {
|
|||||||
minTime,
|
minTime,
|
||||||
getUpdatedExceptionType,
|
getUpdatedExceptionType,
|
||||||
getUpdatedServiceName,
|
getUpdatedServiceName,
|
||||||
queries,
|
compositeData,
|
||||||
],
|
],
|
||||||
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
queryFn: (): Promise<ErrorResponse | SuccessResponse<number>> =>
|
||||||
getErrorCounts({
|
getErrorCounts({
|
||||||
@ -142,7 +146,9 @@ function AllErrors(): JSX.Element {
|
|||||||
start: minTime,
|
start: minTime,
|
||||||
exceptionType: getUpdatedExceptionType,
|
exceptionType: getUpdatedExceptionType,
|
||||||
serviceName: getUpdatedServiceName,
|
serviceName: getUpdatedServiceName,
|
||||||
tags: convertRawQueriesToTraceSelectedTags(queries),
|
tags: convertCompositeQueryToTraceSelectedTags(
|
||||||
|
compositeData?.builder.queryData?.[0]?.filters.items,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
enabled: !loading,
|
enabled: !loading,
|
||||||
},
|
},
|
||||||
@ -429,12 +435,8 @@ function AllErrors(): JSX.Element {
|
|||||||
[pathname],
|
[pathname],
|
||||||
);
|
);
|
||||||
|
|
||||||
const logEventCalledRef = useRef(false);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!isUndefined(errorCountResponse.data?.payload)) {
|
||||||
!logEventCalledRef.current &&
|
|
||||||
!isUndefined(errorCountResponse.data?.payload)
|
|
||||||
) {
|
|
||||||
const selectedEnvironments = queries.find(
|
const selectedEnvironments = queries.find(
|
||||||
(val) => val.tagKey === 'resource_deployment_environment',
|
(val) => val.tagKey === 'resource_deployment_environment',
|
||||||
)?.tagValue;
|
)?.tagValue;
|
||||||
@ -442,9 +444,12 @@ function AllErrors(): JSX.Element {
|
|||||||
logEvent('Exception: List page visited', {
|
logEvent('Exception: List page visited', {
|
||||||
numberOfExceptions: errorCountResponse?.data?.payload,
|
numberOfExceptions: errorCountResponse?.data?.payload,
|
||||||
selectedEnvironments,
|
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
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [errorCountResponse.data?.payload]);
|
}, [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 isApiMonitoringView = (): boolean => routeKey === 'API_MONITORING';
|
||||||
|
|
||||||
|
const isExceptionsView = (): boolean => routeKey === 'ALL_ERROR';
|
||||||
|
|
||||||
const isTracesView = (): boolean =>
|
const isTracesView = (): boolean =>
|
||||||
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||||
|
|
||||||
@ -661,7 +663,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
isMessagingQueues() ||
|
isMessagingQueues() ||
|
||||||
isCloudIntegrationPage() ||
|
isCloudIntegrationPage() ||
|
||||||
isInfraMonitoring() ||
|
isInfraMonitoring() ||
|
||||||
isApiMonitoringView()
|
isApiMonitoringView() ||
|
||||||
|
isExceptionsView()
|
||||||
? 0
|
? 0
|
||||||
: '0 1rem',
|
: '0 1rem',
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { Select, Spin, Tag, Tooltip } from 'antd';
|
|||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import {
|
import {
|
||||||
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
||||||
|
OperatorConfigKeys,
|
||||||
OPERATORS,
|
OPERATORS,
|
||||||
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||||
QUERY_BUILDER_SEARCH_VALUES,
|
QUERY_BUILDER_SEARCH_VALUES,
|
||||||
@ -62,6 +63,7 @@ import {
|
|||||||
getTagToken,
|
getTagToken,
|
||||||
isInNInOperator,
|
isInNInOperator,
|
||||||
} from '../QueryBuilderSearch/utils';
|
} from '../QueryBuilderSearch/utils';
|
||||||
|
import { filterByOperatorConfig } from '../utils';
|
||||||
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
|
||||||
import Suggestions from './Suggestions';
|
import Suggestions from './Suggestions';
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ interface QueryBuilderSearchV2Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
suffixIcon?: React.ReactNode;
|
suffixIcon?: React.ReactNode;
|
||||||
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
hardcodedAttributeKeys?: BaseAutocompleteData[];
|
||||||
|
operatorConfigKey?: OperatorConfigKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Option {
|
export interface Option {
|
||||||
@ -121,6 +124,7 @@ function QueryBuilderSearchV2(
|
|||||||
suffixIcon,
|
suffixIcon,
|
||||||
whereClauseConfig,
|
whereClauseConfig,
|
||||||
hardcodedAttributeKeys,
|
hardcodedAttributeKeys,
|
||||||
|
operatorConfigKey,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
@ -717,15 +721,11 @@ function QueryBuilderSearchV2(
|
|||||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
|
||||||
setDropdownOptions(operatorOptions);
|
|
||||||
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||||
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||||
label: operator,
|
label: operator,
|
||||||
value: operator,
|
value: operator,
|
||||||
}));
|
}));
|
||||||
operatorOptions = [{ label: '', value: '' }, ...operatorOptions];
|
|
||||||
setDropdownOptions(operatorOptions);
|
|
||||||
} else {
|
} else {
|
||||||
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||||
(operator) => ({
|
(operator) => ({
|
||||||
@ -739,9 +739,12 @@ function QueryBuilderSearchV2(
|
|||||||
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
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) {
|
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||||
@ -774,6 +777,7 @@ function QueryBuilderSearchV2(
|
|||||||
isLogsDataSource,
|
isLogsDataSource,
|
||||||
searchValue,
|
searchValue,
|
||||||
suggestionsData?.payload?.attributes,
|
suggestionsData?.payload?.attributes,
|
||||||
|
operatorConfigKey,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// keep the query in sync with the selected tags in logs explorer page
|
// keep the query in sync with the selected tags in logs explorer page
|
||||||
@ -1000,6 +1004,7 @@ QueryBuilderSearchV2.defaultProps = {
|
|||||||
suffixIcon: null,
|
suffixIcon: null,
|
||||||
whereClauseConfig: {},
|
whereClauseConfig: {},
|
||||||
hardcodedAttributeKeys: undefined,
|
hardcodedAttributeKeys: undefined,
|
||||||
|
operatorConfigKey: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueryBuilderSearchV2;
|
export default QueryBuilderSearchV2;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
||||||
|
import { OperatorConfigKeys, OPERATORS_CONFIG } from 'constants/queryBuilder';
|
||||||
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
||||||
import { IOption } from 'hooks/useResourceAttribute/types';
|
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||||
import uniqWith from 'lodash-es/unionWith';
|
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_NEW,
|
||||||
ROUTES.CHANNELS_EDIT,
|
ROUTES.CHANNELS_EDIT,
|
||||||
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
|
||||||
|
ROUTES.ALL_ERROR,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||||
|
@ -2,7 +2,10 @@ import {
|
|||||||
getResourceAttributesTagKeys,
|
getResourceAttributesTagKeys,
|
||||||
getResourceAttributesTagValues,
|
getResourceAttributesTagValues,
|
||||||
} from 'api/metrics/getResourceAttributes';
|
} from 'api/metrics/getResourceAttributes';
|
||||||
import { OperatorConversions } from 'constants/resourceAttributes';
|
import {
|
||||||
|
CompositeQueryOperatorsConfig,
|
||||||
|
OperatorConversions,
|
||||||
|
} from 'constants/resourceAttributes';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||||
import {
|
import {
|
||||||
@ -49,6 +52,32 @@ export const convertOperatorLabelToTraceOperator = (
|
|||||||
OperatorConversions.find((operator) => operator.label === label)
|
OperatorConversions.find((operator) => operator.label === label)
|
||||||
?.traceValue as OperatorValues;
|
?.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 = (
|
export const convertRawQueriesToTraceSelectedTags = (
|
||||||
queries: IResourceAttribute[],
|
queries: IResourceAttribute[],
|
||||||
tagType = 'ResourceAttribute',
|
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 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 history from 'lib/history';
|
||||||
|
import { isNull } from 'lodash-es';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { routes } from './config';
|
import { routes } from './config';
|
||||||
|
import { ExceptionsQuickFiltersConfig } from './utils';
|
||||||
|
|
||||||
function AllErrors(): JSX.Element {
|
function AllErrors(): JSX.Element {
|
||||||
const { pathname } = useLocation();
|
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 (
|
return (
|
||||||
<>
|
<div className={cx('all-errors-page', showFilters ? 'filter-visible' : '')}>
|
||||||
<ResourceAttributesFilter />
|
{showFilters && (
|
||||||
<RouteTab routes={routes} activeKey={pathname} history={history} />
|
<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