diff --git a/frontend/src/api/queryBuilder/getAttributeSuggestions.ts b/frontend/src/api/queryBuilder/getAttributeSuggestions.ts new file mode 100644 index 0000000000..45b380f9e8 --- /dev/null +++ b/frontend/src/api/queryBuilder/getAttributeSuggestions.ts @@ -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 | 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); + } +}; diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 7b7b464b3e..5fe7112796 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -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 = { @@ -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 = { diff --git a/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts b/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts index 33c2b4061f..68331a4c2d 100644 --- a/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts +++ b/frontend/src/constants/shortcuts/logsExplorerShortcuts.ts @@ -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', }; diff --git a/frontend/src/container/LogsExplorerViews/index.tsx b/frontend/src/container/LogsExplorerViews/index.tsx index 6318e1da71..c7214ab260 100644 --- a/frontend/src/container/LogsExplorerViews/index.tsx +++ b/frontend/src/container/LogsExplorerViews/index.tsx @@ -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>; + listQueryKeyRef: MutableRefObject; + chartQueryKeyRef: MutableRefObject; }): 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) => { diff --git a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx index a1ae308efd..7cd58316e8 100644 --- a/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx +++ b/frontend/src/container/LogsExplorerViews/tests/LogsExplorerViews.test.tsx @@ -79,6 +79,9 @@ const renderer = (): RenderResult => {}} + listQueryKeyRef={{ current: {} }} + chartQueryKeyRef={{ current: {} }} /> diff --git a/frontend/src/container/OptionsMenu/constants.ts b/frontend/src/container/OptionsMenu/constants.ts index 2db02f85b8..7a454de8ca 100644 --- a/frontend/src/container/OptionsMenu/constants.ts +++ b/frontend/src/container/OptionsMenu/constants.ts @@ -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, }, ]; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx index aea205d569..f5106dffbe 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/RightToolbarActions.tsx @@ -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; + chartQueryKeyRef?: MutableRefObject; } 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 (
- + {isLoadingQueries ? ( +
+ +
+ ) : ( + + )}
); } + +RightToolbarActions.defaultProps = { + isLoadingQueries: false, + listQueryKeyRef: null, + chartQueryKeyRef: null, +}; diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss index a848cf8680..2eda1b7c86 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/ToolbarActions.styles.scss @@ -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; + } + } } diff --git a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx index 751bdbf99d..79ee020802 100644 --- a/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx +++ b/frontend/src/container/QueryBuilder/components/ToolbarActions/tests/ToolbarActions.test.tsx @@ -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( - , + + , + , ); const stageNRunBtn = queryByText('Stage & Run Query'); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx new file mode 100644 index 0000000000..df8921b4d1 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/ExampleQueriesRendererForLogs.tsx @@ -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 ( +
{ + handleAddTag(value); + }} + > + {label} +
+ ); +} + +interface ExampleQueriesRendererForLogsProps { + label: string; + value: TagFilter; + handleAddTag: (value: TagFilter) => void; +} + +export default ExampleQueriesRendererForLogs; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx new file mode 100644 index 0000000000..a1b5149a05 --- /dev/null +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/OptionRendererForLogs.tsx @@ -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(false); + const optionType = getOptionType(label); + + return ( + setDynamicPlaceholder(value)} + onFocus={(): void => setDynamicPlaceholder(value)} + > + {optionType ? ( + +
+
+ {isIndexed ? ( + + ) : ( +
+ )} + setTruncated(ellipsis) }} + > + {value} + +
+
+
{dataType}
+
+
+ {optionType} +
+
+
+
+ ) : ( + +
+
+ setTruncated(ellipsis) }} + > + {label} + +
+ + )} + + ); +} + +interface OptionRendererProps { + label: string; + value: string; + dataType: string; + isIndexed: boolean; + setDynamicPlaceholder: React.Dispatch>; +} + +export default OptionRendererForLogs; diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss index a6f5fcaf37..db03f5a862 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/QueryBuilderSearch.styles.scss @@ -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); + } + } + } + } + } } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx index eaaccb607d..c1f4b85a11 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/index.tsx @@ -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(false); + const [showAllFilters, setShowAllFilters] = useState(false); + const [dynamicPlacholder, setDynamicPlaceholder] = useState( + placeholder || '', + ); const selectRef = useRef(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) => ( + + + {option.selected && } + + )); + return (
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 ? : null} - suffixIcon={suffixIcon} + suffixIcon={ + // eslint-disable-next-line no-nested-ternary + !isUndefined(suffixIcon) ? ( + suffixIcon + ) : isOpen ? ( + + ) : ( + + ) + } showAction={['focus']} onBlur={handleOnBlur} + popupClassName={isLogsExplorerPage ? 'logs-explorer-popup' : ''} + dropdownRender={(menu): ReactElement => ( +
+ {!searchKey && isLogsExplorerPage && ( +
Suggested Filters
+ )} + {menu} + {isLogsExplorerPage && ( +
+ {!searchKey && tags.length === 0 && ( +
+
Example Queries
+
+ {convertExampleQueriesToOptions(exampleQueries).map((query) => ( + + ))} +
+
+ )} + {!key && !isFetching && !showAllFilters && options.length > 3 && ( + + )} +
+
+ + + to navigate +
+
+ + to update query +
+
+
+ )} +
+ )} > - {options.map((option) => ( - - - {option.selected && } - - ))} + {isLogsExplorerPage + ? customRendererForLogsExplorer + : options.map((option) => ( + + + {option.selected && } + + ))}
); diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts index ec7eba3973..04d8a0f77d 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearch/utils.ts @@ -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(' , '), + })); +} diff --git a/frontend/src/container/QueryBuilder/type.ts b/frontend/src/container/QueryBuilder/type.ts index 892330ebdd..183dd157f8 100644 --- a/frontend/src/container/QueryBuilder/type.ts +++ b/frontend/src/container/QueryBuilder/type.ts @@ -15,4 +15,5 @@ export type Option = { label: string; selected?: boolean; dataType?: string; + isIndexed?: boolean; }; diff --git a/frontend/src/hooks/queryBuilder/useAutoComplete.ts b/frontend/src/hooks/queryBuilder/useAutoComplete.ts index 8872b2dc02..ed1836a0fa 100644 --- a/frontend/src/hooks/queryBuilder/useAutoComplete.ts +++ b/frontend/src/hooks/queryBuilder/useAutoComplete.ts @@ -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(''); const [searchKey, setSearchKey] = useState(''); - 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[]; } diff --git a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts index db54685b8e..5fe5e100fa 100644 --- a/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts +++ b/frontend/src/hooks/queryBuilder/useFetchKeysAndValues.ts @@ -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([]); + const [exampleQueries, setExampleQueries] = useState([]); const [sourceKeys, setSourceKeys] = useState([]); const [results, setResults] = useState([]); const [isAggregateFetching, setAggregateFetching] = useState(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, }; }; diff --git a/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts b/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts new file mode 100644 index 0000000000..d2ee7042cd --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetAttributeSuggestions.ts @@ -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 | ErrorResponse + >, +) => UseQueryResult< + SuccessResponse | 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 | ErrorResponse + >({ + queryKey, + queryFn: () => getAttributeSuggestions(requestData), + ...options, + }); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts index cdcfb3e0c7..04b9deac16 100644 --- a/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetExplorerQueryRange.ts @@ -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, Error>, params?: Record, isDependentOnQB = true, + keyRef?: MutableRefObject, ): UseQueryResult, 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, diff --git a/frontend/src/hooks/queryBuilder/useOptions.ts b/frontend/src/hooks/queryBuilder/useOptions.ts index f050d19f82..2f24dd0d21 100644 --- a/frontend/src/hooks/queryBuilder/useOptions.ts +++ b/frontend/src/hooks/queryBuilder/useOptions.ts @@ -45,6 +45,7 @@ export const useOptions = ( label: `${getLabel(item)}`, value: item.key, dataType: item.dataType, + isIndexed: item?.isIndexed, })), [getLabel], ); diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx index d4422f58fe..505851da10 100644 --- a/frontend/src/pages/LogsExplorer/index.tsx +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -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(); + + const chartQueryKeyRef = useRef(); + + const [isLoadingQueries, setIsLoadingQueries] = useState(false); + const handleToggleShowFrequencyChart = (): void => { setShowFrequencyChart(!showFrequencyChart); }; @@ -82,7 +88,14 @@ function LogsExplorer(): JSX.Element { showFrequencyChart={showFrequencyChart} /> } - rightActions={} + rightActions={ + + } showOldCTA /> @@ -97,6 +110,9 @@ function LogsExplorer(): JSX.Element {
diff --git a/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts b/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts new file mode 100644 index 0000000000..b30f386382 --- /dev/null +++ b/frontend/src/types/api/queryBuilder/getAttributeSuggestions.ts @@ -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[]; +} diff --git a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts index 67503761ca..a8d5f0f324 100644 --- a/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts +++ b/frontend/src/types/api/queryBuilder/queryAutocompleteResponse.ts @@ -21,6 +21,7 @@ export interface BaseAutocompleteData { key: string; type: AutocompleteType | string | null; isJSON?: boolean; + isIndexed?: boolean; } export interface IQueryAutocompleteResponse {