diff --git a/.betterer.results b/.betterer.results index 73297502d33..f6447e1ca38 100644 --- a/.betterer.results +++ b/.betterer.results @@ -4564,10 +4564,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "public/app/features/library-panels/components/LibraryPanelsSearch/reducer.test.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] - ], "public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/packages/grafana-ui/src/components/FilterInput/FilterInput.tsx b/packages/grafana-ui/src/components/FilterInput/FilterInput.tsx index 31537f5d131..315a85d7dbf 100644 --- a/packages/grafana-ui/src/components/FilterInput/FilterInput.tsx +++ b/packages/grafana-ui/src/components/FilterInput/FilterInput.tsx @@ -9,10 +9,11 @@ export interface Props extends Omit, 'onChange'> { value: string | undefined; width?: number; onChange: (value: string) => void; + escapeRegex?: boolean; } export const FilterInput = React.forwardRef( - ({ value, width, onChange, ...restProps }, ref) => { + ({ value, width, onChange, escapeRegex = true, ...restProps }, ref) => { const innerRef = React.useRef(null); const combinedRef = useCombinedRefs(ref, innerRef) as React.Ref; @@ -38,8 +39,10 @@ export const FilterInput = React.forwardRef( suffix={suffix} width={width} type="text" - value={value ? unEscapeStringFromRegex(value) : ''} - onChange={(event) => onChange(escapeStringForRegex(event.currentTarget.value))} + value={escapeRegex ? unEscapeStringFromRegex(value ?? '') : value} + onChange={(event) => + onChange(escapeRegex ? escapeStringForRegex(event.currentTarget.value) : event.currentTarget.value) + } {...restProps} ref={combinedRef} /> diff --git a/public/app/core/components/FolderFilter/FolderFilter.tsx b/public/app/core/components/FolderFilter/FolderFilter.tsx index 698311084eb..8e382fd3421 100644 --- a/public/app/core/components/FolderFilter/FolderFilter.tsx +++ b/public/app/core/components/FolderFilter/FolderFilter.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { DashboardSearchHit } from 'app/features/search/types'; +import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types'; import { FolderInfo, PermissionLevelString } from 'app/types'; export interface FolderFilterProps { @@ -13,34 +13,21 @@ export interface FolderFilterProps { maxMenuHeight?: number; } -export function FolderFilter({ onChange: propsOnChange, maxMenuHeight }: FolderFilterProps): JSX.Element { +export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JSX.Element { const styles = useStyles2(getStyles); const [loading, setLoading] = useState(false); const getOptions = useCallback((searchString: string) => getFoldersAsOptions(searchString, setLoading), []); const debouncedLoadOptions = useMemo(() => debounce(getOptions, 300), [getOptions]); + const [value, setValue] = useState>>([]); - const onChange = useCallback( + const onSelectOptionChange = useCallback( (folders: Array>) => { - const changedFolders = []; - for (const folder of folders) { - if (folder.value) { - changedFolders.push(folder.value); - } - } - propsOnChange(changedFolders); + const changedFolderIds = folders.filter((f) => Boolean(f.value)).map((f) => f.value!); + onChange(changedFolderIds); setValue(folders); }, - [propsOnChange] + [onChange] ); - const selectOptions = { - defaultOptions: true, - isMulti: true, - noOptionsMessage: 'No folders found', - placeholder: 'Filter by folder', - maxMenuHeight, - value, - onChange, - }; return (
@@ -57,28 +44,35 @@ export function FolderFilter({ onChange: propsOnChange, maxMenuHeight }: FolderF )} } aria-label="Folder filter" + defaultOptions />
); } -async function getFoldersAsOptions(searchString: string, setLoading: (loading: boolean) => void) { +async function getFoldersAsOptions( + searchString: string, + setLoading: (loading: boolean) => void +): Promise>> { setLoading(true); const params = { query: searchString, - type: 'dash-folder', + type: DashboardSearchItemType.DashFolder, permission: PermissionLevelString.View, }; // FIXME: stop using id from search and use UID instead - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const searchHits = (await getBackendSrv().search(params)) as DashboardSearchHit[]; + const searchHits: DashboardSearchHit[] = await getBackendSrv().search(params); const options = searchHits.map((d) => ({ label: d.title, value: { id: d.id, title: d.title } })); if (!searchString || 'general'.includes(searchString.toLowerCase())) { options.unshift({ label: 'General', value: { id: 0, title: 'General' } }); diff --git a/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx b/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx index 0578fe8437a..14cc84f8b76 100644 --- a/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx +++ b/public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx @@ -11,9 +11,7 @@ export interface Props { } export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Props): JSX.Element => { - const plugins = useMemo(() => { - return getAllPanelPluginMeta(); - }, []); + const plugins = useMemo(() => getAllPanelPluginMeta(), []); const options = useMemo( () => plugins @@ -24,12 +22,7 @@ export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Prop const [value, setValue] = useState>>([]); const onChange = useCallback( (plugins: Array>) => { - const changedPlugins = []; - for (const plugin of plugins) { - if (plugin.value) { - changedPlugins.push(plugin.value); - } - } + const changedPlugins = plugins.filter((p) => p.value).map((p) => p.value!); propsOnChange(changedPlugins); setValue(plugins); }, diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx index fcd6a5a8086..d77cc42c49d 100644 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx +++ b/public/app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch.tsx @@ -1,26 +1,18 @@ import { css } from '@emotion/css'; -import React, { useReducer } from 'react'; +import React, { useCallback, useState } from 'react'; +import { useDebounce } from 'react-use'; import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data'; -import { HorizontalGroup, useStyles2, VerticalGroup, FilterInput } from '@grafana/ui'; +import { useStyles2, VerticalGroup, FilterInput } from '@grafana/ui'; +import { FolderInfo } from 'app/types'; import { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter'; import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter'; import { SortPicker } from '../../../../core/components/Select/SortPicker'; import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants'; -import { FolderInfo } from '../../../../types'; import { LibraryElementDTO } from '../../types'; import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView'; -import { - folderFilterChanged, - initialLibraryPanelsSearchState, - libraryPanelsSearchReducer, - panelFilterChanged, - searchChanged, - sortChanged, -} from './reducer'; - export enum LibraryPanelsSearchVariant { Tight = 'tight', Spacious = 'spacious', @@ -49,78 +41,51 @@ export const LibraryPanelsSearch = ({ showSort = false, showSecondaryActions = false, }: LibraryPanelsSearchProps): JSX.Element => { - const styles = useStyles2(getStyles); - const [{ sortDirection, panelFilter, folderFilter, searchQuery }, dispatch] = useReducer(libraryPanelsSearchReducer, { - ...initialLibraryPanelsSearchState, - folderFilter: currentFolderId ? [currentFolderId.toString(10)] : [], - }); - const onFilterChange = (searchString: string) => dispatch(searchChanged(searchString)); - const onSortChange = (sorting: SelectableValue) => dispatch(sortChanged(sorting)); - const onFolderFilterChange = (folders: FolderInfo[]) => dispatch(folderFilterChanged(folders)); - const onPanelFilterChange = (plugins: PanelPluginMeta[]) => dispatch(panelFilterChanged(plugins)); + const styles = useStyles2(useCallback((theme) => getStyles(theme, variant), [variant])); - if (variant === LibraryPanelsSearchVariant.Spacious) { - return ( -
- - -
- - {showSort && ( - - )} - - {showFolderFilter && } - {showPanelFilter && } - - -
-
- -
-
-
- ); - } + const [searchQuery, setSearchQuery] = useState(''); + const [debouncedSearchQuery, setDebouncedSearchQuery] = useState(''); + useDebounce(() => setDebouncedSearchQuery(searchQuery), 200, [searchQuery]); + + const [sortDirection, setSortDirection] = useState>({}); + const [folderFilter, setFolderFilter] = useState(currentFolderId ? [String(currentFolderId)] : []); + const [panelFilter, setPanelFilter] = useState([]); + + const sortOrFiltersVisible = showSort || showPanelFilter || showFolderFilter; + const verticalGroupSpacing = variant === LibraryPanelsSearchVariant.Tight ? 'lg' : 'xs'; return (
- -
-
- -
-
- {showSort && } - {showFolderFilter && } - {showPanelFilter && } + +
+
+
+ {sortOrFiltersVisible && ( + + )}
+
void; + onFolderFilterChange: (folder: string[]) => void; + onPanelFilterChange: (plugins: string[]) => void; + variant?: LibraryPanelsSearchVariant; +} + +const SearchControls = React.memo( + ({ + variant = LibraryPanelsSearchVariant.Spacious, + showSort, + showPanelFilter, + showFolderFilter, + sortDirection, + onSortChange, + onFolderFilterChange, + onPanelFilterChange, + }: SearchControlsProps) => { + const styles = useStyles2(useCallback((theme) => getRowStyles(theme, variant), [variant])); + const panelFilterChanged = useCallback( + (plugins: PanelPluginMeta[]) => onPanelFilterChange(plugins.map((p) => p.id)), + [onPanelFilterChange] + ); + const folderFilterChanged = useCallback( + (folders: FolderInfo[]) => onFolderFilterChange(folders.map((f) => String(f.id))), + [onFolderFilterChange] + ); + + return ( +
+ {showSort && } + {(showFolderFilter || showPanelFilter) && ( +
+ {showFolderFilter && } + {showPanelFilter && } +
+ )} +
+ ); + } +); +SearchControls.displayName = 'SearchControls'; + +function getRowStyles(theme: GrafanaTheme2, variant = LibraryPanelsSearchVariant.Spacious) { + const searchRowContainer = css` + display: flex; + gap: ${theme.spacing(1)}; + flex-grow: 1; + flex-direction: row; + justify-content: end; + `; + const searchRowContainerTight = css` + ${searchRowContainer}; + flex-grow: initial; + flex-direction: column; + justify-content: normal; + `; + const filterContainer = css` + display: flex; + flex-direction: row; + margin-left: auto; + gap: 4px; + `; + const filterContainerTight = css` + ${filterContainer}; + flex-direction: column; + margin-left: initial; + `; + + switch (variant) { + case LibraryPanelsSearchVariant.Spacious: + return { + container: searchRowContainer, + filterContainer: filterContainer, + }; + case LibraryPanelsSearchVariant.Tight: + return { + container: searchRowContainerTight, + filterContainer: filterContainerTight, + }; + } +} diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/reducer.test.ts b/public/app/features/library-panels/components/LibraryPanelsSearch/reducer.test.ts deleted file mode 100644 index b6d9f5cb989..00000000000 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/reducer.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { reducerTester } from '../../../../../test/core/redux/reducerTester'; - -import { - folderFilterChanged, - initialLibraryPanelsSearchState, - libraryPanelsSearchReducer, - LibraryPanelsSearchState, - panelFilterChanged, - searchChanged, - sortChanged, -} from './reducer'; - -describe('libraryPanelsSearchReducer', () => { - describe('when searchChanged is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(libraryPanelsSearchReducer, { - ...initialLibraryPanelsSearchState, - }) - .whenActionIsDispatched(searchChanged('searching for')) - .thenStateShouldEqual({ - ...initialLibraryPanelsSearchState, - searchQuery: 'searching for', - }); - }); - }); - - describe('when sortChanged is dispatched', () => { - it('then state should be correct', () => { - reducerTester() - .givenReducer(libraryPanelsSearchReducer, { - ...initialLibraryPanelsSearchState, - }) - .whenActionIsDispatched(sortChanged({ label: 'Ascending', value: 'asc' })) - .thenStateShouldEqual({ - ...initialLibraryPanelsSearchState, - sortDirection: 'asc', - }); - }); - }); - - describe('when panelFilterChanged is dispatched', () => { - it('then state should be correct', () => { - const plugins: any = [ - { id: 'graph', name: 'Graph' }, - { id: 'timeseries', name: 'Time Series' }, - ]; - reducerTester() - .givenReducer(libraryPanelsSearchReducer, { - ...initialLibraryPanelsSearchState, - }) - .whenActionIsDispatched(panelFilterChanged(plugins)) - .thenStateShouldEqual({ - ...initialLibraryPanelsSearchState, - panelFilter: ['graph', 'timeseries'], - }); - }); - }); - - describe('when folderFilterChanged is dispatched', () => { - it('then state should be correct', () => { - const folders: any = [ - { id: 0, name: 'General' }, - { id: 1, name: 'Folder' }, - ]; - reducerTester() - .givenReducer(libraryPanelsSearchReducer, { - ...initialLibraryPanelsSearchState, - }) - .whenActionIsDispatched(folderFilterChanged(folders)) - .thenStateShouldEqual({ - ...initialLibraryPanelsSearchState, - folderFilter: ['0', '1'], - }); - }); - }); -}); diff --git a/public/app/features/library-panels/components/LibraryPanelsSearch/reducer.ts b/public/app/features/library-panels/components/LibraryPanelsSearch/reducer.ts deleted file mode 100644 index 0e009bb76d8..00000000000 --- a/public/app/features/library-panels/components/LibraryPanelsSearch/reducer.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createAction } from '@reduxjs/toolkit'; -import { AnyAction } from 'redux'; - -import { PanelPluginMeta, SelectableValue } from '@grafana/data'; - -import { FolderInfo } from '../../../../types'; - -export interface LibraryPanelsSearchState { - searchQuery: string; - sortDirection?: string; - panelFilter: string[]; - folderFilter: string[]; -} - -export const initialLibraryPanelsSearchState: LibraryPanelsSearchState = { - searchQuery: '', - panelFilter: [], - folderFilter: [], - sortDirection: undefined, -}; - -export const searchChanged = createAction('libraryPanels/search/searchChanged'); -export const sortChanged = createAction>('libraryPanels/search/sortChanged'); -export const panelFilterChanged = createAction('libraryPanels/search/panelFilterChanged'); -export const folderFilterChanged = createAction('libraryPanels/search/folderFilterChanged'); - -export const libraryPanelsSearchReducer = (state: LibraryPanelsSearchState, action: AnyAction) => { - if (searchChanged.match(action)) { - return { ...state, searchQuery: action.payload }; - } - - if (sortChanged.match(action)) { - return { ...state, sortDirection: action.payload.value }; - } - - if (panelFilterChanged.match(action)) { - return { ...state, panelFilter: action.payload.map((p) => p.id) }; - } - - if (folderFilterChanged.match(action)) { - return { ...state, folderFilter: action.payload.map((f) => String(f.id!)) }; - } - - return state; -}; diff --git a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx index facd4dc0a28..3fb0c327952 100644 --- a/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx +++ b/public/app/features/library-panels/components/PanelLibraryOptionsGroup/PanelLibraryOptionsGroup.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; import React, { FC, useCallback, useState } from 'react'; -import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data'; -import { Button, useStyles2, VerticalGroup } from '@grafana/ui'; +import { PanelPluginMeta } from '@grafana/data'; +import { Button, VerticalGroup } from '@grafana/ui'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { PanelModel } from 'app/features/dashboard/state'; import { changeToLibraryPanel } from 'app/features/panel/state/actions'; @@ -20,7 +20,6 @@ interface Props { } export const PanelLibraryOptionsGroup: FC = ({ panel, searchQuery }) => { - const styles = useStyles2(getStyles); const [showingAddPanelModal, setShowingAddPanelModal] = useState(false); const [changeToPanel, setChangeToPanel] = useState(undefined); const [panelFilter, setPanelFilter] = useState([]); @@ -39,21 +38,11 @@ export const PanelLibraryOptionsGroup: FC = ({ panel, searchQuery }) => { } setChangeToPanel(undefined); - dispatch(changeToLibraryPanel(panel, changeToPanel)); }; - const onAddToPanelLibrary = () => { - setShowingAddPanelModal(true); - }; - - const onChangeLibraryPanel = (panel: LibraryElementDTO) => { - setChangeToPanel(panel); - }; - - const onDismissChangeToPanel = () => { - setChangeToPanel(undefined); - }; + const onAddToPanelLibrary = () => setShowingAddPanelModal(true); + const onDismissChangeToPanel = () => setChangeToPanel(undefined); return ( @@ -72,7 +61,7 @@ export const PanelLibraryOptionsGroup: FC = ({ panel, searchQuery }) => { currentPanelId={panel.libraryPanel?.uid} searchString={searchQuery} panelFilter={panelFilter} - onClickCard={onChangeLibraryPanel} + onClickCard={setChangeToPanel} showSecondaryActions />
@@ -93,10 +82,8 @@ export const PanelLibraryOptionsGroup: FC = ({ panel, searchQuery }) => { ); }; -const getStyles = (theme: GrafanaTheme2) => { - return { - libraryPanelsView: css` - width: 100%; - `, - }; +const styles = { + libraryPanelsView: css` + width: 100%; + `, };