mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 09:38:59 +08:00
feat: implement quick filters for the new logs explorer page (#5799)
* feat: logs quick filter * feat: added open button in the closed state * fix: build issues * chore: minor css * feat: handle changes for last used query,states and reset * feat: refactor some code * feat: handle on change functionality * fix: handle only and all * chore: handle empty edge cases * feat: added necessary tooltips * feat: use tag instead of tooltip icon * feat: handle light mode designs * feat: added correct facets * feat: added resize observer for the graph resize * chore: added local storage state for the toggle * chore: make refresh text configurable * feat: added environment and fix build * feat: handle the cases for = and != operators * feat: design changes and zoom out * feat: minor css issue * fix: light mode designs * fix: handle the case for state initialization * fix: onDelete query the last used index should be set to 0
This commit is contained in:
parent
ba95ca682b
commit
4a9847abdd
@ -0,0 +1,145 @@
|
|||||||
|
.checkbox-filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
.filter-header-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.left-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.clear-all {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.values {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.checkbox-value-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.filter-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-string {
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.only-btn:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-value-section:hover {
|
||||||
|
.toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.only-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value:hover {
|
||||||
|
.toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.show-more-text {
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.checkbox-filter {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
.filter-header-checkbox {
|
||||||
|
.left-action {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,503 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './Checkbox.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Checkbox, Input, Skeleton, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
|
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
|
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep, isArray, isEmpty, isEqual } from 'lodash-es';
|
||||||
|
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||||
|
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'nin'];
|
||||||
|
|
||||||
|
function setDefaultValues(
|
||||||
|
values: string[],
|
||||||
|
trueOrFalse: boolean,
|
||||||
|
): Record<string, boolean> {
|
||||||
|
const defaultState: Record<string, boolean> = {};
|
||||||
|
values.forEach((val) => {
|
||||||
|
defaultState[val] = trueOrFalse;
|
||||||
|
});
|
||||||
|
return defaultState;
|
||||||
|
}
|
||||||
|
interface ICheckboxProps {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||||
|
const { filter } = props;
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(filter.defaultOpen);
|
||||||
|
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||||
|
|
||||||
|
const {
|
||||||
|
lastUsedQuery,
|
||||||
|
currentQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetAggregateValues(
|
||||||
|
{
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
dataSource: DataSource.LOGS,
|
||||||
|
aggregateAttribute: '',
|
||||||
|
attributeKey: filter.attributeKey.key,
|
||||||
|
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||||
|
tagType: filter.attributeKey.type || '',
|
||||||
|
searchText: searchText ?? '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isOpen,
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const attributeValues: string[] = useMemo(
|
||||||
|
() =>
|
||||||
|
((Object.values(data?.payload || {}).find((el) => !!el) ||
|
||||||
|
[]) as string[]).filter((val) => !isEmpty(val)),
|
||||||
|
[data?.payload],
|
||||||
|
);
|
||||||
|
const currentAttributeKeys = attributeValues.slice(0, visibleItemsCount);
|
||||||
|
|
||||||
|
// derive the state of each filter key here in the renderer itself and keep it in sync with staged query
|
||||||
|
// also we need to keep a note of last focussed query.
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
const currentFilterState = useMemo(() => {
|
||||||
|
let filterState: Record<string, boolean> = setDefaultValues(
|
||||||
|
attributeValues,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const filterSync = currentQuery?.builder.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items.find((item) => isEqual(item.key, filter.attributeKey));
|
||||||
|
|
||||||
|
if (filterSync) {
|
||||||
|
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
|
if (isArray(filterSync.value)) {
|
||||||
|
filterSync.value.forEach((val) => {
|
||||||
|
filterState[val] = true;
|
||||||
|
});
|
||||||
|
} else if (typeof filterSync.value === 'string') {
|
||||||
|
filterState[filterSync.value] = true;
|
||||||
|
} else if (typeof filterSync.value === 'boolean') {
|
||||||
|
filterState[String(filterSync.value)] = true;
|
||||||
|
} else if (typeof filterSync.value === 'number') {
|
||||||
|
filterState[String(filterSync.value)] = true;
|
||||||
|
}
|
||||||
|
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||||
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
|
if (isArray(filterSync.value)) {
|
||||||
|
filterSync.value.forEach((val) => {
|
||||||
|
filterState[val] = false;
|
||||||
|
});
|
||||||
|
} else if (typeof filterSync.value === 'string') {
|
||||||
|
filterState[filterSync.value] = false;
|
||||||
|
} else if (typeof filterSync.value === 'boolean') {
|
||||||
|
filterState[String(filterSync.value)] = false;
|
||||||
|
} else if (typeof filterSync.value === 'number') {
|
||||||
|
filterState[String(filterSync.value)] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filterState = setDefaultValues(attributeValues, true);
|
||||||
|
}
|
||||||
|
return filterState;
|
||||||
|
}, [
|
||||||
|
attributeValues,
|
||||||
|
currentQuery?.builder.queryData,
|
||||||
|
filter.attributeKey,
|
||||||
|
lastUsedQuery,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||||
|
const isFilterDisabled = useMemo(
|
||||||
|
() =>
|
||||||
|
(currentQuery?.builder?.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items?.filter((item) => isEqual(item.key, filter.attributeKey))
|
||||||
|
?.length || 0) > 1,
|
||||||
|
|
||||||
|
[currentQuery?.builder?.queryData, lastUsedQuery, filter.attributeKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
// variable to check if the current filter has multiple values to its name in the key op value section
|
||||||
|
const isMultipleValuesTrueForTheKey =
|
||||||
|
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||||
|
|
||||||
|
const handleClearFilterAttribute = (): void => {
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items:
|
||||||
|
idx === lastUsedQuery
|
||||||
|
? item.filters.items.filter(
|
||||||
|
(fil) => !isEqual(fil.key, filter.attributeKey),
|
||||||
|
)
|
||||||
|
: [...item.filters.items],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSomeFilterPresentForCurrentAttribute = currentQuery.builder.queryData?.[
|
||||||
|
lastUsedQuery || 0
|
||||||
|
]?.filters?.items?.some((item) => isEqual(item.key, filter.attributeKey));
|
||||||
|
|
||||||
|
const onChange = (
|
||||||
|
value: string,
|
||||||
|
checked: boolean,
|
||||||
|
isOnlyOrAllClicked: boolean,
|
||||||
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
|
): void => {
|
||||||
|
const query = cloneDeep(currentQuery.builder.queryData?.[lastUsedQuery || 0]);
|
||||||
|
|
||||||
|
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||||
|
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||||
|
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||||
|
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||||
|
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||||
|
? 'All'
|
||||||
|
: 'Only'
|
||||||
|
: 'Only';
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(q) => !isEqual(q.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
if (isOnlyOrAll === 'Only') {
|
||||||
|
const newFilterItem: TagFilterItem = {
|
||||||
|
id: uuid(),
|
||||||
|
op: getOperatorValue(OPERATORS.IN),
|
||||||
|
key: filter.attributeKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
query.filters.items = [...query.filters.items, newFilterItem];
|
||||||
|
}
|
||||||
|
} else if (query?.filters?.items) {
|
||||||
|
if (
|
||||||
|
query.filters?.items?.some((item) => isEqual(item.key, filter.attributeKey))
|
||||||
|
) {
|
||||||
|
// if there is already a running filter for the current attribute key then
|
||||||
|
// we split the cases by which particular operator is present right now!
|
||||||
|
const currentFilter = query.filters?.items?.find((q) =>
|
||||||
|
isEqual(q.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
if (currentFilter) {
|
||||||
|
const runningOperator = currentFilter?.op;
|
||||||
|
switch (runningOperator) {
|
||||||
|
case 'in':
|
||||||
|
if (checked) {
|
||||||
|
// if it's an IN operator then if we are checking another value it get's added to the
|
||||||
|
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [...currentFilter.value, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// if the current state wasn't an array we make it one and add our value
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (!checked) {
|
||||||
|
// if we are removing some value when the running operator is IN we filter.
|
||||||
|
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: currentFilter.value.filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.value.length === 0) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if not an array remove the whole thing altogether!
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nin':
|
||||||
|
// if the current running operator is NIN then when unchecking the value it gets
|
||||||
|
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||||
|
if (!checked) {
|
||||||
|
// in case of array add the currentUnselectedValue to the list.
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [...currentFilter.value, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// in case of not an array make it one!
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (checked) {
|
||||||
|
// opposite of above!
|
||||||
|
if (isArray(currentFilter.value)) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
value: currentFilter.value.filter((val) => val !== value),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newFilter.value.length === 0) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '=':
|
||||||
|
if (checked) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
op: getOperatorValue(OPERATORS.IN),
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else if (!checked) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case '!=':
|
||||||
|
if (!checked) {
|
||||||
|
const newFilter = {
|
||||||
|
...currentFilter,
|
||||||
|
op: getOperatorValue(OPERATORS.NIN),
|
||||||
|
value: [currentFilter.value as string, value],
|
||||||
|
};
|
||||||
|
query.filters.items = query.filters.items.map((item) => {
|
||||||
|
if (isEqual(item.key, filter.attributeKey)) {
|
||||||
|
return newFilter;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
} else if (checked) {
|
||||||
|
query.filters.items = query.filters.items.filter(
|
||||||
|
(item) => !isEqual(item.key, filter.attributeKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// case - when there is no filter for the current key that means all are selected right now.
|
||||||
|
const newFilterItem: TagFilterItem = {
|
||||||
|
id: uuid(),
|
||||||
|
op: getOperatorValue(OPERATORS.NIN),
|
||||||
|
key: filter.attributeKey,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
query.filters.items = [...query.filters.items, newFilterItem];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const finalQuery = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
...currentQuery.builder.queryData.map((q, idx) => {
|
||||||
|
if (idx === lastUsedQuery) {
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
redirectWithQueryBuilderData(finalQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="checkbox-filter">
|
||||||
|
<section className="filter-header-checkbox">
|
||||||
|
<section className="left-action">
|
||||||
|
{isOpen ? (
|
||||||
|
<ChevronDown
|
||||||
|
size={13}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(): void => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setVisibleItemsCount(10);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ChevronRight
|
||||||
|
size={13}
|
||||||
|
onClick={(): void => setIsOpen(true)}
|
||||||
|
cursor="pointer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Typography.Text className="title">{filter.title}</Typography.Text>
|
||||||
|
</section>
|
||||||
|
<section className="right-action">
|
||||||
|
{isOpen && (
|
||||||
|
<Typography.Text
|
||||||
|
className="clear-all"
|
||||||
|
onClick={handleClearFilterAttribute}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
{isOpen && isLoading && !attributeValues.length && (
|
||||||
|
<section className="loading">
|
||||||
|
<Skeleton paragraph={{ rows: 4 }} />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{isOpen && !isLoading && (
|
||||||
|
<>
|
||||||
|
<section className="search">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter values"
|
||||||
|
onChange={(e): void => setSearchText(e.target.value)}
|
||||||
|
disabled={isFilterDisabled}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
{attributeValues.length > 0 ? (
|
||||||
|
<section className="values">
|
||||||
|
{currentAttributeKeys.map((value: string) => (
|
||||||
|
<div key={value} className="value">
|
||||||
|
<Checkbox
|
||||||
|
onChange={(e): void => onChange(value, e.target.checked, false)}
|
||||||
|
checked={currentFilterState[value]}
|
||||||
|
disabled={isFilterDisabled}
|
||||||
|
rootClassName="check-box"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
'checkbox-value-section',
|
||||||
|
isFilterDisabled ? 'filter-disabled' : '',
|
||||||
|
)}
|
||||||
|
onClick={(): void => {
|
||||||
|
if (isFilterDisabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange(value, currentFilterState[value], true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filter.customRendererForValue ? (
|
||||||
|
filter.customRendererForValue(value)
|
||||||
|
) : (
|
||||||
|
<Typography.Text
|
||||||
|
className="value-string"
|
||||||
|
ellipsis={{ tooltip: { placement: 'right' } }}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Typography.Text>
|
||||||
|
)}
|
||||||
|
<Button type="text" className="only-btn">
|
||||||
|
{isSomeFilterPresentForCurrentAttribute
|
||||||
|
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||||
|
? 'All'
|
||||||
|
: 'Only'
|
||||||
|
: 'Only'}
|
||||||
|
</Button>
|
||||||
|
<Button type="text" className="toggle-btn">
|
||||||
|
Toggle
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
) : (
|
||||||
|
<section className="no-data">
|
||||||
|
<Typography.Text>No values found</Typography.Text>{' '}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
{visibleItemsCount < attributeValues?.length && (
|
||||||
|
<section className="show-more">
|
||||||
|
<Typography.Text
|
||||||
|
className="show-more-text"
|
||||||
|
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||||
|
>
|
||||||
|
Show More...
|
||||||
|
</Typography.Text>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import './Slider.styles.scss';
|
||||||
|
|
||||||
|
import { IQuickFiltersConfig } from 'components/QuickFilters/QuickFilters';
|
||||||
|
|
||||||
|
interface ISliderProps {
|
||||||
|
filter: IQuickFiltersConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not needed for now build when required
|
||||||
|
export default function Slider(props: ISliderProps): JSX.Element {
|
||||||
|
const { filter } = props;
|
||||||
|
console.log(filter);
|
||||||
|
return <div>Slider</div>;
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
.quick-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10.5px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-tag {
|
||||||
|
display: flex;
|
||||||
|
padding: 5px 9px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid rgba(78, 116, 248, 0.2);
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.divider-filter {
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: #161922;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.quick-filters {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-right: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.left-actions {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.right-actions {
|
||||||
|
.sync-icon {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
124
frontend/src/components/QuickFilters/QuickFilters.tsx
Normal file
124
frontend/src/components/QuickFilters/QuickFilters.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import './QuickFilters.styles.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
FilterOutlined,
|
||||||
|
SyncOutlined,
|
||||||
|
VerticalAlignTopOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import Checkbox from './FilterRenderers/Checkbox/Checkbox';
|
||||||
|
import Slider from './FilterRenderers/Slider/Slider';
|
||||||
|
|
||||||
|
export enum FiltersType {
|
||||||
|
SLIDER = 'SLIDER',
|
||||||
|
CHECKBOX = 'CHECKBOX',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MinMax {
|
||||||
|
MIN = 'MIN',
|
||||||
|
MAX = 'MAX',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SpecficFilterOperations {
|
||||||
|
ALL = 'ALL',
|
||||||
|
ONLY = 'ONLY',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQuickFiltersConfig {
|
||||||
|
type: FiltersType;
|
||||||
|
title: string;
|
||||||
|
attributeKey: BaseAutocompleteData;
|
||||||
|
customRendererForValue?: (value: string) => JSX.Element;
|
||||||
|
defaultOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IQuickFiltersProps {
|
||||||
|
config: IQuickFiltersConfig[];
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||||
|
const { config, handleFilterVisibilityChange } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
lastUsedQuery,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
// clear all the filters for the query which is in sync with filters
|
||||||
|
const handleReset = (): void => {
|
||||||
|
const updatedQuery = cloneDeep(
|
||||||
|
currentQuery?.builder.queryData?.[lastUsedQuery || 0],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedQuery?.filters?.items) {
|
||||||
|
updatedQuery.filters.items = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const preparedQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: idx === lastUsedQuery ? [] : [...item.filters.items],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
redirectWithQueryBuilderData(preparedQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const lastQueryName =
|
||||||
|
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||||
|
return (
|
||||||
|
<div className="quick-filters">
|
||||||
|
<section className="header">
|
||||||
|
<section className="left-actions">
|
||||||
|
<FilterOutlined />
|
||||||
|
<Typography.Text className="text">Filters for</Typography.Text>
|
||||||
|
<Tooltip title={`Filter currently in sync with query ${lastQueryName}`}>
|
||||||
|
<Typography.Text className="sync-tag">{lastQueryName}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
<section className="right-actions">
|
||||||
|
<Tooltip title="Reset All">
|
||||||
|
<SyncOutlined className="sync-icon" onClick={handleReset} />
|
||||||
|
</Tooltip>
|
||||||
|
<div className="divider-filter" />
|
||||||
|
<Tooltip title="Collapse Filters">
|
||||||
|
<VerticalAlignTopOutlined
|
||||||
|
rotate={270}
|
||||||
|
onClick={handleFilterVisibilityChange}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="filters">
|
||||||
|
{config.map((filter) => {
|
||||||
|
switch (filter.type) {
|
||||||
|
case FiltersType.CHECKBOX:
|
||||||
|
return <Checkbox filter={filter} />;
|
||||||
|
case FiltersType.SLIDER:
|
||||||
|
return <Slider filter={filter} />;
|
||||||
|
default:
|
||||||
|
return <Checkbox filter={filter} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -19,4 +19,5 @@ export enum LOCALSTORAGE {
|
|||||||
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
SHOW_EXPLORER_TOOLBAR = 'SHOW_EXPLORER_TOOLBAR',
|
||||||
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
PINNED_ATTRIBUTES = 'PINNED_ATTRIBUTES',
|
||||||
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
THEME_ANALYTICS_V1 = 'THEME_ANALYTICS_V1',
|
||||||
|
SHOW_LOGS_QUICK_FILTERS = 'SHOW_LOGS_QUICK_FILTERS',
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,12 @@
|
|||||||
border: 1px solid rgba(242, 71, 105, 0.4);
|
border: 1px solid rgba(242, 71, 105, 0.4);
|
||||||
color: var(--bg-sakura-400);
|
color: var(--bg-sakura-400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.sync-btn {
|
||||||
|
border: 1px solid rgba(78, 116, 248, 0.2);
|
||||||
|
background: rgba(78, 116, 248, 0.1);
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.formula-btn {
|
&.formula-btn {
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import './QueryBuilder.styles.scss';
|
import './QueryBuilder.styles.scss';
|
||||||
|
|
||||||
import { Button, Col, Divider, Row, Tooltip, Typography } from 'antd';
|
import { Button, Col, Divider, Row, Tooltip, Typography } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
import {
|
import {
|
||||||
MAX_FORMULAS,
|
MAX_FORMULAS,
|
||||||
MAX_QUERIES,
|
MAX_QUERIES,
|
||||||
OPERATORS,
|
OPERATORS,
|
||||||
PANEL_TYPES,
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
// ** Hooks
|
// ** Hooks
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { DatabaseZap, Sigma } from 'lucide-react';
|
import { DatabaseZap, Sigma } from 'lucide-react';
|
||||||
// ** Constants
|
// ** Constants
|
||||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
// ** Components
|
// ** Components
|
||||||
@ -35,6 +38,8 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
panelType,
|
panelType,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
|
setLastUsedQuery,
|
||||||
|
lastUsedQuery,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
@ -46,6 +51,10 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
[config],
|
[config],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const isLogsExplorerPage = pathname === ROUTES.LOGS_EXPLORER;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||||
if (newPanelType === PANEL_TYPES.BAR) {
|
if (newPanelType === PANEL_TYPES.BAR) {
|
||||||
@ -212,6 +221,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
<Col
|
<Col
|
||||||
key={query.queryName}
|
key={query.queryName}
|
||||||
span={24}
|
span={24}
|
||||||
|
onClickCapture={(): void => setLastUsedQuery(index)}
|
||||||
className="query"
|
className="query"
|
||||||
id={`qb-query-${query.queryName}`}
|
id={`qb-query-${query.queryName}`}
|
||||||
>
|
>
|
||||||
@ -265,10 +275,13 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
|
|
||||||
{!isListViewPanel && (
|
{!isListViewPanel && (
|
||||||
<Col span={1} className="query-builder-mini-map">
|
<Col span={1} className="query-builder-mini-map">
|
||||||
{currentQuery.builder.queryData.map((query) => (
|
{currentQuery.builder.queryData.map((query, index) => (
|
||||||
<Button
|
<Button
|
||||||
disabled={isDisabledQueryButton}
|
disabled={isDisabledQueryButton}
|
||||||
className="query-btn"
|
className={cx(
|
||||||
|
'query-btn',
|
||||||
|
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
|
||||||
|
)}
|
||||||
key={query.queryName}
|
key={query.queryName}
|
||||||
onClick={(): void => handleScrollIntoView('query', query.queryName)}
|
onClick={(): void => handleScrollIntoView('query', query.queryName)}
|
||||||
>
|
>
|
||||||
|
@ -44,6 +44,12 @@
|
|||||||
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
border: 1px solid rgba(242, 71, 105, 0.2) !important;
|
||||||
background: rgba(242, 71, 105, 0.1) !important;
|
background: rgba(242, 71, 105, 0.1) !important;
|
||||||
|
|
||||||
|
&.sync-btn {
|
||||||
|
border: 1px solid rgba(78, 116, 248, 0.2) !important;
|
||||||
|
background: rgba(78, 116, 248, 0.1) !important;
|
||||||
|
color: var(--bg-robin-500) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid rgba(242, 71, 105, 0.4) !important;
|
border: 1px solid rgba(242, 71, 105, 0.4) !important;
|
||||||
color: var(--bg-sakura-400) !important;
|
color: var(--bg-sakura-400) !important;
|
||||||
|
@ -4,6 +4,8 @@ import './QBEntityOptions.styles.scss';
|
|||||||
import { Button, Col, Tooltip } from 'antd';
|
import { Button, Col, Tooltip } from 'antd';
|
||||||
import { noop } from 'antd/lib/_util/warning';
|
import { noop } from 'antd/lib/_util/warning';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { isFunction } from 'lodash-es';
|
import { isFunction } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
EyeOff,
|
EyeOff,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
IBuilderQuery,
|
IBuilderQuery,
|
||||||
QueryFunctionProps,
|
QueryFunctionProps,
|
||||||
@ -35,6 +38,7 @@ interface QBEntityOptionsProps {
|
|||||||
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
onQueryFunctionsUpdates?: (functions: QueryFunctionProps[]) => void;
|
||||||
showDeleteButton: boolean;
|
showDeleteButton: boolean;
|
||||||
isListViewPanel?: boolean;
|
isListViewPanel?: boolean;
|
||||||
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QBEntityOptions({
|
export default function QBEntityOptions({
|
||||||
@ -51,6 +55,7 @@ export default function QBEntityOptions({
|
|||||||
showDeleteButton,
|
showDeleteButton,
|
||||||
onQueryFunctionsUpdates,
|
onQueryFunctionsUpdates,
|
||||||
isListViewPanel,
|
isListViewPanel,
|
||||||
|
index,
|
||||||
}: QBEntityOptionsProps): JSX.Element {
|
}: QBEntityOptionsProps): JSX.Element {
|
||||||
const handleCloneEntity = (): void => {
|
const handleCloneEntity = (): void => {
|
||||||
if (isFunction(onCloneQuery)) {
|
if (isFunction(onCloneQuery)) {
|
||||||
@ -58,6 +63,12 @@ export default function QBEntityOptions({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const isLogsExplorerPage = pathname === ROUTES.LOGS_EXPLORER;
|
||||||
|
|
||||||
|
const { lastUsedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const isLogsDataSource = query?.dataSource === DataSource.LOGS;
|
const isLogsDataSource = query?.dataSource === DataSource.LOGS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -98,6 +109,7 @@ export default function QBEntityOptions({
|
|||||||
className={cx(
|
className={cx(
|
||||||
'periscope-btn',
|
'periscope-btn',
|
||||||
entityType === 'query' ? 'query-name' : 'formula-name',
|
entityType === 'query' ? 'query-name' : 'formula-name',
|
||||||
|
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{entityData.queryName}
|
{entityData.queryName}
|
||||||
@ -143,4 +155,5 @@ QBEntityOptions.defaultProps = {
|
|||||||
onQueryFunctionsUpdates: undefined,
|
onQueryFunctionsUpdates: undefined,
|
||||||
showFunctions: false,
|
showFunctions: false,
|
||||||
onCloneQuery: noop,
|
onCloneQuery: noop,
|
||||||
|
index: 0,
|
||||||
};
|
};
|
||||||
|
@ -348,6 +348,7 @@ export const Query = memo(function Query({
|
|||||||
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
|
onQueryFunctionsUpdates={handleQueryFunctionsUpdates}
|
||||||
showDeleteButton={currentQuery.builder.queryData.length > 1}
|
showDeleteButton={currentQuery.builder.queryData.length > 1}
|
||||||
isListViewPanel={isListViewPanel}
|
isListViewPanel={isListViewPanel}
|
||||||
|
index={index}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isCollapse && (
|
{!isCollapse && (
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import './ToolbarActions.styles.scss';
|
import './ToolbarActions.styles.scss';
|
||||||
|
|
||||||
|
import { FilterOutlined } from '@ant-design/icons';
|
||||||
import { Button, Switch, Tooltip, Typography } from 'antd';
|
import { Button, Switch, Tooltip, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { Atom, SquareMousePointer, Terminal } from 'lucide-react';
|
import { Atom, SquareMousePointer, Terminal } from 'lucide-react';
|
||||||
@ -11,6 +12,8 @@ interface LeftToolbarActionsProps {
|
|||||||
onToggleHistrogramVisibility: () => void;
|
onToggleHistrogramVisibility: () => void;
|
||||||
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
|
onChangeSelectedView: (view: SELECTED_VIEWS) => void;
|
||||||
showFrequencyChart: boolean;
|
showFrequencyChart: boolean;
|
||||||
|
showFilter: boolean;
|
||||||
|
handleFilterVisibilityChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTab = 'active-tab';
|
const activeTab = 'active-tab';
|
||||||
@ -23,11 +26,20 @@ export default function LeftToolbarActions({
|
|||||||
onToggleHistrogramVisibility,
|
onToggleHistrogramVisibility,
|
||||||
onChangeSelectedView,
|
onChangeSelectedView,
|
||||||
showFrequencyChart,
|
showFrequencyChart,
|
||||||
|
showFilter,
|
||||||
|
handleFilterVisibilityChange,
|
||||||
}: LeftToolbarActionsProps): JSX.Element {
|
}: LeftToolbarActionsProps): JSX.Element {
|
||||||
const { clickhouse, search, queryBuilder: QB } = items;
|
const { clickhouse, search, queryBuilder: QB } = items;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="left-toolbar">
|
<div className="left-toolbar">
|
||||||
|
{!showFilter && (
|
||||||
|
<Tooltip title="Show Filters">
|
||||||
|
<Button onClick={handleFilterVisibilityChange} className="filter-btn">
|
||||||
|
<FilterOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<div className="left-toolbar-query-actions">
|
<div className="left-toolbar-query-actions">
|
||||||
<Tooltip title="Search">
|
<Tooltip title="Search">
|
||||||
<Button
|
<Button
|
||||||
|
@ -2,6 +2,17 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
.left-toolbar-query-actions {
|
.left-toolbar-query-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@ -35,6 +35,8 @@ describe('ToolbarActions', () => {
|
|||||||
onChangeSelectedView={handleChangeSelectedView}
|
onChangeSelectedView={handleChangeSelectedView}
|
||||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||||
showFrequencyChart
|
showFrequencyChart
|
||||||
|
showFilter
|
||||||
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('search-view')).toBeInTheDocument();
|
expect(screen.getByTestId('search-view')).toBeInTheDocument();
|
||||||
@ -79,6 +81,8 @@ describe('ToolbarActions', () => {
|
|||||||
onChangeSelectedView={handleChangeSelectedView}
|
onChangeSelectedView={handleChangeSelectedView}
|
||||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||||
showFrequencyChart
|
showFrequencyChart
|
||||||
|
showFilter
|
||||||
|
handleFilterVisibilityChange={(): void => {}}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import NoLogs from 'container/NoLogs/NoLogs';
|
|||||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import GetMinMax from 'lib/getMinMax';
|
import GetMinMax from 'lib/getMinMax';
|
||||||
import getTimeString from 'lib/getTimeString';
|
import getTimeString from 'lib/getTimeString';
|
||||||
@ -48,14 +49,7 @@ function TimeSeriesView({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
const width = graphRef.current?.clientWidth
|
|
||||||
? graphRef.current.clientWidth
|
|
||||||
: 700;
|
|
||||||
|
|
||||||
const height = graphRef.current?.clientWidth
|
|
||||||
? graphRef.current.clientHeight
|
|
||||||
: 300;
|
|
||||||
|
|
||||||
const [minTimeScale, setMinTimeScale] = useState<number>();
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
||||||
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
||||||
@ -129,8 +123,8 @@ function TimeSeriesView({
|
|||||||
yAxisUnit: yAxisUnit || '',
|
yAxisUnit: yAxisUnit || '',
|
||||||
apiResponse: data?.payload,
|
apiResponse: data?.payload,
|
||||||
dimensions: {
|
dimensions: {
|
||||||
width,
|
width: containerDimensions.width,
|
||||||
height,
|
height: containerDimensions.height,
|
||||||
},
|
},
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
minTimeScale,
|
minTimeScale,
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import './Toolbar.styles.scss';
|
import './Toolbar.styles.scss';
|
||||||
|
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import NewExplorerCTA from 'container/NewExplorerCTA';
|
import NewExplorerCTA from 'container/NewExplorerCTA';
|
||||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
showAutoRefresh: boolean;
|
showAutoRefresh: boolean;
|
||||||
@ -16,12 +19,20 @@ export default function Toolbar({
|
|||||||
rightActions,
|
rightActions,
|
||||||
showOldCTA,
|
showOldCTA,
|
||||||
}: ToolbarProps): JSX.Element {
|
}: ToolbarProps): JSX.Element {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||||
|
pathname,
|
||||||
|
]);
|
||||||
return (
|
return (
|
||||||
<div className="toolbar">
|
<div className="toolbar">
|
||||||
<div className="leftActions">{leftActions}</div>
|
<div className="leftActions">{leftActions}</div>
|
||||||
<div className="timeRange">
|
<div className="timeRange">
|
||||||
{showOldCTA && <NewExplorerCTA />}
|
{showOldCTA && <NewExplorerCTA />}
|
||||||
<DateTimeSelectionV2 showAutoRefresh={showAutoRefresh} />
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={showAutoRefresh}
|
||||||
|
showRefreshText={!isLogsExplorerPage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rightActions">{rightActions}</div>
|
<div className="rightActions">{rightActions}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,6 +60,7 @@ import { Form, FormContainer, FormItem } from './styles';
|
|||||||
|
|
||||||
function DateTimeSelection({
|
function DateTimeSelection({
|
||||||
showAutoRefresh,
|
showAutoRefresh,
|
||||||
|
showRefreshText = true,
|
||||||
hideShareModal = false,
|
hideShareModal = false,
|
||||||
location,
|
location,
|
||||||
updateTimeInterval,
|
updateTimeInterval,
|
||||||
@ -632,7 +633,7 @@ function DateTimeSelection({
|
|||||||
<NewExplorerCTA />
|
<NewExplorerCTA />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!hasSelectedTimeError && !refreshButtonHidden && (
|
{!hasSelectedTimeError && !refreshButtonHidden && showRefreshText && (
|
||||||
<RefreshText
|
<RefreshText
|
||||||
{...{
|
{...{
|
||||||
onLastRefreshHandler,
|
onLastRefreshHandler,
|
||||||
@ -716,6 +717,7 @@ function DateTimeSelection({
|
|||||||
|
|
||||||
interface DateTimeSelectionV2Props {
|
interface DateTimeSelectionV2Props {
|
||||||
showAutoRefresh: boolean;
|
showAutoRefresh: boolean;
|
||||||
|
showRefreshText?: boolean;
|
||||||
hideShareModal?: boolean;
|
hideShareModal?: boolean;
|
||||||
showOldExplorerCTA?: boolean;
|
showOldExplorerCTA?: boolean;
|
||||||
showResetButton?: boolean;
|
showResetButton?: boolean;
|
||||||
@ -725,6 +727,7 @@ interface DateTimeSelectionV2Props {
|
|||||||
DateTimeSelection.defaultProps = {
|
DateTimeSelection.defaultProps = {
|
||||||
hideShareModal: false,
|
hideShareModal: false,
|
||||||
showOldExplorerCTA: false,
|
showOldExplorerCTA: false,
|
||||||
|
showRefreshText: true,
|
||||||
showResetButton: false,
|
showResetButton: false,
|
||||||
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,
|
defaultRelativeTime: RelativeTimeMap['6hr'] as Time,
|
||||||
};
|
};
|
||||||
|
@ -52,6 +52,7 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
panelType,
|
panelType,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
currentQuery,
|
currentQuery,
|
||||||
|
setLastUsedQuery,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
@ -259,7 +260,13 @@ export const useQueryOperations: UseQueryOperations = ({
|
|||||||
if (currentQuery.builder.queryData.length > 1) {
|
if (currentQuery.builder.queryData.length > 1) {
|
||||||
removeQueryBuilderEntityByIndex('queryData', index);
|
removeQueryBuilderEntityByIndex('queryData', index);
|
||||||
}
|
}
|
||||||
}, [removeQueryBuilderEntityByIndex, index, currentQuery]);
|
setLastUsedQuery(0);
|
||||||
|
}, [
|
||||||
|
currentQuery.builder.queryData.length,
|
||||||
|
setLastUsedQuery,
|
||||||
|
removeQueryBuilderEntityByIndex,
|
||||||
|
index,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleChangeQueryData: HandleChangeQueryData = useCallback(
|
const handleChangeQueryData: HandleChangeQueryData = useCallback(
|
||||||
(key, value) => {
|
(key, value) => {
|
||||||
|
@ -1,11 +1,35 @@
|
|||||||
.log-explorer-query-container {
|
.logs-module-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
height: 100%;
|
||||||
flex: 1;
|
.log-quick-filter-left-section {
|
||||||
|
width: 0%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-explorer-views {
|
.log-module-right-section {
|
||||||
flex: 1;
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
width: 100%;
|
||||||
}
|
.log-explorer-query-container {
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.logs-explorer-views {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.filter-visible {
|
||||||
|
.log-quick-filter-left-section {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-module-right-section {
|
||||||
|
width: calc(100% - 260px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -189,6 +189,8 @@ describe('Logs Explorer Tests', () => {
|
|||||||
initialDataSource: null,
|
initialDataSource: null,
|
||||||
panelType: PANEL_TYPES.TIME_SERIES,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
isEnabledQuery: false,
|
isEnabledQuery: false,
|
||||||
|
lastUsedQuery: 0,
|
||||||
|
setLastUsedQuery: noop,
|
||||||
handleSetQueryData: noop,
|
handleSetQueryData: noop,
|
||||||
handleSetFormulaData: noop,
|
handleSetFormulaData: noop,
|
||||||
handleSetQueryItemData: noop,
|
handleSetQueryItemData: noop,
|
||||||
|
@ -1,25 +1,40 @@
|
|||||||
import './LogsExplorer.styles.scss';
|
import './LogsExplorer.styles.scss';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
|
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||||
|
import cx from 'classnames';
|
||||||
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
import ExplorerCard from 'components/ExplorerCard/ExplorerCard';
|
||||||
|
import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
import LogExplorerQuerySection from 'container/LogExplorerQuerySection';
|
||||||
import LogsExplorerViews from 'container/LogsExplorerViews';
|
import LogsExplorerViews from 'container/LogsExplorerViews';
|
||||||
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
|
||||||
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
|
||||||
import Toolbar from 'container/Toolbar/Toolbar';
|
import Toolbar from 'container/Toolbar/Toolbar';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { isNull } from 'lodash-es';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { WrapperStyled } from './styles';
|
import { WrapperStyled } from './styles';
|
||||||
import { SELECTED_VIEWS } from './utils';
|
import { LogsQuickFiltersConfig, SELECTED_VIEWS } from './utils';
|
||||||
|
|
||||||
function LogsExplorer(): JSX.Element {
|
function LogsExplorer(): JSX.Element {
|
||||||
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
|
const [showFrequencyChart, setShowFrequencyChart] = useState(true);
|
||||||
const [selectedView, setSelectedView] = useState<SELECTED_VIEWS>(
|
const [selectedView, setSelectedView] = useState<SELECTED_VIEWS>(
|
||||||
SELECTED_VIEWS.SEARCH,
|
SELECTED_VIEWS.SEARCH,
|
||||||
);
|
);
|
||||||
|
const [showFilters, setShowFilters] = useState<boolean>(() => {
|
||||||
|
const localStorageValue = getLocalStorageKey(
|
||||||
|
LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS,
|
||||||
|
);
|
||||||
|
if (!isNull(localStorageValue)) {
|
||||||
|
return localStorageValue === 'true';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@ -37,6 +52,14 @@ function LogsExplorer(): JSX.Element {
|
|||||||
setSelectedView(view);
|
setSelectedView(view);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFilterVisibilityChange = (): void => {
|
||||||
|
setLocalStorageApi(
|
||||||
|
LOCALSTORAGE.SHOW_LOGS_QUICK_FILTERS,
|
||||||
|
String(!showFilters),
|
||||||
|
);
|
||||||
|
setShowFilters((prev) => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
// Switch to query builder view if there are more than 1 queries
|
// Switch to query builder view if there are more than 1 queries
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentQuery.builder.queryData.length > 1) {
|
if (currentQuery.builder.queryData.length > 1) {
|
||||||
@ -90,46 +113,60 @@ function LogsExplorer(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
<Toolbar
|
<div className={cx('logs-module-page', showFilters ? 'filter-visible' : '')}>
|
||||||
showAutoRefresh={false}
|
{showFilters && (
|
||||||
leftActions={
|
<section className={cx('log-quick-filter-left-section')}>
|
||||||
<LeftToolbarActions
|
<QuickFilters
|
||||||
items={toolbarViews}
|
config={LogsQuickFiltersConfig}
|
||||||
selectedView={selectedView}
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
onChangeSelectedView={handleChangeSelectedView}
|
|
||||||
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
|
||||||
showFrequencyChart={showFrequencyChart}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightActions={
|
|
||||||
<RightToolbarActions
|
|
||||||
onStageRunQuery={handleRunQuery}
|
|
||||||
listQueryKeyRef={listQueryKeyRef}
|
|
||||||
chartQueryKeyRef={chartQueryKeyRef}
|
|
||||||
isLoadingQueries={isLoadingQueries}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
showOldCTA
|
|
||||||
/>
|
|
||||||
|
|
||||||
<WrapperStyled>
|
|
||||||
<div className="log-explorer-query-container">
|
|
||||||
<div>
|
|
||||||
<ExplorerCard sourcepage={DataSource.LOGS}>
|
|
||||||
<LogExplorerQuerySection selectedView={selectedView} />
|
|
||||||
</ExplorerCard>
|
|
||||||
</div>
|
|
||||||
<div className="logs-explorer-views">
|
|
||||||
<LogsExplorerViews
|
|
||||||
selectedView={selectedView}
|
|
||||||
showFrequencyChart={showFrequencyChart}
|
|
||||||
listQueryKeyRef={listQueryKeyRef}
|
|
||||||
chartQueryKeyRef={chartQueryKeyRef}
|
|
||||||
setIsLoadingQueries={setIsLoadingQueries}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
)}
|
||||||
</WrapperStyled>
|
<section className={cx('log-module-right-section')}>
|
||||||
|
<Toolbar
|
||||||
|
showAutoRefresh={false}
|
||||||
|
leftActions={
|
||||||
|
<LeftToolbarActions
|
||||||
|
showFilter={showFilters}
|
||||||
|
handleFilterVisibilityChange={handleFilterVisibilityChange}
|
||||||
|
items={toolbarViews}
|
||||||
|
selectedView={selectedView}
|
||||||
|
onChangeSelectedView={handleChangeSelectedView}
|
||||||
|
onToggleHistrogramVisibility={handleToggleShowFrequencyChart}
|
||||||
|
showFrequencyChart={showFrequencyChart}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
rightActions={
|
||||||
|
<RightToolbarActions
|
||||||
|
onStageRunQuery={handleRunQuery}
|
||||||
|
listQueryKeyRef={listQueryKeyRef}
|
||||||
|
chartQueryKeyRef={chartQueryKeyRef}
|
||||||
|
isLoadingQueries={isLoadingQueries}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
showOldCTA
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WrapperStyled>
|
||||||
|
<div className="log-explorer-query-container">
|
||||||
|
<div>
|
||||||
|
<ExplorerCard sourcepage={DataSource.LOGS}>
|
||||||
|
<LogExplorerQuerySection selectedView={selectedView} />
|
||||||
|
</ExplorerCard>
|
||||||
|
</div>
|
||||||
|
<div className="logs-explorer-views">
|
||||||
|
<LogsExplorerViews
|
||||||
|
selectedView={selectedView}
|
||||||
|
showFrequencyChart={showFrequencyChart}
|
||||||
|
listQueryKeyRef={listQueryKeyRef}
|
||||||
|
chartQueryKeyRef={chartQueryKeyRef}
|
||||||
|
setIsLoadingQueries={setIsLoadingQueries}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WrapperStyled>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|
||||||
|
|
||||||
export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({
|
|
||||||
...query,
|
|
||||||
builder: {
|
|
||||||
...query.builder,
|
|
||||||
queryData: query.builder.queryData?.map((item) => ({
|
|
||||||
...item,
|
|
||||||
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
export enum SELECTED_VIEWS {
|
|
||||||
SEARCH = 'search',
|
|
||||||
QUERY_BUILDER = 'query-builder',
|
|
||||||
CLICKHOUSE = 'clickhouse',
|
|
||||||
}
|
|
113
frontend/src/pages/LogsExplorer/utils.tsx
Normal file
113
frontend/src/pages/LogsExplorer/utils.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
FiltersType,
|
||||||
|
IQuickFiltersConfig,
|
||||||
|
} from 'components/QuickFilters/QuickFilters';
|
||||||
|
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export const prepareQueryWithDefaultTimestamp = (query: Query): Query => ({
|
||||||
|
...query,
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: query.builder.queryData?.map((item) => ({
|
||||||
|
...item,
|
||||||
|
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
export enum SELECTED_VIEWS {
|
||||||
|
SEARCH = 'search',
|
||||||
|
QUERY_BUILDER = 'query-builder',
|
||||||
|
CLICKHOUSE = 'clickhouse',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LogsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Severity Text',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'severity_text',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: '',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'severity_text--string----true',
|
||||||
|
},
|
||||||
|
defaultOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Environment',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'deployment.environment',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Service Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'service.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
id: 'service.name--string--resource--true',
|
||||||
|
},
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'Hostname',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'hostname',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'tag',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'K8s Cluster Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s.cluster.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'K8s Deployment Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s.deployment.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: false,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FiltersType.CHECKBOX,
|
||||||
|
title: 'K8s Namespace Name',
|
||||||
|
attributeKey: {
|
||||||
|
key: 'k8s.namespace.name',
|
||||||
|
dataType: DataTypes.String,
|
||||||
|
type: 'resource',
|
||||||
|
isColumn: true,
|
||||||
|
isJSON: false,
|
||||||
|
},
|
||||||
|
defaultOpen: false,
|
||||||
|
},
|
||||||
|
];
|
@ -77,6 +77,14 @@ jest.mock(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.ResizeObserver =
|
||||||
|
window.ResizeObserver ||
|
||||||
|
jest.fn().mockImplementation(() => ({
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const successNotification = jest.fn();
|
const successNotification = jest.fn();
|
||||||
jest.mock('hooks/useNotifications', () => ({
|
jest.mock('hooks/useNotifications', () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
|
@ -62,6 +62,8 @@ import { v4 as uuid } from 'uuid';
|
|||||||
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
export const QueryBuilderContext = createContext<QueryBuilderContextType>({
|
||||||
currentQuery: initialQueriesMap.metrics,
|
currentQuery: initialQueriesMap.metrics,
|
||||||
supersetQuery: initialQueriesMap.metrics,
|
supersetQuery: initialQueriesMap.metrics,
|
||||||
|
lastUsedQuery: null,
|
||||||
|
setLastUsedQuery: () => {},
|
||||||
setSupersetQuery: () => {},
|
setSupersetQuery: () => {},
|
||||||
stagedQuery: initialQueriesMap.metrics,
|
stagedQuery: initialQueriesMap.metrics,
|
||||||
initialDataSource: null,
|
initialDataSource: null,
|
||||||
@ -117,6 +119,7 @@ export function QueryBuilderProvider({
|
|||||||
|
|
||||||
const [currentQuery, setCurrentQuery] = useState<QueryState>(queryState);
|
const [currentQuery, setCurrentQuery] = useState<QueryState>(queryState);
|
||||||
const [supersetQuery, setSupersetQuery] = useState<QueryState>(queryState);
|
const [supersetQuery, setSupersetQuery] = useState<QueryState>(queryState);
|
||||||
|
const [lastUsedQuery, setLastUsedQuery] = useState<number | null>(0);
|
||||||
const [stagedQuery, setStagedQuery] = useState<Query | null>(null);
|
const [stagedQuery, setStagedQuery] = useState<Query | null>(null);
|
||||||
|
|
||||||
const [queryType, setQueryType] = useState<EQueryType>(queryTypeParam);
|
const [queryType, setQueryType] = useState<EQueryType>(queryTypeParam);
|
||||||
@ -230,6 +233,8 @@ export function QueryBuilderProvider({
|
|||||||
timeUpdated ? merge(currentQuery, newQueryState) : newQueryState,
|
timeUpdated ? merge(currentQuery, newQueryState) : newQueryState,
|
||||||
);
|
);
|
||||||
setQueryType(type);
|
setQueryType(type);
|
||||||
|
// this is required to reset the last used query when navigating or initializing the query builder
|
||||||
|
setLastUsedQuery(0);
|
||||||
},
|
},
|
||||||
[prepareQueryBuilderData, currentQuery],
|
[prepareQueryBuilderData, currentQuery],
|
||||||
);
|
);
|
||||||
@ -857,6 +862,8 @@ export function QueryBuilderProvider({
|
|||||||
() => ({
|
() => ({
|
||||||
currentQuery: query,
|
currentQuery: query,
|
||||||
supersetQuery: superQuery,
|
supersetQuery: superQuery,
|
||||||
|
lastUsedQuery,
|
||||||
|
setLastUsedQuery,
|
||||||
setSupersetQuery,
|
setSupersetQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
@ -884,6 +891,7 @@ export function QueryBuilderProvider({
|
|||||||
[
|
[
|
||||||
query,
|
query,
|
||||||
superQuery,
|
superQuery,
|
||||||
|
lastUsedQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
panelType,
|
panelType,
|
||||||
|
@ -189,6 +189,8 @@ export type QueryBuilderData = {
|
|||||||
export type QueryBuilderContextType = {
|
export type QueryBuilderContextType = {
|
||||||
currentQuery: Query;
|
currentQuery: Query;
|
||||||
stagedQuery: Query | null;
|
stagedQuery: Query | null;
|
||||||
|
lastUsedQuery: number | null;
|
||||||
|
setLastUsedQuery: Dispatch<SetStateAction<number | null>>;
|
||||||
supersetQuery: Query;
|
supersetQuery: Query;
|
||||||
setSupersetQuery: Dispatch<SetStateAction<QueryState>>;
|
setSupersetQuery: Dispatch<SetStateAction<QueryState>>;
|
||||||
initialDataSource: DataSource | null;
|
initialDataSource: DataSource | null;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user