mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-04 11:25:52 +08:00
feat: client side query builder search (#5891)
* feat: build client side QB search * feat: query builder light mode support + overall UI improvements * fix: preserve the alert rule labels in context * feat: get labels and all possible values from /timeline API * chore: remove unnecessary dropdownRender and optional fields from AttributeKey * chore: merge the styles of .tag * chore: use the correct type for attributeKeys * chore: use the correct values for alert rule state in the context
This commit is contained in:
parent
4aeed392d7
commit
f1ce82ac25
@ -0,0 +1,5 @@
|
|||||||
|
.client-side-qb-search {
|
||||||
|
.ant-select-selection-search {
|
||||||
|
width: max-content !important;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,654 @@
|
|||||||
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
|
||||||
|
import './ClientSideQBSearch.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Select, Tag, Tooltip } from 'antd';
|
||||||
|
import {
|
||||||
|
OPERATORS,
|
||||||
|
QUERY_BUILDER_OPERATORS_BY_TYPES,
|
||||||
|
QUERY_BUILDER_SEARCH_VALUES,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
|
import { CustomTagProps } from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import { selectStyle } from 'container/QueryBuilder/filters/QueryBuilderSearch/config';
|
||||||
|
import { PLACEHOLDER } from 'container/QueryBuilder/filters/QueryBuilderSearch/constant';
|
||||||
|
import { TypographyText } from 'container/QueryBuilder/filters/QueryBuilderSearch/style';
|
||||||
|
import {
|
||||||
|
checkCommaInValue,
|
||||||
|
getOperatorFromValue,
|
||||||
|
getOperatorValue,
|
||||||
|
getTagToken,
|
||||||
|
isInNInOperator,
|
||||||
|
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
|
import {
|
||||||
|
DropdownState,
|
||||||
|
ITag,
|
||||||
|
Option,
|
||||||
|
} from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||||
|
import Suggestions from 'container/QueryBuilder/filters/QueryBuilderSearchV2/Suggestions';
|
||||||
|
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||||
|
import { validationMapper } from 'hooks/queryBuilder/useIsValidTag';
|
||||||
|
import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { isArray, isEmpty, isEqual, isObject } from 'lodash-es';
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
|
import type { BaseSelectRef } from 'rc-select';
|
||||||
|
import {
|
||||||
|
KeyboardEvent,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
BaseAutocompleteData,
|
||||||
|
DataTypes,
|
||||||
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
TagFilter,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
export interface AttributeKey {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttributeValuesMap {
|
||||||
|
[key: string]: AttributeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClientSideQBSearchProps {
|
||||||
|
filters: TagFilter;
|
||||||
|
onChange: (value: TagFilter) => void;
|
||||||
|
whereClauseConfig?: WhereClauseConfig;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
suffixIcon?: React.ReactNode;
|
||||||
|
attributeValuesMap?: AttributeValuesMap;
|
||||||
|
attributeKeys: AttributeKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttributeValue {
|
||||||
|
stringAttributeValues: string[] | [];
|
||||||
|
numberAttributeValues: number[] | [];
|
||||||
|
boolAttributeValues: boolean[] | [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClientSideQBSearch(
|
||||||
|
props: ClientSideQBSearchProps,
|
||||||
|
): React.ReactElement {
|
||||||
|
const {
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
className,
|
||||||
|
suffixIcon,
|
||||||
|
whereClauseConfig,
|
||||||
|
attributeValuesMap,
|
||||||
|
attributeKeys,
|
||||||
|
filters,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const selectRef = useRef<BaseSelectRef>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// create the tags from the initial query here, this should only be computed on the first load as post that tags and query will be always in sync.
|
||||||
|
const [tags, setTags] = useState<ITag[]>(filters.items as ITag[]);
|
||||||
|
|
||||||
|
// this will maintain the current state of in process filter item
|
||||||
|
const [currentFilterItem, setCurrentFilterItem] = useState<ITag | undefined>();
|
||||||
|
|
||||||
|
const [currentState, setCurrentState] = useState<DropdownState>(
|
||||||
|
DropdownState.ATTRIBUTE_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
// to maintain the current running state until the tokenization happens for the tag
|
||||||
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
|
|
||||||
|
const [dropdownOptions, setDropdownOptions] = useState<Option[]>([]);
|
||||||
|
|
||||||
|
const attributeValues = useMemo(() => {
|
||||||
|
if (currentFilterItem?.key?.key) {
|
||||||
|
return attributeValuesMap?.[currentFilterItem.key.key];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stringAttributeValues: [],
|
||||||
|
numberAttributeValues: [],
|
||||||
|
boolAttributeValues: [],
|
||||||
|
};
|
||||||
|
}, [attributeValuesMap, currentFilterItem?.key?.key]);
|
||||||
|
|
||||||
|
const handleDropdownSelect = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
let parsedValue: BaseAutocompleteData | string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parsedValue = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
parsedValue = value;
|
||||||
|
}
|
||||||
|
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||||
|
setCurrentFilterItem((prev) => ({
|
||||||
|
...prev,
|
||||||
|
key: parsedValue as BaseAutocompleteData,
|
||||||
|
op: '',
|
||||||
|
value: '',
|
||||||
|
}));
|
||||||
|
setCurrentState(DropdownState.OPERATOR);
|
||||||
|
setSearchValue((parsedValue as BaseAutocompleteData)?.key);
|
||||||
|
} else if (currentState === DropdownState.OPERATOR) {
|
||||||
|
if (value === OPERATORS.EXISTS || value === OPERATORS.NOT_EXISTS) {
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: currentFilterItem?.key,
|
||||||
|
op: value,
|
||||||
|
value: '',
|
||||||
|
} as ITag,
|
||||||
|
]);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
} else {
|
||||||
|
setCurrentFilterItem((prev) => ({
|
||||||
|
key: prev?.key as BaseAutocompleteData,
|
||||||
|
op: value as string,
|
||||||
|
value: '',
|
||||||
|
}));
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||||
|
setSearchValue(`${currentFilterItem?.key?.key} ${value}`);
|
||||||
|
}
|
||||||
|
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||||
|
const operatorType =
|
||||||
|
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||||
|
const isMulti = operatorType === QUERY_BUILDER_SEARCH_VALUES.MULTIPLY;
|
||||||
|
|
||||||
|
if (isMulti) {
|
||||||
|
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||||
|
// this condition takes care of adding the IN/NIN multi values when pressed enter on an already existing value.
|
||||||
|
// not the best interaction but in sync with what we have today!
|
||||||
|
if (tagValue.includes(String(value))) {
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: currentFilterItem?.key,
|
||||||
|
op: currentFilterItem?.op,
|
||||||
|
value: tagValue,
|
||||||
|
} as ITag,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// this is for adding subsequent comma seperated values
|
||||||
|
const newSearch = [...tagValue];
|
||||||
|
newSearch[newSearch.length === 0 ? 0 : newSearch.length - 1] = value;
|
||||||
|
const newSearchValue = newSearch.join(',');
|
||||||
|
setSearchValue(`${tagKey} ${tagOperator} ${newSearchValue},`);
|
||||||
|
} else {
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: currentFilterItem?.key,
|
||||||
|
op: currentFilterItem?.op,
|
||||||
|
value,
|
||||||
|
} as ITag,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentFilterItem?.key, currentFilterItem?.op, currentState, searchValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = useCallback((value: string) => {
|
||||||
|
setSearchValue(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onInputKeyDownHandler = useCallback(
|
||||||
|
(event: KeyboardEvent<Element>): void => {
|
||||||
|
if (event.key === 'Backspace' && !searchValue) {
|
||||||
|
event.stopPropagation();
|
||||||
|
setTags((prev) => prev.slice(0, -1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[searchValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnBlur = useCallback((): void => {
|
||||||
|
if (searchValue) {
|
||||||
|
const operatorType =
|
||||||
|
operatorTypeMapper[currentFilterItem?.op || ''] || 'NOT_VALID';
|
||||||
|
// if key is added and operator is not present then convert to body CONTAINS key
|
||||||
|
if (
|
||||||
|
currentFilterItem?.key &&
|
||||||
|
isEmpty(currentFilterItem?.op) &&
|
||||||
|
whereClauseConfig?.customKey === 'body' &&
|
||||||
|
whereClauseConfig?.customOp === OPERATORS.CONTAINS
|
||||||
|
) {
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: {
|
||||||
|
key: 'body',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: '',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'body--string----true',
|
||||||
|
},
|
||||||
|
op: OPERATORS.CONTAINS,
|
||||||
|
value: currentFilterItem?.key?.key,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
} else if (
|
||||||
|
currentFilterItem?.op === OPERATORS.EXISTS ||
|
||||||
|
currentFilterItem?.op === OPERATORS.NOT_EXISTS
|
||||||
|
) {
|
||||||
|
// is exists and not exists operator is present then convert directly to tag! no need of value here
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: currentFilterItem?.key,
|
||||||
|
op: currentFilterItem?.op,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
} else if (
|
||||||
|
// if the current state is in sync with the kind of operator used then convert into a tag
|
||||||
|
validationMapper[operatorType]?.(
|
||||||
|
isArray(currentFilterItem?.value)
|
||||||
|
? currentFilterItem?.value.length || 0
|
||||||
|
: 1,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||||
|
op: currentFilterItem?.op as string,
|
||||||
|
value: currentFilterItem?.value || '',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentFilterItem?.key,
|
||||||
|
currentFilterItem?.op,
|
||||||
|
currentFilterItem?.value,
|
||||||
|
searchValue,
|
||||||
|
whereClauseConfig?.customKey,
|
||||||
|
whereClauseConfig?.customOp,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// this useEffect takes care of tokenisation based on the search state
|
||||||
|
useEffect(() => {
|
||||||
|
// if there is no search value reset to the default state
|
||||||
|
if (!searchValue) {
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the current search value based on delimiters
|
||||||
|
const { tagKey, tagOperator, tagValue } = getTagToken(searchValue);
|
||||||
|
|
||||||
|
if (
|
||||||
|
// Case 1 - if key is defined but the search text doesn't match with the set key,
|
||||||
|
// can happen when user selects from dropdown and then deletes a few characters
|
||||||
|
currentFilterItem?.key &&
|
||||||
|
currentFilterItem?.key?.key !== tagKey.split(' ')[0]
|
||||||
|
) {
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
} else if (tagOperator && isEmpty(currentFilterItem?.op)) {
|
||||||
|
// Case 2 -> key is set and now typing for the operator
|
||||||
|
if (
|
||||||
|
tagOperator === OPERATORS.EXISTS ||
|
||||||
|
tagOperator === OPERATORS.NOT_EXISTS
|
||||||
|
) {
|
||||||
|
setTags((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
key: currentFilterItem?.key,
|
||||||
|
op: tagOperator,
|
||||||
|
value: '',
|
||||||
|
} as ITag,
|
||||||
|
]);
|
||||||
|
setCurrentFilterItem(undefined);
|
||||||
|
setSearchValue('');
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_KEY);
|
||||||
|
} else {
|
||||||
|
setCurrentFilterItem((prev) => ({
|
||||||
|
key: prev?.key as BaseAutocompleteData,
|
||||||
|
op: tagOperator,
|
||||||
|
value: '',
|
||||||
|
}));
|
||||||
|
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
// Case 3 -> selected operator from dropdown and then erased a part of it
|
||||||
|
!isEmpty(currentFilterItem?.op) &&
|
||||||
|
tagOperator !== currentFilterItem?.op
|
||||||
|
) {
|
||||||
|
setCurrentFilterItem((prev) => ({
|
||||||
|
key: prev?.key as BaseAutocompleteData,
|
||||||
|
op: '',
|
||||||
|
value: '',
|
||||||
|
}));
|
||||||
|
setCurrentState(DropdownState.OPERATOR);
|
||||||
|
} else if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||||
|
// Case 4 -> the final value state where we set the current filter values and the tokenisation happens on either
|
||||||
|
// dropdown click or blur event
|
||||||
|
const currentValue = {
|
||||||
|
key: currentFilterItem?.key as BaseAutocompleteData,
|
||||||
|
op: currentFilterItem?.op as string,
|
||||||
|
value: tagValue,
|
||||||
|
};
|
||||||
|
if (!isEqual(currentValue, currentFilterItem)) {
|
||||||
|
setCurrentFilterItem((prev) => ({
|
||||||
|
key: prev?.key as BaseAutocompleteData,
|
||||||
|
op: prev?.op as string,
|
||||||
|
value: tagValue,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentFilterItem,
|
||||||
|
currentFilterItem?.key,
|
||||||
|
currentFilterItem?.op,
|
||||||
|
searchValue,
|
||||||
|
currentState,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// the useEffect takes care of setting the dropdown values correctly on change of the current state
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentState === DropdownState.ATTRIBUTE_KEY) {
|
||||||
|
const filteredAttributeKeys = attributeKeys.filter((key) =>
|
||||||
|
key.key.startsWith(searchValue),
|
||||||
|
);
|
||||||
|
setDropdownOptions(
|
||||||
|
filteredAttributeKeys?.map(
|
||||||
|
(key) =>
|
||||||
|
({
|
||||||
|
label: key.key,
|
||||||
|
value: key,
|
||||||
|
} as Option),
|
||||||
|
) || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (currentState === DropdownState.OPERATOR) {
|
||||||
|
const keyOperator = searchValue.split(' ');
|
||||||
|
const partialOperator = keyOperator?.[1];
|
||||||
|
const strippedKey = keyOperator?.[0];
|
||||||
|
|
||||||
|
let operatorOptions;
|
||||||
|
if (currentFilterItem?.key?.dataType) {
|
||||||
|
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES[
|
||||||
|
currentFilterItem.key
|
||||||
|
.dataType as keyof typeof QUERY_BUILDER_OPERATORS_BY_TYPES
|
||||||
|
].map((operator) => ({
|
||||||
|
label: operator,
|
||||||
|
value: operator,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (partialOperator) {
|
||||||
|
operatorOptions = operatorOptions.filter((op) =>
|
||||||
|
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDropdownOptions(operatorOptions);
|
||||||
|
} else if (strippedKey.endsWith('[*]') && strippedKey.startsWith('body.')) {
|
||||||
|
operatorOptions = [OPERATORS.HAS, OPERATORS.NHAS].map((operator) => ({
|
||||||
|
label: operator,
|
||||||
|
value: operator,
|
||||||
|
}));
|
||||||
|
setDropdownOptions(operatorOptions);
|
||||||
|
} else {
|
||||||
|
operatorOptions = QUERY_BUILDER_OPERATORS_BY_TYPES.universal.map(
|
||||||
|
(operator) => ({
|
||||||
|
label: operator,
|
||||||
|
value: operator,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (partialOperator) {
|
||||||
|
operatorOptions = operatorOptions.filter((op) =>
|
||||||
|
op.label.startsWith(partialOperator.toLocaleUpperCase()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setDropdownOptions(operatorOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentState === DropdownState.ATTRIBUTE_VALUE) {
|
||||||
|
const values: Array<string | number | boolean> = [];
|
||||||
|
const { tagValue } = getTagToken(searchValue);
|
||||||
|
if (isArray(tagValue)) {
|
||||||
|
if (!isEmpty(tagValue[tagValue.length - 1]))
|
||||||
|
values.push(tagValue[tagValue.length - 1]);
|
||||||
|
} else if (!isEmpty(tagValue)) values.push(tagValue);
|
||||||
|
|
||||||
|
const currentAttributeValues =
|
||||||
|
attributeValues?.stringAttributeValues ||
|
||||||
|
attributeValues?.numberAttributeValues ||
|
||||||
|
attributeValues?.boolAttributeValues ||
|
||||||
|
[];
|
||||||
|
|
||||||
|
values.push(...currentAttributeValues);
|
||||||
|
|
||||||
|
if (attributeValuesMap) {
|
||||||
|
setDropdownOptions(
|
||||||
|
values.map(
|
||||||
|
(val) =>
|
||||||
|
({
|
||||||
|
label: checkCommaInValue(String(val)),
|
||||||
|
value: val,
|
||||||
|
} as Option),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// If attributeValuesMap is not provided, don't set dropdown options
|
||||||
|
setDropdownOptions([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
attributeValues,
|
||||||
|
currentFilterItem?.key?.dataType,
|
||||||
|
currentState,
|
||||||
|
attributeKeys,
|
||||||
|
searchValue,
|
||||||
|
attributeValuesMap,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const filterTags: IBuilderQuery['filters'] = {
|
||||||
|
op: 'AND',
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
const computedTagValue =
|
||||||
|
tag.value &&
|
||||||
|
Array.isArray(tag.value) &&
|
||||||
|
tag.value[tag.value.length - 1] === ''
|
||||||
|
? tag.value?.slice(0, -1)
|
||||||
|
: tag.value ?? '';
|
||||||
|
filterTags.items.push({
|
||||||
|
id: tag.id || uuid().slice(0, 8),
|
||||||
|
key: tag.key,
|
||||||
|
op: getOperatorValue(tag.op),
|
||||||
|
value: computedTagValue,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isEqual(filters, filterTags)) {
|
||||||
|
onChange(filterTags);
|
||||||
|
setTags(
|
||||||
|
filterTags.items.map((tag) => ({
|
||||||
|
...tag,
|
||||||
|
op: getOperatorFromValue(tag.op),
|
||||||
|
})) as ITag[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [tags]);
|
||||||
|
|
||||||
|
const queryTags = useMemo(
|
||||||
|
() => tags.map((tag) => `${tag.key.key} ${tag.op} ${tag.value}`),
|
||||||
|
[tags],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTagRender = ({
|
||||||
|
value,
|
||||||
|
closable,
|
||||||
|
onClose,
|
||||||
|
}: CustomTagProps): React.ReactElement => {
|
||||||
|
const { tagOperator } = getTagToken(value);
|
||||||
|
const isInNin = isInNInOperator(tagOperator);
|
||||||
|
const chipValue = isInNin
|
||||||
|
? value?.trim()?.replace(/,\s*$/, '')
|
||||||
|
: value?.trim();
|
||||||
|
|
||||||
|
const indexInQueryTags = queryTags.findIndex((qTag) => isEqual(qTag, value));
|
||||||
|
const tagDetails = tags[indexInQueryTags];
|
||||||
|
|
||||||
|
const onCloseHandler = (): void => {
|
||||||
|
onClose();
|
||||||
|
setSearchValue('');
|
||||||
|
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagEditHandler = (value: string): void => {
|
||||||
|
setCurrentFilterItem(tagDetails);
|
||||||
|
setSearchValue(value);
|
||||||
|
setCurrentState(DropdownState.ATTRIBUTE_VALUE);
|
||||||
|
setTags((prev) => prev.filter((t) => !isEqual(t, tagDetails)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled = !!searchValue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="qb-search-bar-tokenised-tags">
|
||||||
|
<Tag
|
||||||
|
closable={!searchValue && closable}
|
||||||
|
onClose={onCloseHandler}
|
||||||
|
className={tagDetails?.key?.type || ''}
|
||||||
|
>
|
||||||
|
<Tooltip title={chipValue}>
|
||||||
|
<TypographyText
|
||||||
|
ellipsis
|
||||||
|
$isInNin={isInNin}
|
||||||
|
disabled={isDisabled}
|
||||||
|
$isEnabled={!!searchValue}
|
||||||
|
onClick={(): void => {
|
||||||
|
if (!isDisabled) tagEditHandler(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chipValue}
|
||||||
|
</TypographyText>
|
||||||
|
</Tooltip>
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const suffixIconContent = useMemo(() => {
|
||||||
|
if (suffixIcon) {
|
||||||
|
return suffixIcon;
|
||||||
|
}
|
||||||
|
return isOpen ? (
|
||||||
|
<ChevronUp
|
||||||
|
size={14}
|
||||||
|
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronDown
|
||||||
|
size={14}
|
||||||
|
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [isDarkMode, isOpen, suffixIcon]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="query-builder-search-v2 ">
|
||||||
|
<Select
|
||||||
|
ref={selectRef}
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
virtual={false}
|
||||||
|
showSearch
|
||||||
|
tagRender={onTagRender}
|
||||||
|
transitionName=""
|
||||||
|
choiceTransitionName=""
|
||||||
|
filterOption={false}
|
||||||
|
open={isOpen}
|
||||||
|
suffixIcon={suffixIconContent}
|
||||||
|
onDropdownVisibleChange={setIsOpen}
|
||||||
|
autoClearSearchValue={false}
|
||||||
|
mode="multiple"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={queryTags}
|
||||||
|
searchValue={searchValue}
|
||||||
|
className={className}
|
||||||
|
rootClassName="query-builder-search client-side-qb-search"
|
||||||
|
disabled={!attributeKeys.length}
|
||||||
|
style={selectStyle}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onSelect={handleDropdownSelect}
|
||||||
|
onInputKeyDown={onInputKeyDownHandler}
|
||||||
|
notFoundContent={null}
|
||||||
|
showAction={['focus']}
|
||||||
|
onBlur={handleOnBlur}
|
||||||
|
>
|
||||||
|
{dropdownOptions.map((option) => {
|
||||||
|
let val = option.value;
|
||||||
|
try {
|
||||||
|
if (isObject(option.value)) {
|
||||||
|
val = JSON.stringify(option.value);
|
||||||
|
} else {
|
||||||
|
val = option.value;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
val = option.value;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Select.Option key={isObject(val) ? `select-option` : val} value={val}>
|
||||||
|
<Suggestions
|
||||||
|
label={option.label}
|
||||||
|
value={option.value}
|
||||||
|
option={currentState}
|
||||||
|
searchValue={searchValue}
|
||||||
|
/>
|
||||||
|
</Select.Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientSideQBSearch.defaultProps = {
|
||||||
|
placeholder: PLACEHOLDER,
|
||||||
|
className: '',
|
||||||
|
suffixIcon: null,
|
||||||
|
whereClauseConfig: {},
|
||||||
|
attributeValuesMap: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClientSideQBSearch;
|
@ -109,8 +109,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-rule {
|
.alert-rule {
|
||||||
&-value,
|
&__value,
|
||||||
&-created-at {
|
&__created-at {
|
||||||
color: var(--text-ink-400);
|
color: var(--text-ink-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import './Table.styles.scss';
|
import './Table.styles.scss';
|
||||||
|
|
||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
|
import { initialFilters } from 'constants/queryBuilder';
|
||||||
import {
|
import {
|
||||||
useGetAlertRuleDetailsTimelineTable,
|
useGetAlertRuleDetailsTimelineTable,
|
||||||
useTimelineTable,
|
useTimelineTable,
|
||||||
} from 'pages/AlertDetails/hooks';
|
} from 'pages/AlertDetails/hooks';
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { timelineTableColumns } from './useTimelineTable';
|
import { timelineTableColumns } from './useTimelineTable';
|
||||||
|
|
||||||
function TimelineTable(): JSX.Element {
|
function TimelineTable(): JSX.Element {
|
||||||
|
const [filters, setFilters] = useState<TagFilter>(initialFilters);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
isRefetching,
|
isRefetching,
|
||||||
@ -18,13 +22,14 @@ function TimelineTable(): JSX.Element {
|
|||||||
data,
|
data,
|
||||||
isValidRuleId,
|
isValidRuleId,
|
||||||
ruleId,
|
ruleId,
|
||||||
} = useGetAlertRuleDetailsTimelineTable();
|
} = useGetAlertRuleDetailsTimelineTable({ filters });
|
||||||
|
|
||||||
const { timelineData, totalItems } = useMemo(() => {
|
const { timelineData, totalItems, labels } = useMemo(() => {
|
||||||
const response = data?.payload?.data;
|
const response = data?.payload?.data;
|
||||||
return {
|
return {
|
||||||
timelineData: response?.items,
|
timelineData: response?.items,
|
||||||
totalItems: response?.total,
|
totalItems: response?.total,
|
||||||
|
labels: response?.labels,
|
||||||
};
|
};
|
||||||
}, [data?.payload?.data]);
|
}, [data?.payload?.data]);
|
||||||
|
|
||||||
@ -42,7 +47,11 @@ function TimelineTable(): JSX.Element {
|
|||||||
<div className="timeline-table">
|
<div className="timeline-table">
|
||||||
<Table
|
<Table
|
||||||
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
|
rowKey={(row): string => `${row.fingerprint}-${row.value}-${row.unixMilli}`}
|
||||||
columns={timelineTableColumns()}
|
columns={timelineTableColumns({
|
||||||
|
filters,
|
||||||
|
labels: labels ?? {},
|
||||||
|
setFilters,
|
||||||
|
})}
|
||||||
dataSource={timelineData}
|
dataSource={timelineData}
|
||||||
pagination={paginationConfig}
|
pagination={paginationConfig}
|
||||||
size="middle"
|
size="middle"
|
||||||
|
@ -1,13 +1,84 @@
|
|||||||
import { EllipsisOutlined } from '@ant-design/icons';
|
import { EllipsisOutlined } from '@ant-design/icons';
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import ClientSideQBSearch, {
|
||||||
|
AttributeKey,
|
||||||
|
} from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
||||||
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
import { ConditionalAlertPopover } from 'container/AlertHistory/AlertPopover/AlertPopover';
|
||||||
import AlertLabels from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
|
import { transformKeyValuesToAttributeValuesMap } from 'container/QueryBuilder/filters/utils';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { Search } from 'lucide-react';
|
||||||
|
import AlertLabels, {
|
||||||
|
AlertLabelsProps,
|
||||||
|
} from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
|
||||||
import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
|
import AlertState from 'pages/AlertDetails/AlertHeader/AlertState/AlertState';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
import { AlertRuleTimelineTableResponse } from 'types/api/alerts/def';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { formatEpochTimestamp } from 'utils/timeUtils';
|
import { formatEpochTimestamp } from 'utils/timeUtils';
|
||||||
|
|
||||||
export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableResponse> => [
|
const transformLabelsToQbKeys = (
|
||||||
|
labels: AlertRuleTimelineTableResponse['labels'],
|
||||||
|
): AttributeKey[] => Object.keys(labels).flatMap((key) => [{ key }]);
|
||||||
|
|
||||||
|
function LabelFilter({
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
labels,
|
||||||
|
}: {
|
||||||
|
setFilters: (filters: TagFilter) => void;
|
||||||
|
filters: TagFilter;
|
||||||
|
labels: AlertLabelsProps['labels'];
|
||||||
|
}): JSX.Element | null {
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const { transformedKeys, attributesMap } = useMemo(
|
||||||
|
() => ({
|
||||||
|
transformedKeys: transformLabelsToQbKeys(labels || {}),
|
||||||
|
attributesMap: transformKeyValuesToAttributeValuesMap(labels),
|
||||||
|
}),
|
||||||
|
[labels],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearch = (tagFilters: TagFilter): void => {
|
||||||
|
const tagFiltersLength = tagFilters.items.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!tagFiltersLength && (!filters || !filters.items.length)) ||
|
||||||
|
tagFiltersLength === filters?.items.length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFilters(tagFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClientSideQBSearch
|
||||||
|
onChange={handleSearch}
|
||||||
|
filters={filters}
|
||||||
|
className="alert-history-label-search"
|
||||||
|
attributeKeys={transformedKeys}
|
||||||
|
attributeValuesMap={attributesMap}
|
||||||
|
suffixIcon={
|
||||||
|
<Search
|
||||||
|
size={14}
|
||||||
|
color={isDarkMode ? Color.TEXT_VANILLA_100 : Color.TEXT_INK_100}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const timelineTableColumns = ({
|
||||||
|
filters,
|
||||||
|
labels,
|
||||||
|
setFilters,
|
||||||
|
}: {
|
||||||
|
filters: TagFilter;
|
||||||
|
labels: AlertLabelsProps['labels'];
|
||||||
|
setFilters: (filters: TagFilter) => void;
|
||||||
|
}): ColumnsType<AlertRuleTimelineTableResponse> => [
|
||||||
{
|
{
|
||||||
title: 'STATE',
|
title: 'STATE',
|
||||||
dataIndex: 'state',
|
dataIndex: 'state',
|
||||||
@ -20,7 +91,9 @@ export const timelineTableColumns = (): ColumnsType<AlertRuleTimelineTableRespon
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'LABELS',
|
title: (
|
||||||
|
<LabelFilter setFilters={setFilters} filters={filters} labels={labels} />
|
||||||
|
),
|
||||||
dataIndex: 'labels',
|
dataIndex: 'labels',
|
||||||
render: (labels): JSX.Element => (
|
render: (labels): JSX.Element => (
|
||||||
<div className="alert-rule-labels">
|
<div className="alert-rule-labels">
|
||||||
|
@ -334,8 +334,8 @@
|
|||||||
|
|
||||||
.qb-search-bar-tokenised-tags {
|
.qb-search-bar-tokenised-tags {
|
||||||
.ant-tag {
|
.ant-tag {
|
||||||
border: 1px solid var(--bg-slate-100);
|
|
||||||
background: var(--bg-vanilla-300);
|
background: var(--bg-vanilla-300);
|
||||||
|
border: 1px solid var(--bg-slate-100);
|
||||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
.ant-typography {
|
.ant-typography {
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
width: 5px;
|
width: 5px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--bg-slate-300);
|
background-color: var(--bg-slate-300);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
.option {
|
||||||
@ -207,6 +208,10 @@
|
|||||||
background: var(--bg-vanilla-300);
|
background: var(--bg-vanilla-300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.option:hover {
|
.option:hover {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AttributeValuesMap } from 'components/ClientSideQBSearch/ClientSideQBSearch';
|
||||||
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
import { HAVING_FILTER_REGEXP } from 'constants/regExp';
|
||||||
import { IOption } from 'hooks/useResourceAttribute/types';
|
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||||
import uniqWith from 'lodash-es/unionWith';
|
import uniqWith from 'lodash-es/unionWith';
|
||||||
@ -92,3 +93,20 @@ export const getValidOrderByResult = (result: IOption[]): IOption[] =>
|
|||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
export const transformKeyValuesToAttributeValuesMap = (
|
||||||
|
attributeValuesMap: Record<string, string[] | number[] | boolean[]>,
|
||||||
|
): AttributeValuesMap =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(attributeValuesMap || {}).map(([key, values]) => [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
stringAttributeValues:
|
||||||
|
typeof values[0] === 'string' ? (values as string[]) : [],
|
||||||
|
numberAttributeValues:
|
||||||
|
typeof values[0] === 'number' ? (values as number[]) : [],
|
||||||
|
boolAttributeValues:
|
||||||
|
typeof values[0] === 'boolean' ? (values as boolean[]) : [],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
@ -4,7 +4,7 @@ import KeyValueLabel from 'periscope/components/KeyValueLabel';
|
|||||||
import SeeMore from 'periscope/components/SeeMore';
|
import SeeMore from 'periscope/components/SeeMore';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type AlertLabelsProps = {
|
export type AlertLabelsProps = {
|
||||||
labels: Record<string, any>;
|
labels: Record<string, any>;
|
||||||
initialCount?: number;
|
initialCount?: number;
|
||||||
};
|
};
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
AlertRuleTopContributorsPayload,
|
AlertRuleTopContributorsPayload,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { PayloadProps } from 'types/api/alerts/get';
|
import { PayloadProps } from 'types/api/alerts/get';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { nanoToMilli } from 'utils/timeUtils';
|
import { nanoToMilli } from 'utils/timeUtils';
|
||||||
|
|
||||||
export const useAlertHistoryQueryParams = (): {
|
export const useAlertHistoryQueryParams = (): {
|
||||||
@ -249,7 +250,11 @@ type GetAlertRuleDetailsTimelineTableProps = GetAlertRuleDetailsApiProps & {
|
|||||||
| undefined;
|
| undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetAlertRuleDetailsTimelineTable = (): GetAlertRuleDetailsTimelineTableProps => {
|
export const useGetAlertRuleDetailsTimelineTable = ({
|
||||||
|
filters,
|
||||||
|
}: {
|
||||||
|
filters: TagFilter;
|
||||||
|
}): GetAlertRuleDetailsTimelineTableProps => {
|
||||||
const { ruleId, startTime, endTime, params } = useAlertHistoryQueryParams();
|
const { ruleId, startTime, endTime, params } = useAlertHistoryQueryParams();
|
||||||
const { updatedOrder, offset } = useMemo(
|
const { updatedOrder, offset } = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -273,6 +278,7 @@ export const useGetAlertRuleDetailsTimelineTable = (): GetAlertRuleDetailsTimeli
|
|||||||
timelineFilter,
|
timelineFilter,
|
||||||
updatedOrder,
|
updatedOrder,
|
||||||
offset,
|
offset,
|
||||||
|
JSON.stringify(filters.items),
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
@ -283,7 +289,7 @@ export const useGetAlertRuleDetailsTimelineTable = (): GetAlertRuleDetailsTimeli
|
|||||||
limit: TIMELINE_TABLE_PAGE_SIZE,
|
limit: TIMELINE_TABLE_PAGE_SIZE,
|
||||||
order: updatedOrder,
|
order: updatedOrder,
|
||||||
offset,
|
offset,
|
||||||
|
filters,
|
||||||
...(timelineFilter && timelineFilter !== TimelineFilter.ALL
|
...(timelineFilter && timelineFilter !== TimelineFilter.ALL
|
||||||
? {
|
? {
|
||||||
state: timelineFilter === TimelineFilter.FIRED ? 'firing' : 'normal',
|
state: timelineFilter === TimelineFilter.FIRED ? 'firing' : 'normal',
|
||||||
|
@ -18,9 +18,13 @@ function AlertRuleProvider({
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = React.useMemo(() => ({ alertRuleState, setAlertRuleState }), [
|
const value = React.useMemo(
|
||||||
alertRuleState,
|
() => ({
|
||||||
]);
|
alertRuleState,
|
||||||
|
setAlertRuleState,
|
||||||
|
}),
|
||||||
|
[alertRuleState],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertRuleContext.Provider value={value}>
|
<AlertRuleContext.Provider value={value}>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { AlertLabelsProps } from 'pages/AlertDetails/AlertHeader/AlertLabels/AlertLabels';
|
||||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||||
|
|
||||||
// default match type for threshold
|
// default match type for threshold
|
||||||
@ -96,7 +97,11 @@ export interface AlertRuleTimelineTableResponse {
|
|||||||
relatedLogsLink: string;
|
relatedLogsLink: string;
|
||||||
}
|
}
|
||||||
export type AlertRuleTimelineTableResponsePayload = {
|
export type AlertRuleTimelineTableResponsePayload = {
|
||||||
data: { items: AlertRuleTimelineTableResponse[]; total: number };
|
data: {
|
||||||
|
items: AlertRuleTimelineTableResponse[];
|
||||||
|
total: number;
|
||||||
|
labels: AlertLabelsProps['labels'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
type AlertState = 'firing' | 'normal' | 'no-data' | 'muted';
|
type AlertState = 'firing' | 'normal' | 'no-data' | 'muted';
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user