mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-18 01:45:57 +08:00
fix: custom where clause value (#3209)
* fix: custom where clause value * fix: operations * fix: return suggestions for body --------- Co-authored-by: Palash Gupta <palashgdev@gmail.com>
This commit is contained in:
parent
872759f579
commit
2c5c972801
@ -1,5 +1,9 @@
|
|||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import {
|
||||||
|
initialQueriesMap,
|
||||||
|
OPERATORS,
|
||||||
|
PANEL_TYPES,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||||
@ -30,6 +34,10 @@ function LogExplorerQuerySection(): JSX.Element {
|
|||||||
const isTable = panelTypes === PANEL_TYPES.TABLE;
|
const isTable = panelTypes === PANEL_TYPES.TABLE;
|
||||||
const config: QueryBuilderProps['filterConfigs'] = {
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
stepInterval: { isHidden: isTable, isDisabled: true },
|
stepInterval: { isHidden: isTable, isDisabled: true },
|
||||||
|
filters: {
|
||||||
|
customKey: 'body',
|
||||||
|
customOp: OPERATORS.CONTAINS,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
@ -1,10 +1,18 @@
|
|||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
|
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
|
||||||
|
|
||||||
|
type FilterConfigs = {
|
||||||
|
[Key in keyof Omit<IBuilderQuery, 'filters'>]: {
|
||||||
|
isHidden: boolean;
|
||||||
|
isDisabled: boolean;
|
||||||
|
};
|
||||||
|
} & { filters: WhereClauseConfig };
|
||||||
|
|
||||||
export type QueryBuilderConfig =
|
export type QueryBuilderConfig =
|
||||||
| {
|
| {
|
||||||
queryVariant: 'static';
|
queryVariant: 'static';
|
||||||
@ -16,8 +24,6 @@ export type QueryBuilderProps = {
|
|||||||
config?: QueryBuilderConfig;
|
config?: QueryBuilderConfig;
|
||||||
panelType: PANEL_TYPES;
|
panelType: PANEL_TYPES;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
filterConfigs?: Partial<
|
filterConfigs?: Partial<FilterConfigs>;
|
||||||
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }>
|
|
||||||
>;
|
|
||||||
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
|
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
|
||||||
};
|
};
|
||||||
|
@ -305,7 +305,11 @@ export const Query = memo(function Query({
|
|||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
<Col flex="1">
|
<Col flex="1">
|
||||||
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={handleChangeTagFilters}
|
||||||
|
whereClauseConfig={filterConfigs?.filters}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Select, Spin, Tag, Tooltip } from 'antd';
|
import { Select, Spin, Tag, Tooltip } from 'antd';
|
||||||
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
|
import {
|
||||||
|
useAutoComplete,
|
||||||
|
WhereClauseConfig,
|
||||||
|
} from 'hooks/queryBuilder/useAutoComplete';
|
||||||
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
|
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
@ -31,6 +34,7 @@ import {
|
|||||||
function QueryBuilderSearch({
|
function QueryBuilderSearch({
|
||||||
query,
|
query,
|
||||||
onChange,
|
onChange,
|
||||||
|
whereClauseConfig,
|
||||||
}: QueryBuilderSearchProps): JSX.Element {
|
}: QueryBuilderSearchProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
updateTag,
|
updateTag,
|
||||||
@ -45,7 +49,7 @@ function QueryBuilderSearch({
|
|||||||
isFetching,
|
isFetching,
|
||||||
setSearchKey,
|
setSearchKey,
|
||||||
searchKey,
|
searchKey,
|
||||||
} = useAutoComplete(query);
|
} = useAutoComplete(query, whereClauseConfig);
|
||||||
|
|
||||||
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
|
||||||
searchValue,
|
searchValue,
|
||||||
@ -169,7 +173,7 @@ function QueryBuilderSearch({
|
|||||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
>
|
>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<Select.Option key={option.label} value={option.label}>
|
<Select.Option key={option.label} value={option.value}>
|
||||||
{option.label}
|
{option.label}
|
||||||
{option.selected && <StyledCheckOutlined />}
|
{option.selected && <StyledCheckOutlined />}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
@ -181,8 +185,13 @@ function QueryBuilderSearch({
|
|||||||
interface QueryBuilderSearchProps {
|
interface QueryBuilderSearchProps {
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
onChange: (value: TagFilter) => void;
|
onChange: (value: TagFilter) => void;
|
||||||
|
whereClauseConfig?: WhereClauseConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilderSearch.defaultProps = {
|
||||||
|
whereClauseConfig: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
export interface CustomTagProps {
|
export interface CustomTagProps {
|
||||||
label: ReactNode;
|
label: ReactNode;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
getRemovePrefixFromKey,
|
getRemovePrefixFromKey,
|
||||||
getTagToken,
|
getTagToken,
|
||||||
isExistsNotExistsOperator,
|
|
||||||
replaceStringWithMaxLength,
|
replaceStringWithMaxLength,
|
||||||
tagRegexp,
|
tagRegexp,
|
||||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
@ -16,7 +15,15 @@ import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
|
|||||||
import { useTag } from './useTag';
|
import { useTag } from './useTag';
|
||||||
import { useTagValidation } from './useTagValidation';
|
import { useTagValidation } from './useTagValidation';
|
||||||
|
|
||||||
export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
|
export type WhereClauseConfig = {
|
||||||
|
customKey: string;
|
||||||
|
customOp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAutoComplete = (
|
||||||
|
query: IBuilderQuery,
|
||||||
|
whereClauseConfig?: WhereClauseConfig,
|
||||||
|
): IAutoComplete => {
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
const [searchKey, setSearchKey] = useState<string>('');
|
const [searchKey, setSearchKey] = useState<string>('');
|
||||||
|
|
||||||
@ -40,11 +47,11 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
|
const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
|
||||||
key,
|
|
||||||
isValidTag,
|
isValidTag,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
query,
|
query,
|
||||||
setSearchKey,
|
setSearchKey,
|
||||||
|
whereClauseConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
@ -59,11 +66,10 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!isMulti) {
|
if (!isMulti) {
|
||||||
if (isExistsNotExistsOperator(value)) handleAddTag(value);
|
handleAddTag(value);
|
||||||
if (isValidTag && !isExistsNotExistsOperator(value)) handleAddTag(value);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleAddTag, isMulti, isValidTag],
|
[handleAddTag, isMulti],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
@ -102,6 +108,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
|
|||||||
isExist,
|
isExist,
|
||||||
results,
|
results,
|
||||||
result,
|
result,
|
||||||
|
whereClauseConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -7,8 +7,11 @@ import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
import { WhereClauseConfig } from './useAutoComplete';
|
||||||
import { useOperators } from './useOperators';
|
import { useOperators } from './useOperators';
|
||||||
|
|
||||||
|
export const WHERE_CLAUSE_CUSTOM_SUFFIX = '-custom';
|
||||||
|
|
||||||
export const useOptions = (
|
export const useOptions = (
|
||||||
key: string,
|
key: string,
|
||||||
keys: BaseAutocompleteData[],
|
keys: BaseAutocompleteData[],
|
||||||
@ -19,6 +22,7 @@ export const useOptions = (
|
|||||||
isExist: boolean,
|
isExist: boolean,
|
||||||
results: string[],
|
results: string[],
|
||||||
result: string[],
|
result: string[],
|
||||||
|
whereClauseConfig?: WhereClauseConfig,
|
||||||
): Option[] => {
|
): Option[] => {
|
||||||
const [options, setOptions] = useState<Option[]>([]);
|
const [options, setOptions] = useState<Option[]>([]);
|
||||||
const operators = useOperators(key, keys);
|
const operators = useOperators(key, keys);
|
||||||
@ -51,21 +55,64 @@ export const useOptions = (
|
|||||||
[key, operator],
|
[key, operator],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getOptionsWithValidOperator = useCallback(
|
||||||
|
(key: string, results: string[], searchValue: string) => {
|
||||||
|
const hasAllResults = results.every((value) => result.includes(value));
|
||||||
|
const values = getKeyOpValue(results);
|
||||||
|
|
||||||
|
return hasAllResults
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: searchValue,
|
||||||
|
value: searchValue,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
label: searchValue,
|
||||||
|
value: searchValue,
|
||||||
|
},
|
||||||
|
...values,
|
||||||
|
];
|
||||||
|
},
|
||||||
|
[getKeyOpValue, result],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getKeyOperatorOptions = useCallback(
|
||||||
|
(key: string) => {
|
||||||
|
const operatorsOptions = operators?.map((operator) => ({
|
||||||
|
value: `${key} ${operator} `,
|
||||||
|
label: `${key} ${operator} `,
|
||||||
|
}));
|
||||||
|
if (whereClauseConfig) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: `${searchValue} `,
|
||||||
|
value: `${searchValue}${WHERE_CLAUSE_CUSTOM_SUFFIX}`,
|
||||||
|
},
|
||||||
|
...operatorsOptions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return operatorsOptions;
|
||||||
|
},
|
||||||
|
[operators, searchValue, whereClauseConfig],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let newOptions: Option[] = [];
|
let newOptions: Option[] = [];
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
newOptions = searchValue
|
newOptions = searchValue
|
||||||
? [
|
? [
|
||||||
{ label: `${searchValue} `, value: `${searchValue} ` },
|
{
|
||||||
|
label: `${searchValue} `,
|
||||||
|
value: `${searchValue} `,
|
||||||
|
},
|
||||||
...getOptionsFromKeys(keys),
|
...getOptionsFromKeys(keys),
|
||||||
]
|
]
|
||||||
: getOptionsFromKeys(keys);
|
: getOptionsFromKeys(keys);
|
||||||
} else if (key && !operator) {
|
} else if (key && !operator) {
|
||||||
newOptions = operators?.map((operator) => ({
|
newOptions = getKeyOperatorOptions(key);
|
||||||
value: `${key} ${operator} `,
|
|
||||||
label: `${key} ${operator} `,
|
|
||||||
}));
|
|
||||||
} else if (key && operator) {
|
} else if (key && operator) {
|
||||||
if (isMulti) {
|
if (isMulti) {
|
||||||
newOptions = results.map((item) => ({
|
newOptions = results.map((item) => ({
|
||||||
@ -75,17 +122,14 @@ export const useOptions = (
|
|||||||
} else if (isExist) {
|
} else if (isExist) {
|
||||||
newOptions = [];
|
newOptions = [];
|
||||||
} else if (isValidOperator) {
|
} else if (isValidOperator) {
|
||||||
const hasAllResults = results.every((value) => result.includes(value));
|
newOptions = getOptionsWithValidOperator(key, results, searchValue);
|
||||||
const values = getKeyOpValue(results);
|
|
||||||
newOptions = hasAllResults
|
|
||||||
? [{ label: searchValue, value: searchValue }]
|
|
||||||
: [{ label: searchValue, value: searchValue }, ...values];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newOptions.length > 0) {
|
if (newOptions.length > 0) {
|
||||||
setOptions(newOptions);
|
setOptions(newOptions);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
whereClauseConfig,
|
||||||
getKeyOpValue,
|
getKeyOpValue,
|
||||||
getOptionsFromKeys,
|
getOptionsFromKeys,
|
||||||
isExist,
|
isExist,
|
||||||
@ -98,6 +142,8 @@ export const useOptions = (
|
|||||||
result,
|
result,
|
||||||
results,
|
results,
|
||||||
searchValue,
|
searchValue,
|
||||||
|
getKeyOperatorOptions,
|
||||||
|
getOptionsWithValidOperator,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
@ -63,9 +63,18 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
|
|
||||||
const getNewListOfAdditionalFilters = useCallback(
|
const getNewListOfAdditionalFilters = useCallback(
|
||||||
(dataSource: DataSource): string[] => {
|
(dataSource: DataSource): string[] => {
|
||||||
|
const additionalFiltersKeys: (keyof Pick<
|
||||||
|
IBuilderQuery,
|
||||||
|
'orderBy' | 'limit' | 'having' | 'stepInterval'
|
||||||
|
>)[] = ['having', 'limit', 'orderBy', 'stepInterval'];
|
||||||
|
|
||||||
const result: string[] = mapOfFilters[dataSource].reduce<string[]>(
|
const result: string[] = mapOfFilters[dataSource].reduce<string[]>(
|
||||||
(acc, item) => {
|
(acc, item) => {
|
||||||
if (filterConfigs && filterConfigs[item.field]?.isHidden) {
|
if (
|
||||||
|
filterConfigs &&
|
||||||
|
filterConfigs[item.field as typeof additionalFiltersKeys[number]]
|
||||||
|
?.isHidden
|
||||||
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,6 @@ export const useSetCurrentKeyAndOperator = (
|
|||||||
let key = '';
|
let key = '';
|
||||||
let operator = '';
|
let operator = '';
|
||||||
let result: string[] = [];
|
let result: string[] = [];
|
||||||
|
|
||||||
if (value) {
|
|
||||||
const { tagKey, tagOperator, tagValue } = getTagToken(value);
|
const { tagKey, tagOperator, tagValue } = getTagToken(value);
|
||||||
const isSuggestKey = keys?.some(
|
const isSuggestKey = keys?.some(
|
||||||
(el) => el?.key === getRemovePrefixFromKey(tagKey),
|
(el) => el?.key === getRemovePrefixFromKey(tagKey),
|
||||||
@ -26,7 +24,6 @@ export const useSetCurrentKeyAndOperator = (
|
|||||||
operator = tagOperator || '';
|
operator = tagOperator || '';
|
||||||
result = tagValue || [];
|
result = tagValue || [];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return [key, operator, result];
|
return [key, operator, result];
|
||||||
}, [value, keys]);
|
}, [value, keys]);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
getOperatorFromValue,
|
getOperatorFromValue,
|
||||||
|
getTagToken,
|
||||||
isExistsNotExistsOperator,
|
isExistsNotExistsOperator,
|
||||||
isInNInOperator,
|
isInNInOperator,
|
||||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
@ -8,6 +9,8 @@ import * as Papa from 'papaparse';
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { WhereClauseConfig } from './useAutoComplete';
|
||||||
|
|
||||||
type IUseTag = {
|
type IUseTag = {
|
||||||
handleAddTag: (value: string) => void;
|
handleAddTag: (value: string) => void;
|
||||||
handleClearTag: (value: string) => void;
|
handleClearTag: (value: string) => void;
|
||||||
@ -24,11 +27,11 @@ type IUseTag = {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const useTag = (
|
export const useTag = (
|
||||||
key: string,
|
|
||||||
isValidTag: boolean,
|
isValidTag: boolean,
|
||||||
handleSearch: (value: string) => void,
|
handleSearch: (value: string) => void,
|
||||||
query: IBuilderQuery,
|
query: IBuilderQuery,
|
||||||
setSearchKey: (value: string) => void,
|
setSearchKey: (value: string) => void,
|
||||||
|
whereClauseConfig?: WhereClauseConfig,
|
||||||
): IUseTag => {
|
): IUseTag => {
|
||||||
const initTagsData = useMemo(
|
const initTagsData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -57,15 +60,31 @@ export const useTag = (
|
|||||||
* Adds a new tag to the tag list.
|
* Adds a new tag to the tag list.
|
||||||
* @param {string} value - The tag value to be added.
|
* @param {string} value - The tag value to be added.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const handleAddTag = useCallback(
|
const handleAddTag = useCallback(
|
||||||
(value: string): void => {
|
(value: string): void => {
|
||||||
|
const { tagKey } = getTagToken(value);
|
||||||
|
const [key, id] = tagKey.split('-');
|
||||||
|
|
||||||
|
if (id === 'custom') {
|
||||||
|
const customValue = whereClauseConfig
|
||||||
|
? `${whereClauseConfig.customKey} ${whereClauseConfig.customOp} ${key}`
|
||||||
|
: '';
|
||||||
|
setTags((prevTags) =>
|
||||||
|
prevTags.includes(customValue) ? prevTags : [...prevTags, customValue],
|
||||||
|
);
|
||||||
|
handleSearch('');
|
||||||
|
setSearchKey('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) {
|
if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) {
|
||||||
setTags((prevTags) => [...prevTags, value]);
|
setTags((prevTags) => [...prevTags, value]);
|
||||||
handleSearch('');
|
handleSearch('');
|
||||||
setSearchKey('');
|
setSearchKey('');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[key, isValidTag, handleSearch, setSearchKey],
|
[whereClauseConfig, isValidTag, handleSearch, setSearchKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user