From e3bbc14febc7c13473b54ccb0ee29174a0372c8b Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Tue, 30 Jun 2020 16:15:46 +0200 Subject: [PATCH] Query history: Add search for query history and starred queries (#25747) * Add searching functionality to Queries tab * Implement search for starred queries * Update tests and add test coverage for searching * Changes based on provided feedback * Remove redundant storing of results * Update naming of onSearchFilterChange function in tests * Update filterQueriesBySearchFilter function * Update filterQueriesBySearchFilter function * Debounce search * Remove console.log() * Use useDebouncedCallback * Update tests, remove redundant props * When in starred, search only in starred * Add timefilter as dependency * Remove redundant dependency, refactor --- public/app/core/utils/richHistory.test.ts | 35 ++++++++ public/app/core/utils/richHistory.ts | 49 +++++++++++- .../RichHistory/RichHistoryQueriesTab.tsx | 80 +++++++++++++------ .../RichHistory/RichHistoryStarredTab.tsx | 63 ++++++++++++--- 4 files changed, 190 insertions(+), 37 deletions(-) diff --git a/public/app/core/utils/richHistory.test.ts b/public/app/core/utils/richHistory.test.ts index 5b4cb4a0a70..f2fac0aaafd 100644 --- a/public/app/core/utils/richHistory.test.ts +++ b/public/app/core/utils/richHistory.test.ts @@ -7,6 +7,7 @@ import { createQueryHeading, deleteAllFromRichHistory, deleteQueryInRichHistory, + filterAndSortQueries, } from './richHistory'; import store from 'app/core/store'; import { SortOrder } from './explore'; @@ -168,6 +169,40 @@ describe('createDateStringFromTs', () => { }); }); +describe('filterQueries', () => { + it('should filter out queries based on data source filter', () => { + const filteredQueries = filterAndSortQueries( + mock.storedHistory, + SortOrder.Ascending, + ['not provided data source'], + '' + ); + expect(filteredQueries).toHaveLength(0); + }); + it('should keep queries based on data source filter', () => { + const filteredQueries = filterAndSortQueries( + mock.storedHistory, + SortOrder.Ascending, + ['datasource history name'], + '' + ); + expect(filteredQueries).toHaveLength(1); + }); + it('should filter out all queries based on search filter', () => { + const filteredQueries = filterAndSortQueries( + mock.storedHistory, + SortOrder.Ascending, + [], + 'i do not exist in query' + ); + expect(filteredQueries).toHaveLength(0); + }); + it('should include queries based on search filter', () => { + const filteredQueries = filterAndSortQueries(mock.storedHistory, SortOrder.Ascending, [], 'query1'); + expect(filteredQueries).toHaveLength(1); + }); +}); + describe('createQueryHeading', () => { it('should correctly create heading for queries when sort order is ascending ', () => { // Have to offset the timezone of a 1 microsecond epoch, and then reverse the changes diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index dc201b59dd9..dae4e205f15 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -270,7 +270,7 @@ export function createQueryHeading(query: RichHistoryQuery, sortOrder: SortOrder return heading; } -export function createQueryText(query: DataQuery, queryDsInstance: DataSourceApi) { +export function createQueryText(query: DataQuery, queryDsInstance: DataSourceApi | undefined) { /* query DatasourceInstance is necessary because we use its getQueryDisplayText method * to format query text */ @@ -338,6 +338,53 @@ export function notEmptyQuery(query: DataQuery) { return false; } +export function filterQueriesBySearchFilter(queries: RichHistoryQuery[], searchFilter: string) { + return queries.filter(query => { + if (query.comment.includes(searchFilter)) { + return true; + } + + const listOfMatchingQueries = query.queries.filter(query => + // Remove fields in which we don't want to be searching + Object.values(_.omit(query, ['datasource', 'key', 'refId', 'hide', 'queryType'])).some(value => + value.toString().includes(searchFilter) + ) + ); + + return listOfMatchingQueries.length > 0; + }); +} + +export function filterQueriesByDataSource(queries: RichHistoryQuery[], listOfDatasourceFilters: string[] | null) { + return listOfDatasourceFilters?.length > 0 + ? queries.filter(q => listOfDatasourceFilters.includes(q.datasourceName)) + : queries; +} + +export function filterQueriesByTime(queries: RichHistoryQuery[], timeFilter: [number, number]) { + return queries.filter( + q => + q.ts < createRetentionPeriodBoundary(timeFilter[0], true) && + q.ts > createRetentionPeriodBoundary(timeFilter[1], false) + ); +} + +export function filterAndSortQueries( + queries: RichHistoryQuery[], + sortOrder: SortOrder, + listOfDatasourceFilters: string[] | null, + searchFilter: string, + timeFilter?: [number, number] +) { + const filteredQueriesByDs = filterQueriesByDataSource(queries, listOfDatasourceFilters); + const filteredQueriesByDsAndSearchFilter = filterQueriesBySearchFilter(filteredQueriesByDs, searchFilter); + const filteredQueriesToBeSorted = timeFilter + ? filterQueriesByTime(filteredQueriesByDsAndSearchFilter, timeFilter) + : filteredQueriesByDsAndSearchFilter; + + return sortQueries(filteredQueriesToBeSorted, sortOrder); +} + /* These functions are created to migrate string queries (from 6.7 release) to DataQueries. They can be removed after 7.1 release. */ function migrateRichHistory(richHistory: RichHistoryQuery[]) { const transformedRichHistory = richHistory.map(query => { diff --git a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx index 812004ae107..33b55a92fe4 100644 --- a/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryQueriesTab.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { css } from 'emotion'; -import { uniqBy } from 'lodash'; +import { uniqBy, debounce } from 'lodash'; // Types import { RichHistoryQuery, ExploreId } from 'app/types/explore'; @@ -11,17 +11,17 @@ import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { SortOrder } from 'app/core/utils/explore'; import { - sortQueries, mapNumbertoTimeInSlider, - createRetentionPeriodBoundary, mapQueriesToHeadings, createDatasourcesList, + filterAndSortQueries, } from 'app/core/utils/richHistory'; // Components import RichHistoryCard from './RichHistoryCard'; import { sortOrderOptions } from './RichHistory'; import { Slider, Select } from '@grafana/ui'; +import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; export interface Props { queries: RichHistoryQuery[]; @@ -74,9 +74,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme, height: number) => { selectors: css` display: flex; justify-content: space-between; + flex-wrap: wrap; + `, + filterInput: css` + margin-bottom: ${theme.spacing.sm}; `, multiselect: css` - width: 60%; + width: 100%; + margin-bottom: ${theme.spacing.sm}; .gf-form-select-box__multi-value { background-color: ${bgColor}; padding: ${theme.spacing.xxs} ${theme.spacing.xs} ${theme.spacing.xxs} ${theme.spacing.sm}; @@ -132,50 +137,67 @@ export function RichHistoryQueriesTab(props: Props) { height, } = props; - const [sliderRetentionFilter, setSliderRetentionFilter] = useState<[number, number]>([0, retentionPeriod]); + const [timeFilter, setTimeFilter] = useState<[number, number]>([0, retentionPeriod]); + const [filteredQueries, setFilteredQueries] = useState([]); + const [searchInput, setSearchInput] = useState(''); const theme = useTheme(); const styles = getStyles(theme, height); + const datasourcesRetrievedFromQueryHistory = uniqBy(queries, 'datasourceName').map(d => d.datasourceName); const listOfDatasources = createDatasourcesList(datasourcesRetrievedFromQueryHistory); - const listOfDatasourceFilters = datasourceFilters?.map(d => d.value); - const filteredQueriesByDatasource = - listOfDatasourceFilters && listOfDatasourceFilters?.length > 0 - ? queries?.filter(q => listOfDatasourceFilters?.includes(q.datasourceName)) - : queries; - - const sortedQueries = sortQueries(filteredQueriesByDatasource, sortOrder); - const queriesWithinSelectedTimeline = sortedQueries?.filter( - q => - q.ts < createRetentionPeriodBoundary(sliderRetentionFilter[0], true) && - q.ts > createRetentionPeriodBoundary(sliderRetentionFilter[1], false) + const filterAndSortQueriesDebounced = useCallback( + debounce((searchValue: string) => { + setFilteredQueries( + filterAndSortQueries( + queries, + sortOrder, + datasourceFilters?.map(d => d.value), + searchValue, + timeFilter + ) + ); + }, 300), + [timeFilter, queries, sortOrder, datasourceFilters] ); + useEffect(() => { + setFilteredQueries( + filterAndSortQueries( + queries, + sortOrder, + datasourceFilters?.map(d => d.value), + searchInput, + timeFilter + ) + ); + }, [timeFilter, queries, sortOrder, datasourceFilters]); + /* mappedQueriesToHeadings is an object where query headings (stringified dates/data sources) * are keys and arrays with queries that belong to that headings are values. */ - let mappedQueriesToHeadings = mapQueriesToHeadings(queriesWithinSelectedTimeline, sortOrder); + const mappedQueriesToHeadings = mapQueriesToHeadings(filteredQueries, sortOrder); return (