feat: support for attribute key suggestions and example queries in logs explorer query builder (#5608)

* feat: qb-suggestions base setup

* chore: make the dropdown a little similar to the designs

* chore: move out example queries from og and add to renderer

* chore: added the handlers for example queries

* chore: hide the example queries as soon as the user starts typing

* feat: handle changes for cancel query

* chore: remove stupid concept of option group

* chore: show only first 3 items and add option to show all filters

* chore: minor css changes and remove transitions

* feat: integrate suggestions api and control re-renders

* feat: added keyboard shortcuts for the dropdown

* fix: design cleanups and touchups

* fix: build issues and tests

* chore: extra safety check for base64 and fix tests

* fix: qs doesn't handle padding in base64 strings, added client logic

* chore: some code comments

* chore: some code comments

* chore: increase the height of the bar when key is set

* chore: address minor designs

* chore: update the keyboard shortcut to cmd+/

* feat: correct the option render for logs for tooltip

* chore: search bar to not loose focus on btn click

* fix: update the spacing and icon for search bar

* chore: address review comments
This commit is contained in:
Vikrant Gupta 2024-08-16 13:11:39 +05:30 committed by GitHub
parent 1308f0f15f
commit 65280cf4e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1070 additions and 44 deletions

View File

@ -0,0 +1,63 @@
import { ApiV3Instance } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError, AxiosResponse } from 'axios';
import { baseAutoCompleteIdKeysOrder } from 'constants/queryBuilder';
import { encode } from 'js-base64';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import createQueryParams from 'lib/createQueryParams';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IGetAttributeSuggestionsPayload,
IGetAttributeSuggestionsSuccessResponse,
} from 'types/api/queryBuilder/getAttributeSuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const getAttributeSuggestions = async ({
searchText,
dataSource,
filters,
}: IGetAttributeSuggestionsPayload): Promise<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
> => {
try {
let base64EncodedFiltersString;
try {
// the replace function is to remove the padding at the end of base64 encoded string which is auto added to make it a multiple of 4
// why ? because the current working of qs doesn't work well with padding
base64EncodedFiltersString = encode(JSON.stringify(filters)).replace(
/=+$/,
'',
);
} catch {
// default base64 encoded string for empty filters object
base64EncodedFiltersString = 'eyJpdGVtcyI6W10sIm9wIjoiQU5EIn0';
}
const response: AxiosResponse<{
data: IGetAttributeSuggestionsSuccessResponse;
}> = await ApiV3Instance.get(
`/filter_suggestions?${createQueryParams({
searchText,
dataSource,
existingFilter: base64EncodedFiltersString,
})}`,
);
const payload: BaseAutocompleteData[] =
response.data.data.attributes?.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})) || [];
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: {
attributes: payload,
example_queries: response.data.data.example_queries,
},
};
} catch (e) {
return ErrorResponseHandler(e as AxiosError);
}
};

View File

@ -52,7 +52,7 @@ export const selectValueDivider = '__';
export const baseAutoCompleteIdKeysOrder: (keyof Omit<
BaseAutocompleteData,
'id' | 'isJSON'
'id' | 'isJSON' | 'isIndexed'
>)[] = ['key', 'dataType', 'type', 'isColumn'];
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
@ -71,6 +71,7 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
export enum QueryBuilderKeys {
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
GET_ATTRIBUTE_SUGGESTIONS = 'GET_ATTRIBUTE_SUGGESTIONS',
}
export const mapOfOperators = {

View File

@ -4,6 +4,7 @@ const userOS = getUserOperatingSystem();
export const LogsExplorerShortcuts = {
StageAndRunQuery: 'enter+meta',
FocusTheSearchBar: 's',
ShowAllFilters: '/+meta',
};
export const LogsExplorerShortcutsName = {
@ -11,9 +12,11 @@ export const LogsExplorerShortcutsName = {
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
}+enter`,
FocusTheSearchBar: 's',
ShowAllFilters: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+/`,
};
export const LogsExplorerShortcutsDescription = {
StageAndRunQuery: 'Stage and Run the current query',
FocusTheSearchBar: 'Shift the focus to the last query filter bar',
ShowAllFilters: 'Toggle all filters in the filters dropdown',
};

View File

@ -48,7 +48,15 @@ import {
} from 'lodash-es';
import { Sliders } from 'lucide-react';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
memo,
MutableRefObject,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { AppState } from 'store/reducers';
@ -72,9 +80,15 @@ import { v4 } from 'uuid';
function LogsExplorerViews({
selectedView,
showFrequencyChart,
setIsLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: {
selectedView: SELECTED_VIEWS;
showFrequencyChart: boolean;
setIsLoadingQueries: React.Dispatch<React.SetStateAction<boolean>>;
listQueryKeyRef: MutableRefObject<any>;
chartQueryKeyRef: MutableRefObject<any>;
}): JSX.Element {
const { notifications } = useNotifications();
const history = useHistory();
@ -214,6 +228,9 @@ function LogsExplorerViews({
{
enabled: !!listChartQuery && panelType === PANEL_TYPES.LIST,
},
{},
undefined,
chartQueryKeyRef,
);
const { data, isLoading, isFetching, isError } = useGetExplorerQueryRange(
@ -232,6 +249,8 @@ function LogsExplorerViews({
end: timeRange.end,
}),
},
undefined,
listQueryKeyRef,
);
const getRequestData = useCallback(
@ -569,6 +588,25 @@ function LogsExplorerViews({
},
});
useEffect(() => {
if (
isLoading ||
isFetching ||
isLoadingListChartData ||
isFetchingListChartData
) {
setIsLoadingQueries(true);
} else {
setIsLoadingQueries(false);
}
}, [
isLoading,
isFetching,
isFetchingListChartData,
isLoadingListChartData,
setIsLoadingQueries,
]);
const flattenLogData = useMemo(
() =>
logs.map((log) => {

View File

@ -79,6 +79,9 @@ const renderer = (): RenderResult =>
<LogsExplorerViews
selectedView={SELECTED_VIEWS.SEARCH}
showFrequencyChart
setIsLoadingQueries={(): void => {}}
listQueryKeyRef={{ current: {} }}
chartQueryKeyRef={{ current: {} }}
/>
</VirtuosoMockContext.Provider>
</QueryBuilderProvider>

View File

@ -18,6 +18,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
isIndexed: false,
},
{
key: 'name',
@ -26,6 +27,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
isIndexed: false,
},
{
key: 'durationNano',
@ -34,6 +36,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
isIndexed: false,
},
{
key: 'httpMethod',
@ -42,6 +45,7 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
isIndexed: false,
},
{
key: 'responseStatusCode',
@ -50,5 +54,6 @@ export const defaultTraceSelectedColumns = [
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
isIndexed: false,
},
];

View File

@ -3,18 +3,27 @@ import './ToolbarActions.styles.scss';
import { Button } from 'antd';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { Play } from 'lucide-react';
import { useEffect } from 'react';
import { Play, X } from 'lucide-react';
import { MutableRefObject, useEffect } from 'react';
import { useQueryClient } from 'react-query';
interface RightToolbarActionsProps {
onStageRunQuery: () => void;
isLoadingQueries?: boolean;
listQueryKeyRef?: MutableRefObject<any>;
chartQueryKeyRef?: MutableRefObject<any>;
}
export default function RightToolbarActions({
onStageRunQuery,
isLoadingQueries,
listQueryKeyRef,
chartQueryKeyRef,
}: RightToolbarActionsProps): JSX.Element {
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const queryClient = useQueryClient();
useEffect(() => {
registerShortcut(LogsExplorerShortcuts.StageAndRunQuery, onStageRunQuery);
@ -25,14 +34,41 @@ export default function RightToolbarActions({
}, [onStageRunQuery]);
return (
<div>
<Button
type="primary"
className="right-toolbar"
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
{isLoadingQueries ? (
<div className="loading-container">
<Button className="loading-btn" loading={isLoadingQueries} />
<Button
icon={<X size={14} />}
className="cancel-run"
onClick={(): void => {
if (listQueryKeyRef?.current) {
queryClient.cancelQueries(listQueryKeyRef.current);
}
if (chartQueryKeyRef?.current) {
queryClient.cancelQueries(chartQueryKeyRef.current);
}
}}
>
Cancel Run
</Button>
</div>
) : (
<Button
type="primary"
className="right-toolbar"
disabled={isLoadingQueries}
onClick={onStageRunQuery}
icon={<Play size={14} />}
>
Stage & Run Query
</Button>
)}
</div>
);
}
RightToolbarActions.defaultProps = {
isLoadingQueries: false,
listQueryKeyRef: null,
chartQueryKeyRef: null,
};

View File

@ -5,8 +5,8 @@
.left-toolbar-query-actions {
display: flex;
border-radius: 2px;
border: 1px solid var(--bg-slate-400, #1d212d);
background: var(--bg-ink-300, #16181d);
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
flex-direction: row;
.prom-ql-icon {
@ -24,7 +24,7 @@
border-radius: 0;
&.active-tab {
background-color: #1d212d;
background-color: var(--bg-slate-400);
}
&:disabled {
@ -33,7 +33,7 @@
}
}
.action-btn + .action-btn {
border-left: 1px solid var(--bg-slate-400, #1d212d);
border-left: 1px solid var(--bg-slate-400);
}
}
@ -51,6 +51,50 @@
background-color: var(--bg-robin-600);
}
.right-actions {
display: flex;
align-items: center;
}
.loading-container {
display: flex;
gap: 8px;
align-items: center;
.loading-btn {
display: flex;
width: 32px;
height: 33px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 6px;
flex-shrink: 0;
border-radius: 2px;
background: var(--bg-slate-300);
box-shadow: none;
border: none;
}
.cancel-run {
display: flex;
height: 33px;
padding: 4px 10px;
justify-content: center;
align-items: center;
gap: 6px;
flex: 1 0 0;
border-radius: 2px;
background: var(--bg-cherry-500);
border-color: none;
}
.cancel-run:hover {
background-color: #ff7875 !important;
color: var(--bg-vanilla-100) !important;
border: none;
}
}
.lightMode {
.left-toolbar {
.left-toolbar-query-actions {
@ -68,4 +112,17 @@
}
}
}
.loading-container {
.loading-btn {
background: var(--bg-vanilla-300);
}
.cancel-run {
color: var(--bg-vanilla-100);
}
.cancel-run:hover {
background-color: #ff7875;
}
}
}

View File

@ -1,6 +1,7 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SELECTED_VIEWS } from 'pages/LogsExplorer/utils';
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
import LeftToolbarActions from '../LeftToolbarActions';
import RightToolbarActions from '../RightToolbarActions';
@ -94,7 +95,9 @@ describe('ToolbarActions', () => {
it('RightToolbarActions - render correctly with props', async () => {
const onStageRunQuery = jest.fn();
const { queryByText } = render(
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
<MockQueryClientProvider>
<RightToolbarActions onStageRunQuery={onStageRunQuery} />,
</MockQueryClientProvider>,
);
const stageNRunBtn = queryByText('Stage & Run Query');

View File

@ -0,0 +1,30 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import './QueryBuilderSearch.styles.scss';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
function ExampleQueriesRendererForLogs({
label,
value,
handleAddTag,
}: ExampleQueriesRendererForLogsProps): JSX.Element {
return (
<div
className="example-query-container"
onClick={(): void => {
handleAddTag(value);
}}
>
<span className="example-query">{label}</span>
</div>
);
}
interface ExampleQueriesRendererForLogsProps {
label: string;
value: TagFilter;
handleAddTag: (value: TagFilter) => void;
}
export default ExampleQueriesRendererForLogs;

View File

@ -0,0 +1,77 @@
import './QueryBuilderSearch.styles.scss';
import { Color } from '@signozhq/design-tokens';
import { Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { Zap } from 'lucide-react';
import { useState } from 'react';
import { getOptionType } from './utils';
function OptionRendererForLogs({
label,
value,
dataType,
isIndexed,
setDynamicPlaceholder,
}: OptionRendererProps): JSX.Element {
const [truncated, setTruncated] = useState<boolean>(false);
const optionType = getOptionType(label);
return (
<span
className="option"
onMouseEnter={(): void => setDynamicPlaceholder(value)}
onFocus={(): void => setDynamicPlaceholder(value)}
>
{optionType ? (
<Tooltip title={truncated ? `${value}` : ''} placement="topLeft">
<div className="logs-options-select">
<section className="left-section">
{isIndexed ? (
<Zap size={12} fill={Color.BG_AMBER_500} />
) : (
<div className="dot" />
)}
<Typography.Text
className="text value"
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
>
{value}
</Typography.Text>
</section>
<section className="right-section">
<div className="text tags data-type-tag">{dataType}</div>
<div className={cx('text tags option-type-tag', optionType)}>
<div className="dot" />
{optionType}
</div>
</section>
</div>
</Tooltip>
) : (
<Tooltip title={truncated ? `${label}` : ''} placement="topLeft">
<div className="without-option-type">
<div className="dot" />
<Typography.Text
className="text"
ellipsis={{ onEllipsis: (ellipsis): void => setTruncated(ellipsis) }}
>
{label}
</Typography.Text>
</div>
</Tooltip>
)}
</span>
);
}
interface OptionRendererProps {
label: string;
value: string;
dataType: string;
isIndexed: boolean;
setDynamicPlaceholder: React.Dispatch<React.SetStateAction<string>>;
}
export default OptionRendererForLogs;

View File

@ -11,6 +11,290 @@
}
}
.logs-popup {
&.hide-scroll {
.rc-virtual-list-holder {
height: 100px;
}
}
}
.logs-explorer-popup {
padding: 0px;
.ant-select-item-group {
padding: 12px 14px 8px 14px;
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
.show-all-filter-props {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 13px;
width: 100%;
cursor: pointer;
.content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.left-section {
display: flex;
align-items: center;
gap: 4px;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.text:hover {
color: var(--bg-vanilla-100);
}
}
.right-section {
display: flex;
align-items: center;
gap: 4px;
.keyboard-shortcut-slash {
width: 16px;
height: 16px;
flex-shrink: 0;
border-radius: 2.286px;
border-top: 1.143px solid var(--bg-ink-200);
border-right: 1.143px solid var(--bg-ink-200);
border-bottom: 2.286px solid var(--bg-ink-200);
border-left: 1.143px solid var(--bg-ink-200);
background: var(--bg-ink-400);
}
}
}
}
.show-all-filter-props:hover {
background: rgba(255, 255, 255, 0.04) !important;
}
.example-queries {
cursor: default;
.heading {
padding: 12px 14px 8px 14px;
color: var(--bg-slate-50);
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 18px; /* 163.636% */
letter-spacing: 0.88px;
text-transform: uppercase;
}
.query-container {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0px 12px 12px 12px;
cursor: pointer;
.example-query {
display: flex;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 10px;
border-radius: 2px;
background: var(--bg-ink-200);
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: normal;
letter-spacing: -0.07px;
width: fit-content;
}
.example-query:hover {
color: var(--bg-vanilla-100);
}
}
}
.ant-select-item-option-grouped {
padding-inline-start: 0px;
padding: 7px 13px;
}
.keyboard-shortcuts {
display: flex;
align-items: center;
border-radius: 0px 0px 4px 4px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
padding: 11px 16px;
cursor: default;
.icons {
width: 16px;
height: 16px;
flex-shrink: 0;
border-radius: 2.286px;
border-top: 1.143px solid var(--Ink-200, #23262e);
border-right: 1.143px solid var(--Ink-200, #23262e);
border-bottom: 2.286px solid var(--Ink-200, #23262e);
border-left: 1.143px solid var(--Ink-200, #23262e);
background: var(--Ink-400, #121317);
}
.keyboard-text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 142.857% */
letter-spacing: -0.07px;
}
.navigate {
display: flex;
align-items: center;
padding-right: 12px;
gap: 4px;
border-right: 1px solid #1d212d;
}
.update-query {
display: flex;
align-items: center;
margin-left: 12px;
gap: 4px;
}
}
.without-option-type {
display: flex;
gap: 8px;
align-items: center;
.dot {
height: 5px;
width: 5px;
border-radius: 50%;
background-color: var(--bg-slate-300);
}
}
.logs-options-select {
display: flex;
align-items: center;
justify-content: space-between;
.text {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.tags {
display: flex;
height: 20px;
padding: 4px 8px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 20px;
}
.dot {
height: 5px;
width: 5px;
border-radius: 50%;
flex-shrink: 0;
}
.left-section {
display: flex;
align-items: center;
gap: 8px;
width: 90%;
.dot {
background-color: var(--bg-slate-300);
}
.value {
width: 100%;
}
}
.right-section {
display: flex;
align-items: center;
gap: 4px;
.data-type-tag {
background: rgba(255, 255, 255, 0.08);
}
.option-type-tag {
display: flex;
gap: 4px;
align-items: center;
padding: 0px 6px;
text-transform: capitalize;
}
.tag {
border-radius: 50px;
background: rgba(189, 153, 121, 0.1);
color: var(--bg-sienna-400);
.dot {
background-color: var(--bg-sienna-400);
}
}
.resource {
border-radius: 50px;
background: rgba(245, 108, 135, 0.1);
color: var(--bg-sakura-400);
.dot {
background-color: var(--bg-sakura-400);
}
}
}
}
.ant-select-item-option-active {
.logs-options-select {
.left-section {
.value {
color: var(--bg-vanilla-100);
}
}
}
}
}
.lightMode {
.query-builder-search {
.ant-select-dropdown {
@ -21,4 +305,108 @@
background-color: var(--bg-vanilla-200) !important;
}
}
.logs-explorer-popup {
.ant-select-item-group {
color: var(--bg-slate-50);
}
.show-all-filter-props {
.content {
.left-section {
.text {
color: var(--bg-ink-400);
}
.text:hover {
color: var(--bg-slate-100);
}
}
.right-section {
.keyboard-shortcut-slash {
border-top: 1.143px solid var(--bg-ink-200);
border-right: 1.143px solid var(--bg-ink-200);
border-bottom: 2.286px solid var(--bg-ink-200);
border-left: 1.143px solid var(--bg-ink-200);
background: var(--bg-vanilla-200);
}
}
}
}
.show-all-filter-props:hover {
background: var(--bg-vanilla-200) !important;
}
.example-queries {
.heading {
color: var(--bg-slate-50);
}
.query-container {
.example-query-container {
.example-query {
background: var(--bg-vanilla-200);
color: var(--bg-ink-400);
}
.example-query:hover {
color: var(--bg-ink-400);
}
}
}
}
.keyboard-shortcuts {
border: 1px solid var(--bg-vanilla-400);
background: var(--bg-vanilla-200);
.icons {
border-top: 1.143px solid var(--Ink-200, #23262e);
border-right: 1.143px solid var(--Ink-200, #23262e);
border-bottom: 2.286px solid var(--Ink-200, #23262e);
border-left: 1.143px solid var(--Ink-200, #23262e);
background: var(--bg-vanilla-200);
}
.keyboard-text {
color: var(--bg-ink-400);
}
.navigate {
border-right: 1px solid #1d212d;
}
}
.logs-options-select {
.text {
color: var(--bg-ink-400);
}
.right-section {
.data-type-tag {
background: var(--bg-vanilla-200);
}
.tag {
background: rgba(189, 153, 121, 0.1);
color: var(--bg-sienna-400);
}
.resource {
background: rgba(245, 108, 135, 0.1);
color: var(--bg-sakura-400);
}
}
}
.ant-select-item-option-active {
.logs-options-select {
.left-section {
.value {
color: var(--bg-ink-100);
}
}
}
}
}
}

View File

@ -1,7 +1,10 @@
/* eslint-disable react/no-unstable-nested-components */
import './QueryBuilderSearch.styles.scss';
import { Select, Spin, Tag, Tooltip } from 'antd';
import { Button, Select, Spin, Tag, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { getDataTypes } from 'container/LogDetailedView/utils';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
@ -11,7 +14,17 @@ import {
} from 'hooks/queryBuilder/useAutoComplete';
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { isEqual } from 'lodash-es';
import { isEqual, isUndefined } from 'lodash-es';
import {
ArrowDown,
ArrowUp,
ChevronDown,
ChevronUp,
Command,
CornerDownLeft,
Filter,
Slash,
} from 'lucide-react';
import type { BaseSelectRef } from 'rc-select';
import {
KeyboardEvent,
@ -23,6 +36,7 @@ import {
useRef,
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import {
BaseAutocompleteData,
DataTypes,
@ -32,14 +46,18 @@ import {
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import { selectStyle } from './config';
import { PLACEHOLDER } from './constant';
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
import OptionRenderer from './OptionRenderer';
import OptionRendererForLogs from './OptionRendererForLogs';
import { StyledCheckOutlined, TypographyText } from './style';
import {
convertExampleQueriesToOptions,
getOperatorValue,
getRemovePrefixFromKey,
getTagToken,
@ -55,6 +73,10 @@ function QueryBuilderSearch({
placeholder,
suffixIcon,
}: QueryBuilderSearchProps): JSX.Element {
const { pathname } = useLocation();
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
pathname,
]);
const {
updateTag,
handleClearTag,
@ -69,14 +91,20 @@ function QueryBuilderSearch({
isFetching,
setSearchKey,
searchKey,
} = useAutoComplete(query, whereClauseConfig);
key,
exampleQueries,
} = useAutoComplete(query, whereClauseConfig, isLogsExplorerPage);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [showAllFilters, setShowAllFilters] = useState<boolean>(false);
const [dynamicPlacholder, setDynamicPlaceholder] = useState<string>(
placeholder || '',
);
const selectRef = useRef<BaseSelectRef>(null);
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
searchValue,
query,
searchKey,
isLogsExplorerPage,
);
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
@ -140,6 +168,12 @@ function QueryBuilderSearch({
handleRunQuery();
setIsOpen(false);
}
if ((event.ctrlKey || event.metaKey) && event.key === '/') {
event.preventDefault();
event.stopPropagation();
setShowAllFilters((prev) => !prev);
}
};
const handleDeselect = useCallback(
@ -229,6 +263,28 @@ function QueryBuilderSearch({
deregisterShortcut(LogsExplorerShortcuts.FocusTheSearchBar);
}, [deregisterShortcut, isLastQuery, registerShortcut]);
useEffect(() => {
if (!isOpen) {
setDynamicPlaceholder(placeholder || '');
}
}, [isOpen, placeholder]);
const userOs = getUserOperatingSystem();
// conditional changes here to use a seperate component to render the example queries based on the option group label
const customRendererForLogsExplorer = options.map((option) => (
<Select.Option key={option.label} value={option.value}>
<OptionRendererForLogs
label={option.label}
value={option.value}
dataType={option.dataType || ''}
isIndexed={option.isIndexed || false}
setDynamicPlaceholder={setDynamicPlaceholder}
/>
{option.selected && <StyledCheckOutlined />}
</Select.Option>
));
return (
<div
style={{
@ -238,7 +294,9 @@ function QueryBuilderSearch({
<Select
ref={selectRef}
getPopupContainer={popupContainer}
virtual
transitionName=""
choiceTransitionName=""
virtual={false}
showSearch
tagRender={onTagRender}
filterOption={false}
@ -246,10 +304,14 @@ function QueryBuilderSearch({
onDropdownVisibleChange={setIsOpen}
autoClearSearchValue={false}
mode="multiple"
placeholder={placeholder}
placeholder={dynamicPlacholder}
value={queryTags}
searchValue={searchValue}
className={className}
className={cx(
className,
isLogsExplorerPage ? 'logs-popup' : '',
!showAllFilters && options.length > 3 && !key ? 'hide-scroll' : '',
)}
rootClassName="query-builder-search"
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
style={selectStyle}
@ -259,20 +321,99 @@ function QueryBuilderSearch({
onDeselect={handleDeselect}
onInputKeyDown={onInputKeyDownHandler}
notFoundContent={isFetching ? <Spin size="small" /> : null}
suffixIcon={suffixIcon}
suffixIcon={
// eslint-disable-next-line no-nested-ternary
!isUndefined(suffixIcon) ? (
suffixIcon
) : isOpen ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)
}
showAction={['focus']}
onBlur={handleOnBlur}
popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''}
dropdownRender={(menu): ReactElement => (
<div>
{!searchKey && isLogsExplorerPage && (
<div className="ant-select-item-group ">Suggested Filters</div>
)}
{menu}
{isLogsExplorerPage && (
<div>
{!searchKey && tags.length === 0 && (
<div className="example-queries">
<div className="heading"> Example Queries </div>
<div className="query-container">
{convertExampleQueriesToOptions(exampleQueries).map((query) => (
<ExampleQueriesRendererForLogs
key={query.label}
label={query.label}
value={query.value}
handleAddTag={onChange}
/>
))}
</div>
</div>
)}
{!key && !isFetching && !showAllFilters && options.length > 3 && (
<Button
type="text"
className="show-all-filter-props"
onClick={(): void => {
setShowAllFilters(true);
// when clicking on the button the search bar looses the focus
selectRef?.current?.focus();
}}
>
<div className="content">
<section className="left-section">
<Filter size={14} />
<Typography.Text className="text">
Show all filters properties
</Typography.Text>
</section>
<section className="right-section">
{userOs === UserOperatingSystem.MACOS ? (
<Command size={14} className="keyboard-shortcut-slash" />
) : (
<ChevronUp size={14} className="keyboard-shortcut-slash" />
)}
+
<Slash size={14} className="keyboard-shortcut-slash" />
</section>
</div>
</Button>
)}
<div className="keyboard-shortcuts">
<section className="navigate">
<ArrowDown size={10} className="icons" />
<ArrowUp size={10} className="icons" />
<span className="keyboard-text">to navigate</span>
</section>
<section className="update-query">
<CornerDownLeft size={10} className="icons" />
<span className="keyboard-text">to update query</span>
</section>
</div>
</div>
)}
</div>
)}
>
{options.map((option) => (
<Select.Option key={option.label} value={option.value}>
<OptionRenderer
label={option.label}
value={option.value}
dataType={option.dataType || ''}
/>
{option.selected && <StyledCheckOutlined />}
</Select.Option>
))}
{isLogsExplorerPage
? customRendererForLogsExplorer
: options.map((option) => (
<Select.Option key={option.label} value={option.value}>
<OptionRenderer
label={option.label}
value={option.value}
dataType={option.dataType || ''}
/>
{option.selected && <StyledCheckOutlined />}
</Select.Option>
))}
</Select>
</div>
);

View File

@ -1,6 +1,8 @@
import { OPERATORS } from 'constants/queryBuilder';
import { MetricsType } from 'container/MetricsApplication/constant';
import { queryFilterTags } from 'hooks/queryBuilder/useTag';
import { parse } from 'papaparse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { orderByValueDelimiter } from '../OrderByFilter/utils';
@ -162,3 +164,17 @@ export function getOptionType(label: string): MetricsType | undefined {
return optionType;
}
/**
*
* @param exampleQueries the example queries based on recommendation engine
* @returns the data formatted to the Option[]
*/
export function convertExampleQueriesToOptions(
exampleQueries: TagFilter[],
): { label: string; value: TagFilter }[] {
return exampleQueries.map((query) => ({
value: query,
label: queryFilterTags(query).join(' , '),
}));
}

View File

@ -15,4 +15,5 @@ export type Option = {
label: string;
selected?: boolean;
dataType?: string;
isIndexed?: boolean;
};

View File

@ -8,7 +8,10 @@ import {
import { Option } from 'container/QueryBuilder/type';
import { parse } from 'papaparse';
import { KeyboardEvent, useCallback, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { useFetchKeysAndValues } from './useFetchKeysAndValues';
import { useOptions, WHERE_CLAUSE_CUSTOM_SUFFIX } from './useOptions';
@ -24,14 +27,16 @@ export type WhereClauseConfig = {
export const useAutoComplete = (
query: IBuilderQuery,
whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean,
): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>('');
const { keys, results, isFetching } = useFetchKeysAndValues(
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
searchValue,
query,
searchKey,
shouldUseSuggestions,
);
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
@ -144,6 +149,8 @@ export const useAutoComplete = (
isFetching,
setSearchKey,
searchKey,
key,
exampleQueries,
};
};
@ -161,4 +168,6 @@ interface IAutoComplete {
isFetching: boolean;
setSearchKey: (value: string) => void;
searchKey: string;
key: string;
exampleQueries: TagFilter[];
}

View File

@ -6,17 +6,21 @@ import {
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
import useDebounceValue from 'hooks/useDebounce';
import { isEqual, uniqWith } from 'lodash-es';
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDebounce } from 'react-use';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { useGetAggregateKeys } from './useGetAggregateKeys';
import { useGetAttributeSuggestions } from './useGetAttributeSuggestions';
type IuseFetchKeysAndValues = {
keys: BaseAutocompleteData[];
@ -24,6 +28,7 @@ type IuseFetchKeysAndValues = {
isFetching: boolean;
sourceKeys: BaseAutocompleteData[];
handleRemoveSourceKey: (newSourceKey: string) => void;
exampleQueries: TagFilter[];
};
/**
@ -37,8 +42,10 @@ export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQuery,
searchKey: string,
shouldUseSuggestions?: boolean,
): IuseFetchKeysAndValues => {
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
const [sourceKeys, setSourceKeys] = useState<BaseAutocompleteData[]>([]);
const [results, setResults] = useState<string[]>([]);
const [isAggregateFetching, setAggregateFetching] = useState<boolean>(false);
@ -60,6 +67,28 @@ export const useFetchKeysAndValues = (
const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY);
const queryFiltersWithoutId = useMemo(
() => ({
...query.filters,
items: query.filters.items.map((item) => {
const filterWithoutId = cloneDeep(item);
unset(filterWithoutId, 'id');
return filterWithoutId;
}),
}),
[query.filters],
);
const memoizedSuggestionsParams = useMemo(
() => [searchKey, query.dataSource, queryFiltersWithoutId],
[query.dataSource, queryFiltersWithoutId, searchKey],
);
const suggestionsParams = useDebounceValue(
memoizedSuggestionsParams,
DEBOUNCE_DELAY,
);
const isQueryEnabled = useMemo(
() =>
query.dataSource === DataSource.METRICS
@ -82,7 +111,26 @@ export const useFetchKeysAndValues = (
aggregateAttribute: query.aggregateAttribute.key,
tagType: query.aggregateAttribute.type ?? null,
},
{ queryKey: [searchParams], enabled: isQueryEnabled },
{
queryKey: [searchParams],
enabled: isQueryEnabled && !shouldUseSuggestions,
},
);
const {
data: suggestionsData,
isFetching: isFetchingSuggestions,
status: fetchingSuggestionsStatus,
} = useGetAttributeSuggestions(
{
searchText: searchKey,
dataSource: query.dataSource,
filters: query.filters,
},
{
queryKey: [suggestionsParams],
enabled: isQueryEnabled && shouldUseSuggestions,
},
);
/**
@ -162,11 +210,41 @@ export const useFetchKeysAndValues = (
}
}, [data?.payload?.attributeKeys, status]);
useEffect(() => {
if (
fetchingSuggestionsStatus === 'success' &&
suggestionsData?.payload?.attributes
) {
setKeys(suggestionsData.payload.attributes);
setSourceKeys((prevState) =>
uniqWith(
[...(suggestionsData.payload.attributes ?? []), ...prevState],
isEqual,
),
);
} else {
setKeys([]);
}
if (
fetchingSuggestionsStatus === 'success' &&
suggestionsData?.payload?.example_queries
) {
setExampleQueries(suggestionsData.payload.example_queries);
} else {
setExampleQueries([]);
}
}, [
suggestionsData?.payload?.attributes,
fetchingSuggestionsStatus,
suggestionsData?.payload?.example_queries,
]);
return {
keys,
results,
isFetching: isFetching || isAggregateFetching,
isFetching: isFetching || isAggregateFetching || isFetchingSuggestions,
sourceKeys,
handleRemoveSourceKey,
exampleQueries,
};
};

View File

@ -0,0 +1,38 @@
import { getAttributeSuggestions } from 'api/queryBuilder/getAttributeSuggestions';
import { QueryBuilderKeys } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
IGetAttributeSuggestionsPayload,
IGetAttributeSuggestionsSuccessResponse,
} from 'types/api/queryBuilder/getAttributeSuggestions';
type UseGetAttributeSuggestions = (
requestData: IGetAttributeSuggestionsPayload,
options?: UseQueryOptions<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
>,
) => UseQueryResult<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
>;
export const useGetAttributeSuggestions: UseGetAttributeSuggestions = (
requestData,
options,
) => {
const queryKey = useMemo(() => {
if (options?.queryKey && Array.isArray(options.queryKey)) {
return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, ...options.queryKey];
}
return [QueryBuilderKeys.GET_ATTRIBUTE_SUGGESTIONS, requestData];
}, [options?.queryKey, requestData]);
return useQuery<
SuccessResponse<IGetAttributeSuggestionsSuccessResponse> | ErrorResponse
>({
queryKey,
queryFn: () => getAttributeSuggestions(requestData),
...options,
});
};

View File

@ -1,6 +1,6 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useMemo } from 'react';
import { MutableRefObject, useMemo } from 'react';
import { UseQueryOptions, UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
@ -19,6 +19,7 @@ export const useGetExplorerQueryRange = (
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
params?: Record<string, unknown>,
isDependentOnQB = true,
keyRef?: MutableRefObject<any>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
const { isEnabledQuery } = useQueryBuilder();
const { selectedTime: globalSelectedInterval, minTime, maxTime } = useSelector<
@ -40,6 +41,11 @@ export const useGetExplorerQueryRange = (
return isEnabledQuery;
}, [options, isEnabledQuery, isDependentOnQB]);
if (keyRef) {
// eslint-disable-next-line no-param-reassign
keyRef.current = [key, globalSelectedInterval, requestData, minTime, maxTime];
}
return useGetQueryRange(
{
graphType: panelType || PANEL_TYPES.LIST,

View File

@ -45,6 +45,7 @@ export const useOptions = (
label: `${getLabel(item)}`,
value: item.key,
dataType: item.dataType,
isIndexed: item?.isIndexed,
})),
[getLabel],
);

View File

@ -9,7 +9,7 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
import Toolbar from 'container/Toolbar/Toolbar';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
import { WrapperStyled } from './styles';
@ -23,6 +23,12 @@ function LogsExplorer(): JSX.Element {
const { handleRunQuery, currentQuery } = useQueryBuilder();
const listQueryKeyRef = useRef<any>();
const chartQueryKeyRef = useRef<any>();
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
const handleToggleShowFrequencyChart = (): void => {
setShowFrequencyChart(!showFrequencyChart);
};
@ -82,7 +88,14 @@ function LogsExplorer(): JSX.Element {
showFrequencyChart={showFrequencyChart}
/>
}
rightActions={<RightToolbarActions onStageRunQuery={handleRunQuery} />}
rightActions={
<RightToolbarActions
onStageRunQuery={handleRunQuery}
listQueryKeyRef={listQueryKeyRef}
chartQueryKeyRef={chartQueryKeyRef}
isLoadingQueries={isLoadingQueries}
/>
}
showOldCTA
/>
@ -97,6 +110,9 @@ function LogsExplorer(): JSX.Element {
<LogsExplorerViews
selectedView={selectedView}
showFrequencyChart={showFrequencyChart}
listQueryKeyRef={listQueryKeyRef}
chartQueryKeyRef={chartQueryKeyRef}
setIsLoadingQueries={setIsLoadingQueries}
/>
</div>
</div>

View File

@ -0,0 +1,15 @@
import { DataSource } from 'types/common/queryBuilder';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
import { TagFilter } from './queryBuilderData';
export interface IGetAttributeSuggestionsPayload {
dataSource: DataSource;
searchText: string;
filters: TagFilter;
}
export interface IGetAttributeSuggestionsSuccessResponse {
attributes: BaseAutocompleteData[];
example_queries: TagFilter[];
}

View File

@ -21,6 +21,7 @@ export interface BaseAutocompleteData {
key: string;
type: AutocompleteType | string | null;
isJSON?: boolean;
isIndexed?: boolean;
}
export interface IQueryAutocompleteResponse {