diff --git a/.betterer.results b/.betterer.results index c8618ec5aaf..d6181e09281 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3617,7 +3617,7 @@ exports[`better eslint`] = { [0, 34, 3, "Unexpected any. Specify a different type.", "193409811"], [0, 39, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/core/services/backend_srv.ts:360059123": [ + "public/app/core/services/backend_srv.ts:2084076322": [ [83, 20, 3, "Unexpected any. Specify a different type.", "193409811"], [153, 63, 3, "Unexpected any. Specify a different type.", "193409811"], [231, 40, 3, "Unexpected any. Specify a different type.", "193409811"], @@ -3632,7 +3632,7 @@ exports[`better eslint`] = { [408, 33, 3, "Unexpected any. Specify a different type.", "193409811"], [412, 31, 3, "Unexpected any. Specify a different type.", "193409811"], [416, 31, 3, "Unexpected any. Specify a different type.", "193409811"], - [427, 16, 3, "Unexpected any. Specify a different type.", "193409811"] + [428, 16, 3, "Unexpected any. Specify a different type.", "193409811"] ], "public/app/core/services/context_srv.ts:698805616": [ [59, 10, 3, "Unexpected any. Specify a different type.", "193409811"], @@ -3712,16 +3712,16 @@ exports[`better eslint`] = { [146, 12, 3, "Unexpected any. Specify a different type.", "193409811"], [159, 12, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/core/services/search_srv.ts:1940811612": [ - [16, 61, 3, "Unexpected any. Specify a different type.", "193409811"], - [17, 57, 3, "Unexpected any. Specify a different type.", "193409811"], - [38, 13, 153, "Do not use any type assertions.", "3055602986"], - [44, 58, 3, "Unexpected any. Specify a different type.", "193409811"], - [51, 9, 15, "Do not use any type assertions.", "4195655835"], - [51, 21, 3, "Unexpected any. Specify a different type.", "193409811"], - [63, 18, 3, "Unexpected any. Specify a different type.", "193409811"], - [64, 20, 3, "Unexpected any. Specify a different type.", "193409811"], - [100, 81, 3, "Unexpected any. Specify a different type.", "193409811"] + "public/app/core/services/search_srv.ts:3529274317": [ + [17, 61, 3, "Unexpected any. Specify a different type.", "193409811"], + [18, 57, 3, "Unexpected any. Specify a different type.", "193409811"], + [39, 13, 153, "Do not use any type assertions.", "3055602986"], + [45, 58, 3, "Unexpected any. Specify a different type.", "193409811"], + [52, 9, 15, "Do not use any type assertions.", "4195655835"], + [52, 21, 3, "Unexpected any. Specify a different type.", "193409811"], + [64, 18, 3, "Unexpected any. Specify a different type.", "193409811"], + [65, 20, 3, "Unexpected any. Specify a different type.", "193409811"], + [101, 81, 3, "Unexpected any. Specify a different type.", "193409811"] ], "public/app/core/services/withFocusedPanelId.ts:1347414526": [ [5, 22, 37, "Do not use any type assertions.", "3344232204"], @@ -4247,9 +4247,9 @@ exports[`better eslint`] = { [157, 27, 44, "Do not use any type assertions.", "720973826"], [158, 40, 44, "Do not use any type assertions.", "720973826"] ], - "public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:1101887836": [ - [19, 33, 27, "Do not use any type assertions.", "1321813536"], - [21, 30, 27, "Do not use any type assertions.", "1321813536"] + "public/app/features/alerting/unified/components/AnnotationDetailsField.tsx:2388344112": [ + [20, 33, 27, "Do not use any type assertions.", "1321813536"], + [22, 30, 27, "Do not use any type assertions.", "1321813536"] ], "public/app/features/alerting/unified/components/Expression.tsx:2035202716": [ [25, 25, 3, "Unexpected any. Specify a different type.", "193409811"], @@ -6727,17 +6727,6 @@ exports[`better eslint`] = { "public/app/features/sandbox/TestStuffPage.tsx:3698683147": [ [134, 30, 29, "Do not use any type assertions.", "3195381622"] ], - "public/app/features/search/components/DashboardSearch.test.tsx:3245889886": [ - [18, 23, 33, "Do not use any type assertions.", "2540133228"], - [30, 15, 3, "Unexpected any. Specify a different type.", "193409811"], - [72, 14, 130, "Do not use any type assertions.", "3429995880"], - [75, 10, 3, "Unexpected any. Specify a different type.", "193409811"] - ], - "public/app/features/search/components/MoveToFolderModal.tsx:3435881653": [ - [31, 21, 48, "Do not use any type assertions.", "646093117"], - [31, 93, 3, "Unexpected any. Specify a different type.", "193409811"], - [64, 51, 15, "Do not use any type assertions.", "3135191849"] - ], "public/app/features/search/components/SearchCard.tsx:417509806": [ [20, 36, 3, "Unexpected any. Specify a different type.", "193409811"], [116, 37, 32, "Do not use any type assertions.", "1966145380"], @@ -6746,18 +6735,6 @@ exports[`better eslint`] = { "public/app/features/search/components/SearchItem.tsx:1278234167": [ [15, 35, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/features/search/components/SearchResults.tsx:4264592788": [ - [19, 35, 3, "Unexpected any. Specify a different type.", "193409811"] - ], - "public/app/features/search/hooks/useDashboardSearch.ts:2034499138": [ - [61, 28, 32, "Do not use any type assertions.", "327142118"] - ], - "public/app/features/search/hooks/useManageDashboards.test.ts:3293235715": [ - [31, 45, 41, "Do not use any type assertions.", "3856094708"], - [31, 45, 15, "Do not use any type assertions.", "363922340"], - [33, 43, 31, "Do not use any type assertions.", "2087413285"], - [33, 43, 13, "Do not use any type assertions.", "2146830713"] - ], "public/app/features/search/hooks/useSearchKeyboardSelection.ts:877924082": [ [87, 24, 42, "Do not use any type assertions.", "3335657801"] ], @@ -6797,25 +6774,6 @@ exports[`better eslint`] = { [49, 50, 3, "Unexpected any. Specify a different type.", "193409811"], [145, 15, 56, "Do not use any type assertions.", "1375039711"] ], - "public/app/features/search/reducers/dashboardSearch.test.ts:1813679195": [ - [5, 66, 17, "Do not use any type assertions.", "3518965757"], - [5, 78, 3, "Unexpected any. Specify a different type.", "193409811"], - [8, 42, 9, "Do not use any type assertions.", "3692209159"], - [8, 48, 3, "Unexpected any. Specify a different type.", "193409811"] - ], - "public/app/features/search/reducers/manageDashboards.test.ts:2083873033": [ - [11, 26, 9, "Do not use any type assertions.", "3692209159"], - [11, 32, 3, "Unexpected any. Specify a different type.", "193409811"], - [16, 43, 3, "Unexpected any. Specify a different type.", "193409811"], - [17, 43, 3, "Unexpected any. Specify a different type.", "193409811"], - [17, 77, 3, "Unexpected any. Specify a different type.", "193409811"], - [23, 44, 3, "Unexpected any. Specify a different type.", "193409811"], - [24, 44, 3, "Unexpected any. Specify a different type.", "193409811"], - [24, 78, 3, "Unexpected any. Specify a different type.", "193409811"] - ], - "public/app/features/search/reducers/manageDashboards.ts:4114284001": [ - [81, 11, 24, "Do not use any type assertions.", "1870559034"] - ], "public/app/features/search/service/bluge.ts:59496993": [ [21, 15, 68, "Do not use any type assertions.", "2766474629"], [34, 17, 128, "Do not use any type assertions.", "437379020"], @@ -6837,50 +6795,12 @@ exports[`better eslint`] = { [99, 18, 61, "Do not use any type assertions.", "2014388220"], [104, 16, 68, "Do not use any type assertions.", "257067967"] ], - "public/app/features/search/types.ts:479421789": [ - [64, 12, 3, "Unexpected any. Specify a different type.", "193409811"], - [89, 54, 3, "Unexpected any. Specify a different type.", "193409811"] + "public/app/features/search/types.ts:762810555": [ + [56, 12, 3, "Unexpected any. Specify a different type.", "193409811"] ], - "public/app/features/search/utils.test.ts:3441767255": [ - [15, 48, 17, "Do not use any type assertions.", "3518965757"], - [15, 60, 3, "Unexpected any. Specify a different type.", "193409811"], - [33, 37, 15, "Do not use any type assertions.", "4195655835"], - [33, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [39, 37, 15, "Do not use any type assertions.", "4195655835"], - [39, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [45, 37, 15, "Do not use any type assertions.", "4195655835"], - [45, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [46, 40, 14, "Do not use any type assertions.", "1705067515"], - [46, 51, 3, "Unexpected any. Specify a different type.", "193409811"], - [53, 37, 15, "Do not use any type assertions.", "4195655835"], - [53, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [58, 37, 15, "Do not use any type assertions.", "4195655835"], - [58, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [63, 37, 15, "Do not use any type assertions.", "4195655835"], - [63, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [64, 40, 14, "Do not use any type assertions.", "1705067515"], - [64, 51, 3, "Unexpected any. Specify a different type.", "193409811"], - [72, 37, 15, "Do not use any type assertions.", "4195655835"], - [72, 49, 3, "Unexpected any. Specify a different type.", "193409811"], - [78, 40, 15, "Do not use any type assertions.", "4195655835"], - [78, 52, 3, "Unexpected any. Specify a different type.", "193409811"], - [103, 29, 3, "Unexpected any. Specify a different type.", "193409811"], - [103, 64, 3, "Unexpected any. Specify a different type.", "193409811"], - [110, 29, 3, "Unexpected any. Specify a different type.", "193409811"], - [110, 64, 3, "Unexpected any. Specify a different type.", "193409811"] - ], - "public/app/features/search/utils.ts:3454404257": [ - [86, 39, 3, "Unexpected any. Specify a different type.", "193409811"], - [109, 11, 54, "Do not use any type assertions.", "1072049889"], - [128, 40, 3, "Unexpected any. Specify a different type.", "193409811"], - [128, 62, 3, "Unexpected any. Specify a different type.", "193409811"], - [143, 5, 28, "Do not use any type assertions.", "2423908815"], - [171, 13, 72, "Do not use any type assertions.", "1798742037"], - [173, 13, 77, "Do not use any type assertions.", "414989384"], - [202, 34, 38, "Do not use any type assertions.", "1471143425"], - [222, 11, 24, "Do not use any type assertions.", "2044269175"], - [246, 28, 17, "Do not use any type assertions.", "1811834489"], - [251, 5, 29, "Do not use any type assertions.", "4135357902"] + "public/app/features/search/utils.ts:3780754211": [ + [34, 28, 17, "Do not use any type assertions.", "1811834489"], + [39, 5, 29, "Do not use any type assertions.", "4135357902"] ], "public/app/features/serviceaccounts/ServiceAccountPage.test.tsx:3979425477": [ [33, 20, 23, "Do not use any type assertions.", "499357842"], diff --git a/package.json b/package.json index 91fdd0db85d..f66a27d6340 100644 --- a/package.json +++ b/package.json @@ -385,7 +385,6 @@ "rst2html": "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809", "rxjs": "7.5.5", "sass": "link:./public/sass", - "search-query-parser": "1.6.0", "selecto": "1.16.2", "semver": "7.3.7", "slate": "0.47.8", diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 8d726b03d84..54c955f279a 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -425,6 +425,7 @@ export class BackendSrv implements BackendService { return this.request({ url: '/api/login/ping', method: 'GET', retry: 1 }); } + /** @deprecated */ search(query: any): Promise { return this.get('/api/search', query); } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index a5030a3c22a..b6111211610 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -13,6 +13,7 @@ interface Sections { [key: string]: Partial; } +/** @deprecated */ export class SearchSrv { private getRecentDashboards(sections: DashboardSection[] | any) { return this.queryForRecentDashboards().then((result: any[]) => { diff --git a/public/app/features/search/components/ActionRow.tsx b/public/app/features/search/components/ActionRow.tsx deleted file mode 100644 index 12b5c0a67cd..00000000000 --- a/public/app/features/search/components/ActionRow.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { css } from '@emotion/css'; -import React, { FC, ChangeEvent, FormEvent } from 'react'; - -import { GrafanaTheme2, SelectableValue } from '@grafana/data'; -import { config } from '@grafana/runtime'; -import { HorizontalGroup, RadioButtonGroup, Checkbox, InlineSwitch, useStyles2 } from '@grafana/ui'; -import { SortPicker } from 'app/core/components/Select/SortPicker'; -import { TagFilter } from 'app/core/components/TagFilter/TagFilter'; -import { SearchSrv } from 'app/core/services/search_srv'; - -import { DashboardQuery, SearchLayout } from '../types'; - -export const layoutOptions = [ - { value: SearchLayout.Folders, icon: 'folder', ariaLabel: 'View by folders' }, - { value: SearchLayout.List, icon: 'list-ul', ariaLabel: 'View as list' }, -]; - -const searchSrv = new SearchSrv(); - -interface Props { - onLayoutChange: (layout: SearchLayout) => void; - setShowPreviews: (newValue: boolean) => void; - onSortChange: (value: SelectableValue) => void; - onStarredFilterChange?: (event: FormEvent) => void; - onTagFilterChange: (tags: string[]) => void; - query: DashboardQuery; - showStarredFilter?: boolean; - hideLayout?: boolean; - showPreviews?: boolean; -} - -export const ActionRow: FC = ({ - onLayoutChange, - setShowPreviews, - onSortChange, - onStarredFilterChange = () => {}, - onTagFilterChange, - query, - showStarredFilter, - hideLayout, - showPreviews, -}) => { - const styles = useStyles2(getStyles); - const previewsEnabled = config.featureToggles.dashboardPreviews; - - return ( -
-
- - {!hideLayout ? ( - - ) : null} - - {previewsEnabled && ( - ) => setShowPreviews(ev.target.checked)} - transparent - /> - )} - -
- - {showStarredFilter && ( -
- -
- )} - -
-
- ); -}; - -ActionRow.displayName = 'ActionRow'; - -export const getStyles = (theme: GrafanaTheme2) => { - return { - actionRow: css` - display: none; - - ${theme.breakpoints.up('md')} { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: ${theme.spacing(2)}; - width: 100%; - } - `, - rowContainer: css` - margin-right: ${theme.spacing(1)}; - `, - checkboxWrapper: css` - label { - line-height: 1.2; - } - `, - }; -}; diff --git a/public/app/features/search/components/ConfirmDeleteModal.tsx b/public/app/features/search/components/ConfirmDeleteModal.tsx deleted file mode 100644 index 2a5e7fcc61a..00000000000 --- a/public/app/features/search/components/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { css } from '@emotion/css'; -import React, { FC } from 'react'; - -import { GrafanaTheme } from '@grafana/data'; -import { ConfirmModal, stylesFactory, useTheme } from '@grafana/ui'; -import { deleteFoldersAndDashboards } from 'app/features/manage-dashboards/state/actions'; - -import { DashboardSection, OnDeleteItems } from '../types'; -import { getCheckedUids } from '../utils'; - -interface Props { - onDeleteItems: OnDeleteItems; - results: DashboardSection[]; - isOpen: boolean; - onDismiss: () => void; -} - -export const ConfirmDeleteModal: FC = ({ results, onDeleteItems, isOpen, onDismiss }) => { - const theme = useTheme(); - const styles = getStyles(theme); - - const uids = getCheckedUids(results); - const { folders, dashboards } = uids; - const folderCount = folders.length; - const dashCount = dashboards.length; - - let text = 'Do you want to delete the '; - let subtitle; - const dashEnding = dashCount === 1 ? '' : 's'; - const folderEnding = folderCount === 1 ? '' : 's'; - - if (folderCount > 0 && dashCount > 0) { - text += `selected folder${folderEnding} and dashboard${dashEnding}?\n`; - subtitle = `All dashboards and alerts of the selected folder${folderEnding} will also be deleted`; - } else if (folderCount > 0) { - text += `selected folder${folderEnding} and all ${folderCount === 1 ? 'its' : 'their'} dashboards and alerts?`; - } else { - text += `selected dashboard${dashEnding}?`; - } - - const deleteItems = () => { - deleteFoldersAndDashboards(folders, dashboards).then(() => { - onDismiss(); - onDeleteItems(folders, dashboards); - }); - }; - - return isOpen ? ( - - {text} {subtitle &&
{subtitle}
} - - } - confirmText="Delete" - onConfirm={deleteItems} - onDismiss={onDismiss} - /> - ) : null; -}; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - subtitle: css` - font-size: ${theme.typography.size.base}; - padding-top: ${theme.spacing.md}; - `, - }; -}); diff --git a/public/app/features/search/components/DashboardListPage.tsx b/public/app/features/search/components/DashboardListPage.tsx index 3ececc199ca..120ed95b645 100644 --- a/public/app/features/search/components/DashboardListPage.tsx +++ b/public/app/features/search/components/DashboardListPage.tsx @@ -4,7 +4,7 @@ import { connect, MapStateToProps } from 'react-redux'; import { useAsync } from 'react-use'; import { NavModel, locationUtil } from '@grafana/data'; -import { config, locationService } from '@grafana/runtime'; +import { locationService } from '@grafana/runtime'; import Page from 'app/core/components/Page/Page'; import { getNavModel } from 'app/core/selectors/navModel'; import { FolderDTO, StoreState } from 'app/types'; @@ -12,7 +12,6 @@ import { FolderDTO, StoreState } from 'app/types'; import { GrafanaRouteComponentProps } from '../../../core/navigation/types'; import { loadFolderPage } from '../loaders'; -import ManageDashboards from './ManageDashboards'; import ManageDashboardsNew from './ManageDashboardsNew'; export interface DashboardListPageRouteParams { @@ -46,22 +45,16 @@ export const DashboardListPage: FC = memo(({ navModel, match, location }) return ( - {Boolean(config.featureToggles.panelTitleSearch) ? ( - - - - ) : ( - - - - )} + + + ); }); diff --git a/public/app/features/search/components/DashboardSearch.test.tsx b/public/app/features/search/components/DashboardSearch.test.tsx deleted file mode 100644 index 778bf68d419..00000000000 --- a/public/app/features/search/components/DashboardSearch.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import React from 'react'; -import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; - -import { selectors } from '@grafana/e2e-selectors'; -import { locationService, setEchoSrv } from '@grafana/runtime'; -import * as MockSearchSrv from 'app/core/services/__mocks__/search_srv'; -import { Echo } from 'app/core/services/echo/Echo'; -import * as SearchSrv from 'app/core/services/search_srv'; - -import { searchResults } from '../testData'; -import { SearchLayout } from '../types'; - -import { DashboardSearchOLD as DashboardSearch, Props } from './DashboardSearch'; - -jest.mock('app/core/services/search_srv'); -// Typecast the mock search so the mock import is correctly recognised by TS -// https://stackoverflow.com/a/53222290 -const { mockSearch } = SearchSrv as typeof MockSearchSrv; - -beforeEach(() => { - jest.useFakeTimers(); - mockSearch.mockClear(); -}); - -afterEach(() => { - jest.useRealTimers(); -}); - -const setup = (testProps?: Partial) => { - const props: any = { - onCloseSearch: () => {}, - ...testProps, - }; - render(); - jest.runOnlyPendingTimers(); -}; - -/** - * Need to wrap component render in async act and use jest.runAllTimers to test - * calls inside useDebounce hook - */ -describe('DashboardSearch', () => { - beforeAll(() => { - setEchoSrv(new Echo()); - }); - - it('should call search api with default query when initialised', async () => { - locationService.push('/'); - setup(); - - await waitFor(() => screen.getByPlaceholderText('Search dashboards by name')); - - expect(mockSearch).toHaveBeenCalledTimes(1); - expect(mockSearch).toHaveBeenCalledWith({ - query: '', - tag: [], - skipRecent: false, - skipStarred: false, - starred: false, - folderIds: [], - layout: SearchLayout.Folders, - sort: undefined, - prevSort: null, - }); - }); - - it('should call api with updated query on query change', async () => { - locationService.push('/'); - setup(); - - const input = await screen.findByPlaceholderText('Search dashboards by name'); - await act((async () => { - await fireEvent.input(input, { target: { value: 'Test' } }); - jest.runOnlyPendingTimers(); - }) as any); - - expect(mockSearch).toHaveBeenCalledWith({ - query: 'Test', - skipRecent: false, - skipStarred: false, - tag: [], - starred: false, - folderIds: [], - layout: SearchLayout.Folders, - sort: undefined, - prevSort: null, - }); - }); - - it("should render 'No results' message when there are no dashboards", async () => { - locationService.push('/'); - setup(); - - const message = await screen.findByText('No dashboards matching your query were found.'); - expect(message).toBeInTheDocument(); - }); - - it('should render search results', async () => { - mockSearch.mockResolvedValueOnce(searchResults); - - locationService.push('/'); - setup(); - - const section = await screen.findAllByTestId(selectors.components.Search.sectionV2); - expect(section).toHaveLength(2); - expect(screen.getAllByTestId(selectors.components.Search.itemsV2)).toHaveLength(1); - }); - - it('should call search with selected tags', async () => { - locationService.push('/'); - setup(); - - await waitFor(() => screen.getByLabelText('Tag filter')); - - const tagComponent = screen.getByLabelText('Tag filter'); - expect(tagComponent).toBeInTheDocument(); - - tagComponent.focus(); - await waitFor(() => selectOptionInTest(tagComponent, 'tag1')); - - await waitFor(() => - expect(mockSearch).toHaveBeenCalledWith({ - query: '', - skipRecent: false, - skipStarred: false, - tag: ['tag1'], - starred: false, - folderIds: [], - layout: SearchLayout.Folders, - sort: undefined, - prevSort: null, - }) - ); - }); - - it('should call search api with provided search params', async () => { - locationService.partial({ query: 'test query', tag: ['tag1'], sort: 'asc' }); - setup({}); - - await waitFor(() => { - expect(mockSearch).toHaveBeenCalledTimes(1); - expect(mockSearch).toHaveBeenCalledWith( - expect.objectContaining({ - query: 'test query', - tag: ['tag1'], - sort: 'asc', - }) - ); - }); - }); -}); diff --git a/public/app/features/search/components/DashboardSearch.tsx b/public/app/features/search/components/DashboardSearch.tsx index 27c32f8d012..5f1818c3335 100644 --- a/public/app/features/search/components/DashboardSearch.tsx +++ b/public/app/features/search/components/DashboardSearch.tsx @@ -1,35 +1,21 @@ import { css } from '@emotion/css'; -import React, { FC, memo, useState } from 'react'; +import React, { useState } from 'react'; import { useDebounce, useLocalStorage } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { CustomScrollbar, IconButton, stylesFactory, useStyles2, useTheme2 } from '@grafana/ui'; +import { IconButton, stylesFactory, useStyles2 } from '@grafana/ui'; import { SEARCH_PANELS_LOCAL_STORAGE_KEY } from '../constants'; -import { useDashboardSearch } from '../hooks/useDashboardSearch'; import { useKeyNavigationListener } from '../hooks/useSearchKeyboardSelection'; import { useSearchQuery } from '../hooks/useSearchQuery'; import { SearchView } from '../page/components/SearchView'; -import { ActionRow } from './ActionRow'; -import { PreviewsSystemRequirements } from './PreviewsSystemRequirements'; -import { SearchField } from './SearchField'; -import { SearchResults } from './SearchResults'; - export interface Props { onCloseSearch: () => void; } -export default function DashboardSearch({ onCloseSearch }: Props) { - if (config.featureToggles.panelTitleSearch) { - // TODO: "folder:current" ???? - return ; - } - return ; -} - -function DashboardSearchNew({ onCloseSearch }: Props) { +export function DashboardSearch({ onCloseSearch }: Props) { const styles = useStyles2(getStyles); const { query, onQueryChange } = useSearchQuery({}); @@ -87,59 +73,6 @@ function DashboardSearchNew({ onCloseSearch }: Props) { ); } -export const DashboardSearchOLD: FC = memo(({ onCloseSearch }) => { - const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery({}); - const { results, loading, onToggleSection, onKeyDown, showPreviews, setShowPreviews } = useDashboardSearch( - query, - onCloseSearch - ); - const theme = useTheme2(); - const styles = getStyles(theme); - - return ( -
-
-
- -
- -
-
-
- - setShowPreviews(false)} - /> - - - -
-
-
- ); -}); - -DashboardSearchOLD.displayName = 'DashboardSearchOLD'; - const getStyles = stylesFactory((theme: GrafanaTheme2) => { return { overlay: css` diff --git a/public/app/features/search/components/ManageDashboards.tsx b/public/app/features/search/components/ManageDashboards.tsx deleted file mode 100644 index 01b201f98bc..00000000000 --- a/public/app/features/search/components/ManageDashboards.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { css } from '@emotion/css'; -import React, { FC, memo, useState } from 'react'; - -import { GrafanaTheme } from '@grafana/data'; -import { FilterInput, Spinner, stylesFactory, useTheme } from '@grafana/ui'; -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { contextSrv } from 'app/core/services/context_srv'; -import { FolderDTO, AccessControlAction } from 'app/types'; - -import { useManageDashboards } from '../hooks/useManageDashboards'; -import { useSearchQuery } from '../hooks/useSearchQuery'; -import { SearchLayout } from '../types'; - -import { ConfirmDeleteModal } from './ConfirmDeleteModal'; -import { DashboardActions } from './DashboardActions'; -import { MoveToFolderModal } from './MoveToFolderModal'; -import { SearchResults } from './SearchResults'; -import { SearchResultsFilter } from './SearchResultsFilter'; - -export interface Props { - folder?: FolderDTO; -} - -const { isEditor } = contextSrv; - -export const ManageDashboards: FC = memo(({ folder }) => { - const folderId = folder?.id; - const folderUid = folder?.uid; - const theme = useTheme(); - const styles = getStyles(theme); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isMoveModalOpen, setIsMoveModalOpen] = useState(false); - const defaultLayout = folderId ? SearchLayout.List : SearchLayout.Folders; - const queryParamsDefaults = { - skipRecent: true, - skipStarred: true, - folderIds: folderId ? [folderId] : [], - layout: defaultLayout, - }; - - const { - query, - hasFilters, - onQueryChange, - onTagFilterChange, - onStarredFilterChange, - onTagAdd, - onSortChange, - onLayoutChange, - } = useSearchQuery(queryParamsDefaults); - - const { - results, - loading, - initialLoading, - allChecked, - hasEditPermissionInFolders, - canMove, - canSave, - canDelete, - onToggleSection, - onToggleChecked, - onToggleAllChecked, - onDeleteItems, - onMoveItems, - noFolders, - showPreviews, - setShowPreviews, - } = useManageDashboards(query, {}, folder); - - const onMoveTo = () => { - setIsMoveModalOpen(true); - }; - - const onItemDelete = () => { - setIsDeleteModalOpen(true); - }; - - if (initialLoading) { - return ; - } - - if (noFolders && !hasFilters) { - return ( - - ); - } - - return ( -
-
-
- -
- -
- -
- - -
- setIsDeleteModalOpen(false)} - /> - setIsMoveModalOpen(false)} - /> -
- ); -}); - -ManageDashboards.displayName = 'ManageDashboards'; - -export default ManageDashboards; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - container: css` - height: 100%; - display: flex; - flex-direction: column; - `, - results: css` - display: flex; - flex-direction: column; - flex: 1 1 0; - height: 100%; - padding-top: ${theme.spacing.lg}; - `, - spinner: css` - display: flex; - justify-content: center; - align-items: center; - min-height: 200px; - `, - }; -}); diff --git a/public/app/features/search/components/MoveToFolderModal.tsx b/public/app/features/search/components/MoveToFolderModal.tsx deleted file mode 100644 index b850f9462ba..00000000000 --- a/public/app/features/search/components/MoveToFolderModal.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { css } from '@emotion/css'; -import React, { FC, useState } from 'react'; - -import { GrafanaTheme } from '@grafana/data'; -import { Button, HorizontalGroup, Modal, stylesFactory, useTheme } from '@grafana/ui'; -import { FolderPicker } from 'app/core/components/Select/FolderPicker'; -import { useAppNotification } from 'app/core/copy/appNotification'; -import { moveDashboards } from 'app/features/manage-dashboards/state/actions'; -import { FolderInfo } from 'app/types'; - -import { DashboardSection, OnMoveItems } from '../types'; -import { getCheckedDashboards } from '../utils'; - -interface Props { - onMoveItems: OnMoveItems; - results: DashboardSection[]; - isOpen: boolean; - onDismiss: () => void; -} - -export const MoveToFolderModal: FC = ({ results, onMoveItems, isOpen, onDismiss }) => { - const [folder, setFolder] = useState(null); - const theme = useTheme(); - const styles = getStyles(theme); - const selectedDashboards = getCheckedDashboards(results); - const notifyApp = useAppNotification(); - - const moveTo = () => { - if (folder && selectedDashboards.length) { - const folderTitle = folder.title ?? 'General'; - - moveDashboards(selectedDashboards.map((d) => d.uid) as string[], folder).then((result: any) => { - if (result.successCount > 0) { - const ending = result.successCount === 1 ? '' : 's'; - const header = `Dashboard${ending} Moved`; - const msg = `${result.successCount} dashboard${ending} moved to ${folderTitle}`; - notifyApp.success(header, msg); - } - - if (result.totalCount === result.alreadyInFolderCount) { - notifyApp.error('Error', `Dashboard already belongs to folder ${folderTitle}`); - } else { - onMoveItems(selectedDashboards, folder); - } - - onDismiss(); - }); - } - }; - - return isOpen ? ( - - <> -
-

- Move the {selectedDashboards.length} selected dashboard{selectedDashboards.length === 1 ? '' : 's'} to the - following folder: -

- setFolder(f as FolderInfo)} /> -
- - - - - - -
- ) : null; -}; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - return { - modal: css` - width: 500px; - `, - content: css` - margin-bottom: ${theme.spacing.lg}; - `, - }; -}); diff --git a/public/app/features/search/components/SearchField.tsx b/public/app/features/search/components/SearchField.tsx deleted file mode 100644 index 274fa528a31..00000000000 --- a/public/app/features/search/components/SearchField.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { FC } from 'react'; - -import { GrafanaTheme } from '@grafana/data'; -import { useStyles } from '@grafana/ui'; - -import { DashboardQuery } from '../types'; - -type Omit = Pick>; - -interface SearchFieldProps extends Omit, 'onChange'> { - query: DashboardQuery; - onChange: (query: string) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; - clearable?: boolean; - width?: number; -} - -const getSearchFieldStyles = (theme: GrafanaTheme) => ({ - wrapper: css` - width: 100%; - display: flex; - position: relative; - align-items: center; - `, - input: css` - box-sizing: border-box; - outline: none; - background-color: transparent; - background: transparent; - border-bottom: 2px solid ${theme.colors.border1}; - font-size: 20px; - line-height: 38px; - width: 100%; - - &::placeholder { - color: ${theme.colors.textWeak}; - } - `, - spacer: css` - flex-grow: 1; - `, - icon: cx( - css` - color: ${theme.colors.textWeak}; - padding: 0 ${theme.spacing.md}; - ` - ), - clearButton: css` - font-size: ${theme.typography.size.sm}; - color: ${theme.colors.textWeak}; - text-decoration: underline; - - &:hover { - cursor: pointer; - color: ${theme.colors.textStrong}; - } - `, -}); - -export const SearchField: FC = ({ query, onChange, size, clearable, className, ...inputProps }) => { - const styles = useStyles(getSearchFieldStyles); - - return ( -
- ) => { - onChange(event.currentTarget.value); - }} - tabIndex={0} - spellCheck={false} - className={styles.input} - {...inputProps} - /> - -
-
- ); -}; diff --git a/public/app/features/search/components/SearchResults.test.tsx b/public/app/features/search/components/SearchResults.test.tsx deleted file mode 100644 index 2b307496b25..00000000000 --- a/public/app/features/search/components/SearchResults.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react'; -import React from 'react'; - -import { selectors } from '@grafana/e2e-selectors'; - -import { generalFolder, searchResults } from '../testData'; -import { SearchLayout } from '../types'; - -import { Props, SearchResults } from './SearchResults'; - -beforeEach(() => { - jest.clearAllMocks(); -}); - -const setup = (propOverrides?: Partial) => { - const props: Props = { - results: searchResults, - onTagSelected: (name: string) => {}, - onToggleSection: () => {}, - editable: false, - layout: SearchLayout.Folders, - }; - - Object.assign(props, propOverrides); - - render(); -}; - -describe('SearchResults', () => { - it('should render result items', () => { - setup(); - expect(screen.getAllByTestId(selectors.components.Search.sectionV2)).toHaveLength(2); - }); - - it('should render section items for expanded section', () => { - setup(); - expect(screen.getAllByText('General', { exact: false })[0]).toBeInTheDocument(); - expect(screen.getByTestId(selectors.components.Search.itemsV2)).toBeInTheDocument(); - expect(screen.getByTestId(selectors.components.Search.dashboardItem('Test 1'))).toBeInTheDocument(); - expect(screen.getByTestId(selectors.components.Search.dashboardItem('Test 2'))).toBeInTheDocument(); - - // Check search cards aren't in the DOM - expect(screen.queryByTestId(selectors.components.Search.cards)).not.toBeInTheDocument(); - expect(screen.queryByTestId(selectors.components.Search.dashboardCard('Test 1'))).not.toBeInTheDocument(); - expect(screen.queryByTestId(selectors.components.Search.dashboardCard('Test 2'))).not.toBeInTheDocument(); - }); - - it('should render search card items for expanded section when showPreviews is enabled', () => { - setup({ showPreviews: true }); - expect(screen.getAllByText('General', { exact: false })[0]).toBeInTheDocument(); - expect(screen.getByTestId(selectors.components.Search.cards)).toBeInTheDocument(); - expect(screen.getByTestId(selectors.components.Search.dashboardCard('Test 1'))).toBeInTheDocument(); - expect(screen.getByTestId(selectors.components.Search.dashboardCard('Test 2'))).toBeInTheDocument(); - - // Check search items aren't in the DOM - expect(screen.queryByTestId(selectors.components.Search.itemsV2)).not.toBeInTheDocument(); - expect(screen.queryByTestId(selectors.components.Search.dashboardItem('Test 1'))).not.toBeInTheDocument(); - expect(screen.queryByTestId(selectors.components.Search.dashboardItem('Test 2'))).not.toBeInTheDocument(); - }); - - it('should not render checkboxes for non-editable results', () => { - setup({ editable: false }); - expect(screen.queryAllByRole('checkbox')).toHaveLength(0); - }); - - it('should render checkboxes for editable results', () => { - setup({ editable: true }); - expect(screen.getAllByRole('checkbox')).toHaveLength(4); - }); - - it('should collapse folder item list on header click', () => { - const mockOnToggleSection = jest.fn(); - setup({ onToggleSection: mockOnToggleSection }); - - fireEvent.click(screen.getAllByText('General', { exact: false })[0]); - expect(mockOnToggleSection).toHaveBeenCalledWith(generalFolder); - }); - - it('should not throw an error if the search results have an empty title', () => { - const mockOnToggleSection = jest.fn(); - const searchResultsEmptyTitle = searchResults.slice(); - searchResultsEmptyTitle[0].title = ''; - expect(() => { - setup({ results: searchResultsEmptyTitle, onToggleSection: mockOnToggleSection }); - }).not.toThrowError(); - }); -}); diff --git a/public/app/features/search/components/SearchResults.tsx b/public/app/features/search/components/SearchResults.tsx deleted file mode 100644 index cafddc351c3..00000000000 --- a/public/app/features/search/components/SearchResults.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { FC, memo } from 'react'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { FixedSizeList, FixedSizeGrid } from 'react-window'; - -import { GrafanaTheme } from '@grafana/data'; -import { selectors } from '@grafana/e2e-selectors'; -import { Spinner, stylesFactory, useTheme } from '@grafana/ui'; - -import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants'; -import { DashboardSection, OnToggleChecked, SearchLayout } from '../types'; - -import { SearchCard } from './SearchCard'; -import { SearchItem } from './SearchItem'; -import { SectionHeader } from './SectionHeader'; - -export interface Props { - editable?: boolean; - loading?: boolean; - onTagSelected: (name: string) => any; - onToggleChecked?: OnToggleChecked; - onToggleSection: (section: DashboardSection) => void; - results: DashboardSection[]; - showPreviews?: boolean; - layout?: string; -} - -const { sectionV2: sectionLabel, itemsV2: itemsLabel, cards: cardsLabel } = selectors.components.Search; - -export const SearchResults: FC = memo( - ({ editable, loading, onTagSelected, onToggleChecked, onToggleSection, results, showPreviews, layout }) => { - const theme = useTheme(); - const styles = getSectionStyles(theme); - const itemProps = { editable, onToggleChecked, onTagSelected }; - const renderFolders = () => { - const Wrapper = showPreviews ? SearchCard : SearchItem; - return ( -
- {results.map((section) => { - return ( -
- {section.title && ( - -
- {section.items.map((item) => ( - - ))} -
-
- )} -
- ); - })} -
- ); - }; - const renderDashboards = () => { - const items = results[0]?.items; - return ( -
- - {({ height, width }) => { - const numColumns = Math.ceil(width / 320); - const cellWidth = width / numColumns; - const cellHeight = (cellWidth - 64) * 0.75 + 56 + 8; - const numRows = Math.ceil(items.length / numColumns); - return showPreviews ? ( - - {({ columnIndex, rowIndex, style }) => { - const index = rowIndex * numColumns + columnIndex; - const item = items[index]; - // The wrapper div is needed as the inner SearchItem has margin-bottom spacing - // And without this wrapper there is no room for that margin - return item ? ( -
  • - -
  • - ) : null; - }} -
    - ) : ( - - {({ index, style }) => { - const item = items[index]; - // The wrapper div is needed as the inner SearchItem has margin-bottom spacing - // And without this wrapper there is no room for that margin - return ( -
  • - -
  • - ); - }} -
    - ); - }} -
    -
    - ); - }; - - if (loading) { - return ; - } else if (!results || !results.length) { - return
    No dashboards matching your query were found.
    ; - } - - return ( -
    - {layout === SearchLayout.Folders ? renderFolders() : renderDashboards()} -
    - ); - } -); - -SearchResults.displayName = 'SearchResults'; - -const getSectionStyles = stylesFactory((theme: GrafanaTheme) => { - const { md, sm } = theme.spacing; - - return { - virtualizedGridItemWrapper: css` - padding: 4px; - `, - wrapper: css` - display: flex; - flex-direction: column; - - > ul { - list-style: none; - } - `, - section: css` - display: flex; - flex-direction: column; - background: ${theme.colors.panelBg}; - border-bottom: solid 1px ${theme.colors.border2}; - `, - sectionItems: css` - margin: 0 24px 0 32px; - `, - spinner: css` - display: flex; - justify-content: center; - align-items: center; - min-height: 100px; - `, - gridContainer: css` - display: grid; - gap: ${sm}; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - margin-bottom: ${md}; - `, - resultsContainer: css` - position: relative; - flex-grow: 10; - margin-bottom: ${md}; - background: ${theme.colors.bg1}; - border: 1px solid ${theme.colors.border1}; - border-radius: 3px; - height: 100%; - `, - noResults: css` - padding: ${md}; - background: ${theme.colors.bg2}; - font-style: italic; - margin-top: ${theme.spacing.md}; - `, - listModeWrapper: css` - position: relative; - height: 100%; - padding: ${md}; - `, - }; -}); diff --git a/public/app/features/search/components/SearchResultsFilter.test.tsx b/public/app/features/search/components/SearchResultsFilter.test.tsx deleted file mode 100644 index 878233f4e9d..00000000000 --- a/public/app/features/search/components/SearchResultsFilter.test.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { fireEvent, render, screen } from '@testing-library/react'; -import React from 'react'; -import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; - -import { SearchLayout } from '../types'; - -import { Props, SearchResultsFilter } from './SearchResultsFilter'; - -jest.mock('app/core/services/search_srv'); - -const noop = jest.fn(); - -beforeEach(() => { - jest.clearAllMocks(); -}); - -const searchQuery = { - starred: false, - sort: null, - prevSort: null, - tag: ['tag'], - query: '', - skipRecent: true, - skipStarred: true, - folderIds: [], - layout: SearchLayout.Folders, -}; - -const setup = (propOverrides?: Partial) => { - const props: Props = { - allChecked: false, - canDelete: false, - canMove: false, - deleteItem: noop, - moveTo: noop, - onStarredFilterChange: noop, - onTagFilterChange: noop, - onToggleAllChecked: noop, - onLayoutChange: noop, - query: searchQuery, - onSortChange: noop, - setShowPreviews: noop, - editable: true, - }; - - Object.assign(props, propOverrides); - - render(); -}; - -describe('SearchResultsFilter', () => { - it('should render "filter by starred" and "filter by tag" filters by default', async () => { - setup(); - expect(await screen.findAllByRole('checkbox')).toHaveLength(1); - expect(screen.queryByText('Move')).not.toBeInTheDocument(); - expect(screen.queryByText('Delete')).not.toBeInTheDocument(); - }); - - it('should render Move and Delete buttons when canDelete is true', () => { - setup({ canDelete: true }); - expect(screen.getAllByRole('checkbox')).toHaveLength(1); - expect(screen.queryByText('Move')).toBeInTheDocument(); - expect(screen.queryByText('Delete')).toBeInTheDocument(); - }); - - it('should render Move and Delete buttons when canMove is true', () => { - setup({ canMove: true }); - expect(screen.getAllByRole('checkbox')).toHaveLength(1); - expect(screen.queryByText('Move')).toBeInTheDocument(); - expect(screen.queryByText('Delete')).toBeInTheDocument(); - }); - - it('should call onStarredFilterChange when "filter by starred" is changed', async () => { - const mockFilterStarred = jest.fn(); - setup({ onStarredFilterChange: mockFilterStarred }); - const checkbox = await screen.findByLabelText(/filter by starred/i); - fireEvent.click(checkbox); - expect(mockFilterStarred).toHaveBeenCalledTimes(1); - }); - - it('should be called with proper filter option when "filter by tags" is changed', async () => { - const mockFilterByTags = jest.fn(); - setup({ - onTagFilterChange: mockFilterByTags, - query: { ...searchQuery, tag: [] }, - }); - const tagComponent = await screen.findByLabelText('Tag filter'); - await tagComponent.focus(); - await selectOptionInTest(tagComponent, 'tag1'); - - expect(mockFilterByTags).toHaveBeenCalledTimes(1); - expect(mockFilterByTags).toHaveBeenCalledWith(['tag1']); - }); -}); diff --git a/public/app/features/search/components/SearchResultsFilter.tsx b/public/app/features/search/components/SearchResultsFilter.tsx deleted file mode 100644 index 51ec8d7ffcf..00000000000 --- a/public/app/features/search/components/SearchResultsFilter.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { css } from '@emotion/css'; -import React, { FC, FormEvent } from 'react'; - -import { GrafanaTheme, SelectableValue } from '@grafana/data'; -import { Button, Checkbox, stylesFactory, useTheme, HorizontalGroup } from '@grafana/ui'; - -import { DashboardQuery, SearchLayout } from '../types'; - -import { ActionRow } from './ActionRow'; -import { PreviewsSystemRequirements } from './PreviewsSystemRequirements'; - -export interface Props { - allChecked?: boolean; - canDelete?: boolean; - canMove?: boolean; - deleteItem: () => void; - hideLayout?: boolean; - moveTo: () => void; - onLayoutChange: (layout: SearchLayout) => void; - setShowPreviews: (newValue: boolean) => void; - onSortChange: (value: SelectableValue) => void; - onStarredFilterChange: (event: FormEvent) => void; - onTagFilterChange: (tags: string[]) => void; - onToggleAllChecked: () => void; - query: DashboardQuery; - showPreviews?: boolean; - editable?: boolean; -} - -export const SearchResultsFilter: FC = ({ - allChecked, - canDelete, - canMove, - deleteItem, - hideLayout, - moveTo, - onLayoutChange, - setShowPreviews, - onSortChange, - onStarredFilterChange, - onTagFilterChange, - onToggleAllChecked, - query, - showPreviews, - editable, -}) => { - const showActions = canDelete || canMove; - const theme = useTheme(); - const styles = getStyles(theme); - - return ( -
    -
    - {editable && ( -
    - -
    - )} - {showActions ? ( - - - - - ) : ( - - )} -
    - setShowPreviews(false)} - /> -
    - ); -}; - -const getStyles = stylesFactory((theme: GrafanaTheme) => { - const { sm, md } = theme.spacing; - return { - wrapper: css` - display: flex; - flex-direction: column; - `, - rowWrapper: css` - height: ${theme.height.md}px; - display: flex; - justify-content: flex-start; - gap: ${theme.spacing.md}; - align-items: center; - margin-bottom: ${sm}; - - > label { - height: 20px; - margin: 0 ${md} 0 ${sm}; - } - `, - checkboxWrapper: css` - label { - line-height: 1.2; - width: max-content; - } - `, - }; -}); diff --git a/public/app/features/search/components/SearchWrapper.tsx b/public/app/features/search/components/SearchWrapper.tsx index 506f8944ef9..7d7881edd75 100644 --- a/public/app/features/search/components/SearchWrapper.tsx +++ b/public/app/features/search/components/SearchWrapper.tsx @@ -4,7 +4,7 @@ import { useUrlParams } from 'app/core/navigation/hooks'; import { defaultQueryParams } from '../reducers/searchQueryReducer'; -import DashboardSearch from './DashboardSearch'; +import { DashboardSearch } from './DashboardSearch'; export const SearchWrapper: FC = memo(() => { const [params, updateUrlParams] = useUrlParams(); diff --git a/public/app/features/search/components/SectionHeader.tsx b/public/app/features/search/components/SectionHeader.tsx deleted file mode 100644 index 43b418985a3..00000000000 --- a/public/app/features/search/components/SectionHeader.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { css, cx } from '@emotion/css'; -import React, { FC, useCallback } from 'react'; -import { useLocalStorage } from 'react-use'; - -import { GrafanaTheme } from '@grafana/data'; -import { CollapsableSection, Icon, stylesFactory, useTheme } from '@grafana/ui'; -import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId'; - -import { DashboardSection, OnToggleChecked } from '../types'; -import { getSectionIcon, getSectionStorageKey } from '../utils'; - -import { SearchCheckbox } from './SearchCheckbox'; - -interface SectionHeaderProps { - editable?: boolean; - onSectionClick: (section: DashboardSection) => void; - onToggleChecked?: OnToggleChecked; - section: DashboardSection; - children: React.ReactNode; -} - -export const SectionHeader: FC = ({ - section, - onSectionClick, - children, - onToggleChecked, - editable = false, -}) => { - const theme = useTheme(); - const styles = getSectionHeaderStyles(theme, section.selected, editable); - const setSectionExpanded = useLocalStorage(getSectionStorageKey(section.title), true)[1]; - - const onSectionExpand = () => { - setSectionExpanded(!section.expanded); - onSectionClick(section); - }; - - const handleCheckboxClick = useCallback( - (ev: React.MouseEvent) => { - ev.stopPropagation(); - ev.preventDefault(); - - onToggleChecked?.(section); - }, - [onToggleChecked, section] - ); - - const id = useUniqueId(); - const labelId = `section-header-label-${id}`; - - return ( - - - -
    - -
    - -
    - {section.title} - {section.url && ( - - | Go to folder - - )} -
    - - } - > - {children} -
    - ); -}; - -const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false, editable: boolean) => { - const { sm } = theme.spacing; - return { - wrapper: cx( - css` - align-items: center; - font-size: ${theme.typography.size.base}; - padding: 12px; - border-bottom: none; - color: ${theme.colors.textWeak}; - z-index: 1; - - &:hover, - &.selected { - color: ${theme.colors.text}; - } - - &:hover, - &:focus-visible, - &:focus-within { - a { - opacity: 1; - } - } - `, - 'pointer', - { selected } - ), - checkbox: css` - padding: 0 ${sm} 0 0; - `, - icon: css` - padding: 0 ${sm} 0 ${editable ? 0 : sm}; - `, - text: css` - flex-grow: 1; - line-height: 24px; - `, - link: css` - padding: 2px 10px 0; - color: ${theme.colors.textWeak}; - opacity: 0; - transition: opacity 150ms ease-in-out; - `, - separator: css` - margin-right: 6px; - `, - content: css` - padding-top: 0px; - padding-bottom: 0px; - `, - }; -}); diff --git a/public/app/features/search/constants.ts b/public/app/features/search/constants.ts index 9563a641fea..1e6483c77f6 100644 --- a/public/app/features/search/constants.ts +++ b/public/app/features/search/constants.ts @@ -7,6 +7,5 @@ export const SECTION_STORAGE_KEY = 'search.sections'; export const GENERAL_FOLDER_ID = 0; export const GENERAL_FOLDER_UID = 'general'; export const GENERAL_FOLDER_TITLE = 'General'; -export const PREVIEWS_LOCAL_STORAGE_KEY = 'grafana.dashboard.previews'; export const SEARCH_PANELS_LOCAL_STORAGE_KEY = 'grafana.search.include.panels'; export const SEARCH_SELECTED_LAYOUT = 'grafana.search.layout'; diff --git a/public/app/features/search/hooks/useDashboardSearch.ts b/public/app/features/search/hooks/useDashboardSearch.ts deleted file mode 100644 index 96267739453..00000000000 --- a/public/app/features/search/hooks/useDashboardSearch.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { KeyboardEvent, useReducer } from 'react'; -import { useDebounce } from 'react-use'; - -import { locationUtil } from '@grafana/data'; -import { locationService } from '@grafana/runtime'; - -import { MOVE_SELECTION_DOWN, MOVE_SELECTION_UP } from '../reducers/actionTypes'; -import { dashboardsSearchState, DashboardsSearchState, searchReducer } from '../reducers/dashboardSearch'; -import { DashboardQuery, DashboardSearchItemType, DashboardSection } from '../types'; -import { findSelected } from '../utils'; - -import { reportDashboardListViewed } from './useManageDashboards'; -import { useSearch } from './useSearch'; -import { useShowDashboardPreviews } from './useShowDashboardPreviews'; - -export const useDashboardSearch = (query: DashboardQuery, onCloseSearch: () => void) => { - const reducer = useReducer(searchReducer, dashboardsSearchState); - const { showPreviews, setShowPreviews, previewFeatureEnabled } = useShowDashboardPreviews(); - const { - state: { results, loading }, - onToggleSection, - dispatch, - } = useSearch(query, reducer, { queryParsing: true }); - - useDebounce( - () => { - reportDashboardListViewed('dashboard_search', showPreviews, previewFeatureEnabled, { - layout: query.layout, - starred: query.starred, - sortValue: query.sort?.value, - query: query.query, - tagCount: query.tag?.length, - }); - }, - 1000, - [ - showPreviews, - previewFeatureEnabled, - query.layout, - query.starred, - query.sort?.value, - query.query?.length, - query.tag?.length, - ] - ); - - const onKeyDown = (event: KeyboardEvent) => { - switch (event.key) { - case 'Escape': - onCloseSearch(); - break; - case 'ArrowUp': - dispatch({ type: MOVE_SELECTION_UP }); - break; - case 'ArrowDown': - dispatch({ type: MOVE_SELECTION_DOWN }); - break; - case 'Enter': - const selectedItem = findSelected(results); - if (selectedItem) { - if (selectedItem.type === DashboardSearchItemType.DashFolder) { - onToggleSection(selectedItem as DashboardSection); - } else { - locationService.push(locationUtil.stripBaseFromUrl(selectedItem.url)); - // Delay closing to prevent current page flicker - setTimeout(onCloseSearch, 0); - } - } - } - }; - - return { - results, - loading, - onToggleSection, - onKeyDown, - showPreviews, - setShowPreviews, - }; -}; diff --git a/public/app/features/search/hooks/useManageDashboards.test.ts b/public/app/features/search/hooks/useManageDashboards.test.ts deleted file mode 100644 index 6c2dd608b0b..00000000000 --- a/public/app/features/search/hooks/useManageDashboards.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { Dispatch } from 'react'; - -import { setEchoSrv } from '@grafana/runtime/src'; -import { Echo } from 'app/core/services/echo/Echo'; - -import { GENERAL_FOLDER_ID } from '../constants'; -import { ManageDashboardsState } from '../reducers/manageDashboards'; -import { DashboardQuery, DashboardSearchItemType, DashboardSection, SearchAction } from '../types'; - -import { useManageDashboards } from './useManageDashboards'; -import * as useSearch from './useSearch'; - -describe('useManageDashboards', () => { - const useSearchMock = jest.spyOn(useSearch, 'useSearch'); - const toggle = async (section: DashboardSection) => section; - - beforeAll(() => { - setEchoSrv(new Echo()); - }); - - function setupTestContext({ results = [] }: { results?: DashboardSection[] } = {}) { - jest.clearAllMocks(); - - const state: ManageDashboardsState = { - results, - loading: false, - selectedIndex: 0, - initialLoading: false, - allChecked: false, - }; - const dispatch: Dispatch = null as unknown as Dispatch; - useSearchMock.mockReturnValue({ state, dispatch, onToggleSection: toggle }); - const dashboardQuery: DashboardQuery = {} as unknown as DashboardQuery; - - const { result } = renderHook(() => useManageDashboards(dashboardQuery, {})); - - return { result }; - } - - describe('when called and only General folder is selected', () => { - it('then canDelete should be false', () => { - const results: DashboardSection[] = [ - { id: 1, checked: false, items: [], title: 'One', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - { - id: GENERAL_FOLDER_ID, - checked: true, - items: [], - title: 'General', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { id: 2, checked: false, items: [], title: 'Two', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - ]; - - const { result } = setupTestContext({ results }); - - expect(result.current.canDelete).toBe(false); - }); - }); - - describe('when called and General folder and another folder are selected', () => { - it('then canDelete should be false', () => { - const results: DashboardSection[] = [ - { - id: 1, - checked: true, - items: [ - { - id: 11, - checked: true, - title: 'Eleven', - type: DashboardSearchItemType.DashDB, - url: '/', - isStarred: false, - tags: [], - uri: '', - }, - ], - title: 'One', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { - id: GENERAL_FOLDER_ID, - checked: true, - items: [ - { - id: 10, - checked: true, - title: 'Ten', - type: DashboardSearchItemType.DashDB, - url: '/', - isStarred: false, - tags: [], - uri: '', - }, - ], - title: 'General', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { id: 2, checked: false, items: [], title: 'Two', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - ]; - - const { result } = setupTestContext({ results }); - - expect(result.current.canDelete).toBe(false); - }); - }); - - describe('when called on an empty General folder that is not selected but another folder is selected', () => { - it('then canDelete should be true', () => { - const results: DashboardSection[] = [ - { - id: 1, - checked: true, - items: [ - { - id: 11, - checked: true, - title: 'Eleven', - type: DashboardSearchItemType.DashDB, - url: '/', - isStarred: false, - tags: [], - uri: '', - }, - ], - title: 'One', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { - id: GENERAL_FOLDER_ID, - checked: false, - items: [], - title: 'General', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { id: 2, checked: false, items: [], title: 'Two', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - ]; - - const { result } = setupTestContext({ results }); - - expect(result.current.canDelete).toBe(true); - }); - }); - - describe('when called on a non empty General folder that is not selected dashboard in General folder is selected', () => { - it('then canDelete should be true', () => { - const results: DashboardSection[] = [ - { - id: GENERAL_FOLDER_ID, - checked: false, - items: [ - { - id: 10, - checked: true, - title: 'Ten', - type: DashboardSearchItemType.DashDB, - url: '/', - isStarred: false, - tags: [], - uri: '', - }, - ], - title: 'General', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - ]; - - const { result } = setupTestContext({ results }); - - expect(result.current.canDelete).toBe(true); - }); - }); - - describe('when called and no folder is selected', () => { - it('then canDelete should be false', () => { - const results: DashboardSection[] = [ - { id: 1, checked: false, items: [], title: 'One', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - { - id: GENERAL_FOLDER_ID, - checked: false, - items: [], - title: 'General', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { id: 2, checked: false, items: [], title: 'Two', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - ]; - - const { result } = setupTestContext({ results }); - - expect(result.current.canDelete).toBe(false); - }); - }); - - describe('when called on an empty folder', () => { - it('then canDelete should be true', () => { - const results: DashboardSection[] = [ - { id: 1, checked: true, items: [], title: 'One', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - { - id: GENERAL_FOLDER_ID, - checked: false, - items: [], - title: 'General', - type: DashboardSearchItemType.DashFolder, - toggle, - url: '/', - }, - { id: 2, checked: false, items: [], title: 'Two', type: DashboardSearchItemType.DashFolder, toggle, url: '/' }, - ]; - - const { result } = setupTestContext({ results }); - - expect(result.current.canDelete).toBe(true); - }); - }); -}); diff --git a/public/app/features/search/hooks/useManageDashboards.ts b/public/app/features/search/hooks/useManageDashboards.ts deleted file mode 100644 index bf55d4ff11f..00000000000 --- a/public/app/features/search/hooks/useManageDashboards.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { useCallback, useMemo, useReducer } from 'react'; -import { useDebounce } from 'react-use'; - -import { reportInteraction } from '@grafana/runtime/src'; -import { contextSrv } from 'app/core/services/context_srv'; -import { FolderDTO } from 'app/types'; - -import { GENERAL_FOLDER_ID } from '../constants'; -import { DELETE_ITEMS, MOVE_ITEMS, TOGGLE_ALL_CHECKED, TOGGLE_CHECKED } from '../reducers/actionTypes'; -import { manageDashboardsReducer, manageDashboardsState, ManageDashboardsState } from '../reducers/manageDashboards'; -import { DashboardQuery, DashboardSection, OnDeleteItems, OnMoveItems, OnToggleChecked, SearchLayout } from '../types'; - -import { useSearch } from './useSearch'; -import { useShowDashboardPreviews } from './useShowDashboardPreviews'; - -const hasChecked = (section: DashboardSection) => { - return section.checked || section.items.some((item) => item.checked); -}; - -export const reportDashboardListViewed = ( - dashboardListType: 'manage_dashboards' | 'dashboard_search', - showPreviews: boolean, - previewsEnabled: boolean, - query: { - layout?: SearchLayout; - starred?: boolean; - sortValue?: string; - query?: string; - tagCount?: number; - } -) => { - const previews = previewsEnabled ? (showPreviews ? 'on' : 'off') : 'feature_disabled'; - reportInteraction(`${dashboardListType}_viewed`, { - previews, - layout: query.layout, - starredFilter: query.starred ?? false, - sort: query.sortValue ?? '', - tagCount: query.tagCount ?? 0, - queryLength: query.query?.length ?? 0, - }); -}; - -export const useManageDashboards = ( - query: DashboardQuery, - state: Partial = {}, - folder?: FolderDTO -) => { - const reducer = useReducer(manageDashboardsReducer, { - ...manageDashboardsState, - ...state, - }); - - const { showPreviews, setShowPreviews, previewFeatureEnabled } = useShowDashboardPreviews(); - useDebounce( - () => { - reportDashboardListViewed('manage_dashboards', showPreviews, previewFeatureEnabled, { - layout: query.layout, - starred: query.starred, - sortValue: query.sort?.value, - query: query.query, - tagCount: query.tag?.length, - }); - }, - 1000, - [ - showPreviews, - previewFeatureEnabled, - query.layout, - query.starred, - query.sort?.value, - query.query?.length, - query.tag?.length, - ] - ); - - const { - state: { results, loading, initialLoading, allChecked }, - onToggleSection, - dispatch, - } = useSearch(query, reducer, {}); - - const onToggleChecked: OnToggleChecked = useCallback( - (item) => { - dispatch({ type: TOGGLE_CHECKED, payload: item }); - }, - [dispatch] - ); - - const onToggleAllChecked = () => { - dispatch({ type: TOGGLE_ALL_CHECKED }); - }; - - const onDeleteItems: OnDeleteItems = (folders, dashboards) => { - dispatch({ type: DELETE_ITEMS, payload: { folders, dashboards } }); - }; - - const onMoveItems: OnMoveItems = (selectedDashboards, folder) => { - dispatch({ type: MOVE_ITEMS, payload: { dashboards: selectedDashboards, folder } }); - }; - - const canMove = useMemo(() => results.some((result) => result.items.some((item) => item.checked)), [results]); - - const canDelete = useMemo(() => { - const somethingChecked = results.some(hasChecked); - const includesGeneralFolder = results.find((result) => result.checked && result.id === GENERAL_FOLDER_ID); - return somethingChecked && !includesGeneralFolder; - }, [results]); - - const canSave = folder?.canSave; - const hasEditPermissionInFolders = folder ? canSave : contextSrv.hasEditPermissionInFolders; - const noFolders = canSave && folder?.id && results.length === 0 && !loading && !initialLoading; - - return { - results, - loading, - initialLoading, - canSave, - allChecked, - hasEditPermissionInFolders, - canMove, - canDelete, - onToggleSection, - onToggleChecked, - onToggleAllChecked, - onDeleteItems, - onMoveItems, - noFolders, - showPreviews, - setShowPreviews, - }; -}; diff --git a/public/app/features/search/hooks/useSearch.ts b/public/app/features/search/hooks/useSearch.ts deleted file mode 100644 index fda96885d65..00000000000 --- a/public/app/features/search/hooks/useSearch.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback, useEffect } from 'react'; -import { useDebounce } from 'react-use'; - -import { backendSrv } from 'app/core/services/backend_srv'; -import { SearchSrv } from 'app/core/services/search_srv'; - -import { FETCH_RESULTS, FETCH_ITEMS, TOGGLE_SECTION, SEARCH_START, FETCH_ITEMS_START } from '../reducers/actionTypes'; -import { DashboardSection, UseSearch } from '../types'; -import { hasId, getParsedQuery } from '../utils'; - -const searchSrv = new SearchSrv(); - -/** - * Base hook for search functionality. - * Returns state and dispatch, among others, from 'reducer' param, so it can be - * further extended. - * @param query - * @param reducer - return result of useReducer - * @param params - custom params - */ -export const useSearch: UseSearch = (query, reducer, params = {}) => { - const { queryParsing } = params; - const [state, dispatch] = reducer; - - const search = () => { - dispatch({ type: SEARCH_START }); - const parsedQuery = getParsedQuery(query, queryParsing); - searchSrv.search(parsedQuery).then((results) => { - dispatch({ type: FETCH_RESULTS, payload: results }); - }); - }; - - // Set loading state before debounced search - useEffect(() => { - dispatch({ type: SEARCH_START }); - }, [query.tag, query.sort, query.starred, query.layout, dispatch]); - - useDebounce(search, 300, [query, queryParsing]); - - const onToggleSection = useCallback( - (section: DashboardSection) => { - if (hasId(section.title) && !section.items.length) { - dispatch({ type: FETCH_ITEMS_START, payload: section.id }); - backendSrv.search({ folderIds: [section.id] }).then((items) => { - dispatch({ type: FETCH_ITEMS, payload: { section, items } }); - dispatch({ type: TOGGLE_SECTION, payload: section }); - }); - } else { - dispatch({ type: TOGGLE_SECTION, payload: section }); - } - }, - [dispatch] - ); - - return { state, dispatch, onToggleSection }; -}; diff --git a/public/app/features/search/hooks/useShowDashboardPreviews.ts b/public/app/features/search/hooks/useShowDashboardPreviews.ts deleted file mode 100644 index 80e50f37e7b..00000000000 --- a/public/app/features/search/hooks/useShowDashboardPreviews.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useLocalStorage } from 'react-use'; - -import { config } from '@grafana/runtime/src'; - -import { PREVIEWS_LOCAL_STORAGE_KEY } from '../constants'; - -export const useShowDashboardPreviews = () => { - const previewFeatureEnabled = Boolean(config.featureToggles.dashboardPreviews); - const [showPreviews, setShowPreviews] = useLocalStorage(PREVIEWS_LOCAL_STORAGE_KEY, false); - - return { showPreviews: Boolean(showPreviews && previewFeatureEnabled), previewFeatureEnabled, setShowPreviews }; -}; diff --git a/public/app/features/search/index.ts b/public/app/features/search/index.ts index 1bdc15141b1..b20121b3e1f 100644 --- a/public/app/features/search/index.ts +++ b/public/app/features/search/index.ts @@ -1,9 +1,2 @@ -export { SearchResults } from './components/SearchResults'; -export { SearchField } from './components/SearchField'; export { SearchItem } from './components/SearchItem'; -export { SearchCheckbox } from './components/SearchCheckbox'; export { SearchWrapper } from './components/SearchWrapper'; -export { SearchResultsFilter } from './components/SearchResultsFilter'; -export { ManageDashboards } from './components/ManageDashboards'; -export { ConfirmDeleteModal } from './components/ConfirmDeleteModal'; -export { MoveToFolderModal } from './components/MoveToFolderModal'; diff --git a/public/app/features/search/reducers/actionTypes.ts b/public/app/features/search/reducers/actionTypes.ts index c843c23f73a..048960c8158 100644 --- a/public/app/features/search/reducers/actionTypes.ts +++ b/public/app/features/search/reducers/actionTypes.ts @@ -1,17 +1,3 @@ -export const FETCH_RESULTS = 'FETCH_RESULTS'; -export const TOGGLE_SECTION = 'TOGGLE_SECTION'; -export const FETCH_ITEMS = 'FETCH_ITEMS'; -export const FETCH_ITEMS_START = 'FETCH_ITEMS_START'; -export const MOVE_SELECTION_UP = 'MOVE_SELECTION_UP'; -export const MOVE_SELECTION_DOWN = 'MOVE_SELECTION_DOWN'; -export const SEARCH_START = 'SEARCH_START'; - -// Manage dashboards -export const TOGGLE_ALL_CHECKED = 'TOGGLE_ALL_CHECKED'; -export const TOGGLE_CHECKED = 'TOGGLE_SECTION_CHECKED'; -export const MOVE_ITEMS = 'MOVE_ITEMS'; -export const DELETE_ITEMS = 'DELETE_ITEMS'; - // Search Query export const TOGGLE_STARRED = 'TOGGLE_STARRED'; export const REMOVE_STARRED = 'REMOVE_STARRED'; diff --git a/public/app/features/search/reducers/dashboardSearch.test.ts b/public/app/features/search/reducers/dashboardSearch.test.ts deleted file mode 100644 index 5f5ebf182ec..00000000000 --- a/public/app/features/search/reducers/dashboardSearch.test.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { searchResults, sections } from '../testData'; - -import { FETCH_ITEMS, FETCH_RESULTS, TOGGLE_SECTION, MOVE_SELECTION_DOWN, MOVE_SELECTION_UP } from './actionTypes'; -import { searchReducer as reducer, dashboardsSearchState } from './dashboardSearch'; - -const defaultState = { selectedIndex: 0, loading: false, results: sections as any[], initialLoading: false }; -describe('Dashboard Search reducer', () => { - it('should return the initial state', () => { - expect(reducer(dashboardsSearchState, {} as any)).toEqual(dashboardsSearchState); - }); - - it('should set the results and mark first item as selected', () => { - const newState = reducer(dashboardsSearchState, { type: FETCH_RESULTS, payload: searchResults }); - expect(newState).toEqual({ loading: false, selectedIndex: 0, results: searchResults, initialLoading: false }); - expect(newState.results[0].selected).toBeTruthy(); - }); - - it('should toggle selected section', () => { - const newState = reducer(defaultState, { type: TOGGLE_SECTION, payload: sections[5] }); - expect(newState.results[5].expanded).toBeFalsy(); - const newState2 = reducer(defaultState, { type: TOGGLE_SECTION, payload: sections[1] }); - expect(newState2.results[1].expanded).toBeTruthy(); - }); - - it('should handle FETCH_ITEMS', () => { - const items = [ - { - id: 4072, - uid: 'OzAIf_rWz', - title: 'New dashboard Copy 3', - type: 'dash-db', - isStarred: false, - }, - { - id: 46, - uid: '8DY63kQZk', - title: 'Stocks', - type: 'dash-db', - isStarred: false, - }, - ]; - const newState = reducer(defaultState, { - type: FETCH_ITEMS, - payload: { - section: sections[2], - items, - }, - }); - expect(newState.results[2].items).toEqual(items); - }); - - it('should handle MOVE_SELECTION_DOWN', () => { - const newState = reducer(defaultState, { - type: MOVE_SELECTION_DOWN, - }); - - expect(newState.selectedIndex).toEqual(1); - expect(newState.results[0].items[0].selected).toBeTruthy(); - - const newState2 = reducer(newState, { - type: MOVE_SELECTION_DOWN, - }); - - expect(newState2.selectedIndex).toEqual(2); - expect(newState2.results[1].selected).toBeTruthy(); - - // Shouldn't go over the visible results length - 1 (9) - const newState3 = reducer( - { ...defaultState, selectedIndex: 9 }, - { - type: MOVE_SELECTION_DOWN, - } - ); - expect(newState3.selectedIndex).toEqual(9); - }); - - it('should handle MOVE_SELECTION_UP', () => { - // shouldn't move beyond 0 - const newState = reducer(defaultState, { - type: MOVE_SELECTION_UP, - }); - - expect(newState.selectedIndex).toEqual(0); - - const newState2 = reducer( - { ...defaultState, selectedIndex: 3 }, - { - type: MOVE_SELECTION_UP, - } - ); - expect(newState2.selectedIndex).toEqual(2); - expect(newState2.results[1].selected).toBeTruthy(); - }); -}); diff --git a/public/app/features/search/reducers/dashboardSearch.ts b/public/app/features/search/reducers/dashboardSearch.ts deleted file mode 100644 index 182b1ba4e5d..00000000000 --- a/public/app/features/search/reducers/dashboardSearch.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { DashboardSection, SearchAction } from '../types'; -import { getFlattenedSections, getLookupField, markSelected } from '../utils'; - -import { - FETCH_ITEMS, - FETCH_RESULTS, - TOGGLE_SECTION, - MOVE_SELECTION_DOWN, - MOVE_SELECTION_UP, - SEARCH_START, - FETCH_ITEMS_START, -} from './actionTypes'; - -export interface DashboardsSearchState { - results: DashboardSection[]; - loading: boolean; - selectedIndex: number; - /** Used for first time page load */ - initialLoading: boolean; -} - -export const dashboardsSearchState: DashboardsSearchState = { - results: [], - loading: true, - initialLoading: true, - selectedIndex: 0, -}; - -export const searchReducer = (state: DashboardsSearchState, action: SearchAction) => { - switch (action.type) { - case SEARCH_START: - if (!state.loading) { - return { ...state, loading: true }; - } - return state; - case FETCH_RESULTS: { - const results = action.payload; - // Highlight the first item ('Starred' folder) - if (results.length > 0) { - results[0].selected = true; - } - return { ...state, results, loading: false, initialLoading: false }; - } - case TOGGLE_SECTION: { - const section = action.payload; - const lookupField = getLookupField(section.title); - return { - ...state, - results: state.results.map((result: DashboardSection) => { - if (section[lookupField] === result[lookupField]) { - return { ...result, expanded: !result.expanded }; - } - return result; - }), - }; - } - case FETCH_ITEMS: { - const { section, items } = action.payload; - return { - ...state, - itemsFetching: false, - results: state.results.map((result: DashboardSection) => { - if (section.id === result.id) { - return { ...result, items, itemsFetching: false }; - } - return result; - }), - }; - } - case FETCH_ITEMS_START: { - const id = action.payload; - if (id) { - return { - ...state, - results: state.results.map((result) => (result.id === id ? { ...result, itemsFetching: true } : result)), - }; - } - return state; - } - case MOVE_SELECTION_DOWN: { - const flatIds = getFlattenedSections(state.results); - if (state.selectedIndex < flatIds.length - 1) { - const newIndex = state.selectedIndex + 1; - const selectedId = flatIds[newIndex]; - return { - ...state, - selectedIndex: newIndex, - results: markSelected(state.results, selectedId), - }; - } - return state; - } - case MOVE_SELECTION_UP: - if (state.selectedIndex > 0) { - const flatIds = getFlattenedSections(state.results); - const newIndex = state.selectedIndex - 1; - const selectedId = flatIds[newIndex]; - return { - ...state, - selectedIndex: newIndex, - results: markSelected(state.results, selectedId), - }; - } - return state; - default: - return state; - } -}; diff --git a/public/app/features/search/reducers/manageDashboards.test.ts b/public/app/features/search/reducers/manageDashboards.test.ts deleted file mode 100644 index a005996cf04..00000000000 --- a/public/app/features/search/reducers/manageDashboards.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { sections } from '../testData'; -import { DashboardSection, UidsToDelete } from '../types'; - -import { TOGGLE_ALL_CHECKED, TOGGLE_CHECKED, DELETE_ITEMS, MOVE_ITEMS } from './actionTypes'; -import { manageDashboardsReducer as reducer, manageDashboardsState as state } from './manageDashboards'; - -// Remove Recent and Starred sections as they're not used in manage dashboards -const results = sections.slice(2); - -describe('Manage dashboards reducer', () => { - it('should return the initial state', () => { - expect(reducer(state, {} as any)).toEqual(state); - }); - - it('should handle TOGGLE_ALL_CHECKED', () => { - const newState = reducer({ ...state, results }, { type: TOGGLE_ALL_CHECKED }); - expect(newState.results.every((result: any) => result.checked === true)).toBe(true); - expect(newState.results.every((result: any) => result.items.every((item: any) => item.checked === true))).toBe( - true - ); - expect(newState.allChecked).toBe(true); - - const newState2 = reducer({ ...newState, results }, { type: TOGGLE_ALL_CHECKED }); - expect(newState2.results.every((result: any) => result.checked === false)).toBe(true); - expect(newState2.results.every((result: any) => result.items.every((item: any) => item.checked === false))).toBe( - true - ); - expect(newState2.allChecked).toBe(false); - }); - - it('should handle TOGGLE_CHECKED sections', () => { - const newState = reducer({ ...state, results }, { type: TOGGLE_CHECKED, payload: results[0] }); - expect(newState.results[0].checked).toBe(true); - expect(newState.results[1].checked).toBeFalsy(); - - const newState2 = reducer(newState, { type: TOGGLE_CHECKED, payload: results[1] }); - expect(newState2.results[0].checked).toBe(true); - expect(newState2.results[1].checked).toBe(true); - }); - - it('should handle TOGGLE_CHECKED items', () => { - const newState = reducer({ ...state, results }, { type: TOGGLE_CHECKED, payload: { id: 4069 } }); - expect(newState.results[3].items[0].checked).toBe(true); - - const newState2 = reducer(newState, { type: TOGGLE_CHECKED, payload: { id: 1 } }); - expect(newState2.results[3].items[0].checked).toBe(true); - expect(newState2.results[3].items[1].checked).toBeFalsy(); - expect(newState2.results[3].items[2].checked).toBe(true); - }); - - it('should handle DELETE_ITEMS', () => { - const toDelete: UidsToDelete = { dashboards: ['OzAIf_rWz', 'lBdLINUWk'], folders: ['search-test-data'] }; - const newState = reducer({ ...state, results }, { type: DELETE_ITEMS, payload: toDelete }); - expect(newState.results).toHaveLength(3); - expect(newState.results[1].id).toEqual(4074); - expect(newState.results[2].items).toHaveLength(1); - expect(newState.results[2].items[0].id).toEqual(4069); - }); - - it('should handle MOVE_ITEMS', () => { - // Move 2 dashboards to a folder with id 2 - const toMove = { - dashboards: [ - { - id: 4072, - uid: 'OzAIf_rWz', - title: 'New dashboard Copy 3', - type: 'dash-db', - isStarred: false, - }, - { - id: 1, - uid: 'lBdLINUWk', - title: 'Prom dash', - type: 'dash-db', - isStarred: true, - }, - ], - folder: { id: 2 }, - }; - const newState = reducer({ ...state, results }, { type: MOVE_ITEMS, payload: toMove }); - expect(newState.results[0].items).toHaveLength(2); - expect(newState.results[0].items[0].uid).toEqual('OzAIf_rWz'); - expect(newState.results[0].items[1].uid).toEqual('lBdLINUWk'); - expect(newState.results[3].items).toHaveLength(1); - expect(newState.results[3].items[0].uid).toEqual('LCFWfl9Zz'); - }); - - it('should not display dashboards in a non-expanded folder', () => { - const general = results.find((res) => res.id === 0); - const toMove = { dashboards: general?.items, folder: { id: 4074 } }; - const newState = reducer({ ...state, results }, { type: MOVE_ITEMS, payload: toMove }); - expect(newState.results.find((res: DashboardSection) => res.id === 4074).items).toHaveLength(0); - expect(newState.results.find((res: DashboardSection) => res.id === 0).items).toHaveLength(0); - }); -}); diff --git a/public/app/features/search/reducers/manageDashboards.ts b/public/app/features/search/reducers/manageDashboards.ts deleted file mode 100644 index ee58d2f422b..00000000000 --- a/public/app/features/search/reducers/manageDashboards.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { DashboardSection, DashboardSectionItem, SearchAction } from '../types'; -import { mergeReducers } from '../utils'; - -import { TOGGLE_ALL_CHECKED, TOGGLE_CHECKED, MOVE_ITEMS, DELETE_ITEMS } from './actionTypes'; -import { dashboardsSearchState, DashboardsSearchState, searchReducer } from './dashboardSearch'; - -export interface ManageDashboardsState extends DashboardsSearchState { - allChecked: boolean; -} - -export const manageDashboardsState: ManageDashboardsState = { - ...dashboardsSearchState, - allChecked: false, -}; - -const reducer = (state: ManageDashboardsState, action: SearchAction) => { - switch (action.type) { - case TOGGLE_ALL_CHECKED: - const newAllChecked = !state.allChecked; - return { - ...state, - results: state.results.map((result) => { - return { - ...result, - checked: newAllChecked, - items: result.items.map((item) => ({ ...item, checked: newAllChecked })), - }; - }), - allChecked: newAllChecked, - }; - case TOGGLE_CHECKED: - const { id } = action.payload; - return { - ...state, - results: state.results.map((result) => { - if (result.id === id) { - return { - ...result, - checked: !result.checked, - items: result.items.map((item) => ({ ...item, checked: !result.checked })), - }; - } - return { - ...result, - items: result.items.map((item) => (item.id === id ? { ...item, checked: !item.checked } : item)), - }; - }), - }; - case MOVE_ITEMS: { - const dashboards: DashboardSectionItem[] = action.payload.dashboards; - const folder: DashboardSection = action.payload.folder; - const uids = dashboards.map((db) => db.uid); - return { - ...state, - results: state.results.map((result) => { - if (folder.id === result.id) { - return result.expanded - ? { - ...result, - items: [...result.items, ...dashboards.map((db) => ({ ...db, checked: false }))], - checked: false, - } - : result; - } else { - return { ...result, items: result.items.filter((item) => !uids.includes(item.uid)) }; - } - }), - }; - } - case DELETE_ITEMS: { - const { folders, dashboards } = action.payload; - if (!folders.length && !dashboards.length) { - return state; - } - return { - ...state, - results: state.results.reduce((filtered, result) => { - if (!folders.includes(result.uid)) { - return [...filtered, { ...result, items: result.items.filter((item) => !dashboards.includes(item.uid)) }]; - } - return filtered; - }, [] as DashboardSection[]), - }; - } - default: - return state; - } -}; - -export const manageDashboardsReducer = mergeReducers([searchReducer, reducer]); diff --git a/public/app/features/search/testData.ts b/public/app/features/search/testData.ts deleted file mode 100644 index bf6f11315e8..00000000000 --- a/public/app/features/search/testData.ts +++ /dev/null @@ -1,361 +0,0 @@ -import { DashboardSearchItemType, DashboardSection, DashboardSectionItem } from './types'; - -function makeSection(sectionPartial: Partial): DashboardSection { - return { - title: 'Default title', - id: Number.MAX_SAFE_INTEGER - 1, - score: -99, - expanded: true, - type: DashboardSearchItemType.DashFolder, - items: [], - url: '/default-url', - ...sectionPartial, - }; -} - -const makeSectionItem = (itemPartial: Partial): DashboardSectionItem => { - return { - id: Number.MAX_SAFE_INTEGER - 2, - uid: 'default-uid', - title: 'Default dashboard title', - type: DashboardSearchItemType.DashDB, - isStarred: false, - tags: [], - uri: 'db/default-slug', - url: '/d/default-uid/default-slug', - ...itemPartial, - }; -}; - -export const generalFolder: DashboardSection = { - id: 0, - title: 'General', - items: [ - { - id: 1, - uid: 'lBdLINUWk', - title: 'Test 1', - uri: 'db/test1', - url: '/d/lBdLINUWk/test1', - type: DashboardSearchItemType.DashDB, - tags: [], - isStarred: false, - checked: true, - }, - { - id: 46, - uid: '8DY63kQZk', - title: 'Test 2', - uri: 'db/test2', - url: '/d/8DY63kQZk/test2', - type: DashboardSearchItemType.DashDB, - tags: [], - isStarred: false, - checked: true, - }, - ], - icon: 'folder-open', - score: 1, - expanded: true, - checked: false, - url: '', - type: DashboardSearchItemType.DashFolder, -}; - -export const searchResults: DashboardSection[] = [ - { - id: 2, - uid: 'JB_zdOUWk', - title: 'gdev dashboards', - expanded: false, - items: [], - url: '/dashboards/f/JB_zdOUWk/gdev-dashboards', - icon: 'folder', - score: 0, - checked: true, - type: DashboardSearchItemType.DashFolder, - }, - generalFolder, -]; - -// Search results with more info -export const sections: DashboardSection[] = [ - makeSection({ - title: 'Starred', - score: -2, - expanded: true, - items: [ - makeSectionItem({ - id: 1, - uid: 'lBdLINUWk', - title: 'Prom dash', - type: DashboardSearchItemType.DashDB, - }), - ], - }), - - makeSection({ - title: 'Recent', - icon: 'clock-o', - score: -1, - expanded: false, - items: [ - makeSectionItem({ - id: 4072, - uid: 'OzAIf_rWz', - title: 'New dashboard Copy 3', - - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 46, - uid: '8DY63kQZk', - title: 'Stocks', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 20, - uid: '7MeksYbmk', - title: 'Alerting with TestData', - type: DashboardSearchItemType.DashDB, - isStarred: false, - folderId: 2, - }), - makeSectionItem({ - id: 4073, - uid: 'j9SHflrWk', - title: 'New dashboard Copy 4', - type: DashboardSearchItemType.DashDB, - isStarred: false, - folderId: 2, - }), - ], - }), - - makeSection({ - id: 2, - uid: 'JB_zdOUWk', - title: 'gdev dashboards', - expanded: true, - url: '/dashboards/f/JB_zdOUWk/gdev-dashboards', - icon: 'folder', - score: 2, - items: [], - }), - - makeSection({ - id: 2568, - uid: 'search-test-data', - title: 'Search test data folder', - expanded: false, - items: [], - url: '/dashboards/f/search-test-data/search-test-data-folder', - icon: 'folder', - score: 3, - }), - - makeSection({ - id: 4074, - uid: 'iN5TFj9Zk', - title: 'Test', - expanded: false, - items: [], - url: '/dashboards/f/iN5TFj9Zk/test', - icon: 'folder', - score: 4, - }), - - makeSection({ - id: 0, - title: 'General', - icon: 'folder-open', - score: 5, - expanded: true, - items: [ - makeSectionItem({ - id: 4069, - uid: 'LCFWfl9Zz', - title: 'New dashboard Copy', - uri: 'db/new-dashboard-copy', - url: '/d/LCFWfl9Zz/new-dashboard-copy', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 4072, - uid: 'OzAIf_rWz', - title: 'New dashboard Copy 3', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 1, - uid: 'lBdLINUWk', - title: 'Prom dash', - type: DashboardSearchItemType.DashDB, - isStarred: true, - }), - ], - }), -]; - -export const checkedGeneralFolder: DashboardSection[] = [ - makeSection({ - id: 4074, - uid: 'other-folder-dash', - title: 'Test', - expanded: false, - type: DashboardSearchItemType.DashFolder, - items: [ - makeSectionItem({ - id: 4072, - uid: 'other-folder-dash-abc', - title: 'New dashboard Copy 3', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 46, - uid: 'other-folder-dash-def', - title: 'Stocks', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - ], - url: '/dashboards/f/iN5TFj9Zk/test', - icon: 'folder', - score: 4, - }), - - makeSection({ - id: 0, - title: 'General', - uid: 'other-folder-abc', - score: 5, - expanded: true, - checked: true, - type: DashboardSearchItemType.DashFolder, - items: [ - makeSectionItem({ - id: 4069, - uid: 'general-abc', - title: 'New dashboard Copy', - uri: 'db/new-dashboard-copy', - url: '/d/LCFWfl9Zz/new-dashboard-copy', - type: DashboardSearchItemType.DashDB, - isStarred: false, - checked: true, - }), - makeSectionItem({ - id: 4072, - uid: 'general-def', - title: 'New dashboard Copy 3', - type: DashboardSearchItemType.DashDB, - isStarred: false, - checked: true, - }), - makeSectionItem({ - id: 1, - uid: 'general-ghi', - title: 'Prom dash', - type: DashboardSearchItemType.DashDB, - isStarred: true, - checked: true, - }), - ], - }), -]; - -export const checkedOtherFolder: DashboardSection[] = [ - makeSection({ - id: 4074, - uid: 'other-folder-abc', - title: 'Test', - expanded: false, - checked: true, - type: DashboardSearchItemType.DashFolder, - items: [ - makeSectionItem({ - id: 4072, - uid: 'other-folder-dash-abc', - title: 'New dashboard Copy 3', - type: DashboardSearchItemType.DashDB, - isStarred: false, - checked: true, - }), - makeSectionItem({ - id: 46, - uid: 'other-folder-dash-def', - title: 'Stocks', - type: DashboardSearchItemType.DashDB, - isStarred: false, - checked: true, - }), - ], - url: '/dashboards/f/iN5TFj9Zk/test', - icon: 'folder', - score: 4, - }), - - makeSection({ - id: 0, - title: 'General', - icon: 'folder-open', - score: 5, - expanded: true, - type: DashboardSearchItemType.DashFolder, - items: [ - makeSectionItem({ - id: 4069, - uid: 'general-abc', - title: 'New dashboard Copy', - uri: 'db/new-dashboard-copy', - url: '/d/LCFWfl9Zz/new-dashboard-copy', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 4072, - uid: 'general-def', - title: 'New dashboard Copy 3', - type: DashboardSearchItemType.DashDB, - isStarred: false, - }), - makeSectionItem({ - id: 1, - uid: 'general-ghi', - title: 'Prom dash', - type: DashboardSearchItemType.DashDB, - isStarred: true, - }), - ], - }), -]; - -export const folderViewAllChecked: DashboardSection[] = [ - makeSection({ - checked: true, - selected: true, - title: '', - items: [ - makeSectionItem({ - id: 4072, - uid: 'other-folder-dash-abc', - title: 'New dashboard Copy 3', - type: DashboardSearchItemType.DashDB, - isStarred: false, - checked: true, - }), - makeSectionItem({ - id: 46, - uid: 'other-folder-dash-def', - title: 'Stocks', - type: DashboardSearchItemType.DashDB, - isStarred: false, - checked: true, - }), - ], - }), -]; diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts index 9f44a6c2cc1..5d16446bb9d 100644 --- a/public/app/features/search/types.ts +++ b/public/app/features/search/types.ts @@ -1,10 +1,7 @@ -import { Dispatch } from 'react'; import { Action } from 'redux'; import { SelectableValue, WithAccessControlMetadata } from '@grafana/data'; -import { FolderInfo } from '../../types'; - export enum DashboardSearchItemType { DashDB = 'dash-db', DashHome = 'dash-home', @@ -56,20 +53,10 @@ export interface DashboardSectionItem { export interface DashboardSearchHit extends DashboardSectionItem, DashboardSection, WithAccessControlMetadata {} -export interface DashboardTag { - term: string; - count: number; -} - export interface SearchAction extends Action { payload?: any; } -export interface UidsToDelete { - folders: string[]; - dashboards: string[]; -} - export interface DashboardQuery { query: string; tag: string[]; @@ -84,22 +71,7 @@ export interface DashboardQuery { layout: SearchLayout; } -export type SearchReducer = [S, Dispatch]; -interface UseSearchParams { - queryParsing?: boolean; - searchCallback?: (folderUid: string | undefined) => any; - folderUid?: string; -} - -export type UseSearch = ( - query: DashboardQuery, - reducer: SearchReducer, - params: UseSearchParams -) => { state: S; dispatch: Dispatch; onToggleSection: (section: DashboardSection) => void }; - export type OnToggleChecked = (item: DashboardSectionItem | DashboardSection) => void; -export type OnDeleteItems = (folders: string[], dashboards: string[]) => void; -export type OnMoveItems = (selectedDashboards: DashboardSectionItem[], folder: FolderInfo | null) => void; export enum SearchLayout { List = 'list', diff --git a/public/app/features/search/utils.test.ts b/public/app/features/search/utils.test.ts index cbae358ab69..209c9450855 100644 --- a/public/app/features/search/utils.test.ts +++ b/public/app/features/search/utils.test.ts @@ -1,168 +1,7 @@ -import { sections, searchResults, checkedGeneralFolder, checkedOtherFolder, folderViewAllChecked } from './testData'; import { SearchQueryParams } from './types'; -import { - findSelected, - getCheckedDashboardsUids, - getCheckedUids, - getFlattenedSections, - markSelected, - mergeReducers, - parseRouteParams, -} from './utils'; +import { parseRouteParams } from './utils'; describe('Search utils', () => { - describe('getFlattenedSections', () => { - it('should return an array of items plus children for expanded items', () => { - const flatSections = getFlattenedSections(sections as any[]); - expect(flatSections).toHaveLength(10); - expect(flatSections).toEqual([ - 'Starred', - 'Starred-1', - 'Recent', - '2', - '2568', - '4074', - '0', - '0-4069', - '0-4072', - '0-1', - ]); - }); - - describe('markSelected', () => { - it('should correctly mark the section item without id as selected', () => { - const results = markSelected(sections as any, 'Recent'); - //@ts-ignore - expect(results[1].selected).toBe(true); - }); - - it('should correctly mark the section item with id as selected', () => { - const results = markSelected(sections as any, '4074'); - //@ts-ignore - expect(results[4].selected).toBe(true); - }); - - it('should mark all other sections as not selected', () => { - const results = markSelected(sections as any, 'Starred'); - const newResults = markSelected(results as any, '0'); - //@ts-ignore - expect(newResults[0].selected).toBeFalsy(); - expect(newResults[5].selected).toBeTruthy(); - }); - - it('should correctly mark an item of a section as selected', () => { - const results = markSelected(sections as any, '0-4072'); - expect(results[5].items[1].selected).toBeTruthy(); - }); - - it('should not mark an item as selected for non-expanded section', () => { - const results = markSelected(sections as any, 'Recent-4072'); - expect(results[1].items[0].selected).toBeFalsy(); - }); - - it('should mark all other items as not selected', () => { - const results = markSelected(sections as any, '0-4069'); - const newResults = markSelected(results as any, '0-1'); - //@ts-ignore - expect(newResults[5].items[0].selected).toBeFalsy(); - expect(newResults[5].items[1].selected).toBeFalsy(); - expect(newResults[5].items[2].selected).toBeTruthy(); - }); - - it('should correctly select one of the same items in different sections', () => { - const results = markSelected(sections as any, 'Starred-1'); - expect(results[0].items[0].selected).toBeTruthy(); - // Same item in diff section - expect(results[5].items[2].selected).toBeFalsy(); - - // Switch order - const newResults = markSelected(sections as any, '0-1'); - expect(newResults[0].items[0].selected).toBeFalsy(); - // Same item in diff section - expect(newResults[5].items[2].selected).toBeTruthy(); - }); - }); - - describe('findSelected', () => { - it('should find selected section', () => { - const results = [...sections, { id: 'Test', selected: true }]; - - const found = findSelected(results); - expect(found?.id).toEqual('Test'); - }); - - it('should find selected item', () => { - const results = [{ expanded: true, id: 'Test', items: [{ id: 1 }, { id: 2, selected: true }, { id: 3 }] }]; - - const found = findSelected(results); - expect(found?.id).toEqual(2); - }); - }); - }); - - describe('mergeReducers', () => { - const reducer1 = (state: any = { reducer1: false }, action: any) => { - if (action.type === 'reducer1') { - return { ...state, reducer1: !state.reducer1 }; - } - return state; - }; - - const reducer2 = (state: any = { reducer2: false }, action: any) => { - if (action.type === 'reducer2') { - return { ...state, reducer2: !state.reducer2 }; - } - return state; - }; - - const mergedReducers = mergeReducers([reducer1, reducer2]); - - it('should merge state from all reducers into one without nesting', () => { - expect(mergedReducers({ reducer1: false }, { type: '' })).toEqual({ reducer1: false }); - }); - - it('should correctly set state from multiple reducers', () => { - const state = { reducer1: false, reducer2: true }; - const newState = mergedReducers(state, { type: 'reducer2' }); - expect(newState).toEqual({ reducer1: false, reducer2: false }); - const newState2 = mergedReducers(newState, { type: 'reducer1' }); - expect(newState2).toEqual({ reducer1: true, reducer2: false }); - }); - }); - - describe('getCheckedUids', () => { - it('should not return any UIDs if no items are checked', () => { - expect(getCheckedUids(sections)).toEqual({ folders: [], dashboards: [] }); - }); - - it('should return only dashboard UIDs if the General folder is checked', () => { - expect(getCheckedUids(checkedGeneralFolder)).toEqual({ - folders: [], - dashboards: ['general-abc', 'general-def', 'general-ghi'], - }); - }); - - it('should return only dashboard UIDs if all items are checked when viewing a folder', () => { - expect(getCheckedUids(folderViewAllChecked)).toEqual({ - folders: [], - dashboards: ['other-folder-dash-abc', 'other-folder-dash-def'], - }); - }); - - it('should return folder + dashboard UIDs when folder is checked in the root view', () => { - expect(getCheckedUids(checkedOtherFolder)).toEqual({ - folders: ['other-folder-abc'], - dashboards: ['other-folder-dash-abc', 'other-folder-dash-def'], - }); - }); - }); - - describe('getCheckedDashboardsUids', () => { - it('should get uids of all checked dashboards', () => { - expect(getCheckedDashboardsUids(searchResults)).toEqual(['lBdLINUWk', '8DY63kQZk']); - }); - }); - describe('parseRouteParams', () => { it('should remove all undefined keys', () => { const params: Partial = { sort: undefined, tag: undefined, query: 'test' }; diff --git a/public/app/features/search/utils.ts b/public/app/features/search/utils.ts index 9d49cca2747..a47674f04ce 100644 --- a/public/app/features/search/utils.ts +++ b/public/app/features/search/utils.ts @@ -1,207 +1,7 @@ -import { parse, SearchParserResult } from 'search-query-parser'; - import { UrlQueryMap } from '@grafana/data'; -import { IconName } from '@grafana/ui'; -import { getDashboardSrv } from '../dashboard/services/DashboardSrv'; - -import { NO_ID_SECTIONS, SECTION_STORAGE_KEY } from './constants'; -import { DashboardQuery, DashboardSection, DashboardSectionItem, SearchAction, UidsToDelete } from './types'; - -/** - * Check if folder has id. Only Recent and Starred folders are the ones without - * ids so far, as they are created manually after results are fetched from API. - * @param str - */ -export const hasId = (str: string) => { - return !NO_ID_SECTIONS.includes(str); -}; - -/** - * Return ids for folders concatenated with their items ids, if section is expanded. - * For items the id format is '{folderId}-{itemId}' to allow mapping them to their folders - * @param sections - */ -export const getFlattenedSections = (sections: DashboardSection[]): string[] => { - return sections.flatMap((section) => { - const id = hasId(section.title) ? String(section.id) : section.title; - - if (section.expanded && section.items.length) { - return [id, ...section.items.map((item) => `${id}-${item.id}`)]; - } - return id; - }); -}; - -/** - * Get all items for currently expanded sections - * @param sections - */ -export const getVisibleItems = (sections: DashboardSection[]) => { - return sections.flatMap((section) => { - if (section.expanded) { - return section.items; - } - return []; - }); -}; -/** - * Since Recent and Starred folders don't have id, title field is used as id - * @param title - title field of the section - */ -export const getLookupField = (title: string) => { - return hasId(title) ? 'id' : 'title'; -}; - -/** - * Go through all the folders and items in expanded folders and toggle their selected - * prop according to currently selected index. Used for item highlighting when navigating - * the search results list using keyboard arrows - * @param sections - * @param selectedId - */ -export const markSelected = (sections: DashboardSection[], selectedId: string) => { - return sections.map((result: DashboardSection) => { - const lookupField = getLookupField(selectedId); - result = { ...result, selected: String(result[lookupField]) === selectedId }; - - if (result.expanded && result.items.length) { - return { - ...result, - items: result.items.map((item) => { - const [sectionId, itemId] = selectedId.split('-'); - const lookup = getLookupField(sectionId); - return { ...item, selected: String(item.id) === itemId && String(result[lookup]) === sectionId }; - }), - }; - } - return result; - }); -}; - -/** - * Find items with property 'selected' set true in a list of folders and their items. - * Does recursive search in the items list. - * @param sections - */ -export const findSelected = (sections: any): DashboardSection | DashboardSectionItem | null => { - let found = null; - for (const section of sections) { - if (section.expanded && section.items.length) { - found = findSelected(section.items); - } - if (section.selected) { - found = section; - } - if (found) { - return found; - } - } - - return null; -}; - -export const parseQuery = (query: string) => { - const parsedQuery = parse(query, { - keywords: ['folder'], - }); - - if (typeof parsedQuery === 'string') { - return { - text: parsedQuery, - } as SearchParserResult; - } - - return parsedQuery; -}; - -/** - * Merge multiple reducers into one, keeping the state structure flat (no nested - * separate state for each reducer). If there are multiple state slices with the same - * key, the latest reducer's state is applied. - * Compared to Redux's combineReducers this allows multiple reducers to operate - * on the same state or different slices of the same state. Useful when multiple - * components have the same structure but different or extra logic when modifying it. - * If reducers have the same action types, the action types from the rightmost reducer - * take precedence - * @param reducers - */ -export const mergeReducers = (reducers: any[]) => (prevState: any, action: SearchAction) => { - return reducers.reduce((nextState, reducer) => ({ ...nextState, ...reducer(nextState, action) }), prevState); -}; - -/** - * Collect all the checked dashboards - * @param sections - */ -export const getCheckedDashboards = (sections: DashboardSection[]): DashboardSectionItem[] => { - if (!sections.length) { - return []; - } - - return sections.reduce((uids, section) => { - return section.items ? [...uids, ...section.items.filter((item) => item.checked)] : uids; - }, [] as DashboardSectionItem[]); -}; - -/** - * Collect uids of all the checked dashboards - * @param sections - */ -export const getCheckedDashboardsUids = (sections: DashboardSection[]) => { - if (!sections.length) { - return []; - } - - return getCheckedDashboards(sections).map((item) => item.uid); -}; - -/** - * Collect uids of all checked folders and dashboards. Used for delete operation, among others - * @param sections - */ -export const getCheckedUids = (sections: DashboardSection[]): UidsToDelete => { - const emptyResults: UidsToDelete = { folders: [], dashboards: [] }; - - if (!sections.length) { - return emptyResults; - } - - return sections.reduce((result, section) => { - if (section?.id !== 0 && section.checked && section.uid) { - return { ...result, folders: [...result.folders, section.uid] } as UidsToDelete; - } else { - return { ...result, dashboards: getCheckedDashboardsUids(sections) } as UidsToDelete; - } - }, emptyResults); -}; - -/** - * When search is done within a dashboard folder, add folder id to the search query - * to narrow down the results to the folder - * @param query - * @param queryParsing - */ -export const getParsedQuery = (query: DashboardQuery, queryParsing = false) => { - const parsedQuery = { ...query, sort: query.sort?.value }; - if (!queryParsing) { - return parsedQuery; - } - - let folderIds: number[] = []; - - if (parseQuery(query.query).folder === 'current') { - try { - const dash = getDashboardSrv().getCurrent(); - if (dash?.meta.folderId) { - folderIds = [dash?.meta.folderId]; - } - } catch (e) { - console.error(e); - } - } - return { ...parsedQuery, query: parseQuery(query.query).text as string, folderIds }; -}; +import { SECTION_STORAGE_KEY } from './constants'; +import { DashboardQuery } from './types'; /** * Check if search query has filters enabled. Excludes folderId @@ -214,18 +14,6 @@ export const hasFilters = (query: DashboardQuery) => { return Boolean(query.query || query.tag?.length > 0 || query.starred || query.sort); }; -/** - * Get section icon depending on expanded state. Currently works for folder icons only - * @param section - */ -export const getSectionIcon = (section: DashboardSection): IconName => { - if (!hasId(section.title)) { - return section.icon as IconName; - } - - return section.expanded ? 'folder-open' : 'folder'; -}; - /** * Get storage key for a dashboard folder by its title * @param title diff --git a/yarn.lock b/yarn.lock index 617dd19823f..f4c53c55342 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21329,7 +21329,6 @@ __metadata: rxjs: 7.5.5 sass: 1.52.3 sass-loader: 13.0.0 - search-query-parser: 1.6.0 selecto: 1.16.2 semver: 7.3.7 sinon: 14.0.0 @@ -33161,13 +33160,6 @@ __metadata: languageName: node linkType: hard -"search-query-parser@npm:1.6.0": - version: 1.6.0 - resolution: "search-query-parser@npm:1.6.0" - checksum: 6ce290e1d1602401ab5e4561f2a9744d1ffd75da5759cc8ead162153258d8470dd86757a3be577772e883d48673dec42b26843fc4aa5155f1ec902112ce92225 - languageName: node - linkType: hard - "secure-compare@npm:3.0.1": version: 3.0.1 resolution: "secure-compare@npm:3.0.1"