From 9796b6b0005c53f70eed6f26a7bf2c2212072019 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Wed, 26 Apr 2023 15:42:25 +0100 Subject: [PATCH] NestedFolders: Connect Search input fields to state manager (#67193) * NestedFolders: Connect Search input fields to state manager * Fix tag list not loading * Clear includePanels checkbox when leaving search * fix test * Fix extra right margin * fix missing style * cleanup * fix placeholder * fix test --- .../BrowseDashboardsPage.test.tsx | 2 +- .../BrowseDashboardsPage.tsx | 38 ++++++++++++------ .../components/BrowseFilters.tsx | 40 +++++++++---------- .../components/SearchView.tsx | 22 +++++----- .../search/components/ManageDashboardsNew.tsx | 8 +--- .../search/page/components/ActionRow.tsx | 37 ++++++++--------- .../search/page/components/ManageActions.tsx | 20 +++++----- .../search/page/components/SearchView.tsx | 2 +- .../search/state/SearchStateManager.ts | 22 ++++++++-- public/app/features/search/tempI18nPhrases.ts | 10 +++++ public/locales/en-US/grafana.json | 4 +- public/locales/fr-FR/grafana.json | 10 ++--- public/locales/pseudo-LOCALE/grafana.json | 4 +- 13 files changed, 121 insertions(+), 98 deletions(-) create mode 100644 public/app/features/search/tempI18nPhrases.ts diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 29f63d4ea30..78d416cae1c 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -61,7 +61,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { it('displays a search input', async () => { render(); - expect(await screen.findByPlaceholderText('Search box')).toBeInTheDocument(); + expect(await screen.findByPlaceholderText('Search for dashboards and folders')).toBeInTheDocument(); }); it('displays the filters and hides the actions initially', async () => { diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx index 34c13125aa3..336e109215f 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx @@ -1,15 +1,15 @@ import { css } from '@emotion/css'; -import React, { memo, useMemo } from 'react'; +import React, { memo, useEffect, useMemo } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { locationSearchToObject } from '@grafana/runtime'; -import { Input, useStyles2 } from '@grafana/ui'; +import { FilterInput, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { buildNavModel } from '../folders/state/navModel'; -import { parseRouteParams } from '../search/utils'; +import { useSearchStateManager } from '../search/state/SearchStateManager'; +import { getSearchPlaceholder } from '../search/tempI18nPhrases'; import { skipToken, useGetFolderQuery } from './api/browseDashboardsAPI'; import { BrowseActions } from './components/BrowseActions/BrowseActions'; @@ -27,13 +27,22 @@ export interface Props extends GrafanaRouteComponentProps { - const styles = useStyles2(getStyles); +const BrowseDashboardsPage = memo(({ match }: Props) => { const { uid: folderUID } = match.params; - const searchState = useMemo(() => { - return parseRouteParams(locationSearchToObject(location.search)); - }, [location.search]); + const styles = useStyles2(getStyles); + const [searchState, stateManager] = useSearchStateManager(); + const isSearching = stateManager.hasSearchFilters(); + + useEffect(() => stateManager.initStateFromUrl(folderUID), [folderUID, stateManager]); + + useEffect(() => { + // Clear the search results when we leave SearchView to prevent old results flashing + // when starting a new search + if (!isSearching && searchState.result) { + stateManager.setState({ result: undefined, includePanels: undefined }); + } + }, [isSearching, searchState.result, stateManager]); const { data: folderDTO } = useGetFolderQuery(folderUID ?? skipToken); const navModel = useMemo(() => (folderDTO ? buildNavModel(folderDTO) : undefined), [folderDTO]); @@ -42,15 +51,20 @@ const BrowseDashboardsPage = memo(({ match, location }: Props) => { return ( - + stateManager.onQueryChange(e)} + /> {hasSelection ? : }
{({ width, height }) => - searchState.query ? ( - + isSearching ? ( + ) : ( ) diff --git a/public/app/features/browse-dashboards/components/BrowseFilters.tsx b/public/app/features/browse-dashboards/components/BrowseFilters.tsx index 36fef5bc4fd..34fec506496 100644 --- a/public/app/features/browse-dashboards/components/BrowseFilters.tsx +++ b/public/app/features/browse-dashboards/components/BrowseFilters.tsx @@ -1,33 +1,29 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { ActionRow } from 'app/features/search/page/components/ActionRow'; -import { SearchLayout } from 'app/features/search/types'; +import { getGrafanaSearcher } from 'app/features/search/service'; +import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; export function BrowseFilters() { - const fakeState = useMemo(() => { - return { - query: '', - tag: [], - starred: false, - layout: SearchLayout.Folders, - eventTrackingNamespace: 'manage_dashboards' as const, - }; - }, []); + const [searchState, stateManager] = useSearchStateManager(); return (
Promise.resolve([])} - getSortOptions={() => Promise.resolve([])} - onLayoutChange={() => {}} - onSortChange={() => {}} - onStarredFilterChange={() => {}} - onTagFilterChange={() => {}} - onDatasourceChange={() => {}} - onPanelTypeChange={() => {}} - onSetIncludePanels={() => {}} + hideLayout + showStarredFilter + state={searchState} + getTagOptions={stateManager.getTagOptions} + getSortOptions={getGrafanaSearcher().getSortOptions} + sortPlaceholder={getGrafanaSearcher().sortPlaceholder} + includePanels={searchState.includePanels ?? false} + onLayoutChange={stateManager.onLayoutChange} + onStarredFilterChange={stateManager.onStarredFilterChange} + onSortChange={stateManager.onSortChange} + onTagFilterChange={stateManager.onTagFilterChange} + onDatasourceChange={stateManager.onDatasourceChange} + onPanelTypeChange={stateManager.onPanelTypeChange} + onSetIncludePanels={stateManager.onSetIncludePanels} />
); diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx index 481fc7926b0..0af421731f4 100644 --- a/public/app/features/browse-dashboards/components/SearchView.tsx +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -1,9 +1,9 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback } from 'react'; import { Spinner } from '@grafana/ui'; import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection'; import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable'; -import { getSearchStateManager } from 'app/features/search/state/SearchStateManager'; +import { useSearchStateManager } from 'app/features/search/state/SearchStateManager'; import { DashboardViewItemKind } from 'app/features/search/types'; import { useDispatch, useSelector } from 'app/types'; @@ -12,20 +12,16 @@ import { setItemSelectionState } from '../state'; interface SearchViewProps { height: number; width: number; - folderUID: string | undefined; } -export function SearchView({ folderUID, width, height }: SearchViewProps) { +export function SearchView({ width, height }: SearchViewProps) { const dispatch = useDispatch(); const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems); const { keyboardEvents } = useKeyNavigationListener(); + const [searchState, stateManager] = useSearchStateManager(); - const stateManager = getSearchStateManager(); - useEffect(() => stateManager.initStateFromUrl(folderUID), [folderUID, stateManager]); - - const state = stateManager.useState(); - const value = state.result; + const value = searchState.result; const selectionChecker = useCallback( (kind: string | undefined, uid: string): boolean => { @@ -55,12 +51,16 @@ export function SearchView({ folderUID, width, height }: SearchViewProps) { if (!value) { return ( -
+
); } + if (value.totalRows === 0) { + return
No search results
; + } + const props: SearchResultsProps = { response: value, selection: selectionChecker, @@ -70,7 +70,7 @@ export function SearchView({ folderUID, width, height }: SearchViewProps) { height: height, onTagSelected: stateManager.onAddTag, keyboardEvents, - onDatasourceChange: state.datasource ? stateManager.onDatasourceChange : undefined, + onDatasourceChange: searchState.datasource ? stateManager.onDatasourceChange : undefined, onClickItem: stateManager.onSearchItemClicked, }; diff --git a/public/app/features/search/components/ManageDashboardsNew.tsx b/public/app/features/search/components/ManageDashboardsNew.tsx index bca54ec5a8e..a2065e5c953 100644 --- a/public/app/features/search/components/ManageDashboardsNew.tsx +++ b/public/app/features/search/components/ManageDashboardsNew.tsx @@ -3,13 +3,13 @@ import React, { useEffect } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2, FilterInput } from '@grafana/ui'; -import { t } from 'app/core/internationalization'; import { contextSrv } from 'app/core/services/context_srv'; import { FolderDTO, AccessControlAction } from 'app/types'; import { useKeyNavigationListener } from '../hooks/useSearchKeyboardSelection'; import { SearchView } from '../page/components/SearchView'; import { getSearchStateManager } from '../state/SearchStateManager'; +import { getSearchPlaceholder } from '../tempI18nPhrases'; import { DashboardActions } from './DashboardActions'; @@ -50,11 +50,7 @@ export const ManageDashboardsNew = React.memo(({ folder }: Props) => { // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus spellCheck={false} - placeholder={ - state.includePanels - ? t('search.search-input.include-panels-placeholder', 'Search for dashboards and panels') - : t('search.search-input.placeholder', 'Search for dashboards') - } + placeholder={getSearchPlaceholder(state.includePanels)} escapeRegex={false} className={styles.searchInput} /> diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index f9881c9351f..57fcc7a4b0b 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -105,25 +105,23 @@ export const ActionRow = ({ )} -
- - {!hideLayout && ( - - )} - onSortChange(change?.value)} - value={state.sort} - getSortOptions={getSortOptions} - placeholder={sortPlaceholder || t('search.actions.sort-placeholder', 'Sort')} - isClearable + + {!hideLayout && ( + - -
+ )} + onSortChange(change?.value)} + value={state.sort} + getSortOptions={getSortOptions} + placeholder={sortPlaceholder || t('search.actions.sort-placeholder', 'Sort')} + isClearable + /> +
); }; @@ -143,9 +141,6 @@ export const getStyles = (theme: GrafanaTheme2) => { width: 100%; } `, - rowContainer: css` - margin-right: ${theme.v1.spacing.md}; - `, checkboxWrapper: css` label { line-height: 1.2; diff --git a/public/app/features/search/page/components/ManageActions.tsx b/public/app/features/search/page/components/ManageActions.tsx index dd8dbe90a2c..563343d0d30 100644 --- a/public/app/features/search/page/components/ManageActions.tsx +++ b/public/app/features/search/page/components/ManageActions.tsx @@ -43,17 +43,15 @@ export function ManageActions({ items, folder, onChange, clearSelection }: Props return (
-
- - - - - -
+ + + + + {isDeleteModalOpen && ( setIsDeleteModalOpen(false)} /> diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index 71ea47e1e54..b1f65393187 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -115,7 +115,7 @@ export const SearchView = ({ showManage, folderDTO, hidePseudoFolders, keyboardE } return ( -
+
{({ width, height }) => { const props: SearchResultsProps = { diff --git a/public/app/features/search/state/SearchStateManager.ts b/public/app/features/search/state/SearchStateManager.ts index f4c77b248d3..47a57ade1ed 100644 --- a/public/app/features/search/state/SearchStateManager.ts +++ b/public/app/features/search/state/SearchStateManager.ts @@ -40,7 +40,7 @@ export class SearchStateManager extends StateManagerBase { doSearchWithDebounce = debounce(() => this.doSearch(), 300); lastQuery?: SearchQuery; - initStateFromUrl(folderUid?: string) { + initStateFromUrl(folderUid?: string, doInitialSearch = true) { const stateFromUrl = parseRouteParams(locationService.getSearchObject()); // Force list view when conditions are specified from the URL @@ -54,8 +54,11 @@ export class SearchStateManager extends StateManagerBase { eventTrackingNamespace: folderUid ? 'manage_dashboards' : 'dashboard_search', }); - this.doSearch(); + if (doInitialSearch && this.hasSearchFilters()) { + this.doSearch(); + } } + /** * Updates internal and url state, then triggers a new search */ @@ -162,7 +165,7 @@ export class SearchStateManager extends StateManagerBase { }; hasSearchFilters() { - return this.state.query || this.state.tag.length || this.state.starred || this.state.panel_type; + return this.state.query || this.state.tag.length || this.state.starred || this.state.panel_type || this.state.sort; } getSearchQuery() { @@ -244,7 +247,11 @@ export class SearchStateManager extends StateManagerBase { // This gets the possible tags from within the query results getTagOptions = (): Promise => { - return getGrafanaSearcher().tags(this.lastQuery!); + const query = this.lastQuery ?? { + kind: ['dashboard', 'folder'], + query: '*', + }; + return getGrafanaSearcher().tags(query); }; /** @@ -299,3 +306,10 @@ export function getSearchStateManager() { return stateManager; } + +export function useSearchStateManager() { + const stateManager = getSearchStateManager(); + const state = stateManager.useState(); + + return [state, stateManager] as const; +} diff --git a/public/app/features/search/tempI18nPhrases.ts b/public/app/features/search/tempI18nPhrases.ts new file mode 100644 index 00000000000..07c2abbf0ef --- /dev/null +++ b/public/app/features/search/tempI18nPhrases.ts @@ -0,0 +1,10 @@ +// Temporary place to collect phrases we reuse between new and old browse/search +// TODO: remove this when new Browse Dashboards UI is no longer feature flagged + +import { t } from 'app/core/internationalization'; + +export function getSearchPlaceholder(includePanels = false) { + return includePanels + ? t('search.search-input.include-panels-placeholder', 'Search for dashboards, folders, and panels') + : t('search.search-input.placeholder', 'Search for dashboards and folders'); +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index bd6610270a1..b8fc661ec47 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -439,8 +439,8 @@ "type-header": "Type" }, "search-input": { - "include-panels-placeholder": "Search for dashboards and panels", - "placeholder": "Search for dashboards" + "include-panels-placeholder": "Search for dashboards, folders, and panels", + "placeholder": "Search for dashboards and folders" } }, "share-modal": { diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 5172e23636e..51b64acea2a 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -55,7 +55,7 @@ "query-tab": "Requête", "stats-tab": "Statistiques", "subtitle": "{{queryCount}} requêtes avec un délai total de requête de {{formatted}}", - "title": "Inspecter : {{panelTitle}}" + "title": "Inspecter\u00a0: {{panelTitle}}" }, "inspect-data": { "data-options": "Options de données", @@ -85,7 +85,7 @@ "panel-json-description": "Le modèle enregistré dans le tableau de bord JSON qui configure comment tout fonctionne.", "panel-json-label": "Panneau JSON", "select-source": "Sélectionner la source", - "unknown": "Objet inconnu : {{show}}" + "unknown": "Objet inconnu\u00a0: {{show}}" }, "inspect-meta": { "no-inspector": "Pas d'inspecteur de métadonnées" @@ -118,7 +118,7 @@ "contact-admin": "Veuillez contacter votre administrateur pour configurer les sources de données.", "explanation": "Pour visualiser vos données, vous devrez d’abord les connecter.", "new-dashboard": "Nouveau tableau de bord", - "preferred": "Connectez votre source de données préférée :", + "preferred": "Connectez votre source de données préférée\u00a0:", "sampleData": "Ou établissez un nouveau tableau de bord avec des exemples de données", "viewAll": "Afficher tout", "welcome": "Bienvenue aux tableaux de bord Grafana !" @@ -148,7 +148,7 @@ }, "library-panels": { "save": { - "error": "Erreur lors de l'enregistrement du panneau de bibliothèque : \"{{errorMsg}}\"", + "error": "Erreur lors de l'enregistrement du panneau de bibliothèque\u00a0: \"{{errorMsg}}\"", "success": "Panneau de bibliothèque enregistré" } }, @@ -495,7 +495,7 @@ "info-text-1": "Un instantané est un moyen instantané de partager publiquement un tableau de bord interactif. Lors de la création, nous supprimons les données sensibles telles que les requêtes (métrique, modèle et annotation) et les liens du panneau, pour ne laisser que les métriques visibles et les noms de séries intégrés dans votre tableau de bord.", "info-text-2": "N'oubliez pas que votre instantané <1>peut être consulté par une personne qui dispose du lien et qui peut accéder à l'URL. Partagez judicieusement.", "local-button": "Instantané local", - "mistake-message": "Avez-vous commis une erreur ? ", + "mistake-message": "Avez-vous commis une erreur\u00a0? ", "name": "Nom de l'instantané", "timeout": "Délai d’expiration (secondes)", "timeout-description": "Vous devrez peut-être configurer la valeur du délai d'expiration si la collecte des métriques de votre tableau de bord prend beaucoup de temps.", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index d1305b90833..6bdd1c43a93 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -439,8 +439,8 @@ "type-header": "Ŧypę" }, "search-input": { - "include-panels-placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş äʼnđ päʼnęľş", - "placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş" + "include-panels-placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş, ƒőľđęřş, äʼnđ päʼnęľş", + "placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş äʼnđ ƒőľđęřş" } }, "share-modal": {