mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Search: support URL query params (#25541)
* Search: connect DashboardSearch * Search: set url params * Search: handle tag params * Search: handle sort params * Search: use getLocationQuery * Search: fix type errors * Docs: Save query params for manage dashboards * Search: extract connect * Search: add layout to URL params * Search: update options * Search: simplify options loading * Search: Fix strict null errors * Search: Change params order * Search: Add tests * Search: handle folder query
This commit is contained in:
parent
a5b38b799a
commit
531e658123
@ -1,5 +1,6 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { AsyncSelect, Icon } from '@grafana/ui';
|
import { useAsync } from 'react-use';
|
||||||
|
import { Select, Icon } from '@grafana/ui';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { DEFAULT_SORT } from 'app/features/search/constants';
|
import { DEFAULT_SORT } from 'app/features/search/constants';
|
||||||
import { SearchSrv } from '../../services/search_srv';
|
import { SearchSrv } from '../../services/search_srv';
|
||||||
@ -19,15 +20,17 @@ const getSortOptions = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SortPicker: FC<Props> = ({ onChange, value, placeholder }) => {
|
export const SortPicker: FC<Props> = ({ onChange, value, placeholder }) => {
|
||||||
return (
|
// Using sync Select and manual options fetching here since we need to find the selected option by value
|
||||||
<AsyncSelect
|
const { loading, value: options } = useAsync<SelectableValue[]>(getSortOptions, []);
|
||||||
|
|
||||||
|
return !loading ? (
|
||||||
|
<Select
|
||||||
width={25}
|
width={25}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={[value]}
|
value={options?.filter(opt => opt.value === value)}
|
||||||
loadOptions={getSortOptions}
|
options={options}
|
||||||
defaultOptions
|
|
||||||
placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`}
|
placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`}
|
||||||
prefix={<Icon name="sort-amount-down" />}
|
prefix={<Icon name="sort-amount-down" />}
|
||||||
/>
|
/>
|
||||||
);
|
) : null;
|
||||||
};
|
};
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import Mousetrap from 'mousetrap';
|
||||||
|
import 'mousetrap-global-bind';
|
||||||
|
import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
|
||||||
|
import { locationUtil } from '@grafana/data';
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { getExploreUrl } from 'app/core/utils/explore';
|
import { getExploreUrl } from 'app/core/utils/explore';
|
||||||
import { store } from 'app/store/store';
|
import { store } from 'app/store/store';
|
||||||
import { AppEventEmitter, CoreEvents } from 'app/types';
|
import { AppEventEmitter, CoreEvents } from 'app/types';
|
||||||
|
|
||||||
import Mousetrap from 'mousetrap';
|
|
||||||
import 'mousetrap-global-bind';
|
|
||||||
import { ContextSrv } from './context_srv';
|
|
||||||
import { ILocationService, IRootScopeService, ITimeoutService } from 'angular';
|
|
||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
import { DashboardModel } from '../../features/dashboard/state';
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
|
||||||
import { SaveDashboardModalProxy } from '../../features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
|
import { SaveDashboardModalProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardModalProxy';
|
||||||
import { locationUtil } from '@grafana/data';
|
import { defaultQueryParams } from 'app/features/search/reducers/searchQueryReducer';
|
||||||
|
import { ContextSrv } from './context_srv';
|
||||||
|
|
||||||
export class KeybindingSrv {
|
export class KeybindingSrv {
|
||||||
helpModal: boolean;
|
helpModal: boolean;
|
||||||
@ -88,7 +88,7 @@ export class KeybindingSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
closeSearch() {
|
closeSearch() {
|
||||||
const search = _.extend(this.$location.search(), { search: null });
|
const search = _.extend(this.$location.search(), { search: null, ...defaultQueryParams });
|
||||||
this.$location.search(search);
|
this.$location.search(search);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,13 +44,13 @@ export const ActionRow: FC<Props> = ({
|
|||||||
{!hideLayout ? (
|
{!hideLayout ? (
|
||||||
<RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={query.layout} />
|
<RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={query.layout} />
|
||||||
) : null}
|
) : null}
|
||||||
<SortPicker onChange={onSortChange} value={query.sort} />
|
<SortPicker onChange={onSortChange} value={query.sort?.value} />
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</div>
|
</div>
|
||||||
<HorizontalGroup spacing="md" width="auto">
|
<HorizontalGroup spacing="md" width="auto">
|
||||||
{showStarredFilter && (
|
{showStarredFilter && (
|
||||||
<div className={styles.checkboxWrapper}>
|
<div className={styles.checkboxWrapper}>
|
||||||
<Checkbox label="Filter by starred" onChange={onStarredFilterChange} />
|
<Checkbox label="Filter by starred" onChange={onStarredFilterChange} value={query.starred} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<TagFilter isClearable tags={query.tag} tagOptions={searchSrv.getDashboardTags} onChange={onTagFilterChange} />
|
<TagFilter isClearable tags={query.tag} tagOptions={searchSrv.getDashboardTags} onChange={onTagFilterChange} />
|
||||||
|
@ -8,7 +8,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
|
|||||||
import { getRouteParams, getUrl } from 'app/core/selectors/location';
|
import { getRouteParams, getUrl } from 'app/core/selectors/location';
|
||||||
import Page from 'app/core/components/Page/Page';
|
import Page from 'app/core/components/Page/Page';
|
||||||
import { loadFolderPage } from '../loaders';
|
import { loadFolderPage } from '../loaders';
|
||||||
import { ManageDashboards } from './ManageDashboards';
|
import ManageDashboards from './ManageDashboards';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from 'enzyme';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { mockSearch } from './mocks';
|
import { mockSearch } from './mocks';
|
||||||
import { DashboardSearch } from './DashboardSearch';
|
import { DashboardSearch, Props } from './DashboardSearch';
|
||||||
import { searchResults } from '../testData';
|
import { searchResults } from '../testData';
|
||||||
import { SearchLayout } from '../types';
|
import { SearchLayout } from '../types';
|
||||||
|
|
||||||
@ -15,9 +15,10 @@ afterEach(() => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
const setup = async (): Promise<any> => {
|
const setup = async (testProps?: Partial<Props>): Promise<any> => {
|
||||||
const props: any = {
|
const props: any = {
|
||||||
onCloseSearch: () => {},
|
onCloseSearch: () => {},
|
||||||
|
...testProps,
|
||||||
};
|
};
|
||||||
let wrapper;
|
let wrapper;
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -117,4 +118,18 @@ describe('DashboardSearch', () => {
|
|||||||
sort: undefined,
|
sort: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call search api with provided search params', async () => {
|
||||||
|
const params = { query: 'test query', tag: ['tag1'], sort: { value: 'asc' } };
|
||||||
|
await setup({ params });
|
||||||
|
|
||||||
|
expect(mockSearch).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockSearch).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
query: 'test query',
|
||||||
|
tag: ['tag1'],
|
||||||
|
sort: 'asc',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,15 +7,19 @@ import { useDashboardSearch } from '../hooks/useDashboardSearch';
|
|||||||
import { SearchField } from './SearchField';
|
import { SearchField } from './SearchField';
|
||||||
import { SearchResults } from './SearchResults';
|
import { SearchResults } from './SearchResults';
|
||||||
import { ActionRow } from './ActionRow';
|
import { ActionRow } from './ActionRow';
|
||||||
|
import { connectWithRouteParams, ConnectProps, DispatchProps } from '../connect';
|
||||||
|
|
||||||
export interface Props {
|
export interface OwnProps {
|
||||||
onCloseSearch: () => void;
|
onCloseSearch: () => void;
|
||||||
folder?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardSearch: FC<Props> = memo(({ onCloseSearch, folder }) => {
|
export type Props = OwnProps & ConnectProps & DispatchProps;
|
||||||
const payload = folder ? { query: `folder:${folder} ` } : {};
|
|
||||||
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery(payload);
|
export const DashboardSearch: FC<Props> = memo(({ onCloseSearch, params, updateLocation }) => {
|
||||||
|
const { query, onQueryChange, onTagFilterChange, onTagAdd, onSortChange, onLayoutChange } = useSearchQuery(
|
||||||
|
params,
|
||||||
|
updateLocation
|
||||||
|
);
|
||||||
const { results, loading, onToggleSection, onKeyDown } = useDashboardSearch(query, onCloseSearch);
|
const { results, loading, onToggleSection, onKeyDown } = useDashboardSearch(query, onCloseSearch);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
@ -54,6 +58,8 @@ export const DashboardSearch: FC<Props> = memo(({ onCloseSearch, folder }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default connectWithRouteParams(DashboardSearch);
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
overlay: css`
|
overlay: css`
|
||||||
|
@ -14,6 +14,7 @@ import { useSearchQuery } from '../hooks/useSearchQuery';
|
|||||||
import { SearchResultsFilter } from './SearchResultsFilter';
|
import { SearchResultsFilter } from './SearchResultsFilter';
|
||||||
import { SearchResults } from './SearchResults';
|
import { SearchResults } from './SearchResults';
|
||||||
import { DashboardActions } from './DashboardActions';
|
import { DashboardActions } from './DashboardActions';
|
||||||
|
import { connectWithRouteParams, ConnectProps, DispatchProps } from '../connect';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
folder?: FolderDTO;
|
folder?: FolderDTO;
|
||||||
@ -21,7 +22,7 @@ export interface Props {
|
|||||||
|
|
||||||
const { isEditor } = contextSrv;
|
const { isEditor } = contextSrv;
|
||||||
|
|
||||||
export const ManageDashboards: FC<Props> = memo(({ folder }) => {
|
export const ManageDashboards: FC<Props & ConnectProps & DispatchProps> = memo(({ folder, params, updateLocation }) => {
|
||||||
const folderId = folder?.id;
|
const folderId = folder?.id;
|
||||||
const folderUid = folder?.uid;
|
const folderUid = folder?.uid;
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -34,6 +35,7 @@ export const ManageDashboards: FC<Props> = memo(({ folder }) => {
|
|||||||
skipStarred: true,
|
skipStarred: true,
|
||||||
folderIds: folderId ? [folderId] : [],
|
folderIds: folderId ? [folderId] : [],
|
||||||
layout: defaultLayout,
|
layout: defaultLayout,
|
||||||
|
...params,
|
||||||
};
|
};
|
||||||
const {
|
const {
|
||||||
query,
|
query,
|
||||||
@ -44,7 +46,7 @@ export const ManageDashboards: FC<Props> = memo(({ folder }) => {
|
|||||||
onTagAdd,
|
onTagAdd,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
onLayoutChange,
|
onLayoutChange,
|
||||||
} = useSearchQuery(queryParams);
|
} = useSearchQuery(queryParams, updateLocation);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
results,
|
results,
|
||||||
@ -147,6 +149,8 @@ export const ManageDashboards: FC<Props> = memo(({ folder }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default connectWithRouteParams(ManageDashboards);
|
||||||
|
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css`
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import React, { FC, memo } from 'react';
|
import React, { FC, memo } from 'react';
|
||||||
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||||
|
import { UrlQueryMap } from '@grafana/data';
|
||||||
import { getLocationQuery } from 'app/core/selectors/location';
|
import { getLocationQuery } from 'app/core/selectors/location';
|
||||||
import { updateLocation } from 'app/core/reducers/location';
|
import { updateLocation } from 'app/core/reducers/location';
|
||||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { DashboardSearch } from './DashboardSearch';
|
import DashboardSearch from './DashboardSearch';
|
||||||
|
import { defaultQueryParams } from '../reducers/searchQueryReducer';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
search?: string | null;
|
search?: string | null;
|
||||||
@ -23,12 +25,13 @@ export const SearchWrapper: FC<Props> = memo(({ search, folder, updateLocation }
|
|||||||
const isOpen = search === 'open';
|
const isOpen = search === 'open';
|
||||||
|
|
||||||
const closeSearch = () => {
|
const closeSearch = () => {
|
||||||
if (search === 'open') {
|
if (isOpen) {
|
||||||
updateLocation({
|
updateLocation({
|
||||||
query: {
|
query: {
|
||||||
search: null,
|
search: null,
|
||||||
folder: null,
|
folder: null,
|
||||||
},
|
...defaultQueryParams,
|
||||||
|
} as UrlQueryMap,
|
||||||
partial: true,
|
partial: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
41
public/app/features/search/connect.ts
Normal file
41
public/app/features/search/connect.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
|
||||||
|
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { getLocationQuery } from 'app/core/selectors/location';
|
||||||
|
import { updateLocation } from 'app/core/reducers/location';
|
||||||
|
import { parseRouteParams } from './utils';
|
||||||
|
import { DashboardQuery } from './types';
|
||||||
|
import { Props as DashboardSearchProps } from './components/DashboardSearch';
|
||||||
|
import { Props as ManageDashboardsProps } from './components/ManageDashboards';
|
||||||
|
|
||||||
|
export interface ConnectProps {
|
||||||
|
params: Partial<DashboardQuery>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchProps {
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = DashboardSearchProps | ManageDashboardsProps;
|
||||||
|
|
||||||
|
const mapStateToProps: MapStateToProps<ConnectProps, Props, StoreState> = state => {
|
||||||
|
const { query, starred, sort, tag, layout, folder } = getLocationQuery(state.location);
|
||||||
|
return parseRouteParams(
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
tag,
|
||||||
|
starred,
|
||||||
|
sort,
|
||||||
|
layout,
|
||||||
|
},
|
||||||
|
folder
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps: MapDispatchToProps<DispatchProps, Props> = {
|
||||||
|
updateLocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectWithRouteParams = (Component: React.FC) =>
|
||||||
|
connectWithStore(Component, mapStateToProps, mapDispatchToProps);
|
@ -1,6 +1,6 @@
|
|||||||
import { FormEvent, useReducer } from 'react';
|
import { FormEvent, useReducer } from 'react';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { defaultQuery, queryReducer } from '../reducers/searchQueryReducer';
|
import { defaultQuery, defaultQueryParams, queryReducer } from '../reducers/searchQueryReducer';
|
||||||
import {
|
import {
|
||||||
ADD_TAG,
|
ADD_TAG,
|
||||||
CLEAR_FILTERS,
|
CLEAR_FILTERS,
|
||||||
@ -10,39 +10,48 @@ import {
|
|||||||
TOGGLE_SORT,
|
TOGGLE_SORT,
|
||||||
TOGGLE_STARRED,
|
TOGGLE_STARRED,
|
||||||
} from '../reducers/actionTypes';
|
} from '../reducers/actionTypes';
|
||||||
import { DashboardQuery, SearchLayout } from '../types';
|
import { DashboardQuery, RouteParams, SearchLayout } from '../types';
|
||||||
import { hasFilters } from '../utils';
|
import { hasFilters } from '../utils';
|
||||||
|
|
||||||
export const useSearchQuery = (queryParams: Partial<DashboardQuery>) => {
|
export const useSearchQuery = (queryParams: Partial<DashboardQuery>, updateLocation = (args: any) => {}) => {
|
||||||
|
const updateLocationQuery = (query: RouteParams) => updateLocation({ query, partial: true });
|
||||||
const initialState = { ...defaultQuery, ...queryParams };
|
const initialState = { ...defaultQuery, ...queryParams };
|
||||||
const [query, dispatch] = useReducer(queryReducer, initialState);
|
const [query, dispatch] = useReducer(queryReducer, initialState);
|
||||||
|
|
||||||
const onQueryChange = (query: string) => {
|
const onQueryChange = (query: string) => {
|
||||||
dispatch({ type: QUERY_CHANGE, payload: query });
|
dispatch({ type: QUERY_CHANGE, payload: query });
|
||||||
|
updateLocationQuery({ query });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTagFilterChange = (tags: string[]) => {
|
const onTagFilterChange = (tags: string[]) => {
|
||||||
dispatch({ type: SET_TAGS, payload: tags });
|
dispatch({ type: SET_TAGS, payload: tags });
|
||||||
|
updateLocationQuery({ tag: tags });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTagAdd = (tag: string) => {
|
const onTagAdd = (tag: string) => {
|
||||||
dispatch({ type: ADD_TAG, payload: tag });
|
dispatch({ type: ADD_TAG, payload: tag });
|
||||||
|
updateLocationQuery({ tag: [...query.tag, tag] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClearFilters = () => {
|
const onClearFilters = () => {
|
||||||
dispatch({ type: CLEAR_FILTERS });
|
dispatch({ type: CLEAR_FILTERS });
|
||||||
|
updateLocationQuery(defaultQueryParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStarredFilterChange = (e: FormEvent<HTMLInputElement>) => {
|
const onStarredFilterChange = (e: FormEvent<HTMLInputElement>) => {
|
||||||
dispatch({ type: TOGGLE_STARRED, payload: (e.target as HTMLInputElement).checked });
|
const starred = (e.target as HTMLInputElement).checked;
|
||||||
|
dispatch({ type: TOGGLE_STARRED, payload: starred });
|
||||||
|
updateLocationQuery({ starred: starred || null });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSortChange = (sort: SelectableValue | null) => {
|
const onSortChange = (sort: SelectableValue | null) => {
|
||||||
dispatch({ type: TOGGLE_SORT, payload: sort });
|
dispatch({ type: TOGGLE_SORT, payload: sort });
|
||||||
|
updateLocationQuery({ sort: sort?.value, layout: SearchLayout.List });
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLayoutChange = (layout: SearchLayout) => {
|
const onLayoutChange = (layout: SearchLayout) => {
|
||||||
dispatch({ type: LAYOUT_CHANGE, payload: layout });
|
dispatch({ type: LAYOUT_CHANGE, payload: layout });
|
||||||
|
updateLocationQuery({ layout });
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -87,7 +87,7 @@ describe('Manage dashboards reducer', () => {
|
|||||||
|
|
||||||
it('should not display dashboards in a non-expanded folder', () => {
|
it('should not display dashboards in a non-expanded folder', () => {
|
||||||
const general = results.find(res => res.id === 0);
|
const general = results.find(res => res.id === 0);
|
||||||
const toMove = { dashboards: general.items, folder: { id: 4074 } };
|
const toMove = { dashboards: general?.items, folder: { id: 4074 } };
|
||||||
const newState = reducer({ ...state, results }, { type: MOVE_ITEMS, payload: toMove });
|
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 === 4074).items).toHaveLength(0);
|
||||||
expect(newState.results.find((res: DashboardSection) => res.id === 0).items).toHaveLength(0);
|
expect(newState.results.find((res: DashboardSection) => res.id === 0).items).toHaveLength(0);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DashboardQuery, SearchAction, SearchLayout } from '../types';
|
import { DashboardQuery, RouteParams, SearchAction, SearchLayout } from '../types';
|
||||||
import {
|
import {
|
||||||
ADD_TAG,
|
ADD_TAG,
|
||||||
CLEAR_FILTERS,
|
CLEAR_FILTERS,
|
||||||
@ -22,6 +22,14 @@ export const defaultQuery: DashboardQuery = {
|
|||||||
layout: SearchLayout.Folders,
|
layout: SearchLayout.Folders,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultQueryParams: RouteParams = {
|
||||||
|
sort: null,
|
||||||
|
starred: null,
|
||||||
|
query: null,
|
||||||
|
tag: null,
|
||||||
|
layout: null,
|
||||||
|
};
|
||||||
|
|
||||||
export const queryReducer = (state: DashboardQuery, action: SearchAction) => {
|
export const queryReducer = (state: DashboardQuery, action: SearchAction) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case QUERY_CHANGE:
|
case QUERY_CHANGE:
|
||||||
|
@ -95,3 +95,11 @@ export enum SearchLayout {
|
|||||||
List = 'list',
|
List = 'list',
|
||||||
Folders = 'folders',
|
Folders = 'folders',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RouteParams {
|
||||||
|
query?: string | null;
|
||||||
|
sort?: string | null;
|
||||||
|
starred?: boolean | null;
|
||||||
|
tag?: string[] | null;
|
||||||
|
layout?: SearchLayout | null;
|
||||||
|
}
|
||||||
|
@ -5,8 +5,10 @@ import {
|
|||||||
getFlattenedSections,
|
getFlattenedSections,
|
||||||
markSelected,
|
markSelected,
|
||||||
mergeReducers,
|
mergeReducers,
|
||||||
|
parseRouteParams,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { sections, searchResults } from './testData';
|
import { sections, searchResults } from './testData';
|
||||||
|
import { RouteParams } from './types';
|
||||||
|
|
||||||
describe('Search utils', () => {
|
describe('Search utils', () => {
|
||||||
describe('getFlattenedSections', () => {
|
describe('getFlattenedSections', () => {
|
||||||
@ -146,4 +148,60 @@ describe('Search utils', () => {
|
|||||||
expect(getCheckedDashboardsUids(searchResults as any[])).toEqual(['lBdLINUWk', '8DY63kQZk']);
|
expect(getCheckedDashboardsUids(searchResults as any[])).toEqual(['lBdLINUWk', '8DY63kQZk']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('parseRouteParams', () => {
|
||||||
|
it('should remove all undefined keys', () => {
|
||||||
|
const params: Partial<RouteParams> = { sort: undefined, tag: undefined, query: 'test' };
|
||||||
|
|
||||||
|
expect(parseRouteParams(params)).toEqual({
|
||||||
|
params: {
|
||||||
|
query: 'test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return tag as array, if present', () => {
|
||||||
|
//@ts-ignore
|
||||||
|
const params = { sort: undefined, tag: 'test', query: 'test' };
|
||||||
|
expect(parseRouteParams(params)).toEqual({
|
||||||
|
params: {
|
||||||
|
query: 'test',
|
||||||
|
tag: ['test'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const params2: Partial<RouteParams> = { sort: undefined, tag: ['test'], query: 'test' };
|
||||||
|
expect(parseRouteParams(params2)).toEqual({
|
||||||
|
params: {
|
||||||
|
query: 'test',
|
||||||
|
tag: ['test'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sort as a SelectableValue', () => {
|
||||||
|
const params: Partial<RouteParams> = { sort: 'test' };
|
||||||
|
|
||||||
|
expect(parseRouteParams(params)).toEqual({
|
||||||
|
params: {
|
||||||
|
sort: { value: 'test' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prepend folder:{folder} to the query if folder is present', () => {
|
||||||
|
expect(parseRouteParams({}, 'current')).toEqual({
|
||||||
|
params: {
|
||||||
|
query: 'folder:current ',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// Prepend to exiting query
|
||||||
|
const params: Partial<RouteParams> = { query: 'test' };
|
||||||
|
expect(parseRouteParams(params, 'current')).toEqual({
|
||||||
|
params: {
|
||||||
|
query: 'folder:current test',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { parse, SearchParserResult } from 'search-query-parser';
|
import { parse, SearchParserResult } from 'search-query-parser';
|
||||||
import { IconName } from '@grafana/ui';
|
import { IconName } from '@grafana/ui';
|
||||||
|
import { UrlQueryMap, UrlQueryValue } from '@grafana/data';
|
||||||
import { DashboardQuery, DashboardSection, DashboardSectionItem, SearchAction, UidsToDelete } from './types';
|
import { DashboardQuery, DashboardSection, DashboardSectionItem, SearchAction, UidsToDelete } from './types';
|
||||||
import { NO_ID_SECTIONS, SECTION_STORAGE_KEY } from './constants';
|
import { NO_ID_SECTIONS, SECTION_STORAGE_KEY } from './constants';
|
||||||
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
||||||
@ -187,9 +188,13 @@ export const getParsedQuery = (query: DashboardQuery, queryParsing = false) => {
|
|||||||
let folderIds: number[] = [];
|
let folderIds: number[] = [];
|
||||||
|
|
||||||
if (parseQuery(query.query).folder === 'current') {
|
if (parseQuery(query.query).folder === 'current') {
|
||||||
const { folderId } = getDashboardSrv().getCurrent().meta;
|
try {
|
||||||
if (folderId) {
|
const { folderId } = getDashboardSrv().getCurrent()?.meta;
|
||||||
folderIds = [folderId];
|
if (folderId) {
|
||||||
|
folderIds = [folderId];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ...parsedQuery, query: parseQuery(query.query).text as string, folderIds };
|
return { ...parsedQuery, query: parseQuery(query.query).text as string, folderIds };
|
||||||
@ -228,3 +233,29 @@ export const getSectionStorageKey = (title: string) => {
|
|||||||
}
|
}
|
||||||
return `${SECTION_STORAGE_KEY}.${title.toLowerCase()}`;
|
return `${SECTION_STORAGE_KEY}.${title.toLowerCase()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove undefined keys from url params object and format non-primitive values
|
||||||
|
* @param params
|
||||||
|
* @param folder
|
||||||
|
*/
|
||||||
|
export const parseRouteParams = (params: UrlQueryMap, folder?: UrlQueryValue) => {
|
||||||
|
const cleanedParams = Object.entries(params).reduce((obj, [key, val]) => {
|
||||||
|
if (!val) {
|
||||||
|
return obj;
|
||||||
|
} else if (key === 'tag' && !Array.isArray(val)) {
|
||||||
|
return { ...obj, tag: [val] as string[] };
|
||||||
|
} else if (key === 'sort') {
|
||||||
|
return { ...obj, sort: { value: val } };
|
||||||
|
}
|
||||||
|
return { ...obj, [key]: val };
|
||||||
|
}, {} as Partial<DashboardQuery>);
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
const folderStr = `folder:${folder}`;
|
||||||
|
return {
|
||||||
|
params: { ...cleanedParams, query: `${folderStr} ${(cleanedParams.query ?? '').replace(folderStr, '')}` },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { params: cleanedParams };
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user