mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-27 19:40:41 +00:00
Compare commits
4 Commits
main
...
feat/saved
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec0b9c843d | ||
|
|
ac4d31ef95 | ||
|
|
ab70655b27 | ||
|
|
94339f3132 |
@@ -97,7 +97,7 @@ interface ToggleAction {
|
||||
isOnlyOrAllClicked?: boolean;
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
quickFilterSource?: QuickFiltersSource;
|
||||
source?: QuickFiltersSource;
|
||||
attributeValues?: string[];
|
||||
}
|
||||
|
||||
@@ -117,8 +117,7 @@ function runToggle(c: ToggleCase): { items: SimpleItem[]; expression: string } {
|
||||
currentQuery: buildQuery(initialItems, initialExpression),
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
quickFilterSource:
|
||||
c.action.quickFilterSource ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
source: c.action.source ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
attributeValues: c.action.attributeValues ?? ['a', 'b', 'c'],
|
||||
value: c.action.value,
|
||||
checked: c.action.checked,
|
||||
@@ -163,7 +162,7 @@ const TOGGLE_CASES: ToggleCase[] = [
|
||||
action: {
|
||||
value: 'a',
|
||||
checked: false,
|
||||
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
// `nin` is what the source asks for, but re-deriving the expression
|
||||
// normalises it. Nothing observes the difference: both infra pages send
|
||||
@@ -314,7 +313,7 @@ const TOGGLE_CASES: ToggleCase[] = [
|
||||
action: {
|
||||
value: 'b',
|
||||
checked: false,
|
||||
quickFilterSource: QuickFiltersSource.INFRA_MONITORING,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
|
||||
@@ -46,18 +46,14 @@ export function removeManagedClauses(expression: string, key: string): string {
|
||||
}
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS: QuickFiltersSource[] = [
|
||||
QuickFiltersSource.INFRA_MONITORING,
|
||||
];
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
export function getNotInOperator(
|
||||
quickFilterSource: QuickFiltersSource,
|
||||
): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(quickFilterSource)) {
|
||||
export function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
@@ -138,7 +134,7 @@ export function applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
quickFilterSource,
|
||||
source,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
@@ -149,7 +145,7 @@ export function applyCheckboxToggle({
|
||||
currentQuery: Query;
|
||||
activeQueryIndex: number;
|
||||
filter: IQuickFiltersConfig;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
value: string;
|
||||
checked: boolean;
|
||||
@@ -244,7 +240,7 @@ export function applyCheckboxToggle({
|
||||
if (sectionType === SectionType.RELATED) {
|
||||
const newFilter: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(quickFilterSource),
|
||||
op: getNotInOperator(source),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
@@ -384,7 +380,7 @@ export function applyCheckboxToggle({
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(quickFilterSource),
|
||||
op: getNotInOperator(source),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
@@ -408,9 +404,7 @@ export function applyCheckboxToggle({
|
||||
// checked=true → user wants to select (IN), checked=false → exclude (NOT IN)
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: checked
|
||||
? getOperatorValue(OPERATORS.IN)
|
||||
: getNotInOperator(quickFilterSource),
|
||||
op: checked ? getOperatorValue(OPERATORS.IN) : getNotInOperator(source),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { SectionType } from './v2/itemRules';
|
||||
|
||||
interface UseCheckboxFilterActionsProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
onFilterChange?: ((query: Query) => void) | null;
|
||||
@@ -38,7 +38,7 @@ interface UseCheckboxFilterActionsReturn {
|
||||
*/
|
||||
function useCheckboxFilterActions({
|
||||
filter,
|
||||
quickFilterSource,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
@@ -65,7 +65,7 @@ function useCheckboxFilterActions({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
quickFilterSource,
|
||||
source,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render, RenderResult } from 'tests/test-utils';
|
||||
import { server, rest } from 'mocks-server/server';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
@@ -20,6 +21,7 @@ export const DEFAULT_FILTER: IQuickFiltersConfig = {
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
};
|
||||
|
||||
@@ -156,7 +158,7 @@ export function renderWithFilter(
|
||||
return render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
|
||||
@@ -33,7 +33,7 @@ import styles from './CheckboxFilterV2.module.scss';
|
||||
|
||||
interface CheckboxFilterV2Props {
|
||||
filter: IQuickFiltersConfig;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
useFieldApis: QuickFilterCheckboxUseFieldApis;
|
||||
@@ -42,18 +42,13 @@ interface CheckboxFilterV2Props {
|
||||
export default function CheckboxFilterV2(
|
||||
props: CheckboxFilterV2Props,
|
||||
): JSX.Element {
|
||||
const {
|
||||
quickFilterSource,
|
||||
filter,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
useFieldApis,
|
||||
} = props;
|
||||
const { source, filter, onFilterChange, onQuickFilterChange, useFieldApis } =
|
||||
props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
@@ -80,7 +75,7 @@ export default function CheckboxFilterV2(
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace: useFieldApis.metricNamespace,
|
||||
quickFilterSource,
|
||||
source,
|
||||
startUnixMilli: useFieldApis.startUnixMilli,
|
||||
endUnixMilli: useFieldApis.endUnixMilli,
|
||||
enabled: isOpen,
|
||||
@@ -109,7 +104,7 @@ export default function CheckboxFilterV2(
|
||||
|
||||
const { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
quickFilterSource,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -44,7 +44,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
source={QuickFiltersSource.AI_OBSERVABILITY}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -59,8 +59,6 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
expect(params.get('endUnixMilli')).toBe(
|
||||
String(DEFAULT_USE_FIELD_APIS.endUnixMilli),
|
||||
);
|
||||
expect(params.get('signal')).toBeNull();
|
||||
expect(params.get('source')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps non-AI sources on the signal-wide endpoint', async () => {
|
||||
@@ -72,7 +70,7 @@ describe('CheckboxFilterV2 - AI observability routing', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'custom.query = "value"',
|
||||
@@ -82,7 +82,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: null,
|
||||
@@ -116,7 +116,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -154,7 +154,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -222,7 +222,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -264,7 +264,7 @@ describe('CheckboxFilterV2 - existingQuery calculation', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={USE_FIELD_APIS_AUTO_DERIVE}
|
||||
/>,
|
||||
undefined,
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
TelemetrytypesSignalDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { render } from 'tests/test-utils';
|
||||
|
||||
import { QuickFiltersSource } from '../../../../types';
|
||||
|
||||
import CheckboxFilterV2 from '../CheckboxFilterV2';
|
||||
import {
|
||||
DEFAULT_FILTER,
|
||||
DEFAULT_USE_FIELD_APIS,
|
||||
setupServer,
|
||||
} from '../CheckboxFilterV2.testUtils';
|
||||
|
||||
setupServer();
|
||||
|
||||
describe('CheckboxFilterV2 - field APIs', () => {
|
||||
it('sends the signal and source of the quick filter source to the signal-wide endpoint', async () => {
|
||||
const requests: URLSearchParams[] = [];
|
||||
server.use(
|
||||
rest.get('http://localhost/api/v1/fields/values', (req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: { values: { stringValues: ['ingest'] } },
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.METER_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(screen.findByText('ingest')).resolves.toBeInTheDocument();
|
||||
expect(requests[0].get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
|
||||
expect(requests[0].get('source')).toBe(TelemetrytypesSourceDTO.meter);
|
||||
});
|
||||
});
|
||||
@@ -72,7 +72,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -101,7 +101,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -119,7 +119,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -173,7 +173,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -240,7 +240,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -288,7 +288,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -330,7 +330,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -394,7 +394,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -440,7 +440,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -468,7 +468,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -490,7 +490,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -516,7 +516,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, defaultOpen: false }}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -568,7 +568,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -601,7 +601,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -627,7 +627,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -652,7 +652,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -691,7 +691,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -714,7 +714,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -761,7 +761,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -837,7 +837,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
onFilterChange={onFilterChange}
|
||||
/>,
|
||||
@@ -971,7 +971,7 @@ describe('CheckboxFilterV2 - interactions', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={{ ...DEFAULT_FILTER, customRendererForValue: customRenderer }}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -55,7 +55,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -106,7 +106,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -140,7 +140,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -185,7 +185,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -221,7 +221,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -273,7 +273,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -321,7 +321,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
undefined,
|
||||
@@ -373,7 +373,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -424,7 +424,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -466,7 +466,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
@@ -521,7 +521,7 @@ describe('CheckboxFilterV2 - item rules', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={{
|
||||
...DEFAULT_USE_FIELD_APIS,
|
||||
existingQuery: 'service.name = "api"',
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -47,7 +47,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={closedFilter}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -104,7 +104,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -133,7 +133,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -152,7 +152,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -172,7 +172,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
@@ -195,7 +195,7 @@ describe('CheckboxFilterV2 - states', () => {
|
||||
render(
|
||||
<CheckboxFilterV2
|
||||
filter={DEFAULT_FILTER}
|
||||
quickFilterSource={QuickFiltersSource.TRACES_EXPLORER}
|
||||
source={QuickFiltersSource.TRACES_EXPLORER}
|
||||
useFieldApis={DEFAULT_USE_FIELD_APIS}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { useMemo } from 'react';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldValuesConfig } from 'api/querySuggestions/types';
|
||||
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseFieldValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
searchText: string;
|
||||
existingQuery?: string;
|
||||
metricNamespace?: string;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
source?: QuickFiltersSource;
|
||||
startUnixMilli?: number;
|
||||
endUnixMilli?: number;
|
||||
enabled: boolean;
|
||||
@@ -38,13 +38,12 @@ export function useFieldValues({
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
quickFilterSource,
|
||||
source,
|
||||
startUnixMilli,
|
||||
endUnixMilli,
|
||||
enabled,
|
||||
}: UseFieldValuesProps): UseFieldValuesReturn {
|
||||
const isAIObservability =
|
||||
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
const isAIObservability = source === QuickFiltersSource.AI_OBSERVABILITY;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
@@ -60,12 +59,14 @@ export function useFieldValues({
|
||||
endUnixMilli,
|
||||
}
|
||||
: {
|
||||
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[quickFilterSource],
|
||||
signal: filter.dataSource
|
||||
? DATA_SOURCE_TO_SIGNAL[filter.dataSource]
|
||||
: undefined,
|
||||
name: filter.attributeKey.key,
|
||||
searchText,
|
||||
existingQuery,
|
||||
metricNamespace,
|
||||
source: QUICK_FILTERS_SOURCE_TO_SOURCE[quickFilterSource],
|
||||
source: source ? QUICK_FILTERS_SOURCE_TO_SOURCE[source] : undefined,
|
||||
startUnixMilli,
|
||||
// This field does not affect the backend but I wanted to keep it here
|
||||
// in case we add the support in the future
|
||||
|
||||
@@ -31,11 +31,11 @@ export type { FilterType };
|
||||
function Duration({
|
||||
filter,
|
||||
onFilterChange,
|
||||
quickFilterSource,
|
||||
source,
|
||||
}: {
|
||||
filter: IQuickFiltersConfig;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
}): JSX.Element {
|
||||
const [selectedFilters, setSelectedFilters] =
|
||||
useState<
|
||||
@@ -52,7 +52,7 @@ function Duration({
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
|
||||
@@ -49,18 +49,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
className,
|
||||
config,
|
||||
handleFilterVisibilityChange,
|
||||
quickFilterSource,
|
||||
source,
|
||||
onFilterChange,
|
||||
onQuickFilterChange,
|
||||
signal,
|
||||
showFilterCollapse = true,
|
||||
showQueryName = true,
|
||||
useFieldApis,
|
||||
savedViewsHeader,
|
||||
} = props;
|
||||
const savedFiltersSource =
|
||||
quickFilterSource === QuickFiltersSource.INFRA_MONITORING
|
||||
? undefined
|
||||
: quickFilterSource;
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [params, setParams] = useApiMonitoringParams();
|
||||
const showIP = params.showIP ?? true;
|
||||
@@ -71,7 +68,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
isCustomFiltersLoading,
|
||||
} = useFilterConfig({ savedFiltersSource, config });
|
||||
} = useFilterConfig({ signal, config });
|
||||
const {
|
||||
deniedPermissions: deniedSettingsPermissions,
|
||||
isLoading: isSettingsChecking,
|
||||
@@ -115,15 +112,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
isListView && quickFilterSource === QuickFiltersSource.TRACES_EXPLORER;
|
||||
isListView && source === QuickFiltersSource.TRACES_EXPLORER;
|
||||
|
||||
// AI observability builds a single query in the row-level views, so there is
|
||||
// no query for the selector to switch between.
|
||||
const isAIObservabilityRowView =
|
||||
quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
source === QuickFiltersSource.AI_OBSERVABILITY &&
|
||||
(isListView || panelType === PANEL_TYPES.TRACE);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(quickFilterSource);
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
@@ -293,7 +290,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
const renderContent = (): JSX.Element => (
|
||||
<>
|
||||
{quickFilterSource === QuickFiltersSource.API_MONITORING && (
|
||||
{source === QuickFiltersSource.API_MONITORING && (
|
||||
<div className="api-quick-filters-header">
|
||||
<Typography.Text>Show IP addresses</Typography.Text>
|
||||
<Switch
|
||||
@@ -316,7 +313,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
return (
|
||||
<Duration
|
||||
key={filter.attributeKey.key}
|
||||
quickFilterSource={quickFilterSource}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
/>
|
||||
@@ -327,7 +324,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
return (
|
||||
<CheckboxV2
|
||||
key={filter.attributeKey.key}
|
||||
quickFilterSource={quickFilterSource}
|
||||
source={source}
|
||||
filter={filter}
|
||||
onFilterChange={onFilterChange}
|
||||
onQuickFilterChange={onQuickFilterChange}
|
||||
@@ -366,14 +363,14 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
<div className="quick-filters-container">
|
||||
<div className="quick-filters">
|
||||
{savedViewsHeader}
|
||||
{quickFilterSource !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
{source !== QuickFiltersSource.INFRA_MONITORING && (
|
||||
<section className="header">
|
||||
{renderLeftActions()}
|
||||
{renderRightActions()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{savedFiltersSource ? (
|
||||
{signal ? (
|
||||
<AuthZGuardContent
|
||||
checks={[QuickFilterReadPermission]}
|
||||
fallbackOnLoading={filtersSkeleton}
|
||||
@@ -394,9 +391,9 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{isSettingsOpen && savedFiltersSource && (
|
||||
{isSettingsOpen && (
|
||||
<QuickFiltersSettings
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
signal={signal}
|
||||
setIsSettingsOpen={setIsSettingsOpen}
|
||||
customFilters={customFilters}
|
||||
refetchCustomFilters={refetchCustomFilters}
|
||||
@@ -410,6 +407,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
QuickFilters.defaultProps = {
|
||||
onFilterChange: null,
|
||||
signal: '',
|
||||
config: [],
|
||||
showFilterCollapse: true,
|
||||
showQueryName: true,
|
||||
|
||||
@@ -34,17 +34,17 @@ jest.mock('container/SavedViews/SavedViewsRestore', () => ({
|
||||
jest.mock('../../QuickFilters', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
quickFilterSource,
|
||||
source,
|
||||
savedViewsHeader,
|
||||
handleFilterVisibilityChange,
|
||||
}: {
|
||||
quickFilterSource: string;
|
||||
source: string;
|
||||
savedViewsHeader?: React.ReactNode;
|
||||
handleFilterVisibilityChange?: () => void;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="quick-filters">
|
||||
{savedViewsHeader}
|
||||
{quickFilterSource}
|
||||
{source}
|
||||
{handleFilterVisibilityChange && (
|
||||
<Button
|
||||
size="md"
|
||||
@@ -62,7 +62,7 @@ jest.mock('../../QuickFilters', () => ({
|
||||
}));
|
||||
|
||||
const quickFilterProps = {
|
||||
quickFilterSource: QuickFiltersSource.TRACES_EXPLORER,
|
||||
source: QuickFiltersSource.TRACES_EXPLORER,
|
||||
useFieldApis: { startUnixMilli: 0, endUnixMilli: 0 },
|
||||
};
|
||||
const savedViewProps = { source: SavedviewtypesSourceDTO.traces };
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import {
|
||||
QuickfiltertypesSourceDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { FieldKeysConfig } from 'api/querySuggestions/types';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { QUICK_FILTER_SOURCE_TO_SIGNAL } from 'components/QuickFilters/constants';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import {
|
||||
@@ -15,6 +13,7 @@ import {
|
||||
FieldDataType,
|
||||
TelemetryFieldKey,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL } from 'types/common/queryBuilder';
|
||||
|
||||
function OtherFiltersSkeleton(): JSX.Element {
|
||||
return (
|
||||
@@ -33,20 +32,18 @@ function OtherFiltersSkeleton(): JSX.Element {
|
||||
}
|
||||
|
||||
function OtherFilters({
|
||||
savedFiltersSource,
|
||||
signal,
|
||||
inputValue,
|
||||
addedFilters,
|
||||
setAddedFilters,
|
||||
}: {
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
signal: SignalType | undefined;
|
||||
inputValue: string;
|
||||
addedFilters: TelemetryFieldKey[];
|
||||
setAddedFilters: React.Dispatch<React.SetStateAction<TelemetryFieldKey[]>>;
|
||||
}): JSX.Element {
|
||||
const isMeterDataSource =
|
||||
savedFiltersSource === QuickfiltertypesSourceDTO.meter;
|
||||
const isAIObservability =
|
||||
savedFiltersSource === QuickfiltertypesSourceDTO.ai_observability;
|
||||
const isMeterDataSource = signal === SignalType.METER_EXPLORER;
|
||||
const isAIObservability = signal === SignalType.AI_OBSERVABILITY;
|
||||
|
||||
const builderQueryType: BuilderQueryType | undefined = isAIObservability
|
||||
? 'builder_ai_query'
|
||||
@@ -56,7 +53,9 @@ function OtherFilters({
|
||||
? { searchText: inputValue }
|
||||
: {
|
||||
searchText: inputValue,
|
||||
signal: QUICK_FILTER_SOURCE_TO_SIGNAL[savedFiltersSource],
|
||||
signal: signal
|
||||
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
|
||||
: undefined,
|
||||
source: isMeterDataSource ? TelemetrytypesSourceDTO.meter : undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Button as SignozButton } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button } from 'antd';
|
||||
import { Check, TableColumnsSplit, X } from '@signozhq/icons';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { SignalType } from '../types';
|
||||
import AddedFilters from './AddedFilters';
|
||||
import useQuickFilterSettings from './hooks/useQuickFilterSettings';
|
||||
import OtherFilters from './OtherFilters';
|
||||
@@ -12,12 +12,12 @@ import OtherFilters from './OtherFilters';
|
||||
import './QuickFiltersSettings.styles.scss';
|
||||
|
||||
function QuickFiltersSettings({
|
||||
savedFiltersSource,
|
||||
signal,
|
||||
setIsSettingsOpen,
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
}: {
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
signal: SignalType | undefined;
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
@@ -37,7 +37,7 @@ function QuickFiltersSettings({
|
||||
setIsSettingsOpen,
|
||||
customFilters,
|
||||
refetchCustomFilters,
|
||||
savedFiltersSource,
|
||||
signal,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -73,7 +73,7 @@ function QuickFiltersSettings({
|
||||
setAddedFilters={setAddedFilters}
|
||||
/>
|
||||
<OtherFilters
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
signal={signal}
|
||||
inputValue={debouncedInputValue}
|
||||
addedFilters={addedFilters}
|
||||
setAddedFilters={setAddedFilters}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { SignalType } from '../../types';
|
||||
import OtherFilters from '../OtherFilters';
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
@@ -43,12 +43,10 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
);
|
||||
});
|
||||
|
||||
function renderOtherFilters(
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO,
|
||||
): void {
|
||||
function renderOtherFilters(signal: SignalType): void {
|
||||
render(
|
||||
<OtherFilters
|
||||
savedFiltersSource={savedFiltersSource}
|
||||
signal={signal}
|
||||
inputValue=""
|
||||
addedFilters={[]}
|
||||
setAddedFilters={jest.fn()}
|
||||
@@ -57,7 +55,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
}
|
||||
|
||||
it('reads AI observability keys from their own endpoint', async () => {
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
|
||||
await expect(
|
||||
screen.findByText('gen_ai.request.model'),
|
||||
@@ -66,7 +64,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
});
|
||||
|
||||
it('does not narrow the AI keys by fieldContext', async () => {
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.ai_observability);
|
||||
renderOtherFilters(SignalType.AI_OBSERVABILITY);
|
||||
|
||||
// A `trace` context would return only the computed per-trace aggregates,
|
||||
// which cannot be filtered on.
|
||||
@@ -75,7 +73,7 @@ describe('OtherFilters - AI observability keys', () => {
|
||||
});
|
||||
|
||||
it('keeps other signals on the signal-wide keys endpoint', async () => {
|
||||
renderOtherFilters(QuickfiltertypesSourceDTO.traces);
|
||||
renderOtherFilters(SignalType.TRACES);
|
||||
|
||||
await expect(screen.findByText('http.route')).resolves.toBeInTheDocument();
|
||||
await waitFor(() => expect(aiKeysParams).toBeUndefined());
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const SIGNAL_DATA_SOURCE_MAP = {
|
||||
[SignalType.LOGS]: DataSource.LOGS,
|
||||
[SignalType.TRACES]: DataSource.TRACES,
|
||||
[SignalType.EXCEPTIONS]: DataSource.TRACES,
|
||||
[SignalType.API_MONITORING]: DataSource.TRACES,
|
||||
[SignalType.METER_EXPLORER]: DataSource.METRICS,
|
||||
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
TelemetrytypesFieldContextDTO,
|
||||
TelemetrytypesFieldDataTypeDTO,
|
||||
QuickfiltertypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SignalType } from 'components/QuickFilters/types';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
@@ -16,7 +16,7 @@ interface UseQuickFilterSettingsProps {
|
||||
setIsSettingsOpen: (isSettingsOpen: boolean) => void;
|
||||
customFilters: TelemetryFieldKey[];
|
||||
refetchCustomFilters: () => void;
|
||||
savedFiltersSource: QuickfiltertypesSourceDTO;
|
||||
signal?: SignalType;
|
||||
}
|
||||
|
||||
interface UseQuickFilterSettingsReturn {
|
||||
@@ -37,7 +37,7 @@ const useQuickFilterSettings = ({
|
||||
customFilters,
|
||||
setIsSettingsOpen,
|
||||
refetchCustomFilters,
|
||||
savedFiltersSource,
|
||||
signal,
|
||||
}: UseQuickFilterSettingsProps): UseQuickFilterSettingsReturn => {
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const [debouncedInputValue, setDebouncedInputValue] = useState<string>('');
|
||||
@@ -113,19 +113,21 @@ const useQuickFilterSettings = ({
|
||||
);
|
||||
|
||||
const handleSaveChanges = useCallback((): void => {
|
||||
updateCustomFilters({
|
||||
pathParams: { source: savedFiltersSource },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}, [addedFilters, savedFiltersSource, updateCustomFilters]);
|
||||
if (signal) {
|
||||
updateCustomFilters({
|
||||
pathParams: { source: signal },
|
||||
data: {
|
||||
// Send only the stored TelemetryFieldKey fields; the composite `key`
|
||||
// is UI-only.
|
||||
filters: addedFilters.map((filter) => ({
|
||||
name: filter.name,
|
||||
fieldContext: filter.fieldContext as TelemetrytypesFieldContextDTO,
|
||||
fieldDataType: filter.fieldDataType as TelemetrytypesFieldDataTypeDTO,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [addedFilters, signal, updateCustomFilters]);
|
||||
|
||||
return {
|
||||
handleSettingsClose,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
import { QuickFiltersSource } from './types';
|
||||
|
||||
export const QUICK_FILTER_SOURCE_TO_SIGNAL: Record<
|
||||
QuickFiltersSource,
|
||||
TelemetrytypesSignalDTO
|
||||
> = {
|
||||
[QuickFiltersSource.LOGS_EXPLORER]: TelemetrytypesSignalDTO.logs,
|
||||
[QuickFiltersSource.TRACES_EXPLORER]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.EXCEPTIONS]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.API_MONITORING]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.AI_OBSERVABILITY]: TelemetrytypesSignalDTO.traces,
|
||||
[QuickFiltersSource.METER_EXPLORER]: TelemetrytypesSignalDTO.metrics,
|
||||
[QuickFiltersSource.INFRA_MONITORING]: TelemetrytypesSignalDTO.metrics,
|
||||
};
|
||||
@@ -10,26 +10,26 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
* In ListView most sources use index 0; TRACES_EXPLORER and every non-ListView
|
||||
* mode track the last focused query.
|
||||
*/
|
||||
function useActiveQueryIndex(quickFilterSource: QuickFiltersSource): number {
|
||||
function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
const { lastUsedQuery, panelType } = useQueryBuilder();
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
// AI observability builds a single query in the row-level views, so its
|
||||
// filters always drive the first one there.
|
||||
if (quickFilterSource === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
if (source === QuickFiltersSource.AI_OBSERVABILITY) {
|
||||
return isListView || panelType === PANEL_TYPES.TRACE
|
||||
? 0
|
||||
: lastUsedQuery || 0;
|
||||
}
|
||||
|
||||
if (isListView) {
|
||||
return quickFilterSource === QuickFiltersSource.TRACES_EXPLORER
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, panelType, quickFilterSource, lastUsedQuery]);
|
||||
}, [isListView, panelType, source, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
|
||||
import { IQuickFiltersConfig } from '../types';
|
||||
import { IQuickFiltersConfig, SignalType } from '../types';
|
||||
import { getFilterConfig } from '../utils';
|
||||
|
||||
interface UseFilterConfigProps {
|
||||
savedFiltersSource?: QuickfiltertypesSourceDTO;
|
||||
signal?: SignalType;
|
||||
config: IQuickFiltersConfig[];
|
||||
}
|
||||
interface UseFilterConfigReturn {
|
||||
@@ -19,7 +18,7 @@ interface UseFilterConfigReturn {
|
||||
}
|
||||
|
||||
const useFilterConfig = ({
|
||||
savedFiltersSource,
|
||||
signal,
|
||||
config,
|
||||
}: UseFilterConfigProps): UseFilterConfigReturn => {
|
||||
const {
|
||||
@@ -27,8 +26,8 @@ const useFilterConfig = ({
|
||||
data,
|
||||
refetch,
|
||||
} = useGetQuickFilters(
|
||||
{ source: savedFiltersSource ?? '' },
|
||||
{ query: { enabled: !!savedFiltersSource } },
|
||||
{ source: signal ?? '' },
|
||||
{ query: { enabled: !!signal } },
|
||||
);
|
||||
|
||||
const customFilters = useMemo<TelemetryFieldKey[]>(
|
||||
@@ -36,11 +35,11 @@ const useFilterConfig = ({
|
||||
[data],
|
||||
);
|
||||
|
||||
const isDynamicFilters = !!savedFiltersSource;
|
||||
const isDynamicFilters = !!signal;
|
||||
|
||||
const filterConfig = useMemo(
|
||||
() => getFilterConfig(customFilters, config),
|
||||
[config, customFilters],
|
||||
() => getFilterConfig(signal, customFilters, config),
|
||||
[config, customFilters, signal],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -89,15 +89,6 @@ export const handlers = [
|
||||
fieldValuesHandler(['checkout', 'frontend', 'payments']),
|
||||
];
|
||||
|
||||
export const noSavedFiltersHandler = rest.get(
|
||||
'http://localhost/api/v2/quick_filters/logs',
|
||||
(_req, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json(quickFiltersResponse(QuickfiltertypesSourceDTO.logs, [])),
|
||||
),
|
||||
);
|
||||
|
||||
export const loadingFiltersHandlers = [
|
||||
rest.get('http://localhost/api/v2/quick_filters/logs', (_req, res, ctx) =>
|
||||
res(ctx.delay('infinite')),
|
||||
|
||||
@@ -15,9 +15,8 @@ import {
|
||||
loadingFiltersHandlers,
|
||||
queryBuilder,
|
||||
selectedServiceQueryBuilder,
|
||||
noSavedFiltersHandler,
|
||||
} from './QuickFilters.stories.mocks';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Quick Filters',
|
||||
@@ -34,7 +33,8 @@ const meta = {
|
||||
args: {
|
||||
config: checkboxConfig,
|
||||
handleFilterVisibilityChange: (): void => undefined,
|
||||
quickFilterSource: QuickFiltersSource.LOGS_EXPLORER,
|
||||
signal: SignalType.LOGS,
|
||||
source: QuickFiltersSource.LOGS_EXPLORER,
|
||||
useFieldApis: fieldApis,
|
||||
},
|
||||
parameters: {
|
||||
@@ -94,16 +94,13 @@ export const LoadingFilters: Story = {
|
||||
|
||||
/** Empty: a loaded quick-filter configuration with no filters has no result rows. */
|
||||
export const NoResults: Story = {
|
||||
args: { config: [] },
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
|
||||
},
|
||||
args: { config: [], signal: undefined },
|
||||
};
|
||||
|
||||
/** Selection: an expanded checkbox shows the actual selected service values. */
|
||||
export const SelectedExpandedCheckbox: Story = {
|
||||
args: { signal: undefined },
|
||||
parameters: {
|
||||
msw: { handlers: [noSavedFiltersHandler, ...handlers] },
|
||||
signoz: {
|
||||
queryBuilder: selectedServiceQueryBuilder,
|
||||
},
|
||||
@@ -114,18 +111,12 @@ export const SelectedExpandedCheckbox: Story = {
|
||||
* Every tooltip the panel renders, held open: the reveal on each truncated
|
||||
* filter value. Nothing bounds those values, so the panel is answered with
|
||||
* service names long enough to be cut. The Service name filter carries them, so
|
||||
* the workspace's own saved filter list is answered empty.
|
||||
* the signal that would add the workspace's own dynamic filters is left off.
|
||||
*/
|
||||
export const Tooltips: TooltipsStory = {
|
||||
args: { tooltipsOpen: true },
|
||||
args: { signal: undefined, tooltipsOpen: true },
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
noSavedFiltersHandler,
|
||||
fieldValuesHandler(LONG_FILTER_VALUES),
|
||||
...handlers,
|
||||
],
|
||||
},
|
||||
msw: { handlers: [fieldValuesHandler(LONG_FILTER_VALUES), ...handlers] },
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { rest } from 'msw';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { QuickFiltersSource } from '../types';
|
||||
import { QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -35,7 +35,8 @@ jest.mock('container/ApiMonitoring/queryParams');
|
||||
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const fieldsKeysURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
@@ -80,10 +81,11 @@ afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderWithSavedList(): void {
|
||||
function renderWithSignal(): void {
|
||||
render(
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
|
||||
source={QuickFiltersSource.LOGS_EXPLORER}
|
||||
signal={SIGNAL}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
@@ -93,7 +95,7 @@ function renderWithSavedList(): void {
|
||||
function renderStaticConfig(): void {
|
||||
render(
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={QuickFiltersConfig}
|
||||
handleFilterVisibilityChange={jest.fn()}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
@@ -106,7 +108,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('shows the inline denial instead of the filters, header stays', async () => {
|
||||
server.use(setupAuthzDeny(QuickFilterReadPermission));
|
||||
|
||||
renderWithSavedList();
|
||||
renderWithSignal();
|
||||
|
||||
await expect(
|
||||
screen.findByText(NOT_AUTHORIZED_TEXT),
|
||||
@@ -131,7 +133,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('renders the filters but disables the settings trigger', async () => {
|
||||
server.use(setupAuthzDeny(QuickFilterUpdatePermission));
|
||||
|
||||
renderWithSavedList();
|
||||
renderWithSignal();
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -156,7 +158,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
it('renders the filters and opens settings from the trigger', async () => {
|
||||
server.use(setupAuthzAdmin());
|
||||
|
||||
renderWithSavedList();
|
||||
renderWithSignal();
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -174,7 +176,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('static config pages (no saved list)', () => {
|
||||
describe('static config pages (no signal)', () => {
|
||||
it('is not gated even when every permission is denied', async () => {
|
||||
server.use(setupAuthzDenyAll());
|
||||
|
||||
@@ -193,7 +195,7 @@ describe('QuickFilters - AuthZ', () => {
|
||||
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => res(ctx.delay('infinite'))),
|
||||
);
|
||||
|
||||
renderWithSavedList();
|
||||
renderWithSignal();
|
||||
|
||||
await waitFor(() =>
|
||||
// eslint-disable-next-line testing-library/no-node-access
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import {
|
||||
ApiMonitoringParams,
|
||||
useApiMonitoringParams,
|
||||
} from 'container/ApiMonitoring/queryParams';
|
||||
import { getHostsQuickFiltersConfig } from 'container/InfraMonitoringHostsV2/utils';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
otherFiltersResponse,
|
||||
@@ -19,7 +16,7 @@ import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import QuickFilters from '../QuickFilters';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource } from '../types';
|
||||
import { IQuickFiltersConfig, QuickFiltersSource, SignalType } from '../types';
|
||||
import { QuickFiltersConfig, QuickFiltersFieldApis } from './constants';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -36,9 +33,9 @@ const mockSetApiMonitoringParams = jest.fn() as jest.MockedFunction<
|
||||
const mockUseApiMonitoringParams = jest.mocked(useApiMonitoringParams);
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/logs`;
|
||||
const anyQuickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/:source`;
|
||||
const SIGNAL = SignalType.LOGS;
|
||||
const quickFiltersListURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const saveQuickFiltersURL = `${BASE_URL}/api/v2/quick_filters/${SIGNAL}`;
|
||||
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v1/fields/keys`;
|
||||
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
|
||||
|
||||
@@ -57,9 +54,6 @@ const setupServer = (): void => {
|
||||
rest.get(quickFiltersListURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(quickFiltersListResponse)),
|
||||
),
|
||||
rest.get(anyQuickFiltersListURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json({ status: 'success', data: { filters: [] } })),
|
||||
),
|
||||
rest.get(quickFiltersSuggestionsURL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(otherFiltersResponse)),
|
||||
),
|
||||
@@ -74,24 +68,48 @@ const setupServer = (): void => {
|
||||
};
|
||||
|
||||
function TestQuickFilters({
|
||||
quickFilterSource = QuickFiltersSource.EXCEPTIONS,
|
||||
signal = SignalType.LOGS,
|
||||
config = QuickFiltersConfig,
|
||||
}: {
|
||||
quickFilterSource?: QuickFiltersSource;
|
||||
signal?: SignalType;
|
||||
config?: IQuickFiltersConfig[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QuickFilters
|
||||
quickFilterSource={quickFilterSource}
|
||||
source={QuickFiltersSource.EXCEPTIONS}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TestQuickFilters.defaultProps = {
|
||||
quickFilterSource: QuickFiltersSource.EXCEPTIONS,
|
||||
signal: '',
|
||||
config: QuickFiltersConfig,
|
||||
};
|
||||
|
||||
function TestQuickFiltersApiMonitoring({
|
||||
signal = SignalType.LOGS,
|
||||
config = QuickFiltersConfig,
|
||||
}: {
|
||||
signal?: SignalType;
|
||||
config?: IQuickFiltersConfig[];
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QuickFilters
|
||||
source={QuickFiltersSource.API_MONITORING}
|
||||
config={config}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
signal={signal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TestQuickFiltersApiMonitoring.defaultProps = {
|
||||
signal: '',
|
||||
config: QuickFiltersConfig,
|
||||
};
|
||||
|
||||
@@ -269,14 +287,10 @@ describe('Quick Filters', () => {
|
||||
it('toggles Show IP addresses and updates API Monitoring params', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.API_MONITORING} />,
|
||||
);
|
||||
render(<TestQuickFiltersApiMonitoring />);
|
||||
|
||||
// Switch should be rendered and initially checked
|
||||
await expect(
|
||||
screen.findByText('Show IP addresses'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.getByText('Show IP addresses')).toBeInTheDocument();
|
||||
const toggle = screen.getByRole('switch');
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true');
|
||||
|
||||
@@ -294,9 +308,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('loads the custom filters correctly', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
|
||||
expect(screen.getByText('Filters for')).toBeInTheDocument();
|
||||
expect(screen.getByText(QUERY_NAME)).toBeInTheDocument();
|
||||
@@ -356,9 +368,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -386,9 +396,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -409,9 +417,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('removes a filter from ADDED FILTERS and moves it to OTHER FILTERS', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -440,9 +446,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('restores original filter state on Discard', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -484,9 +488,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
it('saves the updated filters by calling PUT with correct payload', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
|
||||
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
|
||||
@@ -523,9 +525,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
pointerEventsCheck: 0,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
const { getByTestId } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await screen.findByText(FILTER_SERVICE_NAME);
|
||||
expect(screen.getByText('Duration')).toBeInTheDocument();
|
||||
|
||||
@@ -579,7 +579,7 @@ describe('Quick Filters with custom filters', () => {
|
||||
});
|
||||
|
||||
describe('Quick Filters refetch behavior', () => {
|
||||
it('fetches custom filters on every mount for a page with a saved list', async () => {
|
||||
it('fetches custom filters on every mount when signal is provided', async () => {
|
||||
let getCalls = 0;
|
||||
|
||||
server.use(
|
||||
@@ -589,18 +589,14 @@ describe('Quick Filters refetch behavior', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
const { unmount } = render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
unmount();
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
@@ -608,24 +604,19 @@ describe('Quick Filters refetch behavior', () => {
|
||||
expect(getCalls).toBe(2);
|
||||
});
|
||||
|
||||
it('does not fetch custom filters for a page without a saved list', async () => {
|
||||
it('does not fetch custom filters when signal is undefined', async () => {
|
||||
let getCalls = 0;
|
||||
|
||||
server.use(
|
||||
rest.get(anyQuickFiltersListURL, (_req, res, ctx) => {
|
||||
rest.get(quickFiltersListURL, (_req, res, ctx) => {
|
||||
getCalls += 1;
|
||||
return res(ctx.status(200), ctx.json(quickFiltersListResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.INFRA_MONITORING} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={undefined} />);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(getCalls).toBe(0);
|
||||
await waitFor(() => expect(getCalls).toBe(0));
|
||||
});
|
||||
|
||||
it('refetches custom filters after saving settings', async () => {
|
||||
@@ -644,9 +635,7 @@ describe('Quick Filters refetch behavior', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -698,9 +687,7 @@ describe('Quick Filters refetch behavior', () => {
|
||||
);
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(
|
||||
<TestQuickFilters quickFilterSource={QuickFiltersSource.LOGS_EXPLORER} />,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} />);
|
||||
|
||||
await expect(
|
||||
screen.findByText(FILTER_SERVICE_NAME),
|
||||
@@ -731,47 +718,10 @@ describe('Quick Filters refetch behavior', () => {
|
||||
),
|
||||
);
|
||||
|
||||
render(
|
||||
<TestQuickFilters
|
||||
quickFilterSource={QuickFiltersSource.LOGS_EXPLORER}
|
||||
config={[]}
|
||||
/>,
|
||||
);
|
||||
render(<TestQuickFilters signal={SIGNAL} config={[]} />);
|
||||
|
||||
await expect(
|
||||
screen.findByText('No filters found'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Quick Filters field APIs', () => {
|
||||
it('sends the page signal for every filter, the infra Environment filter included', async () => {
|
||||
const requests: URLSearchParams[] = [];
|
||||
server.use(
|
||||
rest.get(fieldsValuesURL, (req, res, ctx) => {
|
||||
requests.push(req.url.searchParams);
|
||||
return res(ctx.status(200), ctx.json(quickFiltersFieldValuesResponse));
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig()}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={QuickFiltersFieldApis}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requests.map((params) => params.get('name'))).toContain(
|
||||
INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
|
||||
),
|
||||
);
|
||||
const environment = requests.find(
|
||||
(params) =>
|
||||
params.get('name') === INFRA_MONITORING_ATTR_KEYS.DEPLOYMENT_ENVIRONMENT,
|
||||
);
|
||||
expect(environment?.get('signal')).toBe(TelemetrytypesSignalDTO.metrics);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { QuickfiltertypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export enum FiltersType {
|
||||
SLIDER = 'SLIDER',
|
||||
@@ -19,6 +19,15 @@ export enum SpecficFilterOperations {
|
||||
ONLY = 'ONLY',
|
||||
}
|
||||
|
||||
export enum SignalType {
|
||||
TRACES = 'traces',
|
||||
LOGS = 'logs',
|
||||
API_MONITORING = 'api_monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai_observability',
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing export from signozhq/ui/checkbox, TODO(H4ad): Add and remove this type definition
|
||||
*/
|
||||
@@ -30,6 +39,7 @@ export interface IQuickFiltersConfig {
|
||||
attributeKey: BaseAutocompleteData;
|
||||
aggregateOperator?: string;
|
||||
aggregateAttribute?: string;
|
||||
dataSource?: DataSource;
|
||||
customRendererForValue?: (value: string) => JSX.Element;
|
||||
defaultOpen: boolean;
|
||||
}
|
||||
@@ -43,9 +53,11 @@ export interface QuickFilterChangeEventData {
|
||||
export interface IQuickFiltersProps {
|
||||
config: IQuickFiltersConfig[];
|
||||
handleFilterVisibilityChange?: () => void;
|
||||
quickFilterSource: QuickFiltersSource;
|
||||
source: QuickFiltersSource;
|
||||
onFilterChange?: (query: Query) => void;
|
||||
onQuickFilterChange?: (data: QuickFilterChangeEventData) => void;
|
||||
/** Pass to fetch quick filters for this signal; omit to use `config` as-is */
|
||||
signal?: SignalType;
|
||||
className?: string;
|
||||
showFilterCollapse?: boolean;
|
||||
showQueryName?: boolean;
|
||||
@@ -53,19 +65,15 @@ export interface IQuickFiltersProps {
|
||||
savedViewsHeader?: ReactNode;
|
||||
}
|
||||
|
||||
export const QuickFiltersSource = {
|
||||
LOGS_EXPLORER: QuickfiltertypesSourceDTO.logs,
|
||||
TRACES_EXPLORER: QuickfiltertypesSourceDTO.traces,
|
||||
EXCEPTIONS: QuickfiltertypesSourceDTO.exceptions,
|
||||
API_MONITORING: QuickfiltertypesSourceDTO.api_monitoring,
|
||||
METER_EXPLORER: QuickfiltertypesSourceDTO.meter,
|
||||
AI_OBSERVABILITY: QuickfiltertypesSourceDTO.ai_observability,
|
||||
/** No saved filter list: renders `config` as given. */
|
||||
INFRA_MONITORING: 'infra-monitoring',
|
||||
} as const;
|
||||
|
||||
export type QuickFiltersSource =
|
||||
(typeof QuickFiltersSource)[keyof typeof QuickFiltersSource];
|
||||
export enum QuickFiltersSource {
|
||||
LOGS_EXPLORER = 'logs-explorer',
|
||||
INFRA_MONITORING = 'infra-monitoring',
|
||||
TRACES_EXPLORER = 'traces-explorer',
|
||||
API_MONITORING = 'api-monitoring',
|
||||
EXCEPTIONS = 'exceptions',
|
||||
METER_EXPLORER = 'meter',
|
||||
AI_OBSERVABILITY = 'ai-observability',
|
||||
}
|
||||
|
||||
export type QuickFilterCheckboxUseFieldApis = {
|
||||
startUnixMilli: number;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
|
||||
|
||||
import { FiltersType, IQuickFiltersConfig } from './types';
|
||||
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
|
||||
|
||||
const FILTER_TITLE_MAP: Record<string, string> = {
|
||||
duration_nano: 'Duration',
|
||||
@@ -49,10 +50,11 @@ const getFilterType = (att: TelemetryFieldKey): FiltersType => {
|
||||
};
|
||||
|
||||
export const getFilterConfig = (
|
||||
signal?: SignalType,
|
||||
customFilters?: TelemetryFieldKey[],
|
||||
config?: IQuickFiltersConfig[],
|
||||
): IQuickFiltersConfig[] => {
|
||||
if (!customFilters?.length) {
|
||||
if (!customFilters?.length || !signal) {
|
||||
return config || [];
|
||||
}
|
||||
|
||||
@@ -61,6 +63,7 @@ export const getFilterConfig = (
|
||||
({
|
||||
type: getFilterType(att),
|
||||
title: getFilterName(att.name),
|
||||
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
|
||||
attributeKey: {
|
||||
id: att.name,
|
||||
key: att.name,
|
||||
|
||||
@@ -14,6 +14,8 @@ export enum LOCALSTORAGE {
|
||||
TRACES_VIEW_COLUMNS = 'TRACES_VIEW_COLUMNS',
|
||||
AI_OBSERVABILITY_TRACE_VIEW_COLUMNS = 'AI_OBSERVABILITY_TRACE_VIEW_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_COLUMNS = 'AI_OBSERVABILITY_LIST_COLUMNS',
|
||||
AI_OBSERVABILITY_LIST_OPTIONS = 'AI_OBSERVABILITY_LIST_OPTIONS',
|
||||
AI_OBSERVABILITY_TRACE_OPTIONS = 'AI_OBSERVABILITY_TRACE_OPTIONS',
|
||||
LOGS_LIST_COLUMNS = 'LOGS_LIST_COLUMNS',
|
||||
LOGS_LIST_COLUMN_SIZING = 'LOGS_LIST_COLUMN_SIZING',
|
||||
LOGGED_IN_USER_NAME = 'LOGGED_IN_USER_NAME',
|
||||
|
||||
@@ -334,7 +334,6 @@ function AnomalyAlertEvaluationView({
|
||||
<Checkbox
|
||||
color="primary"
|
||||
key={seriesKey}
|
||||
testId="anomaly-series-option"
|
||||
name="series"
|
||||
value={selectedSeries === seriesKey}
|
||||
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
|
||||
import DomainList from './Domains/DomainList';
|
||||
@@ -24,7 +24,8 @@ function Explorer(): JSX.Element {
|
||||
showFilters
|
||||
quickFilterProps={{
|
||||
className: 'qf-api-monitoring',
|
||||
quickFilterSource: QuickFiltersSource.API_MONITORING,
|
||||
source: QuickFiltersSource.API_MONITORING,
|
||||
signal: SignalType.API_MONITORING,
|
||||
showQueryName: false,
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
|
||||
@@ -91,6 +91,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -101,6 +102,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -111,6 +113,7 @@ export const ApiMonitoringQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
},
|
||||
dataSource: DataSource.TRACES,
|
||||
defaultOpen: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,12 +7,11 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { ALERT_TYPE_URL_MAP } from './constants';
|
||||
|
||||
// The setup guide button only exists in the classic form.
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string; search: string } => ({
|
||||
pathname: `${process.env.FRONTEND_API_ENDPOINT}${ROUTES.ALERTS_NEW}`,
|
||||
search: 'ruleType=anomaly_rule&showClassicCreateAlertsPage=true',
|
||||
search: 'ruleType=anomaly_rule',
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -21,7 +20,7 @@ jest.mock('react-router-dom-v5-compat', () => ({
|
||||
useNavigationType: jest.fn(() => 'PUSH'),
|
||||
useLocation: jest.fn(() => ({
|
||||
pathname: '/alerts/new',
|
||||
search: 'ruleType=anomaly_rule&showClassicCreateAlertsPage=true',
|
||||
search: 'ruleType=anomaly_rule',
|
||||
hash: '',
|
||||
state: null,
|
||||
})),
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
LicensePlatform,
|
||||
LicenseResModel,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -11,10 +8,6 @@ import SelectAlertType from '..';
|
||||
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
const CLOUD_LICENSE = {
|
||||
platform: LicensePlatform.CLOUD,
|
||||
} as LicenseResModel;
|
||||
|
||||
describe('SelectAlertType', () => {
|
||||
const mockOnSelect = jest.fn();
|
||||
|
||||
@@ -26,7 +19,15 @@ describe('SelectAlertType', () => {
|
||||
it('should render all alert type options when anomaly detection is enabled', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
activeLicense: CLOUD_LICENSE,
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
@@ -38,19 +39,6 @@ describe('SelectAlertType', () => {
|
||||
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should offer the anomaly based alert on an enterprise self-hosted license', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
activeLicense: {
|
||||
platform: LicensePlatform.SELF_HOSTED,
|
||||
} as LicenseResModel,
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
expect(screen.getByText('anomaly_based_alert')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all alert type options except anomaly based alert when anomaly detection is disabled', () => {
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
@@ -74,7 +62,15 @@ describe('SelectAlertType', () => {
|
||||
it('should call onSelect with anomaly based alert type', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
activeLicense: CLOUD_LICENSE,
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<SelectAlertType onSelect={mockOnSelect} />);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { Badge } from '@signozhq/ui/badge';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { useIsAnomalyDetectionAvailable } from 'container/CreateAlertV2/useIsAnomalyDetectionAvailable';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
@@ -16,8 +17,11 @@ import { OptionType } from './types';
|
||||
|
||||
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
const { t } = useTranslation(['alerts']);
|
||||
const { featureFlags } = useAppContext();
|
||||
|
||||
const isAnomalyDetectionEnabled = useIsAnomalyDetectionAvailable();
|
||||
const isAnomalyDetectionEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||
?.active || false;
|
||||
|
||||
const optionList = getOptionList(t, isAnomalyDetectionEnabled);
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import {
|
||||
LicensePlatform,
|
||||
LicenseResModel,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
@@ -78,10 +75,6 @@ const useUrlQuerySpy = jest.spyOn(useUrlQueryHooks, 'default');
|
||||
const useSafeNavigateSpy = jest.spyOn(navigateHooks, 'useSafeNavigate');
|
||||
const useAppContextSpy = jest.spyOn(appHooks, 'useAppContext');
|
||||
|
||||
const CLOUD_LICENSE = {
|
||||
platform: LicensePlatform.CLOUD,
|
||||
} as LicenseResModel;
|
||||
|
||||
const mockSetUrlQuery = jest.fn();
|
||||
const mockToString = jest.fn();
|
||||
const mockGetUrlQuery = jest.fn();
|
||||
@@ -159,7 +152,7 @@ describe('CreateAlertRule', () => {
|
||||
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render new flow with the anomaly alert type when ruleType is anomaly_rule', () => {
|
||||
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
|
||||
mockGetUrlQuery.mockImplementation((key: string) => {
|
||||
if (key === QueryParams.showClassicCreateAlertsPage) {
|
||||
return 'false';
|
||||
@@ -170,9 +163,8 @@ describe('CreateAlertRule', () => {
|
||||
return null;
|
||||
});
|
||||
render(<CreateAlertRule />);
|
||||
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(AlertTypes.ANOMALY_BASED_ALERT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(FORM_ALERT_RULES_TEXT)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(CREATE_ALERT_V2_TEXT)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should use alertType from URL when provided', () => {
|
||||
@@ -280,7 +272,15 @@ describe('CreateAlertRule', () => {
|
||||
it('should navigate with anomaly detection params for anomaly alert', () => {
|
||||
useAppContextSpy.mockReturnValue({
|
||||
...getAppContextMockState({}),
|
||||
activeLicense: CLOUD_LICENSE,
|
||||
featureFlags: [
|
||||
{
|
||||
name: FeatureKeys.ANOMALY_DETECTION,
|
||||
active: true,
|
||||
usage: 0,
|
||||
usage_limit: -1,
|
||||
route: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<CreateAlertRule />);
|
||||
|
||||
@@ -41,7 +41,7 @@ function CreateRules(): JSX.Element {
|
||||
|
||||
useEffect(() => {
|
||||
if (isTypeSelectionMode) {
|
||||
void logEvent('Alert: New alert data source selection page visited', {});
|
||||
logEvent('Alert: New alert data source selection page visited', {});
|
||||
}
|
||||
}, [isTypeSelectionMode]);
|
||||
|
||||
@@ -100,7 +100,10 @@ function CreateRules(): JSX.Element {
|
||||
return <SelectAlertType onSelect={handleSelectType} />;
|
||||
}
|
||||
|
||||
if (showClassicCreateAlertsPageFlag) {
|
||||
if (
|
||||
showClassicCreateAlertsPageFlag ||
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
:global(.ant-tooltip).tabTooltip {
|
||||
max-width: 360px;
|
||||
}
|
||||
|
||||
.switchNote {
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
@@ -1,30 +1,20 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { Activity, ChartLine } from '@signozhq/icons';
|
||||
import { ChartLine } from '@signozhq/icons';
|
||||
import { useChannelOptions } from 'hooks/notificationChannels/useChannelOptions';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
||||
import Stepper from '../Stepper';
|
||||
import { useIsAnomalyDetectionAvailable } from '../useIsAnomalyDetectionAvailable';
|
||||
import AlertThreshold from './AlertThreshold';
|
||||
import AnomalyThreshold from './AnomalyThreshold';
|
||||
import {
|
||||
ANOMALY_TAB_SWITCH_NOTE,
|
||||
ANOMALY_TAB_TOOLTIP,
|
||||
THRESHOLD_TAB_SWITCH_NOTE,
|
||||
THRESHOLD_TAB_TOOLTIP,
|
||||
} from './constants';
|
||||
import { useAnomalyFunctionSync } from './useAnomalyFunctionSync';
|
||||
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
|
||||
|
||||
import styles from './AlertCondition.module.scss';
|
||||
import './styles.scss';
|
||||
|
||||
function AlertCondition(): JSX.Element {
|
||||
const { alertType, setAlertType } = useCreateAlertState();
|
||||
useAnomalyFunctionSync();
|
||||
|
||||
const {
|
||||
data: channels,
|
||||
@@ -33,12 +23,9 @@ function AlertCondition(): JSX.Element {
|
||||
refetch: refreshChannels,
|
||||
} = useChannelOptions();
|
||||
|
||||
const isAnomalyDetectionEnabled = useIsAnomalyDetectionAvailable();
|
||||
|
||||
// Anomaly rules stay editable even without a cloud or enterprise license.
|
||||
const showMultipleTabs =
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
(isAnomalyDetectionEnabled && alertType === AlertTypes.METRICS_BASED_ALERT);
|
||||
alertType === AlertTypes.METRICS_BASED_ALERT;
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
@@ -46,15 +33,16 @@ function AlertCondition(): JSX.Element {
|
||||
icon: <ChartLine size={14} data-testid="threshold-view" />,
|
||||
value: AlertTypes.METRICS_BASED_ALERT,
|
||||
},
|
||||
...(showMultipleTabs
|
||||
? [
|
||||
{
|
||||
label: 'Anomaly',
|
||||
icon: <Activity size={14} data-testid="anomaly-view" />,
|
||||
value: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// Hide anomaly tab for now
|
||||
// ...(showMultipleTabs
|
||||
// ? [
|
||||
// {
|
||||
// label: 'Anomaly',
|
||||
// icon: <Activity size={14} data-testid="anomaly-view" />,
|
||||
// value: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
// },
|
||||
// ]
|
||||
// : []),
|
||||
];
|
||||
|
||||
const handleAlertTypeChange = (value: AlertTypes): void => {
|
||||
@@ -64,23 +52,11 @@ function AlertCondition(): JSX.Element {
|
||||
setAlertType(value);
|
||||
};
|
||||
|
||||
const getTabTooltip = (tab: { value: AlertTypes }): ReactNode => {
|
||||
const isAnomalyTab = tab.value === AlertTypes.ANOMALY_BASED_ALERT;
|
||||
const description = isAnomalyTab
|
||||
? ANOMALY_TAB_TOOLTIP
|
||||
: THRESHOLD_TAB_TOOLTIP;
|
||||
if (tab.value === alertType) {
|
||||
return description;
|
||||
const getTabTooltip = (tab: { value: AlertTypes }): string => {
|
||||
if (tab.value === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
return ANOMALY_TAB_TOOLTIP;
|
||||
}
|
||||
const switchNote = isAnomalyTab
|
||||
? ANOMALY_TAB_SWITCH_NOTE
|
||||
: THRESHOLD_TAB_SWITCH_NOTE;
|
||||
return (
|
||||
<>
|
||||
<div>{description}</div>
|
||||
<div className={styles.switchNote}>{switchNote}</div>
|
||||
</>
|
||||
);
|
||||
return THRESHOLD_TAB_TOOLTIP;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -89,11 +65,7 @@ function AlertCondition(): JSX.Element {
|
||||
<div className="alert-condition">
|
||||
<div className="alert-condition-tabs">
|
||||
{tabs.map((tab) => (
|
||||
<Tooltip
|
||||
key={tab.value}
|
||||
title={getTabTooltip(tab)}
|
||||
overlayClassName={styles.tabTooltip}
|
||||
>
|
||||
<Tooltip key={tab.value} title={getTabTooltip(tab)}>
|
||||
<Button
|
||||
className={classNames('list-view-tab', 'explorer-view-option', {
|
||||
'active-tab': alertType === tab.value,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Select, Tooltip } from 'antd';
|
||||
import { Button, Select, Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import getRandomColor from 'lib/getRandomColor';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_INFO_THRESHOLD,
|
||||
INITIAL_RANDOM_THRESHOLD,
|
||||
INITIAL_WARNING_THRESHOLD,
|
||||
THRESHOLD_MATCH_TYPE_OPTIONS,
|
||||
THRESHOLD_OPERATOR_OPTIONS,
|
||||
} from '../context/constants';
|
||||
@@ -19,8 +23,7 @@ import {
|
||||
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||
import { normalizeMatchType, normalizeOperator } from '../utils';
|
||||
import ThresholdItem from './ThresholdItem';
|
||||
import { AnomalyAndThresholdProps } from './types';
|
||||
import { useThresholdList } from './useThresholdList';
|
||||
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
|
||||
import {
|
||||
getCategoryByOptionId,
|
||||
getCategorySelectOptionByName,
|
||||
@@ -68,7 +71,42 @@ function AlertThreshold({
|
||||
selectedCategory || '',
|
||||
);
|
||||
|
||||
const { addThreshold, removeThreshold, updateThreshold } = useThresholdList();
|
||||
const addThreshold = (): void => {
|
||||
let newThreshold;
|
||||
if (thresholdState.thresholds.length === 1) {
|
||||
newThreshold = { ...INITIAL_WARNING_THRESHOLD, id: v4() };
|
||||
} else if (thresholdState.thresholds.length === 2) {
|
||||
newThreshold = { ...INITIAL_INFO_THRESHOLD, id: v4() };
|
||||
} else {
|
||||
newThreshold = {
|
||||
...INITIAL_RANDOM_THRESHOLD,
|
||||
id: v4(),
|
||||
color: getRandomColor(),
|
||||
};
|
||||
}
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [...thresholdState.thresholds, newThreshold],
|
||||
});
|
||||
};
|
||||
|
||||
const removeThreshold = (id: string): void => {
|
||||
if (thresholdState.thresholds.length > 1) {
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholdState.thresholds.filter((t) => t.id !== id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateThreshold: UpdateThreshold = (id, field, value) => {
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholdState.thresholds.map((t) =>
|
||||
t.id === id ? { ...t, [field]: value } : t,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const onTooltipOpenChange = (open: boolean): void => {
|
||||
// Stop propagation of click events on tooltip text to dropdown
|
||||
@@ -256,12 +294,11 @@ function AlertThreshold({
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="dashed"
|
||||
color="secondary"
|
||||
size="md"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={(): void => addThreshold()}
|
||||
testId="add-threshold-button"
|
||||
type="dashed"
|
||||
icon={<Plus size={16} />}
|
||||
onClick={addThreshold}
|
||||
className="add-threshold-btn"
|
||||
data-testid="add-threshold-button"
|
||||
>
|
||||
Add Threshold
|
||||
</Button>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.querySelect {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.algorithmSelect,
|
||||
.seasonalitySelect {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.operatorSelect,
|
||||
.matchTypeSelect {
|
||||
width: 180px;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { useMemo } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Plus } from '@signozhq/icons';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
@@ -10,20 +10,19 @@ import {
|
||||
ANOMALY_SEASONALITY_OPTIONS,
|
||||
ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS,
|
||||
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
|
||||
INITIAL_ANOMALY_THRESHOLD,
|
||||
ANOMALY_TIME_DURATION_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from '../context/types';
|
||||
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||
import { normalizeMatchType, normalizeOperator } from '../utils';
|
||||
import ThresholdItem from './ThresholdItem';
|
||||
import { AnomalyAndThresholdProps } from './types';
|
||||
import { useThresholdList } from './useThresholdList';
|
||||
import { getQueryNames, RoutingPolicyBanner } from './utils';
|
||||
|
||||
import styles from './AnomalyThreshold.module.scss';
|
||||
import {
|
||||
getQueryNames,
|
||||
NotificationChannelsNotFoundContent,
|
||||
RoutingPolicyBanner,
|
||||
} from './utils';
|
||||
|
||||
function AnomalyThreshold({
|
||||
channels,
|
||||
@@ -31,6 +30,7 @@ function AnomalyThreshold({
|
||||
isErrorChannels,
|
||||
refreshChannels,
|
||||
}: AnomalyAndThresholdProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const {
|
||||
thresholdState,
|
||||
setThresholdState,
|
||||
@@ -42,13 +42,23 @@ function AnomalyThreshold({
|
||||
|
||||
const queryNames = getQueryNames(currentQuery);
|
||||
|
||||
const { addThreshold, removeThreshold, updateThreshold } = useThresholdList();
|
||||
const deviationOptions = useMemo(() => {
|
||||
const options = [];
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
options.push({ label: i.toString(), value: i });
|
||||
}
|
||||
return options;
|
||||
}, []);
|
||||
|
||||
const handleAddThreshold = (): void => {
|
||||
addThreshold({
|
||||
thresholdValue: Math.max(
|
||||
1,
|
||||
INITIAL_ANOMALY_THRESHOLD.thresholdValue - thresholdState.thresholds.length,
|
||||
const updateThreshold = (
|
||||
id: string,
|
||||
field: string,
|
||||
value: string | string[],
|
||||
): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholdState.thresholds.map((t) =>
|
||||
t.id === id ? { ...t, [field]: value } : t,
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -59,12 +69,11 @@ function AnomalyThreshold({
|
||||
{/* Sentence 1 */}
|
||||
<div className="alert-condition-sentence">
|
||||
<Typography.Text data-testid="notification-text" className="sentence-text">
|
||||
Send a notification when
|
||||
Send notification when the observed value for
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={thresholdState.selectedQuery}
|
||||
data-testid="query-select"
|
||||
className={styles.querySelect}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_SELECTED_QUERY',
|
||||
@@ -73,16 +82,50 @@ function AnomalyThreshold({
|
||||
}}
|
||||
options={queryNames}
|
||||
/>
|
||||
<Typography.Text
|
||||
data-testid="evaluation-window-text"
|
||||
className="sentence-text"
|
||||
>
|
||||
during the last
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={thresholdState.evaluationWindow}
|
||||
data-testid="evaluation-window-select"
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_EVALUATION_WINDOW',
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
options={ANOMALY_TIME_DURATION_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<div className="alert-condition-sentence">
|
||||
{/* Sentence 2 */}
|
||||
<Typography.Text data-testid="threshold-text" className="sentence-text">
|
||||
is
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={thresholdState.thresholds[0].thresholdValue}
|
||||
data-testid="threshold-value-select"
|
||||
onChange={(value): void => {
|
||||
updateThreshold(
|
||||
thresholdState.thresholds[0].id,
|
||||
'thresholdValue',
|
||||
value.toString(),
|
||||
);
|
||||
}}
|
||||
options={deviationOptions}
|
||||
/>
|
||||
<Typography.Text data-testid="deviations-text" className="sentence-text">
|
||||
deviations
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={
|
||||
(normalizeOperator(thresholdState.operator) ??
|
||||
thresholdState.operator) as AlertThresholdOperator
|
||||
}
|
||||
data-testid="operator-select"
|
||||
className={styles.operatorSelect}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_OPERATOR',
|
||||
@@ -95,7 +138,7 @@ function AnomalyThreshold({
|
||||
data-testid="predicted-data-text"
|
||||
className="sentence-text"
|
||||
>
|
||||
the prediction
|
||||
the predicted data
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={
|
||||
@@ -103,7 +146,6 @@ function AnomalyThreshold({
|
||||
thresholdState.matchType) as AlertThresholdMatchType
|
||||
}
|
||||
data-testid="match-type-select"
|
||||
className={styles.matchTypeSelect}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_MATCH_TYPE',
|
||||
@@ -112,22 +154,15 @@ function AnomalyThreshold({
|
||||
}}
|
||||
options={ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||
/>
|
||||
<Typography.Text
|
||||
data-testid="evaluation-window-text"
|
||||
className="sentence-text"
|
||||
>
|
||||
during the <EvaluationSettings rollingOnly />
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{/* Sentence 3 */}
|
||||
<div className="alert-condition-sentence">
|
||||
{/* Sentence 2 */}
|
||||
<Typography.Text data-testid="using-the-text" className="sentence-text">
|
||||
using the
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={thresholdState.algorithm}
|
||||
data-testid="algorithm-select"
|
||||
className={styles.algorithmSelect}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_ALGORITHM',
|
||||
@@ -145,7 +180,6 @@ function AnomalyThreshold({
|
||||
<Select
|
||||
value={thresholdState.seasonality}
|
||||
data-testid="seasonality-select"
|
||||
className={styles.seasonalitySelect}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_SEASONALITY',
|
||||
@@ -154,39 +188,52 @@ function AnomalyThreshold({
|
||||
}}
|
||||
options={ANOMALY_SEASONALITY_OPTIONS}
|
||||
/>
|
||||
<Typography.Text data-testid="seasonality-text" className="sentence-text">
|
||||
seasonality
|
||||
</Typography.Text>
|
||||
{notificationSettings.routingPolicies ? (
|
||||
<>
|
||||
<Typography.Text
|
||||
data-testid="seasonality-text"
|
||||
className="sentence-text"
|
||||
>
|
||||
seasonality to
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={thresholdState.thresholds[0].channels}
|
||||
onChange={(value): void =>
|
||||
updateThreshold(thresholdState.thresholds[0].id, 'channels', value)
|
||||
}
|
||||
style={{ width: 350 }}
|
||||
options={channels.map((channel) => ({
|
||||
value: channel.id,
|
||||
label: channel.name,
|
||||
}))}
|
||||
mode="multiple"
|
||||
placeholder="Select notification channels"
|
||||
showSearch
|
||||
maxTagCount={2}
|
||||
maxTagPlaceholder={(omittedValues): string =>
|
||||
`+${omittedValues.length} more`
|
||||
}
|
||||
maxTagTextLength={10}
|
||||
filterOption={(input, option): boolean =>
|
||||
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
|
||||
}
|
||||
status={isErrorChannels ? 'error' : undefined}
|
||||
disabled={isLoadingChannels}
|
||||
notFoundContent={
|
||||
<NotificationChannelsNotFoundContent
|
||||
user={user}
|
||||
refreshChannels={refreshChannels}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text data-testid="seasonality-text" className="sentence-text">
|
||||
seasonality
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Sentence 3 */}
|
||||
<div className="thresholds-section">
|
||||
{thresholdState.thresholds.map((threshold, index) => (
|
||||
<ThresholdItem
|
||||
key={threshold.id}
|
||||
threshold={threshold}
|
||||
updateThreshold={updateThreshold}
|
||||
removeThreshold={removeThreshold}
|
||||
showRemoveButton={index !== 0 && thresholdState.thresholds.length > 1}
|
||||
channels={channels}
|
||||
isLoadingChannels={isLoadingChannels}
|
||||
units={[]}
|
||||
isErrorChannels={isErrorChannels}
|
||||
refreshChannels={refreshChannels}
|
||||
isAnomaly
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
variant="dashed"
|
||||
color="secondary"
|
||||
size="md"
|
||||
prefix={<Plus size={14} />}
|
||||
onClick={handleAddThreshold}
|
||||
testId="add-threshold-button"
|
||||
>
|
||||
Add Threshold
|
||||
</Button>
|
||||
</div>
|
||||
<RoutingPolicyBanner
|
||||
notificationSettings={notificationSettings}
|
||||
setNotificationSettings={setNotificationSettings}
|
||||
|
||||
@@ -21,7 +21,6 @@ function ThresholdItem({
|
||||
isErrorChannels,
|
||||
refreshChannels,
|
||||
isLoadingChannels,
|
||||
isAnomaly = false,
|
||||
}: ThresholdItemProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { thresholdState, notificationSettings } = useCreateAlertState();
|
||||
@@ -105,16 +104,10 @@ function ThresholdItem({
|
||||
style={{ width: 200 }}
|
||||
data-testid="threshold-name-input"
|
||||
/>
|
||||
{isAnomaly ? (
|
||||
<Typography.Text className="sentence-text">at</Typography.Text>
|
||||
) : (
|
||||
<>
|
||||
<Typography.Text className="sentence-text">on value</Typography.Text>
|
||||
<Typography.Text className="sentence-text highlighted-text">
|
||||
{getOperatorSymbol()}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
<Typography.Text className="sentence-text">on value</Typography.Text>
|
||||
<Typography.Text className="sentence-text highlighted-text">
|
||||
{getOperatorSymbol()}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder="Enter threshold value"
|
||||
value={threshold.thresholdValue}
|
||||
@@ -125,11 +118,7 @@ function ThresholdItem({
|
||||
type="number"
|
||||
data-testid="threshold-value-input"
|
||||
/>
|
||||
{isAnomaly ? (
|
||||
<Typography.Text className="sentence-text">deviations</Typography.Text>
|
||||
) : (
|
||||
yAxisUnitSelect
|
||||
)}
|
||||
{yAxisUnitSelect}
|
||||
{!notificationSettings.routingPolicies && (
|
||||
<>
|
||||
<Typography.Text className="sentence-text">send to</Typography.Text>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IAppContext } from 'providers/App/types';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import AlertCondition from '../AlertCondition';
|
||||
@@ -65,55 +65,54 @@ jest.mock('../AnomalyThreshold', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const mockHandleSetQueryData: jest.MockedFunction<
|
||||
QueryBuilderContextType['handleSetQueryData']
|
||||
> = jest.fn();
|
||||
const mockRedirectWithQueryBuilderData: jest.MockedFunction<
|
||||
QueryBuilderContextType['redirectWithQueryBuilderData']
|
||||
> = jest.fn();
|
||||
const mockCurrentQuery = {
|
||||
dataSource: 'METRICS',
|
||||
queryName: 'A',
|
||||
builder: {
|
||||
queryData: [{ dataSource: 'METRICS' }],
|
||||
queryFormulas: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Mock useQueryBuilder hook
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: (): unknown => ({
|
||||
currentQuery: mockCurrentQuery,
|
||||
redirectWithQueryBuilderData: mockRedirectWithQueryBuilderData,
|
||||
handleSetQueryData: mockHandleSetQueryData,
|
||||
useQueryBuilder: (): {
|
||||
currentQuery: {
|
||||
builder: { queryData: unknown[]; queryFormulas: unknown[] };
|
||||
dataSource: string;
|
||||
queryName: string;
|
||||
};
|
||||
redirectWithQueryBuilderData: () => void;
|
||||
} => ({
|
||||
currentQuery: {
|
||||
dataSource: 'METRICS',
|
||||
queryName: 'A',
|
||||
builder: {
|
||||
queryData: [{ dataSource: 'METRICS' }],
|
||||
queryFormulas: [],
|
||||
},
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const createTestQueryClient = (): QueryClient =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderAlertCondition = (
|
||||
alertType?: string,
|
||||
appContextOverrides?: Partial<IAppContext>,
|
||||
): ReturnType<typeof render> =>
|
||||
render(
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<AlertCondition />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: alertType ? `/?alertType=${alertType}` : '/',
|
||||
appContextOverrides,
|
||||
},
|
||||
): ReturnType<typeof render> => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const initialEntries = alertType ? [`/?alertType=${alertType}`] : undefined;
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<AlertCondition />
|
||||
</CreateAlertProvider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
const clickText = async (text: string): Promise<void> => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByText(text));
|
||||
};
|
||||
|
||||
describe('AlertCondition', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders the stepper with correct step number and label', () => {
|
||||
renderAlertCondition();
|
||||
expect(screen.getByTestId(STEPPER_TEST_ID)).toHaveTextContent(
|
||||
@@ -126,9 +125,10 @@ describe('AlertCondition', () => {
|
||||
|
||||
// Verify default alertType is METRICS_BASED_ALERT (shows AlertThreshold component)
|
||||
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// expect(
|
||||
// screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
|
||||
// ).not.toBeInTheDocument();
|
||||
|
||||
// Verify threshold tab is active by default
|
||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||
@@ -136,7 +136,8 @@ describe('AlertCondition', () => {
|
||||
|
||||
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders threshold tab by default', () => {
|
||||
@@ -151,7 +152,8 @@ describe('AlertCondition', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders anomaly tab when alert type supports multiple tabs', () => {
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('renders anomaly tab when alert type supports multiple tabs', () => {
|
||||
renderAlertCondition();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
@@ -165,47 +167,36 @@ describe('AlertCondition', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('warns in the tab tooltip that switching changes what gets saved', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderAlertCondition();
|
||||
|
||||
await user.hover(screen.getByText(ANOMALY_TAB_TEXT));
|
||||
await expect(
|
||||
screen.findByText(/saves this as an anomaly-based alert/),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByText(ANOMALY_TAB_TEXT));
|
||||
await user.hover(screen.getByText(THRESHOLD_TAB_TEXT));
|
||||
await expect(
|
||||
screen.findByText(/saves this as a metrics-based alert/),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows AnomalyThreshold component when alert type is anomaly based', async () => {
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('shows AnomalyThreshold component when alert type is anomaly based', () => {
|
||||
renderAlertCondition();
|
||||
|
||||
// Click on anomaly tab to switch to anomaly-based alert
|
||||
await clickText(ANOMALY_TAB_TEXT);
|
||||
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
|
||||
fireEvent.click(anomalyTab);
|
||||
|
||||
expect(screen.getByTestId(ANOMALY_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('switches between threshold and anomaly tabs', async () => {
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('switches between threshold and anomaly tabs', () => {
|
||||
renderAlertCondition();
|
||||
|
||||
// Initially shows threshold component
|
||||
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||
|
||||
// Click anomaly tab
|
||||
await clickText(ANOMALY_TAB_TEXT);
|
||||
const anomalyTab = screen.getByText(ANOMALY_TAB_TEXT);
|
||||
fireEvent.click(anomalyTab);
|
||||
|
||||
// Should show anomaly component
|
||||
expect(screen.getByTestId(ANOMALY_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
|
||||
|
||||
// Click threshold tab
|
||||
await clickText(THRESHOLD_TAB_TEXT);
|
||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||
fireEvent.click(thresholdTab);
|
||||
|
||||
// Should show threshold component again
|
||||
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||
@@ -214,40 +205,8 @@ describe('AlertCondition', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds the anomaly function with the default z-score when switching to anomaly', async () => {
|
||||
renderAlertCondition();
|
||||
expect(mockHandleSetQueryData).not.toHaveBeenCalled();
|
||||
|
||||
await clickText(ANOMALY_TAB_TEXT);
|
||||
|
||||
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
|
||||
0,
|
||||
expect.objectContaining({
|
||||
functions: [
|
||||
{
|
||||
name: 'anomaly',
|
||||
args: [],
|
||||
namedArgs: { z_score_threshold: 3, seasonality: 'hourly' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the current query when switching between threshold and anomaly', async () => {
|
||||
renderAlertCondition();
|
||||
|
||||
await clickText(ANOMALY_TAB_TEXT);
|
||||
|
||||
expect(mockRedirectWithQueryBuilderData).toHaveBeenCalledWith(
|
||||
mockCurrentQuery,
|
||||
{ alertType: AlertTypes.ANOMALY_BASED_ALERT, ruleType: 'anomaly_rule' },
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('applies active tab styling correctly', async () => {
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('applies active tab styling correctly', () => {
|
||||
renderAlertCondition();
|
||||
|
||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||
@@ -258,7 +217,7 @@ describe('AlertCondition', () => {
|
||||
expect(anomalyTab.closest(ACTIVE_TAB_CLASS)).not.toBeInTheDocument();
|
||||
|
||||
// Click anomaly tab
|
||||
await clickText(ANOMALY_TAB_TEXT);
|
||||
fireEvent.click(anomalyTab);
|
||||
|
||||
// Anomaly tab should be active now
|
||||
expect(anomalyTab.closest(ACTIVE_TAB_CLASS)).toBeInTheDocument();
|
||||
@@ -268,17 +227,11 @@ describe('AlertCondition', () => {
|
||||
it('shows multiple tabs for METRICS_BASED_ALERT', () => {
|
||||
renderAlertCondition('METRIC_BASED_ALERT');
|
||||
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the anomaly tab for METRICS_BASED_ALERT on the community edition', () => {
|
||||
renderAlertCondition('METRIC_BASED_ALERT', { activeLicense: null });
|
||||
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.queryByText(ANOMALY_TAB_TEXT)).not.toBeInTheDocument();
|
||||
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows multiple tabs for ANOMALY_BASED_ALERT', () => {
|
||||
@@ -286,8 +239,9 @@ describe('AlertCondition', () => {
|
||||
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows only threshold tab for LOGS_BASED_ALERT', () => {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { render, screen, userEvent, within } from 'tests/test-utils';
|
||||
import { ChannelOption } from 'hooks/notificationChannels/useChannelOptions';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
|
||||
import * as context from '../../context';
|
||||
import { AlertThresholdMatchType } from '../../context/types';
|
||||
import AnomalyThreshold from '../AnomalyThreshold';
|
||||
|
||||
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
@@ -20,11 +22,12 @@ jest.mock('uplot', () => {
|
||||
|
||||
const mockSetAlertState = jest.fn();
|
||||
const mockSetThresholdState = jest.fn();
|
||||
const mockContextState = createMockAlertContextState({
|
||||
setThresholdState: mockSetThresholdState,
|
||||
setAlertState: mockSetAlertState,
|
||||
});
|
||||
const useCreateAlertStateSpy = jest.spyOn(context, 'useCreateAlertState');
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
setThresholdState: mockSetThresholdState,
|
||||
setAlertState: mockSetAlertState,
|
||||
}),
|
||||
);
|
||||
|
||||
// Mock useQueryBuilder hook
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -49,12 +52,10 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const channels: ChannelOption[] = [{ id: '1', name: 'slack-alerts' }];
|
||||
|
||||
const renderAnomalyThreshold = (): ReturnType<typeof render> =>
|
||||
render(
|
||||
<AnomalyThreshold
|
||||
channels={channels}
|
||||
channels={[]}
|
||||
isLoadingChannels={false}
|
||||
isErrorChannels={false}
|
||||
refreshChannels={jest.fn()}
|
||||
@@ -64,117 +65,31 @@ const renderAnomalyThreshold = (): ReturnType<typeof render> =>
|
||||
describe('AnomalyThreshold', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useCreateAlertStateSpy.mockReturnValue(mockContextState);
|
||||
});
|
||||
|
||||
const pickOption = async (testId: string, title: string): Promise<void> => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(within(screen.getByTestId(testId)).getByRole('combobox'));
|
||||
await user.click(await screen.findByTitle(title));
|
||||
};
|
||||
|
||||
it('renders the condition sentence in the same order as threshold alerts', () => {
|
||||
it('renders the first condition sentence', () => {
|
||||
renderAnomalyThreshold();
|
||||
expect(screen.getByTestId('notification-text')).toHaveTextContent(
|
||||
'Send a notification when',
|
||||
);
|
||||
expect(screen.getByTestId('query-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('notification-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('evaluation-window-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('evaluation-window-select')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the second condition sentence', () => {
|
||||
renderAnomalyThreshold();
|
||||
expect(screen.getByTestId('threshold-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('threshold-value-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('deviations-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('operator-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('predicted-data-text')).toHaveTextContent(
|
||||
'the prediction',
|
||||
);
|
||||
expect(screen.getByTestId('predicted-data-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('match-type-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('evaluation-settings-button')).toHaveTextContent(
|
||||
'Rolling',
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the algorithm and seasonality sentence', () => {
|
||||
it('renders the third condition sentence', () => {
|
||||
renderAnomalyThreshold();
|
||||
expect(screen.getByTestId('using-the-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('algorithm-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('algorithm-with-text')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('seasonality-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('seasonality-text')).toHaveTextContent(
|
||||
'seasonality',
|
||||
);
|
||||
});
|
||||
|
||||
it('renames the severity from its threshold row', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderAnomalyThreshold();
|
||||
await user.type(screen.getByTestId('threshold-name-input'), 'x');
|
||||
expect(mockSetThresholdState).toHaveBeenCalledWith({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [expect.objectContaining({ label: 'criticalx' })],
|
||||
});
|
||||
});
|
||||
|
||||
it('takes a typed deviation', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
useCreateAlertStateSpy.mockReturnValue({
|
||||
...mockContextState,
|
||||
thresholdState: {
|
||||
...mockContextState.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...mockContextState.thresholdState.thresholds[0],
|
||||
thresholdValue: '' as unknown as number,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
renderAnomalyThreshold();
|
||||
await user.type(screen.getByTestId('threshold-value-input'), '4');
|
||||
expect(mockSetThresholdState).toHaveBeenCalledWith({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [expect.objectContaining({ thresholdValue: '4' })],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['ON AVERAGE', AlertThresholdMatchType.ON_AVERAGE],
|
||||
['LAST', AlertThresholdMatchType.LAST],
|
||||
])('offers the %s match type', async (title, matchType) => {
|
||||
renderAnomalyThreshold();
|
||||
await pickOption('match-type-select', title);
|
||||
expect(mockSetThresholdState).toHaveBeenCalledWith({
|
||||
type: 'SET_MATCH_TYPE',
|
||||
payload: matchType,
|
||||
});
|
||||
});
|
||||
|
||||
it('picks channels by name when routing policies are off', async () => {
|
||||
renderAnomalyThreshold();
|
||||
await pickOption('threshold-notification-channel-select', 'slack-alerts');
|
||||
expect(mockSetThresholdState).toHaveBeenCalledWith({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [expect.objectContaining({ channels: ['slack-alerts'] })],
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the next severity one deviation lower', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderAnomalyThreshold();
|
||||
await user.click(screen.getByTestId('add-threshold-button'));
|
||||
expect(mockSetThresholdState).toHaveBeenCalledWith({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [
|
||||
mockContextState.thresholdState.thresholds[0],
|
||||
expect.objectContaining({ label: 'warning', thresholdValue: 2 }),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the channel picker when routing policies are on', () => {
|
||||
useCreateAlertStateSpy.mockReturnValue({
|
||||
...mockContextState,
|
||||
notificationSettings: {
|
||||
...mockContextState.notificationSettings,
|
||||
routingPolicies: true,
|
||||
},
|
||||
});
|
||||
renderAnomalyThreshold();
|
||||
expect(
|
||||
screen.queryByTestId('threshold-notification-channel-select'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('seasonality-text')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -149,14 +149,6 @@ describe('ThresholdItem', () => {
|
||||
expect(valueInput).toHaveValue(100);
|
||||
});
|
||||
|
||||
it('asks for deviations instead of a unit for anomaly alerts', () => {
|
||||
renderThresholdItem({ isAnomaly: true });
|
||||
|
||||
expect(screen.getByText('deviations')).toBeInTheDocument();
|
||||
expect(screen.queryByText('on value')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('threshold-unit-select')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders unit selector with correct value', () => {
|
||||
renderThresholdItem();
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from '../../context/constants';
|
||||
import { Threshold } from '../../context/types';
|
||||
|
||||
import { getSmallestDeviation, syncAnomalyFunction } from '../utils';
|
||||
|
||||
const timeShift: QueryFunction = { name: 'timeShift', args: [{ value: 3600 }] };
|
||||
const anomaly = (
|
||||
zScoreThreshold: number,
|
||||
seasonality: string,
|
||||
): QueryFunction => ({
|
||||
name: 'anomaly',
|
||||
args: [],
|
||||
namedArgs: { z_score_threshold: zScoreThreshold, seasonality },
|
||||
});
|
||||
|
||||
describe('syncAnomalyFunction', () => {
|
||||
it('adds the anomaly function after the existing functions', () => {
|
||||
expect(
|
||||
syncAnomalyFunction([timeShift], {
|
||||
z_score_threshold: 3,
|
||||
seasonality: 'daily',
|
||||
}),
|
||||
).toStrictEqual([timeShift, anomaly(3, 'daily')]);
|
||||
});
|
||||
|
||||
it('replaces stale args and gives the same result when run again', () => {
|
||||
const namedArgs = { z_score_threshold: 5, seasonality: 'weekly' };
|
||||
const synced = syncAnomalyFunction(
|
||||
[anomaly(3, 'hourly'), timeShift],
|
||||
namedArgs,
|
||||
);
|
||||
expect(synced).toStrictEqual([timeShift, anomaly(5, 'weekly')]);
|
||||
expect(syncAnomalyFunction(synced, namedArgs)).toStrictEqual(synced);
|
||||
});
|
||||
|
||||
it('removes the anomaly function when no args are given', () => {
|
||||
expect(
|
||||
syncAnomalyFunction([anomaly(3, 'daily'), timeShift], undefined),
|
||||
).toStrictEqual([timeShift]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSmallestDeviation', () => {
|
||||
it.each([
|
||||
[[3, 2], 2],
|
||||
[['2.5', 4], 2.5],
|
||||
[[3, '', 0], 3],
|
||||
[[''], 0],
|
||||
])('picks the smallest valid deviation from %p', (values, expected) => {
|
||||
const thresholds = values.map(
|
||||
(thresholdValue) =>
|
||||
({ ...INITIAL_CRITICAL_THRESHOLD, thresholdValue }) as Threshold,
|
||||
);
|
||||
expect(getSmallestDeviation(thresholds)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -4,11 +4,5 @@ export const THRESHOLD_TAB_TOOLTIP =
|
||||
export const ANOMALY_TAB_TOOLTIP =
|
||||
'An alert is triggered whenever the metric deviates from an expected pattern.';
|
||||
|
||||
export const THRESHOLD_TAB_SWITCH_NOTE =
|
||||
'Switching keeps your query but saves this as a metrics-based alert, not an anomaly alert.';
|
||||
|
||||
export const ANOMALY_TAB_SWITCH_NOTE =
|
||||
'Switching keeps your query but saves this as an anomaly-based alert. Anomaly detection works on Query Builder metric queries only.';
|
||||
|
||||
export const ROUTING_POLICIES_ROUTE =
|
||||
'/alerts?tab=Configuration&subTab=routing-policies';
|
||||
|
||||
@@ -90,6 +90,8 @@
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
width: 240px;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
@@ -253,6 +255,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-threshold-btn {
|
||||
margin-top: 8px;
|
||||
border: 1px dashed var(--l1-border);
|
||||
color: var(--l2-foreground);
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
height: 32px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.anticon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.routing-policies-info-banner {
|
||||
@@ -290,6 +314,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.anomaly-threshold-container {
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.condensed-alert-threshold-container,
|
||||
.condensed-anomaly-threshold-container {
|
||||
width: 100%;
|
||||
|
||||
@@ -26,13 +26,6 @@ export interface ThresholdItemProps {
|
||||
units: DefaultOptionType[];
|
||||
isErrorChannels: boolean;
|
||||
refreshChannels: () => void;
|
||||
isAnomaly?: boolean;
|
||||
}
|
||||
|
||||
export interface ThresholdList {
|
||||
addThreshold: (overrides?: Partial<Threshold>) => void;
|
||||
removeThreshold: (thresholdId: string) => void;
|
||||
updateThreshold: UpdateThreshold;
|
||||
}
|
||||
|
||||
export interface AnomalyAndThresholdProps {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import { getSmallestDeviation, syncAnomalyFunction } from './utils';
|
||||
|
||||
// These args only drive the preview; the rule reads the threshold target and condition.seasonality.
|
||||
export function useAnomalyFunctionSync(): void {
|
||||
const { alertType, thresholdState } = useCreateAlertState();
|
||||
const { currentQuery, handleSetQueryData } = useQueryBuilder();
|
||||
|
||||
const isAnomalyAlert = alertType === AlertTypes.ANOMALY_BASED_ALERT;
|
||||
const zScoreThreshold = getSmallestDeviation(thresholdState.thresholds);
|
||||
const { seasonality } = thresholdState;
|
||||
|
||||
useEffect(() => {
|
||||
const namedArgs = isAnomalyAlert
|
||||
? { z_score_threshold: zScoreThreshold, seasonality }
|
||||
: undefined;
|
||||
currentQuery.builder.queryData.forEach((queryData, index) => {
|
||||
const currentFunctions = queryData.functions || [];
|
||||
const functions = syncAnomalyFunction(currentFunctions, namedArgs);
|
||||
if (!isEqual(functions, currentFunctions)) {
|
||||
handleSetQueryData(index, { ...queryData, functions });
|
||||
}
|
||||
});
|
||||
}, [
|
||||
currentQuery.builder.queryData,
|
||||
isAnomalyAlert,
|
||||
zScoreThreshold,
|
||||
seasonality,
|
||||
handleSetQueryData,
|
||||
]);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import getRandomColor from 'lib/getRandomColor';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
INITIAL_INFO_THRESHOLD,
|
||||
INITIAL_RANDOM_THRESHOLD,
|
||||
INITIAL_WARNING_THRESHOLD,
|
||||
} from '../context/constants';
|
||||
import { Threshold } from '../context/types';
|
||||
import { ThresholdList, UpdateThreshold } from './types';
|
||||
|
||||
export function useThresholdList(): ThresholdList {
|
||||
const { thresholdState, setThresholdState } = useCreateAlertState();
|
||||
const { thresholds } = thresholdState;
|
||||
|
||||
const addThreshold = (overrides?: Partial<Threshold>): void => {
|
||||
let newThreshold;
|
||||
if (thresholds.length === 1) {
|
||||
newThreshold = { ...INITIAL_WARNING_THRESHOLD, id: v4() };
|
||||
} else if (thresholds.length === 2) {
|
||||
newThreshold = { ...INITIAL_INFO_THRESHOLD, id: v4() };
|
||||
} else {
|
||||
newThreshold = {
|
||||
...INITIAL_RANDOM_THRESHOLD,
|
||||
id: v4(),
|
||||
color: getRandomColor(),
|
||||
};
|
||||
}
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [...thresholds, { ...newThreshold, ...overrides }],
|
||||
});
|
||||
};
|
||||
|
||||
const removeThreshold = (id: string): void => {
|
||||
if (thresholds.length > 1) {
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholds.filter((t) => t.id !== id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateThreshold: UpdateThreshold = (id, field, value) => {
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholds.map((t) => (t.id === id ? { ...t, [field]: value } : t)),
|
||||
});
|
||||
};
|
||||
|
||||
return { addThreshold, removeThreshold, updateThreshold };
|
||||
}
|
||||
@@ -15,13 +15,11 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { ArrowRight } from '@signozhq/icons';
|
||||
import { IUser } from 'providers/App/types';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { ROUTING_POLICIES_ROUTE } from './constants';
|
||||
import { Threshold } from '../context/types';
|
||||
import { RoutingPolicyBannerProps } from './types';
|
||||
|
||||
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
@@ -45,24 +43,6 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
}
|
||||
|
||||
export function getSmallestDeviation(thresholds: Threshold[]): number {
|
||||
const deviations = thresholds
|
||||
.map(({ thresholdValue }) => Number(thresholdValue))
|
||||
.filter((deviation) => deviation > 0);
|
||||
return deviations.length > 0 ? Math.min(...deviations) : 0;
|
||||
}
|
||||
|
||||
export function syncAnomalyFunction(
|
||||
functions: QueryFunction[],
|
||||
namedArgs: QueryFunction['namedArgs'],
|
||||
): QueryFunction[] {
|
||||
const otherFunctions = functions.filter(({ name }) => name !== 'anomaly');
|
||||
if (!namedArgs) {
|
||||
return otherFunctions;
|
||||
}
|
||||
return [...otherFunctions, { name: 'anomaly', args: [], namedArgs }];
|
||||
}
|
||||
|
||||
export function getCategoryByOptionId(id: string): string | undefined {
|
||||
const categories = getYAxisCategories(YAxisSource.ALERTS);
|
||||
return categories.find((category) =>
|
||||
@@ -243,22 +223,17 @@ function TooltipExample({
|
||||
thresholdValue: number;
|
||||
matchType: AlertThresholdMatchType;
|
||||
}): JSX.Element {
|
||||
const pointsLabel =
|
||||
matchType === AlertThresholdMatchType.IN_TOTAL
|
||||
? 'error counts'
|
||||
: 'data points';
|
||||
return (
|
||||
<div className="tooltip-example">
|
||||
<strong>Example:</strong>
|
||||
<br />
|
||||
<span className="translate-safe">
|
||||
{`Say, For a 5-minute window (configured in Evaluation settings), 1 min aggregation interval (set up in query) → 5 ${pointsLabel}: [${dataPoints.join(', ')}]`}
|
||||
</span>
|
||||
<br />
|
||||
<span className="translate-safe">
|
||||
{`With threshold ${operatorSymbol} ${thresholdValue}: `}
|
||||
</span>
|
||||
{children}
|
||||
Say, For a 5-minute window (configured in Evaluation settings), 1 min
|
||||
aggregation interval (set up in query) → 5{' '}
|
||||
{matchType === AlertThresholdMatchType.IN_TOTAL
|
||||
? 'error counts'
|
||||
: 'data points'}
|
||||
: [{dataPoints.join(', ')}]<br />
|
||||
With threshold {operatorSymbol} {thresholdValue}: {children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,11 @@ import { ChevronDown, ChevronUp } from '@signozhq/icons';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import EvaluationWindowPopover from './EvaluationWindowPopover';
|
||||
import { IEvaluationSettingsProps } from './types';
|
||||
import { getEvaluationWindowTypeText, getTimeframeText } from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
function EvaluationSettings({
|
||||
rollingOnly = false,
|
||||
}: IEvaluationSettingsProps): JSX.Element {
|
||||
function EvaluationSettings(): JSX.Element {
|
||||
const { evaluationWindow, setEvaluationWindow } = useCreateAlertState();
|
||||
|
||||
const [isEvaluationWindowPopoverOpen, setIsEvaluationWindowPopoverOpen] =
|
||||
@@ -27,7 +24,6 @@ function EvaluationSettings({
|
||||
<EvaluationWindowPopover
|
||||
evaluationWindow={evaluationWindow}
|
||||
setEvaluationWindow={setEvaluationWindow}
|
||||
rollingOnly={rollingOnly}
|
||||
/>
|
||||
}
|
||||
trigger="click"
|
||||
|
||||
@@ -20,12 +20,7 @@ import { useKeyboardNavigationForEvaluationWindowPopover } from './useKeyboardNa
|
||||
function EvaluationWindowPopover({
|
||||
evaluationWindow,
|
||||
setEvaluationWindow,
|
||||
rollingOnly = false,
|
||||
}: IEvaluationWindowPopoverProps): JSX.Element {
|
||||
const windowTypeOptions = rollingOnly
|
||||
? EVALUATION_WINDOW_TYPE.filter((option) => option.value === 'rolling')
|
||||
: EVALUATION_WINDOW_TYPE;
|
||||
|
||||
const { containerRef, firstItemRef } =
|
||||
useKeyboardNavigationForEvaluationWindowPopover({
|
||||
onSelect: (value: string, sectionId: string): void => {
|
||||
@@ -141,7 +136,7 @@ function EvaluationWindowPopover({
|
||||
<div className="evaluation-window-content">
|
||||
{renderEvaluationWindowContent(
|
||||
'EVALUATION WINDOW',
|
||||
windowTypeOptions,
|
||||
EVALUATION_WINDOW_TYPE,
|
||||
evaluationWindow.windowType,
|
||||
(value: string): void =>
|
||||
setEvaluationWindow({
|
||||
|
||||
@@ -39,18 +39,6 @@ describe('EvaluationWindowPopover', () => {
|
||||
expect(screen.getByText(EVALUATION_WINDOW_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('offers only rolling windows when rollingOnly is set', () => {
|
||||
render(
|
||||
<EvaluationWindowPopover
|
||||
evaluationWindow={mockEvaluationWindow}
|
||||
setEvaluationWindow={mockSetEvaluationWindow}
|
||||
rollingOnly
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Rolling')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Cumulative')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all window type options with rolling selected', () => {
|
||||
render(
|
||||
<EvaluationWindowPopover
|
||||
|
||||
@@ -31,14 +31,9 @@ export enum CumulativeWindowTimeframes {
|
||||
CURRENT_MONTH = 'currentMonth',
|
||||
}
|
||||
|
||||
export interface IEvaluationSettingsProps {
|
||||
rollingOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface IEvaluationWindowPopoverProps {
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||
rollingOnly?: boolean;
|
||||
}
|
||||
|
||||
export interface IEvaluationWindowDetailsProps {
|
||||
|
||||
@@ -13,7 +13,10 @@ import APIError from 'types/api/error';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import { buildCreateAlertRulePayload, validateCreateAlertState } from './utils';
|
||||
import {
|
||||
buildCreateThresholdAlertRulePayload,
|
||||
validateCreateAlertState,
|
||||
} from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
import {
|
||||
@@ -81,7 +84,7 @@ function Footer(): JSX.Element {
|
||||
);
|
||||
|
||||
const handleTestNotification = useCallback((): void => {
|
||||
const payload = buildCreateAlertRulePayload({
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
@@ -114,12 +117,11 @@ function Footer(): JSX.Element {
|
||||
notificationSettings,
|
||||
currentQuery,
|
||||
testAlertRule,
|
||||
handleApiError,
|
||||
]);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const handleSaveAlert = useCallback((): void => {
|
||||
const payload = buildCreateAlertRulePayload({
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
@@ -171,7 +173,6 @@ function Footer(): JSX.Element {
|
||||
createAlertRule,
|
||||
safeNavigate,
|
||||
handleApiError,
|
||||
queryClient,
|
||||
]);
|
||||
|
||||
const isSavingAlertRule = isCreatingAlertRule || isUpdatingAlertRule;
|
||||
|
||||
@@ -48,6 +48,7 @@ const mockAlertContextState = createMockAlertContextState({
|
||||
selectedQuery: 'A',
|
||||
operator: AlertThresholdOperator.ABOVE_BELOW,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
evaluationWindow: '5m0s',
|
||||
algorithm: 'standard',
|
||||
seasonality: 'hourly',
|
||||
thresholds: [
|
||||
|
||||
@@ -9,10 +9,8 @@ import {
|
||||
} from 'container/CreateAlertV2/context/constants';
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertThresholdOperator,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
Seasonality,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -20,7 +18,6 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { BuildCreateAlertRulePayloadArgs } from '../types';
|
||||
import {
|
||||
buildCreateAlertRulePayload,
|
||||
buildCreateThresholdAlertRulePayload,
|
||||
getAlertOnAbsentProps,
|
||||
getEnforceMinimumDatapointsProps,
|
||||
@@ -99,33 +96,6 @@ describe('Footer utils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['', 'Please enter a deviation greater than 0'],
|
||||
[0, 'Please enter a deviation greater than 0'],
|
||||
[-2, 'Please enter a deviation greater than 0'],
|
||||
['2.5', null],
|
||||
])('when the anomaly deviation is %p', (thresholdValue, expected) => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
alertType: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
basicAlertState: { ...args.basicAlertState, name: 'test name' },
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
thresholdValue: thresholdValue as number,
|
||||
},
|
||||
],
|
||||
},
|
||||
notificationSettings: {
|
||||
...args.notificationSettings,
|
||||
routingPolicies: true,
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(expected);
|
||||
});
|
||||
|
||||
it('when threshold channels are not provided but routing policies are enabled', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
@@ -603,93 +573,4 @@ describe('Footer utils', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('buildCreateAlertRulePayload', () => {
|
||||
const mockCreateAlertContextState = createMockAlertContextState();
|
||||
const thresholdState = mockCreateAlertContextState.thresholdState;
|
||||
const anomalyArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
basicAlertState: mockCreateAlertContextState.alertState,
|
||||
thresholdState: {
|
||||
...thresholdState,
|
||||
seasonality: Seasonality.DAILY,
|
||||
thresholds: [
|
||||
{ ...thresholdState.thresholds[0], thresholdValue: 3, unit: 'ms' },
|
||||
],
|
||||
},
|
||||
advancedOptions: mockCreateAlertContextState.advancedOptions,
|
||||
evaluationWindow: {
|
||||
...mockCreateAlertContextState.evaluationWindow,
|
||||
windowType: 'rolling',
|
||||
timeframe: '1h0m0s',
|
||||
},
|
||||
notificationSettings: mockCreateAlertContextState.notificationSettings,
|
||||
query: initialQueriesMap.metrics,
|
||||
alertType: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
};
|
||||
|
||||
it('builds a v2alpha1 anomaly rule with a rolling window', () => {
|
||||
const props = buildCreateAlertRulePayload(anomalyArgs);
|
||||
expect(props.ruleType).toBe('anomaly_rule');
|
||||
expect(props.alertType).toBe(AlertTypes.METRICS_BASED_ALERT);
|
||||
expect(props.schemaVersion).toBe('v2alpha1');
|
||||
expect(props.condition.algorithm).toBe('standard');
|
||||
expect(props.condition.seasonality).toBe('daily');
|
||||
expect(props.condition.thresholds?.spec[0]).toMatchObject({
|
||||
target: 3,
|
||||
targetUnit: '',
|
||||
});
|
||||
expect(props.evaluation).toStrictEqual({
|
||||
kind: 'rolling',
|
||||
spec: { evalWindow: '1h0m0s', frequency: '1m' },
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[AlertThresholdOperator.IS_BELOW, -3],
|
||||
['2', -3],
|
||||
[AlertThresholdOperator.IS_ABOVE, 3],
|
||||
[AlertThresholdOperator.ABOVE_BELOW, 3],
|
||||
])('signs the z-score target for operator %s', (operator, target) => {
|
||||
const props = buildCreateAlertRulePayload({
|
||||
...anomalyArgs,
|
||||
thresholdState: { ...anomalyArgs.thresholdState, operator },
|
||||
});
|
||||
expect(props.condition.thresholds?.spec[0].target).toBe(target);
|
||||
expect(props.condition.thresholds?.spec[0].op).toBe(operator);
|
||||
});
|
||||
|
||||
it('signs every anomaly severity the same way', () => {
|
||||
const props = buildCreateAlertRulePayload({
|
||||
...anomalyArgs,
|
||||
thresholdState: {
|
||||
...anomalyArgs.thresholdState,
|
||||
operator: AlertThresholdOperator.IS_BELOW,
|
||||
thresholds: [
|
||||
{ ...anomalyArgs.thresholdState.thresholds[0], thresholdValue: 3 },
|
||||
{
|
||||
...anomalyArgs.thresholdState.thresholds[0],
|
||||
id: 'warning',
|
||||
label: 'warning',
|
||||
thresholdValue: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(
|
||||
props.condition.thresholds?.spec.map(({ name, target }) => [name, target]),
|
||||
).toStrictEqual([
|
||||
['critical', -3],
|
||||
['warning', -2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('builds a threshold rule for other alert types', () => {
|
||||
const props = buildCreateAlertRulePayload({
|
||||
...anomalyArgs,
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
});
|
||||
expect(props.ruleType).toBe('threshold_rule');
|
||||
expect(props.condition.seasonality).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
BasicThreshold,
|
||||
PostableAlertRuleV2,
|
||||
@@ -10,10 +9,8 @@ import {
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
|
||||
|
||||
import { normalizeOperator } from '../context/conditionNormalizers';
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertThresholdOperator,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from '../context/types';
|
||||
@@ -34,8 +31,7 @@ export function getFormattedTimeValue(timeValue: number, unit: string): string {
|
||||
export function validateCreateAlertState(
|
||||
args: BuildCreateAlertRulePayloadArgs,
|
||||
): string | null {
|
||||
const { alertType, basicAlertState, thresholdState, notificationSettings } =
|
||||
args;
|
||||
const { basicAlertState, thresholdState, notificationSettings } = args;
|
||||
|
||||
// Validate alert name
|
||||
if (!basicAlertState.name) {
|
||||
@@ -48,12 +44,6 @@ export function validateCreateAlertState(
|
||||
if (!threshold.label) {
|
||||
return 'Please enter a label for each threshold';
|
||||
}
|
||||
if (
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT &&
|
||||
!(Number(threshold.thresholdValue) > 0)
|
||||
) {
|
||||
return 'Please enter a deviation greater than 0';
|
||||
}
|
||||
// this runs during render, so a throw here takes down the whole page
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
|
||||
return 'Please select at least one channel for each threshold or enable routing policies';
|
||||
@@ -299,43 +289,56 @@ export function buildCreateThresholdAlertRulePayload(
|
||||
}
|
||||
|
||||
// Build Create Anomaly Alert Rule Payload
|
||||
// TODO: Update this function before enabling anomaly alert rule creation
|
||||
export function buildCreateAnomalyAlertRulePayload(
|
||||
args: BuildCreateAlertRulePayloadArgs,
|
||||
): PostableAlertRuleV2 {
|
||||
const { thresholdState } = args;
|
||||
const payload = buildCreateThresholdAlertRulePayload(args);
|
||||
const {
|
||||
alertType,
|
||||
basicAlertState,
|
||||
query,
|
||||
notificationSettings,
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
} = args;
|
||||
|
||||
// Targets are literal z-scores: "below N deviations" fires under -N.
|
||||
// A target unit would rescale the z-score, so it is always cleared.
|
||||
const sign =
|
||||
normalizeOperator(thresholdState.operator) === AlertThresholdOperator.IS_BELOW
|
||||
? -1
|
||||
: 1;
|
||||
const thresholds: BasicThreshold[] = (
|
||||
payload.condition.thresholds?.spec || []
|
||||
).map((threshold) => ({
|
||||
...threshold,
|
||||
target: sign * Math.abs(threshold.target),
|
||||
targetUnit: '',
|
||||
}));
|
||||
const compositeQuery = compositeQueryToQueryEnvelope({
|
||||
builderQueries: {
|
||||
...mapQueryDataToApi(query.builder.queryData, 'queryName').data,
|
||||
...mapQueryDataToApi(query.builder.queryFormulas, 'queryName').data,
|
||||
},
|
||||
promQueries: mapQueryDataToApi(query.promql, 'name').data,
|
||||
chQueries: mapQueryDataToApi(query.clickhouse_sql, 'name').data,
|
||||
queryType: query.queryType,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: basicAlertState.yAxisUnit,
|
||||
});
|
||||
|
||||
const alertOnAbsentProps = getAlertOnAbsentProps(advancedOptions);
|
||||
const enforceMinimumDatapointsProps =
|
||||
getEnforceMinimumDatapointsProps(advancedOptions);
|
||||
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
const notificationSettingsProps =
|
||||
getNotificationSettingsProps(notificationSettings);
|
||||
|
||||
return {
|
||||
...payload,
|
||||
alert: basicAlertState.name,
|
||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
alertType,
|
||||
condition: {
|
||||
...payload.condition,
|
||||
thresholds: { kind: 'basic', spec: thresholds },
|
||||
algorithm: thresholdState.algorithm,
|
||||
seasonality: thresholdState.seasonality,
|
||||
compositeQuery,
|
||||
...alertOnAbsentProps,
|
||||
...enforceMinimumDatapointsProps,
|
||||
},
|
||||
labels: basicAlertState.labels,
|
||||
annotations: {
|
||||
description: notificationSettings.description,
|
||||
summary: notificationSettings.description,
|
||||
},
|
||||
notificationSettings: notificationSettingsProps,
|
||||
evaluation: evaluationProps,
|
||||
version: '',
|
||||
schemaVersion: '',
|
||||
source: window?.location.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCreateAlertRulePayload(
|
||||
args: BuildCreateAlertRulePayloadArgs,
|
||||
): PostableAlertRuleV2 {
|
||||
return args.alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||
? buildCreateAnomalyAlertRulePayload(args)
|
||||
: buildCreateThresholdAlertRulePayload(args);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
@@ -74,23 +74,20 @@ function QuerySection(): JSX.Element {
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
|
||||
const handleFetchingStateChange = useCallback((isFetching: boolean): void => {
|
||||
setIsLoadingQueries(isFetching);
|
||||
if (isFetching) {
|
||||
useEffect(() => {
|
||||
if (isLoadingQueries) {
|
||||
setIsCancelled(false);
|
||||
}
|
||||
}, []);
|
||||
}, [isLoadingQueries]);
|
||||
|
||||
const handleCancelQuery = useCallback(() => {
|
||||
void queryClient.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
|
||||
queryClient.cancelQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
|
||||
setIsCancelled(true);
|
||||
}, [queryClient]);
|
||||
|
||||
const runQueryHandler = useCallback(() => {
|
||||
setIsCancelled(false);
|
||||
void queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW,
|
||||
]);
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.ALERT_RULES_CHART_PREVIEW]);
|
||||
// Reset the source param when the query is changed
|
||||
// Then manually run the query
|
||||
if (source === YAxisSource.DASHBOARDS && didQueryChange) {
|
||||
@@ -132,11 +129,6 @@ function QuerySection(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const isAnomalyDetection = alertType === AlertTypes.ANOMALY_BASED_ALERT;
|
||||
const activeTab = isAnomalyDetection
|
||||
? AlertTypes.METRICS_BASED_ALERT
|
||||
: alertType;
|
||||
|
||||
return (
|
||||
<div className="query-section">
|
||||
<Stepper stepNumber={1} label="Define the query" />
|
||||
@@ -144,7 +136,7 @@ function QuerySection(): JSX.Element {
|
||||
alertDef={alertDef}
|
||||
source={source}
|
||||
isCancelled={isCancelled}
|
||||
onFetchingStateChange={handleFetchingStateChange}
|
||||
onFetchingStateChange={setIsLoadingQueries}
|
||||
/>
|
||||
<div className="query-section-tabs">
|
||||
<div className="query-section-query-actions">
|
||||
@@ -152,12 +144,9 @@ function QuerySection(): JSX.Element {
|
||||
<Button
|
||||
key={tab.value}
|
||||
className={classNames('list-view-tab', 'explorer-view-option', {
|
||||
'active-tab': activeTab === tab.value,
|
||||
'active-tab': alertType === tab.value,
|
||||
})}
|
||||
onClick={(): void => {
|
||||
if (isAnomalyDetection && tab.value === activeTab) {
|
||||
return;
|
||||
}
|
||||
setAlertType(tab.value as AlertTypes);
|
||||
}}
|
||||
>
|
||||
@@ -176,7 +165,6 @@ function QuerySection(): JSX.Element {
|
||||
handleCancelQuery={handleCancelQuery}
|
||||
alertDef={alertDef}
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
isAnomalyDetection={isAnomalyDetection}
|
||||
key={currentQuery.queryType}
|
||||
ruleId=""
|
||||
hideTitle
|
||||
|
||||
@@ -61,12 +61,10 @@ jest.mock(
|
||||
alertType,
|
||||
panelType,
|
||||
setQueryCategory,
|
||||
isAnomalyDetection,
|
||||
}: any): JSX.Element {
|
||||
return (
|
||||
<div data-testid="query-section-component">
|
||||
<div data-testid="query-category">{queryCategory}</div>
|
||||
<div data-testid="is-anomaly-detection">{String(isAnomalyDetection)}</div>
|
||||
<div data-testid="alert-type">{alertType}</div>
|
||||
<div data-testid="panel-type">{panelType}</div>
|
||||
<button
|
||||
@@ -129,11 +127,11 @@ const queryClient = new QueryClient({
|
||||
},
|
||||
},
|
||||
});
|
||||
const renderQuerySection = (search = ''): ReturnType<typeof render> =>
|
||||
const renderQuerySection = (): ReturnType<typeof render> =>
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter initialEntries={[`/alerts/new${search}`]}>
|
||||
<MemoryRouter>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<QuerySection />
|
||||
</CreateAlertProvider>
|
||||
@@ -203,27 +201,6 @@ describe('QuerySection', () => {
|
||||
expect(metricsTab).toHaveClass(ACTIVE_TAB_CLASS);
|
||||
});
|
||||
|
||||
it('keeps Metrics active and restricts query types for anomaly alerts', () => {
|
||||
renderQuerySection(`?${QueryParams.ruleType}=anomaly_rule`);
|
||||
|
||||
expect(screen.getByText(METRICS_TEXT).closest('button')).toHaveClass(
|
||||
ACTIVE_TAB_CLASS,
|
||||
);
|
||||
expect(screen.getByTestId('is-anomaly-detection')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('stays on the anomaly alert when its active Metrics tab is clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderQuerySection(`?${QueryParams.ruleType}=anomaly_rule`);
|
||||
|
||||
await user.click(screen.getByText(METRICS_TEXT));
|
||||
|
||||
expect(
|
||||
mockUseQueryBuilder.redirectWithQueryBuilderData,
|
||||
).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId('is-anomaly-detection')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('handles alert type change when clicking on different tabs', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderQuerySection();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import { defaultPostableAlertRuleV2 } from '../constants';
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
import {
|
||||
getAdvancedOptionsStateFromAlertDef,
|
||||
getColorForThreshold,
|
||||
getAlertTypeFromAlertDef,
|
||||
getCreateAlertLocalStateFromAlertDef,
|
||||
getEvaluationWindowStateFromAlertDef,
|
||||
getNotificationSettingsStateFromAlertDef,
|
||||
@@ -273,7 +271,7 @@ describe('CreateAlertV2 utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('getThresholdStateFromAlertDef maps thresholds, query, operator and match type', () => {
|
||||
describe('getThresholdStateFromAlertDef', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
annotations: {
|
||||
@@ -318,81 +316,6 @@ describe('CreateAlertV2 utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef for anomaly rules', () => {
|
||||
const anomalyDef: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
ruleType: 'anomaly_rule',
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
algorithm: 'standard',
|
||||
seasonality: 'weekly',
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: -3,
|
||||
targetUnit: '',
|
||||
channels: ['email'],
|
||||
matchType: AlertThresholdMatchType.ALL_THE_TIME,
|
||||
op: AlertThresholdOperator.IS_BELOW,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
evaluation: {
|
||||
kind: 'rolling',
|
||||
spec: { evalWindow: '1h0m0s', frequency: '1m' },
|
||||
},
|
||||
};
|
||||
|
||||
it('shows deviations unsigned and restores the anomaly settings', () => {
|
||||
expect(getThresholdStateFromAlertDef(anomalyDef)).toMatchObject({
|
||||
operator: AlertThresholdOperator.IS_BELOW,
|
||||
thresholds: [{ thresholdValue: 3 }],
|
||||
algorithm: 'standard',
|
||||
seasonality: 'weekly',
|
||||
});
|
||||
});
|
||||
|
||||
it('loads the anomaly window into the shared evaluation window', () => {
|
||||
expect(getEvaluationWindowStateFromAlertDef(anomalyDef)).toMatchObject({
|
||||
windowType: 'rolling',
|
||||
timeframe: '1h0m0s',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps signed targets for threshold rules', () => {
|
||||
const state = getThresholdStateFromAlertDef({
|
||||
...anomalyDef,
|
||||
ruleType: 'threshold_rule',
|
||||
});
|
||||
expect(state.thresholds[0].thresholdValue).toBe(-3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlertTypeFromAlertDef', () => {
|
||||
it('maps anomaly rules to the anomaly alert type', () => {
|
||||
expect(
|
||||
getAlertTypeFromAlertDef({
|
||||
...defaultPostableAlertRuleV2,
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
ruleType: 'anomaly_rule',
|
||||
}),
|
||||
).toBe(AlertTypes.ANOMALY_BASED_ALERT);
|
||||
});
|
||||
|
||||
it('keeps the stored alert type for other rules', () => {
|
||||
expect(
|
||||
getAlertTypeFromAlertDef({
|
||||
...defaultPostableAlertRuleV2,
|
||||
alertType: AlertTypes.LOGS_BASED_ALERT,
|
||||
ruleType: 'threshold_rule',
|
||||
}),
|
||||
).toBe(AlertTypes.LOGS_BASED_ALERT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef null channels', () => {
|
||||
it('falls back to an empty array so downstream consumers never see null', () => {
|
||||
const def: PostableAlertRuleV2 = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter, useHistory } from 'react-router-dom';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { INITIAL_CREATE_ALERT_STATE } from '../constants';
|
||||
@@ -40,20 +41,20 @@ function Probe(): JSX.Element {
|
||||
}
|
||||
|
||||
function renderWithSearch(search: string): void {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
render(
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<Probe />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{ initialRoute: `/alerts/new${search}` },
|
||||
<MemoryRouter initialEntries={[`/alerts/new${search}`]}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<Probe />
|
||||
</CreateAlertProvider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
const clickTestId = async (testId: string): Promise<void> => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
await user.click(screen.getByTestId(testId));
|
||||
};
|
||||
|
||||
function serializeThreshold(thresholdValue: number): string {
|
||||
return encodeURIComponent(
|
||||
JSON.stringify([
|
||||
@@ -96,13 +97,6 @@ describe('CreateAlertProvider — URL-declared prefill (issue #5291)', () => {
|
||||
expect(screen.getByTestId('window-type')).toHaveTextContent('rolling');
|
||||
});
|
||||
|
||||
it('starts new anomaly alerts at 3 deviations', () => {
|
||||
renderWithSearch(`?ruleType=anomaly_rule`);
|
||||
|
||||
expect(screen.getByTestId('threshold-count')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('3');
|
||||
});
|
||||
|
||||
it('applies the meter evaluation window when the preset is declared', () => {
|
||||
// Ingestion-style prefill: explicit in_total + meter preset.
|
||||
renderWithSearch(
|
||||
@@ -120,17 +114,17 @@ describe('CreateAlertProvider — URL-declared prefill (issue #5291)', () => {
|
||||
// Reproduces the #12137 regression: on the alert overview page the query builder
|
||||
// rewrites location.search after the alert loads, which used to re-run the prefill
|
||||
// effect and RESET the loaded threshold back to 0.
|
||||
function SearchMutator({
|
||||
to = '/alerts/overview?compositeQuery=normalized&ruleId=r1',
|
||||
}: {
|
||||
to?: string;
|
||||
}): JSX.Element {
|
||||
function SearchMutator(): JSX.Element {
|
||||
const routerHistory = useHistory();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mutate-search"
|
||||
onClick={(): void => routerHistory.replace(to)}
|
||||
onClick={(): void =>
|
||||
routerHistory.replace(
|
||||
'/alerts/overview?compositeQuery=normalized&ruleId=r1',
|
||||
)
|
||||
}
|
||||
>
|
||||
change search
|
||||
</button>
|
||||
@@ -138,7 +132,7 @@ function SearchMutator({
|
||||
}
|
||||
|
||||
describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
|
||||
it('keeps loaded thresholds when the query builder rewrites location.search', async () => {
|
||||
it('keeps loaded thresholds when the query builder rewrites location.search', () => {
|
||||
const initialAlertState = {
|
||||
...INITIAL_CREATE_ALERT_STATE,
|
||||
threshold: {
|
||||
@@ -153,158 +147,30 @@ describe('CreateAlertProvider — edit mode ignores URL prefill', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
render(
|
||||
<CreateAlertProvider
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
isEditMode
|
||||
ruleId="r1"
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<Probe />
|
||||
<SearchMutator />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{ initialRoute: '/alerts/overview?ruleId=r1' },
|
||||
<MemoryRouter initialEntries={['/alerts/overview?ruleId=r1']}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CreateAlertProvider
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
isEditMode
|
||||
ruleId="r1"
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<Probe />
|
||||
<SearchMutator />
|
||||
</CreateAlertProvider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
|
||||
|
||||
// Simulate the query builder normalizing the query into the URL post-load.
|
||||
await clickTestId('mutate-search');
|
||||
fireEvent.click(screen.getByTestId('mutate-search'));
|
||||
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
|
||||
});
|
||||
});
|
||||
|
||||
function ThresholdSetter(): JSX.Element {
|
||||
const { thresholdState, setThresholdState } = useCreateAlertState();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="set-threshold"
|
||||
onClick={(): void =>
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [{ ...thresholdState.thresholds[0], thresholdValue: 245 }],
|
||||
})
|
||||
}
|
||||
>
|
||||
set threshold
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
describe('CreateAlertProvider — create mode', () => {
|
||||
it('keeps edited thresholds when running the query rewrites location.search', async () => {
|
||||
render(
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<Probe />
|
||||
<ThresholdSetter />
|
||||
<SearchMutator to="/alerts/new?alertType=METRIC_BASED_ALERT&compositeQuery=ran" />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{ initialRoute: '/alerts/new?alertType=METRIC_BASED_ALERT' },
|
||||
);
|
||||
|
||||
await clickTestId('set-threshold');
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
|
||||
|
||||
await clickTestId('mutate-search');
|
||||
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('245');
|
||||
});
|
||||
});
|
||||
|
||||
function AnomalySwitcher(): JSX.Element {
|
||||
const { setAlertType } = useCreateAlertState();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="switch-to-anomaly"
|
||||
onClick={(): void => setAlertType(AlertTypes.ANOMALY_BASED_ALERT)}
|
||||
>
|
||||
switch to anomaly
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
describe('CreateAlertProvider — edit mode type switch', () => {
|
||||
it('starts the anomaly condition at 3 deviations instead of the loaded threshold', async () => {
|
||||
const initialAlertState = {
|
||||
...INITIAL_CREATE_ALERT_STATE,
|
||||
threshold: {
|
||||
...INITIAL_CREATE_ALERT_STATE.threshold,
|
||||
thresholds: [
|
||||
{
|
||||
...INITIAL_CREATE_ALERT_STATE.threshold.thresholds[0],
|
||||
thresholdValue: 500,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
render(
|
||||
<CreateAlertProvider
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
isEditMode
|
||||
ruleId="r1"
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<Probe />
|
||||
<AnomalySwitcher />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{ initialRoute: '/alerts/overview?ruleId=r1' },
|
||||
);
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('500');
|
||||
|
||||
await clickTestId('switch-to-anomaly');
|
||||
|
||||
expect(screen.getByTestId('threshold-value')).toHaveTextContent('3');
|
||||
});
|
||||
|
||||
it('keeps the meter preset from bringing back a cumulative window for anomaly', async () => {
|
||||
render(
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<Probe />
|
||||
<AnomalySwitcher />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{
|
||||
initialRoute: '/alerts/new?matchType=in_total&evaluationWindowPreset=meter',
|
||||
},
|
||||
);
|
||||
expect(screen.getByTestId('window-type')).toHaveTextContent('cumulative');
|
||||
|
||||
await clickTestId('switch-to-anomaly');
|
||||
|
||||
expect(screen.getByTestId('window-type')).toHaveTextContent('rolling');
|
||||
});
|
||||
|
||||
it('moves a cumulative window back to rolling when switching to anomaly', async () => {
|
||||
render(
|
||||
<CreateAlertProvider
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
isEditMode
|
||||
ruleId="r1"
|
||||
initialAlertState={{
|
||||
...INITIAL_CREATE_ALERT_STATE,
|
||||
evaluationWindow: {
|
||||
...INITIAL_CREATE_ALERT_STATE.evaluationWindow,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentHour',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Probe />
|
||||
<AnomalySwitcher />
|
||||
</CreateAlertProvider>,
|
||||
undefined,
|
||||
{ initialRoute: '/alerts/overview?ruleId=r1' },
|
||||
);
|
||||
expect(screen.getByTestId('window-type')).toHaveTextContent('cumulative');
|
||||
|
||||
await clickTestId('switch-to-anomaly');
|
||||
|
||||
expect(screen.getByTestId('window-type')).toHaveTextContent('rolling');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
Seasonality,
|
||||
TimeDuration,
|
||||
} from '../types';
|
||||
import {
|
||||
advancedOptionsReducer,
|
||||
@@ -265,6 +266,7 @@ describe('CreateAlertV2 Context Utils', () => {
|
||||
selectedQuery: 'B',
|
||||
operator: AlertThresholdOperator.IS_BELOW,
|
||||
matchType: AlertThresholdMatchType.ALL_THE_TIME,
|
||||
evaluationWindow: TimeDuration.TEN_MINUTES,
|
||||
algorithm: Algorithm.STANDARD,
|
||||
seasonality: Seasonality.DAILY,
|
||||
thresholds: [],
|
||||
@@ -278,6 +280,7 @@ describe('CreateAlertV2 Context Utils', () => {
|
||||
selectedQuery: 'C',
|
||||
operator: AlertThresholdOperator.IS_EQUAL_TO,
|
||||
matchType: AlertThresholdMatchType.ON_AVERAGE,
|
||||
evaluationWindow: TimeDuration.ONE_HOUR,
|
||||
algorithm: Algorithm.STANDARD,
|
||||
seasonality: Seasonality.WEEKLY,
|
||||
thresholds: [],
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
NotificationSettingsState,
|
||||
Seasonality,
|
||||
Threshold,
|
||||
TimeDuration,
|
||||
} from './types';
|
||||
|
||||
export const INITIAL_ALERT_STATE: AlertState = {
|
||||
@@ -34,11 +35,6 @@ export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
||||
color: Color.BG_SAKURA_500,
|
||||
};
|
||||
|
||||
export const INITIAL_ANOMALY_THRESHOLD: Threshold = {
|
||||
...INITIAL_CRITICAL_THRESHOLD,
|
||||
thresholdValue: 3,
|
||||
};
|
||||
|
||||
export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'warning',
|
||||
@@ -73,6 +69,7 @@ export const INITIAL_ALERT_THRESHOLD_STATE: AlertThresholdState = {
|
||||
selectedQuery: 'A',
|
||||
operator: AlertThresholdOperator.IS_ABOVE,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
evaluationWindow: TimeDuration.FIVE_MINUTES,
|
||||
algorithm: Algorithm.STANDARD,
|
||||
seasonality: Seasonality.HOURLY,
|
||||
thresholds: [INITIAL_CRITICAL_THRESHOLD],
|
||||
@@ -139,8 +136,8 @@ export const THRESHOLD_OPERATOR_OPTIONS = [
|
||||
];
|
||||
|
||||
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
|
||||
{ value: AlertThresholdOperator.IS_ABOVE, label: 'ABOVE' },
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'BELOW' },
|
||||
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
|
||||
{ value: AlertThresholdOperator.ABOVE_BELOW, label: 'ABOVE/BELOW' },
|
||||
];
|
||||
|
||||
@@ -155,8 +152,16 @@ export const THRESHOLD_MATCH_TYPE_OPTIONS = [
|
||||
export const ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS = [
|
||||
{ value: AlertThresholdMatchType.AT_LEAST_ONCE, label: 'AT LEAST ONCE' },
|
||||
{ value: AlertThresholdMatchType.ALL_THE_TIME, label: 'ALL THE TIME' },
|
||||
{ value: AlertThresholdMatchType.ON_AVERAGE, label: 'ON AVERAGE' },
|
||||
{ value: AlertThresholdMatchType.LAST, label: 'LAST' },
|
||||
];
|
||||
|
||||
export const ANOMALY_TIME_DURATION_OPTIONS = [
|
||||
{ value: TimeDuration.FIVE_MINUTES, label: '5 minutes' },
|
||||
{ value: TimeDuration.TEN_MINUTES, label: '10 minutes' },
|
||||
{ value: TimeDuration.FIFTEEN_MINUTES, label: '15 minutes' },
|
||||
{ value: TimeDuration.ONE_HOUR, label: '1 hour' },
|
||||
{ value: TimeDuration.THREE_HOURS, label: '3 hours' },
|
||||
{ value: TimeDuration.FOUR_HOURS, label: '4 hours' },
|
||||
{ value: TimeDuration.TWENTY_FOUR_HOURS, label: '24 hours' },
|
||||
];
|
||||
|
||||
export const ANOMALY_ALGORITHM_OPTIONS = [
|
||||
|
||||
@@ -22,14 +22,9 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import {
|
||||
INITIAL_ANOMALY_THRESHOLD,
|
||||
INITIAL_CREATE_ALERT_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
} from './constants';
|
||||
import { INITIAL_CREATE_ALERT_STATE } from './constants';
|
||||
import {
|
||||
EvaluationWindowPreset,
|
||||
pickAlertPrefillParams,
|
||||
resolveUrlAlertPrefill,
|
||||
} from './resolveUrlAlertPrefill';
|
||||
import {
|
||||
@@ -134,10 +129,9 @@ export function CreateAlertProvider(
|
||||
const ruleNameFromURL = queryParams.get(QueryParams.ruleName);
|
||||
const yAxisUnitFromURL = queryParams.get(QueryParams.yAxisUnit);
|
||||
// Prefill declared in the URL; applied verbatim, agnostic of the producer.
|
||||
const prefillParams = pickAlertPrefillParams(location.search);
|
||||
const urlPrefill = useMemo(
|
||||
() => resolveUrlAlertPrefill(new URLSearchParams(prefillParams)),
|
||||
[prefillParams],
|
||||
() => resolveUrlAlertPrefill(new URLSearchParams(location.search)),
|
||||
[location.search],
|
||||
);
|
||||
|
||||
const [alertType, setAlertType] = useState<AlertTypes>(() => {
|
||||
@@ -147,41 +141,14 @@ export function CreateAlertProvider(
|
||||
return getInitialAlertTypeFromURL(queryParams, currentQuery);
|
||||
});
|
||||
|
||||
const resetThresholdState = useCallback((type: AlertTypes): void => {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'RESET',
|
||||
},
|
||||
});
|
||||
|
||||
if (type === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: [INITIAL_ANOMALY_THRESHOLD],
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAlertTypeChange = useCallback(
|
||||
(value: AlertTypes): void => {
|
||||
// Threshold and anomaly alerts share the metric query, so switching keeps it.
|
||||
const metricAlertTypes = [
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
AlertTypes.ANOMALY_BASED_ALERT,
|
||||
];
|
||||
const keepsQuery =
|
||||
value !== alertType &&
|
||||
metricAlertTypes.includes(value) &&
|
||||
metricAlertTypes.includes(alertType);
|
||||
const queryToRedirect = keepsQuery
|
||||
? currentQuery
|
||||
: mapQueryDataFromApi(buildInitialAlertDef(value).condition.compositeQuery);
|
||||
const queryToRedirect = buildInitialAlertDef(value);
|
||||
const currentQueryToRedirect = mapQueryDataFromApi(
|
||||
queryToRedirect.condition.compositeQuery,
|
||||
);
|
||||
redirectWithQueryBuilderData(
|
||||
queryToRedirect,
|
||||
currentQueryToRedirect,
|
||||
{
|
||||
[QueryParams.alertType]: value,
|
||||
[QueryParams.ruleType]:
|
||||
@@ -193,28 +160,8 @@ export function CreateAlertProvider(
|
||||
true,
|
||||
);
|
||||
setAlertType(value);
|
||||
if (isEditMode && keepsQuery) {
|
||||
resetThresholdState(value);
|
||||
}
|
||||
if (
|
||||
value === AlertTypes.ANOMALY_BASED_ALERT &&
|
||||
createAlertState.evaluationWindow.windowType !== 'rolling'
|
||||
) {
|
||||
setEvaluationWindow({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
alertType,
|
||||
createAlertState.evaluationWindow.windowType,
|
||||
currentQuery,
|
||||
isEditMode,
|
||||
redirectWithQueryBuilderData,
|
||||
resetThresholdState,
|
||||
setEvaluationWindow,
|
||||
],
|
||||
[redirectWithQueryBuilderData],
|
||||
);
|
||||
|
||||
const ruleNameAppliedRef = useRef(false);
|
||||
@@ -229,7 +176,12 @@ export function CreateAlertProvider(
|
||||
return;
|
||||
}
|
||||
|
||||
resetThresholdState(alertType);
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.THRESHOLD,
|
||||
action: {
|
||||
type: 'RESET',
|
||||
},
|
||||
});
|
||||
|
||||
if (urlPrefill.thresholds) {
|
||||
setCreateAlertState({
|
||||
@@ -261,10 +213,7 @@ export function CreateAlertProvider(
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
urlPrefill.evaluationWindowPreset === EvaluationWindowPreset.METER &&
|
||||
alertType !== AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
if (urlPrefill.evaluationWindowPreset === EvaluationWindowPreset.METER) {
|
||||
setCreateAlertState({
|
||||
slice: CreateAlertSlice.EVALUATION_WINDOW,
|
||||
action: {
|
||||
@@ -294,14 +243,7 @@ export function CreateAlertProvider(
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
alertType,
|
||||
urlPrefill,
|
||||
ruleNameFromURL,
|
||||
yAxisUnitFromURL,
|
||||
isEditMode,
|
||||
resetThresholdState,
|
||||
]);
|
||||
}, [alertType, urlPrefill, ruleNameFromURL, yAxisUnitFromURL, isEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && initialAlertState) {
|
||||
|
||||
@@ -7,13 +7,6 @@ import {
|
||||
Threshold,
|
||||
} from './types';
|
||||
|
||||
const PREFILL_PARAMS = [
|
||||
QueryParams.thresholds,
|
||||
QueryParams.matchType,
|
||||
QueryParams.compareOp,
|
||||
QueryParams.evaluationWindowPreset,
|
||||
];
|
||||
|
||||
export enum EvaluationWindowPreset {
|
||||
METER = 'meter',
|
||||
}
|
||||
@@ -46,18 +39,6 @@ function parseEvaluationWindowPreset(
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function pickAlertPrefillParams(search: string): string {
|
||||
const params = new URLSearchParams(search);
|
||||
const prefillParams = new URLSearchParams();
|
||||
PREFILL_PARAMS.forEach((key) => {
|
||||
const value = params.get(key);
|
||||
if (value !== null) {
|
||||
prefillParams.set(key, value);
|
||||
}
|
||||
});
|
||||
return prefillParams.toString();
|
||||
}
|
||||
|
||||
/** URL → declarative prefill plan; the consumer applies it without knowing the producer. */
|
||||
export function resolveUrlAlertPrefill(
|
||||
params: URLSearchParams,
|
||||
|
||||
@@ -119,11 +119,23 @@ export interface AlertThresholdState {
|
||||
// canonical enum value.
|
||||
operator: AlertThresholdOperator | string;
|
||||
matchType: AlertThresholdMatchType | string;
|
||||
evaluationWindow: string;
|
||||
algorithm: string;
|
||||
seasonality: string;
|
||||
thresholds: Threshold[];
|
||||
}
|
||||
|
||||
export enum TimeDuration {
|
||||
ONE_MINUTE = '1m0s',
|
||||
FIVE_MINUTES = '5m0s',
|
||||
TEN_MINUTES = '10m0s',
|
||||
FIFTEEN_MINUTES = '15m0s',
|
||||
ONE_HOUR = '1h0m0s',
|
||||
THREE_HOURS = '3h0m0s',
|
||||
FOUR_HOURS = '4h0m0s',
|
||||
TWENTY_FOUR_HOURS = '24h0m0s',
|
||||
}
|
||||
|
||||
export enum Algorithm {
|
||||
STANDARD = 'standard',
|
||||
}
|
||||
@@ -138,6 +150,7 @@ export type AlertThresholdAction =
|
||||
| { type: 'SET_SELECTED_QUERY'; payload: string }
|
||||
| { type: 'SET_OPERATOR'; payload: AlertThresholdOperator }
|
||||
| { type: 'SET_MATCH_TYPE'; payload: AlertThresholdMatchType }
|
||||
| { type: 'SET_EVALUATION_WINDOW'; payload: string }
|
||||
| { type: 'SET_ALGORITHM'; payload: string }
|
||||
| { type: 'SET_SEASONALITY'; payload: string }
|
||||
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
|
||||
|
||||
@@ -124,10 +124,6 @@ export const alertThresholdReducer = (
|
||||
return { ...state, operator: action.payload };
|
||||
case 'SET_MATCH_TYPE':
|
||||
return { ...state, matchType: action.payload };
|
||||
case 'SET_ALGORITHM':
|
||||
return { ...state, algorithm: action.payload };
|
||||
case 'SET_SEASONALITY':
|
||||
return { ...state, seasonality: action.payload };
|
||||
case 'SET_THRESHOLDS':
|
||||
return { ...state, thresholds: action.payload };
|
||||
case 'RESET':
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
|
||||
export const useIsAnomalyDetectionAvailable = (): boolean => {
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
return isCloudUser || isEnterpriseSelfHostedUser;
|
||||
};
|
||||
@@ -4,8 +4,6 @@ import { Spin } from 'antd';
|
||||
import { TIMEZONE_DATA } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { getRandomColor } from 'container/ExplorerOptions/utils';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@@ -250,18 +248,13 @@ export {
|
||||
export function getThresholdStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): AlertThresholdState {
|
||||
const isAnomalyRule =
|
||||
alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
|
||||
return {
|
||||
...INITIAL_ALERT_THRESHOLD_STATE,
|
||||
thresholds:
|
||||
alertDef.condition.thresholds?.spec.map((threshold) => ({
|
||||
id: v4(),
|
||||
label: threshold.name,
|
||||
// anomaly targets are signed z-scores; the operator carries the direction
|
||||
thresholdValue: isAnomalyRule
|
||||
? Math.abs(threshold.target)
|
||||
: threshold.target,
|
||||
thresholdValue: threshold.target,
|
||||
recoveryThresholdValue: null,
|
||||
unit: threshold.targetUnit,
|
||||
color: getColorForThreshold(threshold.name),
|
||||
@@ -275,23 +268,9 @@ export function getThresholdStateFromAlertDef(
|
||||
matchType:
|
||||
alertDef.condition.thresholds?.spec[0].matchType ||
|
||||
AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
algorithm:
|
||||
alertDef.condition.algorithm || INITIAL_ALERT_THRESHOLD_STATE.algorithm,
|
||||
seasonality:
|
||||
alertDef.condition.seasonality || INITIAL_ALERT_THRESHOLD_STATE.seasonality,
|
||||
};
|
||||
}
|
||||
|
||||
// Anomaly rules are stored as metric alerts; only the rule type tells them apart.
|
||||
export function getAlertTypeFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2 | undefined,
|
||||
): AlertTypes {
|
||||
if (alertDef?.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||
}
|
||||
return alertDef?.alertType as AlertTypes;
|
||||
}
|
||||
|
||||
export function getCreateAlertLocalStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2 | undefined,
|
||||
): CreateAlertState {
|
||||
|
||||
@@ -8,6 +8,7 @@ import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import Spinner from 'components/Spinner';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -31,7 +32,7 @@ import getTimeString from 'lib/getTimeString';
|
||||
import history from 'lib/history';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useIsAnomalyDetectionAvailable } from 'container/CreateAlertV2/useIsAnomalyDetectionAvailable';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -119,6 +120,8 @@ function ChartPreview({
|
||||
selectedTime: globalSelectedInterval,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const { featureFlags } = useAppContext();
|
||||
|
||||
const handleBackNavigation = (): void => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const startTime = searchParams.get(QueryParams.startTime);
|
||||
@@ -287,7 +290,9 @@ function ChartPreview({
|
||||
!queryResponse.isLoading &&
|
||||
(!queryResponse.isError || isCancelled);
|
||||
|
||||
const isAnomalyDetectionEnabled = useIsAnomalyDetectionAvailable();
|
||||
const isAnomalyDetectionEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||
?.active || false;
|
||||
|
||||
const isWarning = !isEmpty(queryResponse.data?.warning);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/Que
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -38,7 +39,7 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
import { useAlertRuleOptional } from 'providers/Alert';
|
||||
import { useIsAnomalyDetectionAvailable } from 'container/CreateAlertV2/useIsAnomalyDetectionAvailable';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -100,6 +101,7 @@ function FormAlertRules({
|
||||
}: FormAlertRuleProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
const { featureFlags } = useAppContext();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
@@ -810,7 +812,9 @@ function FormAlertRules({
|
||||
},
|
||||
];
|
||||
|
||||
const isAnomalyDetectionEnabled = useIsAnomalyDetectionAvailable();
|
||||
const isAnomalyDetectionEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.ANOMALY_DETECTION)
|
||||
?.active || false;
|
||||
|
||||
const source = useMemo(
|
||||
() => urlQuery.get(QueryParams.source) as YAxisSource,
|
||||
|
||||
@@ -243,7 +243,7 @@ function Hosts(): JSX.Element {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={getHostsQuickFiltersConfig()}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={{
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TriangleAlert } from '@signozhq/icons';
|
||||
import TanStackTable from 'components/TanStackTableView';
|
||||
import { INFRA_MONITORING_ATTR_KEYS } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
const HOSTNAME_DOCS_URL =
|
||||
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/#host-name-is-blankempty';
|
||||
|
||||
@@ -73,6 +74,7 @@ export function getHostsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.SYSTEM_CPU_LOAD_AVERAGE_15M,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -85,6 +87,7 @@ export function getHostsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.SYSTEM_CPU_LOAD_AVERAGE_15M,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -321,7 +321,7 @@ export default function InfraMonitoringK8s(): JSX.Element {
|
||||
</div>
|
||||
{selectedCategoryConfig && (
|
||||
<QuickFilters
|
||||
quickFilterSource={QuickFiltersSource.INFRA_MONITORING}
|
||||
source={QuickFiltersSource.INFRA_MONITORING}
|
||||
config={selectedCategoryConfig}
|
||||
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||
useFieldApis={selectedCategoryUseFieldApis}
|
||||
|
||||
@@ -228,6 +228,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -241,6 +242,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -254,6 +256,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -267,6 +270,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -280,6 +284,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -293,6 +298,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -306,6 +312,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -319,6 +326,7 @@ export function GetPodsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -347,6 +355,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -360,6 +369,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -373,6 +383,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -386,6 +397,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -399,6 +411,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -412,6 +425,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -425,6 +439,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -438,6 +453,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -451,6 +467,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -464,6 +481,7 @@ export function GetContainersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.CONTAINER_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
@@ -492,6 +510,7 @@ export function GetNodesQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -505,6 +524,7 @@ export function GetNodesQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -533,6 +553,7 @@ export function GetNamespaceQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -546,6 +567,7 @@ export function GetNamespaceQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -574,6 +596,7 @@ export function GetClustersQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -602,6 +625,7 @@ export function GetVolumesQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_CAPACITY,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -615,6 +639,7 @@ export function GetVolumesQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_CAPACITY,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -628,6 +653,7 @@ export function GetVolumesQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_VOLUME_CAPACITY,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -656,6 +682,7 @@ export function GetDeploymentsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -669,6 +696,7 @@ export function GetDeploymentsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -682,6 +710,7 @@ export function GetDeploymentsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -710,6 +739,7 @@ export function GetStatefulsetsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -723,6 +753,7 @@ export function GetStatefulsetsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -736,6 +767,7 @@ export function GetStatefulsetsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -764,6 +796,7 @@ export function GetDaemonsetsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -776,6 +809,7 @@ export function GetDaemonsetsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -788,6 +822,7 @@ export function GetDaemonsetsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -816,6 +851,7 @@ export function GetJobsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -828,6 +864,7 @@ export function GetJobsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
@@ -840,6 +877,7 @@ export function GetJobsQuickFiltersConfig(): IQuickFiltersConfig[] {
|
||||
},
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: INFRA_MONITORING_ATTR_KEYS.K8S_POD_CPU_USAGE,
|
||||
dataSource: DataSource.METRICS,
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useQueryClient } from 'react-query';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { initialQueryAIWithType } from 'constants/queryBuilder';
|
||||
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
|
||||
@@ -191,9 +191,11 @@ function Explorer(): JSX.Element {
|
||||
testId="llm-observability-explorer"
|
||||
showFilters={isOpen}
|
||||
onToggleFilters={(): void => setOpen(!isOpen)}
|
||||
savedViewProps={{ source: SavedviewtypesSourceDTO.ai_observability }}
|
||||
quickFilterProps={{
|
||||
className: 'qf-traces-explorer',
|
||||
quickFilterSource: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
source: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
signal: SignalType.AI_OBSERVABILITY,
|
||||
useFieldApis: quickFiltersFieldApis,
|
||||
}}
|
||||
>
|
||||
@@ -225,11 +227,11 @@ function Explorer(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>
|
||||
<div>
|
||||
<div className="query-section-container">
|
||||
<QuerySection />
|
||||
</div>
|
||||
</ExplorerCard>
|
||||
</div>
|
||||
|
||||
<div className="traces-explorer-views">
|
||||
{isCancelled && (
|
||||
|
||||
@@ -37,11 +37,8 @@ import { getListOrderBy, setListOrderBy } from 'utils/explorerUtils';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { getListViewQuery } from '../explorerUtils';
|
||||
import {
|
||||
defaultSelectedColumns,
|
||||
PER_PAGE_OPTIONS,
|
||||
TIMESTAMP_FIELD,
|
||||
} from './configs';
|
||||
import { PER_PAGE_OPTIONS, TIMESTAMP_FIELD } from './configs';
|
||||
import { useListViewColumns } from './useListViewColumns';
|
||||
import './ListView.styles.scss';
|
||||
|
||||
import styles from './ListView.module.scss';
|
||||
@@ -77,6 +74,23 @@ function ListView({
|
||||
loading: timeRangeUpdateLoading,
|
||||
} = useSelector<AppState, GlobalReducer>((state) => state.globalTime);
|
||||
|
||||
const {
|
||||
selectedFields,
|
||||
fieldsSelectorConfig,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
} = useListViewColumns();
|
||||
|
||||
// Sorted names: stable on reorder, changes on add/remove (the API only returns selected columns).
|
||||
const selectColumnsSignature = useMemo(
|
||||
() =>
|
||||
selectedFields
|
||||
.map((field) => field.name)
|
||||
.sort()
|
||||
.join(','),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
QueryParams.pagination,
|
||||
);
|
||||
@@ -97,12 +111,14 @@ function ListView({
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
],
|
||||
[
|
||||
stagedQuery,
|
||||
panelType,
|
||||
globalSelectedTime,
|
||||
paginationConfig,
|
||||
selectColumnsSignature,
|
||||
maxTime,
|
||||
minTime,
|
||||
],
|
||||
@@ -123,7 +139,7 @@ function ListView({
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationConfig,
|
||||
selectColumns: defaultSelectedColumns,
|
||||
selectColumns: selectedFields,
|
||||
},
|
||||
},
|
||||
ENTITY_VERSION_V5,
|
||||
@@ -156,13 +172,13 @@ function ListView({
|
||||
[queryTableDataResult],
|
||||
);
|
||||
|
||||
// TODO(ai-explorer): static columns until the preferences framework lands.
|
||||
const columns = useMemo<TableColumnDef<TracesTableRow>[]>(
|
||||
() =>
|
||||
[TIMESTAMP_FIELD, ...defaultSelectedColumns].map((field) =>
|
||||
getFieldColumn(field),
|
||||
),
|
||||
[],
|
||||
[
|
||||
TIMESTAMP_FIELD,
|
||||
...selectedFields.filter((field) => field.name !== TIMESTAMP_FIELD.name),
|
||||
].map((field) => getFieldColumn(field)),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const rows = useMemo(
|
||||
@@ -204,6 +220,7 @@ function ListView({
|
||||
isLoading={isFetching}
|
||||
totalCount={rows.length}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
config={fieldsSelectorConfig}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -211,7 +228,6 @@ function ListView({
|
||||
data={rows}
|
||||
columns={columns}
|
||||
columnStorageKey={LOCALSTORAGE.AI_OBSERVABILITY_LIST_COLUMNS}
|
||||
respectColumnOrder
|
||||
panelType="LIST"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading}
|
||||
@@ -219,6 +235,8 @@ function ListView({
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={onColumnOrderChange}
|
||||
onColumnRemove={onColumnRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import { getFieldColumn } from '../../TracesTable/getFieldColumn';
|
||||
import { defaultSelectedColumns } from '../configs';
|
||||
import { LAST_COLUMN_ERROR, useListViewColumns } from '../useListViewColumns';
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
toast: { error: jest.fn(), success: jest.fn() },
|
||||
}));
|
||||
|
||||
const [SERVICE_NAME, SPAN_NAME, DURATION] = defaultSelectedColumns;
|
||||
const MODEL = {
|
||||
name: 'gen_ai.request.model',
|
||||
signal: 'traces',
|
||||
fieldContext: 'attribute',
|
||||
fieldDataType: 'string',
|
||||
} as TelemetryFieldKey;
|
||||
|
||||
function Harness(): JSX.Element {
|
||||
const {
|
||||
selectedFields,
|
||||
fieldsSelectorConfig,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
} = useListViewColumns();
|
||||
const removeFirst = (): void =>
|
||||
onColumnRemove(getFieldColumn(selectedFields[0]).id);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="columns">
|
||||
{selectedFields.map((field) => field.name).join(',')}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): void =>
|
||||
fieldsSelectorConfig.fieldsSelector?.onFieldsChange([SPAN_NAME, MODEL])
|
||||
}
|
||||
>
|
||||
save drawer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): void =>
|
||||
onColumnOrderChange(
|
||||
[DURATION, SERVICE_NAME, SPAN_NAME].map((field) => getFieldColumn(field)),
|
||||
)
|
||||
}
|
||||
>
|
||||
reorder
|
||||
</button>
|
||||
<button type="button" onClick={removeFirst}>
|
||||
remove first
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderHarness = (): void => {
|
||||
render(<Harness />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
});
|
||||
};
|
||||
|
||||
describe('useListViewColumns', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('falls back to the default columns when nothing is stored', () => {
|
||||
renderHarness();
|
||||
|
||||
expect(screen.getByTestId('columns')).toHaveTextContent(
|
||||
defaultSelectedColumns.map((field) => field.name).join(','),
|
||||
);
|
||||
});
|
||||
|
||||
it('saves the drawer selection', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderHarness();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'save drawer' }));
|
||||
|
||||
expect(screen.getByTestId('columns')).toHaveTextContent(
|
||||
'name,gen_ai.request.model',
|
||||
);
|
||||
});
|
||||
|
||||
it('follows the table order on a header drag, keeping unlisted columns last', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderHarness();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'reorder' }));
|
||||
|
||||
expect(screen.getByTestId('columns')).toHaveTextContent(
|
||||
/^duration_nano,service\.name,name,/,
|
||||
);
|
||||
});
|
||||
|
||||
it('removes a column from the header but never the last one', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
renderHarness();
|
||||
await user.click(screen.getByRole('button', { name: 'save drawer' }));
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'remove first' }));
|
||||
expect(screen.getByTestId('columns')).toHaveTextContent(
|
||||
/^gen_ai\.request\.model$/,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'remove first' }));
|
||||
expect(screen.getByTestId('columns')).toHaveTextContent(
|
||||
/^gen_ai\.request\.model$/,
|
||||
);
|
||||
expect(jest.mocked(toast.error)).toHaveBeenCalledWith(LAST_COLUMN_ERROR);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import type { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
|
||||
import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { defaultSelectedColumns } from './configs';
|
||||
|
||||
export const LAST_COLUMN_ERROR = 'There must be at least one selected column';
|
||||
|
||||
const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
|
||||
interface UseListViewColumns {
|
||||
selectedFields: TelemetryFieldKey[];
|
||||
fieldsSelectorConfig: OptionsMenuConfig;
|
||||
onColumnOrderChange: (cols: TableColumnDef<TracesTableRow>[]) => void;
|
||||
onColumnRemove: (columnId: string) => void;
|
||||
}
|
||||
|
||||
export function useListViewColumns(): UseListViewColumns {
|
||||
const {
|
||||
aiObservability: { columns, updateColumns },
|
||||
} = usePreferenceContext();
|
||||
|
||||
const selectedFields = columns.list ?? defaultSelectedColumns;
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(next: TelemetryFieldKey[]): void => updateColumns('list', next),
|
||||
[updateColumns],
|
||||
);
|
||||
|
||||
const fieldsSelectorConfig = useMemo<OptionsMenuConfig>(
|
||||
() => ({ fieldsSelector: { value: selectedFields, onFieldsChange } }),
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<TracesTableRow>[]): void => {
|
||||
const orderIndex = new Map(cols.map((col, index) => [col.id, index]));
|
||||
updateColumns(
|
||||
'list',
|
||||
[...selectedFields].sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
),
|
||||
);
|
||||
},
|
||||
[selectedFields, updateColumns],
|
||||
);
|
||||
|
||||
const onColumnRemove = useCallback(
|
||||
(columnId: string): void => {
|
||||
const next = selectedFields.filter(
|
||||
(field) => columnIdOf(field) !== columnId,
|
||||
);
|
||||
if (!next.length) {
|
||||
toast.error(LAST_COLUMN_ERROR);
|
||||
return;
|
||||
}
|
||||
updateColumns('list', next);
|
||||
},
|
||||
[selectedFields, updateColumns],
|
||||
);
|
||||
|
||||
return {
|
||||
selectedFields,
|
||||
fieldsSelectorConfig,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
};
|
||||
}
|
||||
@@ -69,6 +69,8 @@ function TracesView({
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
requiredFields,
|
||||
isLoading: isColumnsLoading,
|
||||
canPersistColumns,
|
||||
@@ -183,15 +185,6 @@ function TracesView({
|
||||
[canPersistColumns, selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
// Rendering the pool unfiltered would surface columns the defaults keep hidden.
|
||||
const tableColumns = useMemo(
|
||||
() =>
|
||||
canPersistColumns
|
||||
? columns
|
||||
: columns.filter((column) => column.defaultVisibility !== false),
|
||||
[canPersistColumns, columns],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.actionsContainer}>
|
||||
@@ -226,13 +219,12 @@ function TracesView({
|
||||
|
||||
<TracesTable
|
||||
data={rows}
|
||||
columns={tableColumns}
|
||||
columns={columns}
|
||||
columnStorageKey={
|
||||
canPersistColumns
|
||||
? LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS
|
||||
: undefined
|
||||
}
|
||||
respectColumnOrder
|
||||
panelType="TRACE"
|
||||
getRowHref={getTraceLink}
|
||||
isLoading={isLoading || isColumnsLoading}
|
||||
@@ -240,6 +232,8 @@ function TracesView({
|
||||
isError={isError}
|
||||
error={error}
|
||||
isFilterApplied={isFilterApplied}
|
||||
onColumnOrderChange={onColumnOrderChange}
|
||||
onColumnRemove={onColumnRemove}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { getFieldColumn, TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
@@ -40,15 +41,51 @@ export const sortByDefaultOrder = (
|
||||
): TelemetryFieldKey[] =>
|
||||
[...fields].sort((a, b) => orderRank(a) - orderRank(b));
|
||||
|
||||
/** Anything the keys endpoint adds beyond the ordered set starts hidden; only applied at first init, since the store persists hidden ids. */
|
||||
/** Anything the keys endpoint adds beyond the ordered set starts hidden; only applied while no selection is stored. */
|
||||
const DEFAULT_VISIBLE_FIELDS = new Set(DEFAULT_COLUMN_ORDER);
|
||||
|
||||
/** Matches the id getFieldColumn derives, so fields and columns address alike. */
|
||||
export const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
|
||||
export const getDefaultTraceViewFields = (
|
||||
availableFields: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] =>
|
||||
availableFields.filter((field) => DEFAULT_VISIBLE_FIELDS.has(field.name));
|
||||
|
||||
/** The selection the old column store encoded; `null` when it matches the defaults. */
|
||||
export const getLegacyTraceViewSelection = (
|
||||
availableFields: TelemetryFieldKey[],
|
||||
hiddenColumnIds: string[],
|
||||
columnOrder: string[],
|
||||
): TelemetryFieldKey[] | null => {
|
||||
const hidden = new Set(hiddenColumnIds);
|
||||
|
||||
// A saved order lists exactly the columns the user kept.
|
||||
if (columnOrder.length) {
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
const kept = availableFields
|
||||
.filter((field) => orderIndex.has(columnIdOf(field)))
|
||||
.filter((field) => !hidden.has(columnIdOf(field)));
|
||||
return kept.length
|
||||
? kept.sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? 0) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? 0),
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
const defaults = getDefaultTraceViewFields(availableFields);
|
||||
const kept = defaults.filter((field) => !hidden.has(columnIdOf(field)));
|
||||
return kept.length === defaults.length ? null : kept;
|
||||
};
|
||||
|
||||
export const buildTraceViewColumns = (
|
||||
fields: TelemetryFieldKey[],
|
||||
): TableColumnDef<TracesTableRow>[] =>
|
||||
fields.map((field) => ({
|
||||
...getFieldColumn(field),
|
||||
defaultVisibility: DEFAULT_VISIBLE_FIELDS.has(field.name),
|
||||
// The shared column builder pins anything in TIMESTAMP_FIELD_NAMES; these stay movable.
|
||||
enableMove: true,
|
||||
enableRemove: field.name !== TRACE_ID_COLUMN_ID,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
|
||||
import { mergeExtraFields } from 'utils/extraFields';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import {
|
||||
hideColumn,
|
||||
initializeFromDefaults,
|
||||
setColumnOrder,
|
||||
showColumn,
|
||||
useColumnOrder,
|
||||
useHiddenColumnIds,
|
||||
} from 'components/TanStackTableView/useColumnStore';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { buildCompositeKey } from 'container/OptionsMenu/utils';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { TracesTableRow } from '../TracesTable/getFieldColumn';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
|
||||
@@ -23,27 +21,41 @@ import {
|
||||
} from '../constants';
|
||||
import {
|
||||
buildTraceViewColumns,
|
||||
columnIdOf,
|
||||
getDefaultTraceViewFields,
|
||||
getLegacyTraceViewSelection,
|
||||
sortByDefaultOrder,
|
||||
TRACE_ID_COLUMN_ID,
|
||||
} from './configs';
|
||||
|
||||
const STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
/** Matches the id getFieldColumn derives, so fields and columns address alike. */
|
||||
const columnIdOf = (field: TelemetryFieldKey): string =>
|
||||
buildCompositeKey(field.name, field.fieldContext, field.fieldDataType);
|
||||
/** Now only column widths; it held the selection as hidden ids + order before the preference slice. */
|
||||
const TABLE_STORAGE_KEY = LOCALSTORAGE.AI_OBSERVABILITY_TRACE_VIEW_COLUMNS;
|
||||
|
||||
interface UseTraceViewColumns {
|
||||
columns: TableColumnDef<TracesTableRow>[];
|
||||
selectedFields: TelemetryFieldKey[];
|
||||
onFieldsChange: (next: TelemetryFieldKey[]) => void;
|
||||
onColumnOrderChange: (cols: TableColumnDef<TracesTableRow>[]) => void;
|
||||
onColumnRemove: (columnId: string) => void;
|
||||
requiredFields: readonly string[];
|
||||
isLoading: boolean;
|
||||
/** False until the keys fetch lands; a partial set must not reach the persisted store. */
|
||||
canPersistColumns: boolean;
|
||||
}
|
||||
|
||||
// TODO(ai-explorer): browser-local only, unlike the list views' `?options=` columns.
|
||||
const withTraceId = (
|
||||
fields: TelemetryFieldKey[],
|
||||
availableFields: TelemetryFieldKey[],
|
||||
): TelemetryFieldKey[] => {
|
||||
if (fields.some((field) => field.name === TRACE_ID_COLUMN_ID)) {
|
||||
return fields;
|
||||
}
|
||||
const traceId = availableFields.find(
|
||||
(field) => field.name === TRACE_ID_COLUMN_ID,
|
||||
);
|
||||
return traceId ? [traceId, ...fields] : fields;
|
||||
};
|
||||
|
||||
export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
const {
|
||||
data: fetchedFields = [],
|
||||
@@ -66,60 +78,99 @@ export function useTraceViewColumns(): UseTraceViewColumns {
|
||||
[fetchedFields],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => buildTraceViewColumns(availableFields),
|
||||
[availableFields],
|
||||
const {
|
||||
aiObservability: {
|
||||
columns: { trace: storedFields },
|
||||
updateColumns,
|
||||
},
|
||||
} = usePreferenceContext();
|
||||
|
||||
// Captured on first render, before the table prunes hidden ids it no longer renders.
|
||||
const legacyHiddenIds = useHiddenColumnIds(TABLE_STORAGE_KEY);
|
||||
const legacyOrder = useColumnOrder(TABLE_STORAGE_KEY);
|
||||
const legacy = useRef({ hiddenIds: legacyHiddenIds, order: legacyOrder });
|
||||
|
||||
const migratedFields = useMemo(
|
||||
() =>
|
||||
isSuccess && !storedFields
|
||||
? getLegacyTraceViewSelection(
|
||||
availableFields,
|
||||
legacy.current.hiddenIds,
|
||||
legacy.current.order,
|
||||
)
|
||||
: null,
|
||||
[isSuccess, storedFields, availableFields],
|
||||
);
|
||||
|
||||
// Defaults from a partial column set would persist as the user's own choice.
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
initializeFromDefaults(STORAGE_KEY, columns);
|
||||
if (!migratedFields) {
|
||||
return;
|
||||
}
|
||||
}, [isSuccess, columns]);
|
||||
|
||||
const hiddenColumnIds = useHiddenColumnIds(STORAGE_KEY);
|
||||
const columnOrder = useColumnOrder(STORAGE_KEY);
|
||||
updateColumns('trace', migratedFields);
|
||||
// Clear the old encoding so a later reset to defaults isn't migrated again.
|
||||
setColumnOrder(TABLE_STORAGE_KEY, []);
|
||||
legacy.current.hiddenIds.forEach((id) => showColumn(TABLE_STORAGE_KEY, id));
|
||||
legacy.current = { hiddenIds: [], order: [] };
|
||||
}, [migratedFields, updateColumns]);
|
||||
|
||||
const selectedFields = useMemo(() => {
|
||||
const hidden = new Set(hiddenColumnIds);
|
||||
const orderIndex = new Map(columnOrder.map((id, index) => [id, index]));
|
||||
const byName = new Map(availableFields.map((field) => [field.name, field]));
|
||||
const source =
|
||||
storedFields ?? migratedFields ?? getDefaultTraceViewFields(availableFields);
|
||||
// Stored fields the endpoint no longer reports are dropped.
|
||||
const resolved = source
|
||||
.map((field) => byName.get(field.name))
|
||||
.filter((field): field is TelemetryFieldKey => !!field);
|
||||
return withTraceId(resolved, availableFields);
|
||||
}, [availableFields, storedFields, migratedFields]);
|
||||
|
||||
return availableFields
|
||||
.filter((field) => !hidden.has(columnIdOf(field)))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
);
|
||||
}, [availableFields, hiddenColumnIds, columnOrder]);
|
||||
const columns = useMemo(
|
||||
() => buildTraceViewColumns(selectedFields),
|
||||
[selectedFields],
|
||||
);
|
||||
|
||||
const onFieldsChange = useCallback(
|
||||
(next: TelemetryFieldKey[]): void => {
|
||||
if (!isSuccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keptIds = new Set(next.map(columnIdOf));
|
||||
|
||||
columns.forEach((column) => {
|
||||
if (keptIds.has(column.id) || column.id === TRACE_ID_COLUMN_ID) {
|
||||
showColumn(STORAGE_KEY, column.id);
|
||||
} else {
|
||||
hideColumn(STORAGE_KEY, column.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Columns missing from the order sort last, so the visible ones suffice.
|
||||
setColumnOrder(STORAGE_KEY, next.map(columnIdOf));
|
||||
updateColumns('trace', withTraceId(next, availableFields));
|
||||
},
|
||||
[columns, isSuccess],
|
||||
[isSuccess, availableFields, updateColumns],
|
||||
);
|
||||
|
||||
const onColumnOrderChange = useCallback(
|
||||
(cols: TableColumnDef<TracesTableRow>[]): void => {
|
||||
const orderIndex = new Map(cols.map((col, index) => [col.id, index]));
|
||||
onFieldsChange(
|
||||
[...selectedFields].sort(
|
||||
(a, b) =>
|
||||
(orderIndex.get(columnIdOf(a)) ?? Infinity) -
|
||||
(orderIndex.get(columnIdOf(b)) ?? Infinity),
|
||||
),
|
||||
);
|
||||
},
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
const onColumnRemove = useCallback(
|
||||
(columnId: string): void => {
|
||||
if (columnId === TRACE_ID_COLUMN_ID) {
|
||||
return;
|
||||
}
|
||||
onFieldsChange(
|
||||
selectedFields.filter((field) => columnIdOf(field) !== columnId),
|
||||
);
|
||||
},
|
||||
[selectedFields, onFieldsChange],
|
||||
);
|
||||
|
||||
return {
|
||||
columns,
|
||||
selectedFields,
|
||||
onFieldsChange,
|
||||
onColumnOrderChange,
|
||||
onColumnRemove,
|
||||
requiredFields: [TRACE_ID_COLUMN_ID],
|
||||
isLoading: !isFetched,
|
||||
canPersistColumns: isSuccess,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
@@ -130,7 +130,8 @@ function Explorer(): JSX.Element {
|
||||
savedViewProps={{ source: SavedviewtypesSourceDTO.meter }}
|
||||
quickFilterProps={{
|
||||
className: 'qf-meter-explorer',
|
||||
quickFilterSource: QuickFiltersSource.METER_EXPLORER,
|
||||
source: QuickFiltersSource.METER_EXPLORER,
|
||||
signal: SignalType.METER_EXPLORER,
|
||||
showFilterCollapse: true,
|
||||
showQueryName: false,
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
|
||||
@@ -2,18 +2,16 @@ import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useGetSavedView } from 'api/generated/services/saved-view';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useOptionsMenu from 'container/OptionsMenu/useOptionsMenu';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { buildSavedViewReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/saved-view.permissions';
|
||||
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import { SAVED_VIEW_OPTIONS_DATA_SOURCE } from '../constants';
|
||||
import { UseActiveSavedViewResult } from '../types';
|
||||
import { hasUnsavedViewChanges } from '../utils/hasUnsavedViewChanges';
|
||||
import { useSavedViewColumns } from './useSavedViewColumns';
|
||||
|
||||
export function useActiveSavedView(
|
||||
source: SavedviewtypesSourceDTO,
|
||||
@@ -21,10 +19,7 @@ export function useActiveSavedView(
|
||||
const { viewKey } = useGetSavedViewParams();
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
const { hasColumnChanges } = useSavedViewColumns(source);
|
||||
|
||||
const { isLoading: isCheckingRead, deniedPermissions } = useAuthZ(
|
||||
[buildSavedViewReadPermission(viewKey)],
|
||||
@@ -59,20 +54,20 @@ export function useActiveSavedView(
|
||||
lastResult.current.hasUnsavedChanges
|
||||
);
|
||||
}
|
||||
return hasUnsavedViewChanges({
|
||||
view,
|
||||
stagedQuery,
|
||||
panelType: panelType ?? PANEL_TYPES.LIST,
|
||||
options: optionsDataSource ? options : undefined,
|
||||
});
|
||||
return (
|
||||
hasUnsavedViewChanges({
|
||||
view,
|
||||
stagedQuery,
|
||||
panelType: panelType ?? PANEL_TYPES.LIST,
|
||||
}) || hasColumnChanges(view)
|
||||
);
|
||||
}, [
|
||||
view,
|
||||
isError,
|
||||
isStagedQueryCurrent,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
optionsDataSource,
|
||||
options,
|
||||
hasColumnChanges,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -15,18 +15,14 @@ import {
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import useOptionsMenu from 'container/OptionsMenu/useOptionsMenu';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
|
||||
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
import { getUnstableCurrentSearchParams } from 'utils/getUnstableCurrentSearchParams';
|
||||
|
||||
import {
|
||||
SAVED_VIEW_OPTIONS_DATA_SOURCE,
|
||||
SAVED_VIEW_SCHEMA_VERSION,
|
||||
SAVED_VIEW_TOAST_POSITION,
|
||||
SAVED_VIEW_URL_PARAMS,
|
||||
@@ -38,10 +34,10 @@ import {
|
||||
} from '../events';
|
||||
import { UseSavedViewActionsResult } from '../types';
|
||||
import { getSavedViewQuery } from '../utils/getSavedViewQuery';
|
||||
import { getViewColumnsAndFormatting } from '../utils/getViewColumnsAndFormatting';
|
||||
import { invalidateSavedViews } from '../utils/invalidateSavedViews';
|
||||
import { toSavedViewSpec } from '../utils/toSavedViewSpec';
|
||||
import { useLastUsedView } from './useLastUsedView';
|
||||
import { useSavedViewColumns } from './useSavedViewColumns';
|
||||
import { useSavedViewLogEvent } from './useSavedViewLogEvent';
|
||||
|
||||
// Columns and formatting need no handling here: with a viewKey in the url the
|
||||
@@ -60,15 +56,7 @@ export function useSavedViewActions(
|
||||
const { viewKey } = useGetSavedViewParams();
|
||||
const logSavedViewEvent = useSavedViewLogEvent(source);
|
||||
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
const preferences = usePreferenceContext();
|
||||
const preferencesSlice =
|
||||
optionsDataSource === DataSource.TRACES
|
||||
? preferences.traces
|
||||
: preferences.logs;
|
||||
const { options, applyViewColumns } = useSavedViewColumns(source);
|
||||
|
||||
const { mutateAsync: createSavedView, isLoading: isCreating } =
|
||||
useCreateSavedView();
|
||||
@@ -86,10 +74,10 @@ export function useSavedViewActions(
|
||||
query: stagedQuery ?? currentQuery,
|
||||
panelType: panelType ?? PANEL_TYPES.LIST,
|
||||
displayName,
|
||||
options: optionsDataSource ? options : undefined,
|
||||
options,
|
||||
color,
|
||||
}),
|
||||
[stagedQuery, currentQuery, panelType, optionsDataSource, options],
|
||||
[stagedQuery, currentQuery, panelType, options],
|
||||
);
|
||||
|
||||
const selectView = useCallback(
|
||||
@@ -105,22 +93,13 @@ export function useSavedViewActions(
|
||||
|
||||
const revertView = useCallback(
|
||||
(view: SavedviewtypesSavedViewDTO): void => {
|
||||
if (optionsDataSource) {
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
optionsDataSource,
|
||||
);
|
||||
preferencesSlice.updateColumns(columns);
|
||||
if (formatting) {
|
||||
preferencesSlice.updateFormatting(formatting);
|
||||
}
|
||||
}
|
||||
applyViewColumns(view);
|
||||
selectView(view);
|
||||
logSavedViewEvent(SavedViewEvents.ChangesDiscarded, {
|
||||
[SavedViewEventKeys.ViewId]: view.id,
|
||||
});
|
||||
},
|
||||
[optionsDataSource, preferencesSlice, selectView, logSavedViewEvent],
|
||||
[applyViewColumns, selectView, logSavedViewEvent],
|
||||
);
|
||||
|
||||
// The query builder redirects only ever add params, so the url is rebuilt
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
SavedviewtypesSavedViewDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import {
|
||||
panelTypeToColumnsView,
|
||||
sanitizeColumns,
|
||||
} from 'providers/preferences/configs/aiObservabilityLoaderConfig';
|
||||
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { SAVED_VIEW_OPTIONS_DATA_SOURCE } from '../constants';
|
||||
import { SavedViewOptions, UseSavedViewColumnsResult } from '../types';
|
||||
import { getViewColumnsAndFormatting } from '../utils/getViewColumnsAndFormatting';
|
||||
import { hasUnsavedColumnChanges } from '../utils/hasUnsavedViewChanges';
|
||||
|
||||
/** The columns a saved view stores and restores, per source. */
|
||||
export function useSavedViewColumns(
|
||||
source: SavedviewtypesSourceDTO,
|
||||
): UseSavedViewColumnsResult {
|
||||
const isAIObservability = source === SavedviewtypesSourceDTO.ai_observability;
|
||||
const optionsDataSource = SAVED_VIEW_OPTIONS_DATA_SOURCE[source];
|
||||
|
||||
const { options } = useOptionsMenu({
|
||||
dataSource: optionsDataSource ?? DataSource.LOGS,
|
||||
});
|
||||
const preferences = usePreferenceContext();
|
||||
const preferencesSlice =
|
||||
optionsDataSource === DataSource.TRACES
|
||||
? preferences.traces
|
||||
: preferences.logs;
|
||||
|
||||
// AI views hold the columns of the tab they were saved from.
|
||||
const { panelType } = useQueryBuilder();
|
||||
const activeTab = panelTypeToColumnsView(panelType ?? undefined);
|
||||
const { columns: aiColumns, updateColumns: updateAIColumns } =
|
||||
preferences.aiObservability;
|
||||
const activeTabColumns = activeTab ? aiColumns[activeTab] : null;
|
||||
|
||||
const savedOptions = useMemo((): SavedViewOptions | undefined => {
|
||||
if (isAIObservability) {
|
||||
// Omitted while the tab is on its defaults.
|
||||
return activeTabColumns ? { selectColumns: activeTabColumns } : undefined;
|
||||
}
|
||||
return optionsDataSource ? options : undefined;
|
||||
}, [isAIObservability, activeTabColumns, optionsDataSource, options]);
|
||||
|
||||
const hasColumnChanges = useCallback(
|
||||
(view: SavedviewtypesSavedViewDTO): boolean => {
|
||||
if (isAIObservability) {
|
||||
if (!activeTab) {
|
||||
return false;
|
||||
}
|
||||
const toNames = (fields: { name: string }[] | null): string[] | null =>
|
||||
fields?.map((field) => field.name) ?? null;
|
||||
return !isEqual(
|
||||
toNames(sanitizeColumns(activeTab, view.spec.selectedFields)),
|
||||
toNames(activeTabColumns),
|
||||
);
|
||||
}
|
||||
return optionsDataSource
|
||||
? hasUnsavedColumnChanges({ view, dataSource: optionsDataSource, options })
|
||||
: false;
|
||||
},
|
||||
[isAIObservability, activeTab, activeTabColumns, optionsDataSource, options],
|
||||
);
|
||||
|
||||
const applyViewColumns = useCallback(
|
||||
(view: SavedviewtypesSavedViewDTO): void => {
|
||||
if (isAIObservability) {
|
||||
const tab = panelTypeToColumnsView(view.spec.panelType);
|
||||
if (tab) {
|
||||
updateAIColumns(tab, sanitizeColumns(tab, view.spec.selectedFields) ?? []);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!optionsDataSource) {
|
||||
return;
|
||||
}
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
optionsDataSource,
|
||||
);
|
||||
preferencesSlice.updateColumns(columns);
|
||||
if (formatting) {
|
||||
preferencesSlice.updateFormatting(formatting);
|
||||
}
|
||||
},
|
||||
[isAIObservability, updateAIColumns, optionsDataSource, preferencesSlice],
|
||||
);
|
||||
|
||||
return { options: savedOptions, hasColumnChanges, applyViewColumns };
|
||||
}
|
||||
@@ -14,10 +14,23 @@ import { SavedViewCreatedFrom } from './events';
|
||||
|
||||
export type SavedViewSourcePage = DataSource | 'meter';
|
||||
|
||||
export type SavedViewOptions = Pick<
|
||||
OptionsQuery,
|
||||
'selectColumns' | 'format' | 'maxLines' | 'fontSize'
|
||||
>;
|
||||
/** Formatting is Logs-only. */
|
||||
export type SavedViewOptions = Pick<OptionsQuery, 'selectColumns'> &
|
||||
Partial<Pick<OptionsQuery, 'format' | 'maxLines' | 'fontSize'>>;
|
||||
|
||||
export interface HasUnsavedColumnChangesArgs {
|
||||
view: SavedviewtypesSavedViewDTO;
|
||||
dataSource: DataSource;
|
||||
options: SavedViewOptions;
|
||||
}
|
||||
|
||||
export interface UseSavedViewColumnsResult {
|
||||
/** What a saved spec stores; `undefined` when the source saves no columns. */
|
||||
options: SavedViewOptions | undefined;
|
||||
hasColumnChanges: (view: SavedviewtypesSavedViewDTO) => boolean;
|
||||
/** Puts the view's columns back, as on discard. */
|
||||
applyViewColumns: (view: SavedviewtypesSavedViewDTO) => void;
|
||||
}
|
||||
|
||||
export interface ToSavedViewSpecArgs {
|
||||
query: Query;
|
||||
|
||||
@@ -3,7 +3,10 @@ import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMapp
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { HasUnsavedViewChangesArgs } from '../types';
|
||||
import {
|
||||
HasUnsavedColumnChangesArgs,
|
||||
HasUnsavedViewChangesArgs,
|
||||
} from '../types';
|
||||
import { getSavedViewQuery } from './getSavedViewQuery';
|
||||
import { toSavedViewPanelType } from './toSavedViewSpec';
|
||||
import { getViewColumnsAndFormatting } from './getViewColumnsAndFormatting';
|
||||
@@ -76,7 +79,19 @@ export function hasUnsavedViewChanges({
|
||||
return false;
|
||||
}
|
||||
|
||||
const dataSource = stagedQuery.builder.queryData[0]?.dataSource;
|
||||
return hasUnsavedColumnChanges({
|
||||
view,
|
||||
dataSource: stagedQuery.builder.queryData[0]?.dataSource,
|
||||
options,
|
||||
});
|
||||
}
|
||||
|
||||
/** Logs/Traces columns and formatting against the view's, defaults filled in. */
|
||||
export function hasUnsavedColumnChanges({
|
||||
view,
|
||||
dataSource,
|
||||
options,
|
||||
}: HasUnsavedColumnChangesArgs): boolean {
|
||||
const { columns, formatting } = getViewColumnsAndFormatting(
|
||||
view.spec,
|
||||
dataSource,
|
||||
|
||||
@@ -10,13 +10,11 @@ import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
|
||||
import {
|
||||
getAlertTypeFromAlertDef,
|
||||
getCreateAlertLocalStateFromAlertDef,
|
||||
} from 'container/CreateAlertV2/utils';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { fromRuleDTOToPostableRuleV2 } from 'types/api/alerts/convert';
|
||||
|
||||
@@ -76,7 +74,7 @@ function AlertDetails(): JSX.Element {
|
||||
|
||||
const handleTabChange = (route: string): void => {
|
||||
if (route === ROUTES.ALERT_HISTORY) {
|
||||
void logEvent('Alert History tab: Visited', { ruleId });
|
||||
logEvent('Alert History tab: Visited', { ruleId });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,7 +89,7 @@ function AlertDetails(): JSX.Element {
|
||||
<CreateAlertProvider
|
||||
ruleId={ruleId || ''}
|
||||
isEditMode
|
||||
initialAlertType={getAlertTypeFromAlertDef(alertRuleDetails)}
|
||||
initialAlertType={alertRuleDetails?.alertType as AlertTypes}
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -14,11 +14,9 @@ import {
|
||||
ALERT_SCHEMAS,
|
||||
notificationChannelsResponse,
|
||||
CHANNEL_MAX,
|
||||
RULE_KINDS,
|
||||
RULE_STATE_CHOICES,
|
||||
SEVERITY_CHOICES,
|
||||
type AlertSchema,
|
||||
type RuleKind,
|
||||
type RuleStateChoice,
|
||||
type SeverityChoice,
|
||||
} from '../../stories/__story_mockdata__/alerts';
|
||||
@@ -48,13 +46,6 @@ export const alertOverviewMocks = defineStoryMocks({
|
||||
options: ALERT_SCHEMAS,
|
||||
value: 'v2',
|
||||
}),
|
||||
ruleKind: choiceControl<RuleKind>('Rule kind', {
|
||||
group: RULE,
|
||||
description:
|
||||
'`anomaly` fires on how far each point strays from the seasonal forecast, in deviations, rather than on a fixed value.',
|
||||
options: RULE_KINDS,
|
||||
value: 'threshold',
|
||||
}),
|
||||
ruleState: choiceControl<RuleStateChoice>('State', {
|
||||
group: RULE,
|
||||
description: 'The badge next to the rule name in the header.',
|
||||
@@ -95,7 +86,6 @@ export const alertOverviewMocks = defineStoryMocks({
|
||||
severity: values.ruleSeverity,
|
||||
state: values.ruleState,
|
||||
schema: values.alertSchema,
|
||||
kind: values.ruleKind,
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -44,35 +44,6 @@ export const ClassicSchema: Story = {
|
||||
args: { alertSchema: 'classic' },
|
||||
};
|
||||
|
||||
/**
|
||||
* An anomaly rule in the current schema: the condition reads in deviations from
|
||||
* the forecast, and the preview draws the band the rule compares against.
|
||||
*/
|
||||
export const AnomalyRule: Story = {
|
||||
args: { ruleKind: 'anomaly' },
|
||||
};
|
||||
|
||||
/**
|
||||
* The anomaly rule with one series picked in the preview: that series drawn
|
||||
* against its forecast, inside the band of deviations the rule allows.
|
||||
*/
|
||||
export const AnomalyRuleForecastBand: Story = {
|
||||
args: { ruleKind: 'anomaly' },
|
||||
play: async ({ canvasElement }): Promise<void> => {
|
||||
const [firstSeries] = await within(canvasElement).findAllByTestId(
|
||||
'anomaly-series-option',
|
||||
undefined,
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
await userEvent.click(firstSeries);
|
||||
},
|
||||
};
|
||||
|
||||
/** An anomaly rule written before the current schema, still in the classic form. */
|
||||
export const ClassicAnomalyRule: Story = {
|
||||
args: { ruleKind: 'anomaly', alertSchema: 'classic' },
|
||||
};
|
||||
|
||||
/** A rule someone turned off: the toggle in the header is what turns it back on. */
|
||||
export const Disabled: Story = {
|
||||
args: { ruleState: 'disabled' },
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
import type {
|
||||
MetricRangePayloadV5,
|
||||
QueryRangeRequestV5,
|
||||
TimeSeries,
|
||||
TimeSeriesValue,
|
||||
} from 'types/api/v5/queryRange';
|
||||
|
||||
import {
|
||||
@@ -54,97 +52,23 @@ export const alertPreviewSeries = async (
|
||||
return queryRangeV5EmptyResponse(queryName);
|
||||
}
|
||||
|
||||
const series = Array.from({ length: count }, (_unused, index) => ({
|
||||
labels: [{ key: { name: 'host.name' }, value: HOSTS[index % HOSTS.length] }],
|
||||
values: timeSeriesPoints({
|
||||
start: body.start,
|
||||
end: body.end,
|
||||
base: 55 + index * 6,
|
||||
amplitude: 12,
|
||||
seed: index * 3,
|
||||
}),
|
||||
}));
|
||||
const response = queryRangeV5TimeSeriesResponse([{ queryName, series }]);
|
||||
|
||||
const zScore = requestedZScore(body);
|
||||
if (zScore === undefined) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// The anomaly engine answers with the forecast, its band and the z-score per
|
||||
// point next to the observed series, which is what the anomaly chart draws.
|
||||
const forecast = series.map((observed, index) => ({
|
||||
labels: observed.labels,
|
||||
values: timeSeriesPoints({
|
||||
start: body.start,
|
||||
end: body.end,
|
||||
base: 55 + index * 6,
|
||||
amplitude: 9,
|
||||
seed: index * 3 + 1,
|
||||
}),
|
||||
}));
|
||||
const shifted = (by: number): TimeSeries[] =>
|
||||
forecast.map(({ labels, values }) => ({
|
||||
labels,
|
||||
values: values.map((point) => ({ ...point, value: point.value + by })),
|
||||
}));
|
||||
const anomalyScores = series.map(({ labels, values }, index) => ({
|
||||
labels,
|
||||
values: values.map((point, pointIndex): TimeSeriesValue => ({
|
||||
...point,
|
||||
value:
|
||||
(point.value - forecast[index].values[pointIndex].value) / ANOMALY_SPREAD,
|
||||
})),
|
||||
}));
|
||||
|
||||
return {
|
||||
data: {
|
||||
...response.data,
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName,
|
||||
aggregations: [
|
||||
{
|
||||
index: 0,
|
||||
alias: '',
|
||||
meta: {},
|
||||
series,
|
||||
predictedSeries: forecast,
|
||||
upperBoundSeries: shifted(zScore * ANOMALY_SPREAD),
|
||||
lowerBoundSeries: shifted(-zScore * ANOMALY_SPREAD),
|
||||
anomalyScores,
|
||||
},
|
||||
],
|
||||
},
|
||||
return queryRangeV5TimeSeriesResponse([
|
||||
{
|
||||
queryName,
|
||||
series: Array.from({ length: count }, (_unused, index) => ({
|
||||
labels: [
|
||||
{ key: { name: 'host.name' }, value: HOSTS[index % HOSTS.length] },
|
||||
],
|
||||
},
|
||||
values: timeSeriesPoints({
|
||||
start: body.start,
|
||||
end: body.end,
|
||||
base: 55 + index * 6,
|
||||
amplitude: 12,
|
||||
seed: index * 3,
|
||||
}),
|
||||
})),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const ANOMALY_SPREAD = 4;
|
||||
|
||||
// The engine sizes the band from the `anomaly` function's `z_score_threshold`.
|
||||
const requestedZScore = (body: QueryRangeRequestV5): number | undefined => {
|
||||
const anomaly = body.compositeQuery?.queries
|
||||
?.flatMap(
|
||||
(query) =>
|
||||
(
|
||||
query.spec as {
|
||||
functions?: {
|
||||
name: string;
|
||||
args?: { name?: string; value?: unknown }[];
|
||||
}[];
|
||||
}
|
||||
).functions ?? [],
|
||||
)
|
||||
.find(({ name }) => name === 'anomaly');
|
||||
if (!anomaly) {
|
||||
return undefined;
|
||||
}
|
||||
const zScore = anomaly.args?.find(({ name }) => name === 'z_score_threshold');
|
||||
return Number(zScore?.value ?? 3);
|
||||
]);
|
||||
};
|
||||
|
||||
const METRIC_SEEDS: MetricsexplorertypesListMetricDTO[] = [
|
||||
|
||||
@@ -7,26 +7,22 @@ import {
|
||||
MetrictypesSpaceAggregationDTO,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTimeAggregationDTO,
|
||||
Querybuildertypesv5FunctionNameDTO,
|
||||
Querybuildertypesv5QueryBuilderQueryGithubComSigNozSignozPkgTypesQuerybuildertypesQuerybuildertypesv5MetricAggregationDTOSignal as MetricsSignal,
|
||||
Querybuildertypesv5QueryEnvelopeBuilderDTOType,
|
||||
Querybuildertypesv5ReduceToDTO,
|
||||
RuletypesAlertStateDTO,
|
||||
RuletypesAlertTypeDTO,
|
||||
RuletypesCompareOperatorDTO,
|
||||
RuletypesEvaluationRollingDTOKind,
|
||||
RuletypesMatchTypeDTO,
|
||||
RuletypesPanelTypeDTO,
|
||||
RuletypesQueryTypeDTO,
|
||||
RuletypesRuleTypeDTO,
|
||||
RuletypesSeasonalityDTO,
|
||||
RuletypesThresholdBasicDTOKind,
|
||||
type AlertmanagertypesDeprecatedGettableAlertDTO,
|
||||
type GetAlerts200,
|
||||
type GetRuleByID200,
|
||||
type ListRules200,
|
||||
type RenderErrorResponseDTO,
|
||||
type Querybuildertypesv5FunctionDTO,
|
||||
type RuletypesAlertCompositeQueryDTO,
|
||||
type RuletypesRuleConditionDTO,
|
||||
type RuletypesRuleDTO,
|
||||
@@ -74,10 +70,6 @@ export const ALERT_SCHEMAS = ['v2', 'classic'] as const;
|
||||
|
||||
export type AlertSchema = (typeof ALERT_SCHEMAS)[number];
|
||||
|
||||
export const RULE_KINDS = ['threshold', 'anomaly'] as const;
|
||||
|
||||
export type RuleKind = (typeof RULE_KINDS)[number];
|
||||
|
||||
export const CHANNEL_TYPES = [
|
||||
'slack',
|
||||
'webhook',
|
||||
@@ -98,10 +90,7 @@ export type ChannelType = (typeof CHANNEL_TYPES)[number];
|
||||
* One query envelope is enough for the alert form to mount its query builder
|
||||
* over the rule, and it is the query the preview chart is drawn for.
|
||||
*/
|
||||
const compositeQuery = (
|
||||
seed: RuleSeed,
|
||||
functions?: Querybuildertypesv5FunctionDTO[],
|
||||
): RuletypesAlertCompositeQueryDTO => ({
|
||||
const compositeQuery = (seed: RuleSeed): RuletypesAlertCompositeQueryDTO => ({
|
||||
queryType: RuletypesQueryTypeDTO.builder,
|
||||
panelType: RuletypesPanelTypeDTO.graph,
|
||||
unit: seed.unit,
|
||||
@@ -125,60 +114,11 @@ const compositeQuery = (
|
||||
groupBy: [],
|
||||
order: [],
|
||||
stepInterval: 60,
|
||||
functions,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* An anomaly rule fires on the z-score of each point against the seasonal
|
||||
* forecast, so its targets are deviations: the classic form stores `below 3`, the
|
||||
* current schema the signed `-3`. The `anomaly` function on the query is what
|
||||
* makes the preview draw the forecast band.
|
||||
*/
|
||||
const anomalyCondition = (seed: RuleSeed): RuletypesRuleConditionDTO => {
|
||||
return {
|
||||
...condition(seed),
|
||||
compositeQuery: compositeQuery(seed, [
|
||||
{
|
||||
name: Querybuildertypesv5FunctionNameDTO.anomaly,
|
||||
args: [
|
||||
{ name: 'z_score_threshold', value: 2 },
|
||||
{ name: 'seasonality', value: 'daily' },
|
||||
],
|
||||
},
|
||||
]),
|
||||
op: RuletypesCompareOperatorDTO.below,
|
||||
matchType: RuletypesMatchTypeDTO.all_the_times,
|
||||
target: 3,
|
||||
targetUnit: '',
|
||||
algorithm: 'standard',
|
||||
seasonality: RuletypesSeasonalityDTO.daily,
|
||||
thresholds: {
|
||||
kind: RuletypesThresholdBasicDTOKind.basic,
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
matchType: RuletypesMatchTypeDTO.all_the_times,
|
||||
op: RuletypesCompareOperatorDTO.below,
|
||||
target: -3,
|
||||
targetUnit: '',
|
||||
channels: ['ops-slack'],
|
||||
},
|
||||
{
|
||||
name: 'warning',
|
||||
matchType: RuletypesMatchTypeDTO.all_the_times,
|
||||
op: RuletypesCompareOperatorDTO.below,
|
||||
target: -2,
|
||||
targetUnit: '',
|
||||
channels: ['ops-slack'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const condition = (seed: RuleSeed): RuletypesRuleConditionDTO => ({
|
||||
compositeQuery: compositeQuery(seed),
|
||||
op: RuletypesCompareOperatorDTO.above,
|
||||
@@ -356,7 +296,6 @@ export interface RuleShape {
|
||||
severity: SeverityChoice;
|
||||
state: RuleStateChoice;
|
||||
schema?: AlertSchema;
|
||||
kind?: RuleKind;
|
||||
}
|
||||
|
||||
const buildRule = (index: number, shape: RuleShape): RuletypesRuleDTO => {
|
||||
@@ -366,34 +305,23 @@ const buildRule = (index: number, shape: RuleShape): RuletypesRuleDTO => {
|
||||
shape.state === 'mixed'
|
||||
? seed.state
|
||||
: (shape.state as RuletypesAlertStateDTO);
|
||||
const isAnomaly = shape.kind === 'anomaly';
|
||||
|
||||
return {
|
||||
id: `rule-${index + 1}`,
|
||||
alert: ruleName(index),
|
||||
alertType: isAnomaly
|
||||
? RuletypesAlertTypeDTO.METRIC_BASED_ALERT
|
||||
: seed.alertType,
|
||||
ruleType: isAnomaly
|
||||
? RuletypesRuleTypeDTO.anomaly_rule
|
||||
: RuletypesRuleTypeDTO.threshold_rule,
|
||||
alertType: seed.alertType,
|
||||
ruleType: RuletypesRuleTypeDTO.threshold_rule,
|
||||
state,
|
||||
disabled: state === RuletypesAlertStateDTO.disabled,
|
||||
condition: isAnomaly ? anomalyCondition(seed) : condition(seed),
|
||||
condition: condition(seed),
|
||||
labels: { severity, ...seed.labels },
|
||||
annotations: {
|
||||
summary: `${seed.alert} crossed its threshold of ${seed.target}`,
|
||||
description:
|
||||
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}',
|
||||
},
|
||||
evalWindow: isAnomaly ? '1h0m0s' : '5m0s',
|
||||
evalWindow: '5m0s',
|
||||
frequency: '1m0s',
|
||||
...(isAnomaly && {
|
||||
evaluation: {
|
||||
kind: RuletypesEvaluationRollingDTOKind.rolling,
|
||||
spec: { evalWindow: '1h0m0s', frequency: '1m0s' },
|
||||
},
|
||||
}),
|
||||
createdAt: ago((index + 3) * DAY),
|
||||
updatedAt: ago((index + 1) * HOUR),
|
||||
createdBy: 'ada@signoz.io',
|
||||
|
||||
@@ -8,7 +8,7 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
|
||||
import QuickFiltersLayout from 'components/QuickFilters/QuickFiltersLayout/QuickFiltersLayout';
|
||||
import { useSignalFieldApis } from 'components/QuickFilters/hooks/useSignalFieldApis';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||
@@ -64,7 +64,8 @@ function AllErrors(): JSX.Element {
|
||||
onToggleFilters={handleFilterVisibilityChange}
|
||||
quickFilterProps={{
|
||||
className: 'qf-exceptions',
|
||||
quickFilterSource: QuickFiltersSource.EXCEPTIONS,
|
||||
source: QuickFiltersSource.EXCEPTIONS,
|
||||
signal: SignalType.EXCEPTIONS,
|
||||
useFieldApis: quickFilterFieldApis,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
/**
|
||||
* Which alert the page is building. The page reads this off the URL, so the
|
||||
* control is a route rather than a response: with no type at all it stays on
|
||||
* the picker, `classic-form` opts back into the classic form, and everything else
|
||||
* the picker, anomaly detection routes to the classic form, and everything else
|
||||
* opens the current one.
|
||||
*/
|
||||
const ALERT_MODES = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user