mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
LibraryPanelSearch: Refactor and fix hyphen issue (#55314)
This commit is contained in:
@@ -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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
[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": [
|
"public/app/features/library-panels/components/LibraryPanelsView/actions.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'onChange'> {
|
|||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
width?: number;
|
width?: number;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
escapeRegex?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterInput = React.forwardRef<HTMLInputElement, Props>(
|
export const FilterInput = React.forwardRef<HTMLInputElement, Props>(
|
||||||
({ value, width, onChange, ...restProps }, ref) => {
|
({ value, width, onChange, escapeRegex = true, ...restProps }, ref) => {
|
||||||
const innerRef = React.useRef<HTMLInputElement>(null);
|
const innerRef = React.useRef<HTMLInputElement>(null);
|
||||||
const combinedRef = useCombinedRefs(ref, innerRef) as React.Ref<HTMLInputElement>;
|
const combinedRef = useCombinedRefs(ref, innerRef) as React.Ref<HTMLInputElement>;
|
||||||
|
|
||||||
@@ -38,8 +39,10 @@ export const FilterInput = React.forwardRef<HTMLInputElement, Props>(
|
|||||||
suffix={suffix}
|
suffix={suffix}
|
||||||
width={width}
|
width={width}
|
||||||
type="text"
|
type="text"
|
||||||
value={value ? unEscapeStringFromRegex(value) : ''}
|
value={escapeRegex ? unEscapeStringFromRegex(value ?? '') : value}
|
||||||
onChange={(event) => onChange(escapeStringForRegex(event.currentTarget.value))}
|
onChange={(event) =>
|
||||||
|
onChange(escapeRegex ? escapeStringForRegex(event.currentTarget.value) : event.currentTarget.value)
|
||||||
|
}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
ref={combinedRef}
|
ref={combinedRef}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui';
|
import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
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';
|
import { FolderInfo, PermissionLevelString } from 'app/types';
|
||||||
|
|
||||||
export interface FolderFilterProps {
|
export interface FolderFilterProps {
|
||||||
@@ -13,34 +13,21 @@ export interface FolderFilterProps {
|
|||||||
maxMenuHeight?: number;
|
maxMenuHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FolderFilter({ onChange: propsOnChange, maxMenuHeight }: FolderFilterProps): JSX.Element {
|
export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JSX.Element {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const getOptions = useCallback((searchString: string) => getFoldersAsOptions(searchString, setLoading), []);
|
const getOptions = useCallback((searchString: string) => getFoldersAsOptions(searchString, setLoading), []);
|
||||||
const debouncedLoadOptions = useMemo(() => debounce(getOptions, 300), [getOptions]);
|
const debouncedLoadOptions = useMemo(() => debounce(getOptions, 300), [getOptions]);
|
||||||
|
|
||||||
const [value, setValue] = useState<Array<SelectableValue<FolderInfo>>>([]);
|
const [value, setValue] = useState<Array<SelectableValue<FolderInfo>>>([]);
|
||||||
const onChange = useCallback(
|
const onSelectOptionChange = useCallback(
|
||||||
(folders: Array<SelectableValue<FolderInfo>>) => {
|
(folders: Array<SelectableValue<FolderInfo>>) => {
|
||||||
const changedFolders = [];
|
const changedFolderIds = folders.filter((f) => Boolean(f.value)).map((f) => f.value!);
|
||||||
for (const folder of folders) {
|
onChange(changedFolderIds);
|
||||||
if (folder.value) {
|
|
||||||
changedFolders.push(folder.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
propsOnChange(changedFolders);
|
|
||||||
setValue(folders);
|
setValue(folders);
|
||||||
},
|
},
|
||||||
[propsOnChange]
|
[onChange]
|
||||||
);
|
);
|
||||||
const selectOptions = {
|
|
||||||
defaultOptions: true,
|
|
||||||
isMulti: true,
|
|
||||||
noOptionsMessage: 'No folders found',
|
|
||||||
placeholder: 'Filter by folder',
|
|
||||||
maxMenuHeight,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@@ -57,28 +44,35 @@ export function FolderFilter({ onChange: propsOnChange, maxMenuHeight }: FolderF
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<AsyncMultiSelect
|
<AsyncMultiSelect
|
||||||
{...selectOptions}
|
value={value}
|
||||||
|
onChange={onSelectOptionChange}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
loadOptions={debouncedLoadOptions}
|
loadOptions={debouncedLoadOptions}
|
||||||
|
maxMenuHeight={maxMenuHeight}
|
||||||
|
placeholder="Filter by folder"
|
||||||
|
noOptionsMessage="No folders found"
|
||||||
prefix={<Icon name="filter" />}
|
prefix={<Icon name="filter" />}
|
||||||
aria-label="Folder filter"
|
aria-label="Folder filter"
|
||||||
|
defaultOptions
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFoldersAsOptions(searchString: string, setLoading: (loading: boolean) => void) {
|
async function getFoldersAsOptions(
|
||||||
|
searchString: string,
|
||||||
|
setLoading: (loading: boolean) => void
|
||||||
|
): Promise<Array<SelectableValue<FolderInfo>>> {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
query: searchString,
|
query: searchString,
|
||||||
type: 'dash-folder',
|
type: DashboardSearchItemType.DashFolder,
|
||||||
permission: PermissionLevelString.View,
|
permission: PermissionLevelString.View,
|
||||||
};
|
};
|
||||||
|
|
||||||
// FIXME: stop using id from search and use UID instead
|
// FIXME: stop using id from search and use UID instead
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
const searchHits: DashboardSearchHit[] = await getBackendSrv().search(params);
|
||||||
const searchHits = (await getBackendSrv().search(params)) as DashboardSearchHit[];
|
|
||||||
const options = searchHits.map((d) => ({ label: d.title, value: { id: d.id, title: d.title } }));
|
const options = searchHits.map((d) => ({ label: d.title, value: { id: d.id, title: d.title } }));
|
||||||
if (!searchString || 'general'.includes(searchString.toLowerCase())) {
|
if (!searchString || 'general'.includes(searchString.toLowerCase())) {
|
||||||
options.unshift({ label: 'General', value: { id: 0, title: 'General' } });
|
options.unshift({ label: 'General', value: { id: 0, title: 'General' } });
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Props): JSX.Element => {
|
export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Props): JSX.Element => {
|
||||||
const plugins = useMemo<PanelPluginMeta[]>(() => {
|
const plugins = useMemo<PanelPluginMeta[]>(() => getAllPanelPluginMeta(), []);
|
||||||
return getAllPanelPluginMeta();
|
|
||||||
}, []);
|
|
||||||
const options = useMemo(
|
const options = useMemo(
|
||||||
() =>
|
() =>
|
||||||
plugins
|
plugins
|
||||||
@@ -24,12 +22,7 @@ export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Prop
|
|||||||
const [value, setValue] = useState<Array<SelectableValue<PanelPluginMeta>>>([]);
|
const [value, setValue] = useState<Array<SelectableValue<PanelPluginMeta>>>([]);
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(plugins: Array<SelectableValue<PanelPluginMeta>>) => {
|
(plugins: Array<SelectableValue<PanelPluginMeta>>) => {
|
||||||
const changedPlugins = [];
|
const changedPlugins = plugins.filter((p) => p.value).map((p) => p.value!);
|
||||||
for (const plugin of plugins) {
|
|
||||||
if (plugin.value) {
|
|
||||||
changedPlugins.push(plugin.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
propsOnChange(changedPlugins);
|
propsOnChange(changedPlugins);
|
||||||
setValue(plugins);
|
setValue(plugins);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import { css } from '@emotion/css';
|
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 { 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 { FolderFilter } from '../../../../core/components/FolderFilter/FolderFilter';
|
||||||
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
|
import { PanelTypeFilter } from '../../../../core/components/PanelTypeFilter/PanelTypeFilter';
|
||||||
import { SortPicker } from '../../../../core/components/Select/SortPicker';
|
import { SortPicker } from '../../../../core/components/Select/SortPicker';
|
||||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants';
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../../core/constants';
|
||||||
import { FolderInfo } from '../../../../types';
|
|
||||||
import { LibraryElementDTO } from '../../types';
|
import { LibraryElementDTO } from '../../types';
|
||||||
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
|
import { LibraryPanelsView } from '../LibraryPanelsView/LibraryPanelsView';
|
||||||
|
|
||||||
import {
|
|
||||||
folderFilterChanged,
|
|
||||||
initialLibraryPanelsSearchState,
|
|
||||||
libraryPanelsSearchReducer,
|
|
||||||
panelFilterChanged,
|
|
||||||
searchChanged,
|
|
||||||
sortChanged,
|
|
||||||
} from './reducer';
|
|
||||||
|
|
||||||
export enum LibraryPanelsSearchVariant {
|
export enum LibraryPanelsSearchVariant {
|
||||||
Tight = 'tight',
|
Tight = 'tight',
|
||||||
Spacious = 'spacious',
|
Spacious = 'spacious',
|
||||||
@@ -49,78 +41,51 @@ export const LibraryPanelsSearch = ({
|
|||||||
showSort = false,
|
showSort = false,
|
||||||
showSecondaryActions = false,
|
showSecondaryActions = false,
|
||||||
}: LibraryPanelsSearchProps): JSX.Element => {
|
}: LibraryPanelsSearchProps): JSX.Element => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(useCallback((theme) => getStyles(theme, variant), [variant]));
|
||||||
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<string>) => dispatch(sortChanged(sorting));
|
|
||||||
const onFolderFilterChange = (folders: FolderInfo[]) => dispatch(folderFilterChanged(folders));
|
|
||||||
const onPanelFilterChange = (plugins: PanelPluginMeta[]) => dispatch(panelFilterChanged(plugins));
|
|
||||||
|
|
||||||
if (variant === LibraryPanelsSearchVariant.Spacious) {
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
return (
|
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
|
||||||
<div className={styles.container}>
|
useDebounce(() => setDebouncedSearchQuery(searchQuery), 200, [searchQuery]);
|
||||||
<VerticalGroup spacing="lg">
|
|
||||||
<FilterInput
|
const [sortDirection, setSortDirection] = useState<SelectableValue<string>>({});
|
||||||
value={searchQuery}
|
const [folderFilter, setFolderFilter] = useState<string[]>(currentFolderId ? [String(currentFolderId)] : []);
|
||||||
onChange={onFilterChange}
|
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
||||||
placeholder={'Search by name or description'}
|
|
||||||
width={0}
|
const sortOrFiltersVisible = showSort || showPanelFilter || showFolderFilter;
|
||||||
/>
|
const verticalGroupSpacing = variant === LibraryPanelsSearchVariant.Tight ? 'lg' : 'xs';
|
||||||
<div className={styles.buttonRow}>
|
|
||||||
<HorizontalGroup
|
|
||||||
spacing="sm"
|
|
||||||
justify={(showSort && showPanelFilter) || showFolderFilter ? 'space-between' : 'flex-end'}
|
|
||||||
>
|
|
||||||
{showSort && (
|
|
||||||
<SortPicker value={sortDirection} onChange={onSortChange} filter={['alpha-asc', 'alpha-desc']} />
|
|
||||||
)}
|
|
||||||
<HorizontalGroup
|
|
||||||
spacing="sm"
|
|
||||||
justify={showFolderFilter && showPanelFilter ? 'space-between' : 'flex-end'}
|
|
||||||
>
|
|
||||||
{showFolderFilter && <FolderFilter onChange={onFolderFilterChange} />}
|
|
||||||
{showPanelFilter && <PanelTypeFilter onChange={onPanelFilterChange} />}
|
|
||||||
</HorizontalGroup>
|
|
||||||
</HorizontalGroup>
|
|
||||||
</div>
|
|
||||||
<div className={styles.libraryPanelsView}>
|
|
||||||
<LibraryPanelsView
|
|
||||||
onClickCard={onClick}
|
|
||||||
searchString={searchQuery}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
panelFilter={panelFilter}
|
|
||||||
folderFilter={folderFilter}
|
|
||||||
currentPanelId={currentPanelId}
|
|
||||||
showSecondaryActions={showSecondaryActions}
|
|
||||||
perPage={perPage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</VerticalGroup>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<VerticalGroup spacing="xs">
|
<VerticalGroup spacing={verticalGroupSpacing}>
|
||||||
<div className={styles.tightButtonRow}>
|
<div className={styles.gridContainer}>
|
||||||
<div className={styles.tightFilter}>
|
<div className={styles.filterInputWrapper}>
|
||||||
<FilterInput value={searchQuery} onChange={onFilterChange} placeholder={'Search by name'} width={0} />
|
<FilterInput
|
||||||
</div>
|
value={searchQuery}
|
||||||
<div className={styles.tightSortFilter}>
|
onChange={setSearchQuery}
|
||||||
{showSort && <SortPicker value={sortDirection} onChange={onSortChange} />}
|
placeholder="Search by name or description"
|
||||||
{showFolderFilter && <FolderFilter onChange={onFolderFilterChange} maxMenuHeight={200} />}
|
width={0}
|
||||||
{showPanelFilter && <PanelTypeFilter onChange={onPanelFilterChange} maxMenuHeight={200} />}
|
escapeRegex={false}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{sortOrFiltersVisible && (
|
||||||
|
<SearchControls
|
||||||
|
showSort={showSort}
|
||||||
|
showPanelFilter={showPanelFilter}
|
||||||
|
showFolderFilter={showFolderFilter}
|
||||||
|
onSortChange={setSortDirection}
|
||||||
|
onFolderFilterChange={setFolderFilter}
|
||||||
|
onPanelFilterChange={setPanelFilter}
|
||||||
|
sortDirection={sortDirection.value}
|
||||||
|
variant={variant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.libraryPanelsView}>
|
<div className={styles.libraryPanelsView}>
|
||||||
<LibraryPanelsView
|
<LibraryPanelsView
|
||||||
onClickCard={onClick}
|
onClickCard={onClick}
|
||||||
searchString={searchQuery}
|
searchString={debouncedSearchQuery}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection.value}
|
||||||
panelFilter={panelFilter}
|
panelFilter={panelFilter}
|
||||||
folderFilter={folderFilter}
|
folderFilter={folderFilter}
|
||||||
currentPanelId={currentPanelId}
|
currentPanelId={currentPanelId}
|
||||||
@@ -133,34 +98,118 @@ export const LibraryPanelsSearch = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2, variant: LibraryPanelsSearchVariant) {
|
||||||
|
const tightLayout = css`
|
||||||
|
flex-direction: row;
|
||||||
|
row-gap: ${theme.spacing(1)};
|
||||||
|
`;
|
||||||
return {
|
return {
|
||||||
|
filterInputWrapper: css`
|
||||||
|
flex-grow: ${variant === LibraryPanelsSearchVariant.Tight ? 1 : 'initial'};
|
||||||
|
`,
|
||||||
container: css`
|
container: css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: ${theme.spacing(1)};
|
padding: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
buttonRow: css`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: ${theme.spacing(2)}; // Clear types link
|
|
||||||
`,
|
|
||||||
tightButtonRow: css`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: ${theme.spacing(4)}; // Clear types link
|
|
||||||
`,
|
|
||||||
tightFilter: css`
|
|
||||||
flex-grow: 1;
|
|
||||||
`,
|
|
||||||
tightSortFilter: css`
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: ${theme.spacing(0, 0, 0, 0.5)};
|
|
||||||
`,
|
|
||||||
libraryPanelsView: css`
|
libraryPanelsView: css`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`,
|
`,
|
||||||
|
gridContainer: css`
|
||||||
|
${variant === LibraryPanelsSearchVariant.Tight ? tightLayout : ''};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
column-gap: ${theme.spacing(1)};
|
||||||
|
row-gap: ${theme.spacing(1)};
|
||||||
|
padding-bottom: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SearchControlsProps {
|
||||||
|
showSort: boolean;
|
||||||
|
showPanelFilter: boolean;
|
||||||
|
showFolderFilter: boolean;
|
||||||
|
sortDirection?: string;
|
||||||
|
onSortChange: (sortValue: SelectableValue) => 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 (
|
||||||
|
<div className={styles.container}>
|
||||||
|
{showSort && <SortPicker value={sortDirection} onChange={onSortChange} filter={['alpha-asc', 'alpha-desc']} />}
|
||||||
|
{(showFolderFilter || showPanelFilter) && (
|
||||||
|
<div className={styles.filterContainer}>
|
||||||
|
{showFolderFilter && <FolderFilter onChange={folderFilterChanged} />}
|
||||||
|
{showPanelFilter && <PanelTypeFilter onChange={panelFilterChanged} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<LibraryPanelsSearchState>()
|
|
||||||
.givenReducer(libraryPanelsSearchReducer, {
|
|
||||||
...initialLibraryPanelsSearchState,
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(searchChanged('searching for'))
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...initialLibraryPanelsSearchState,
|
|
||||||
searchQuery: 'searching for',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when sortChanged is dispatched', () => {
|
|
||||||
it('then state should be correct', () => {
|
|
||||||
reducerTester<LibraryPanelsSearchState>()
|
|
||||||
.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<LibraryPanelsSearchState>()
|
|
||||||
.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<LibraryPanelsSearchState>()
|
|
||||||
.givenReducer(libraryPanelsSearchReducer, {
|
|
||||||
...initialLibraryPanelsSearchState,
|
|
||||||
})
|
|
||||||
.whenActionIsDispatched(folderFilterChanged(folders))
|
|
||||||
.thenStateShouldEqual({
|
|
||||||
...initialLibraryPanelsSearchState,
|
|
||||||
folderFilter: ['0', '1'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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<string>('libraryPanels/search/searchChanged');
|
|
||||||
export const sortChanged = createAction<SelectableValue<string>>('libraryPanels/search/sortChanged');
|
|
||||||
export const panelFilterChanged = createAction<PanelPluginMeta[]>('libraryPanels/search/panelFilterChanged');
|
|
||||||
export const folderFilterChanged = createAction<FolderInfo[]>('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;
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { FC, useCallback, useState } from 'react';
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PanelPluginMeta } from '@grafana/data';
|
import { PanelPluginMeta } from '@grafana/data';
|
||||||
import { Button, useStyles2, VerticalGroup } from '@grafana/ui';
|
import { Button, VerticalGroup } from '@grafana/ui';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { PanelModel } from 'app/features/dashboard/state';
|
import { PanelModel } from 'app/features/dashboard/state';
|
||||||
import { changeToLibraryPanel } from 'app/features/panel/state/actions';
|
import { changeToLibraryPanel } from 'app/features/panel/state/actions';
|
||||||
@@ -20,7 +20,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
|
const [showingAddPanelModal, setShowingAddPanelModal] = useState(false);
|
||||||
const [changeToPanel, setChangeToPanel] = useState<LibraryElementDTO | undefined>(undefined);
|
const [changeToPanel, setChangeToPanel] = useState<LibraryElementDTO | undefined>(undefined);
|
||||||
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
const [panelFilter, setPanelFilter] = useState<string[]>([]);
|
||||||
@@ -39,21 +38,11 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setChangeToPanel(undefined);
|
setChangeToPanel(undefined);
|
||||||
|
|
||||||
dispatch(changeToLibraryPanel(panel, changeToPanel));
|
dispatch(changeToLibraryPanel(panel, changeToPanel));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddToPanelLibrary = () => {
|
const onAddToPanelLibrary = () => setShowingAddPanelModal(true);
|
||||||
setShowingAddPanelModal(true);
|
const onDismissChangeToPanel = () => setChangeToPanel(undefined);
|
||||||
};
|
|
||||||
|
|
||||||
const onChangeLibraryPanel = (panel: LibraryElementDTO) => {
|
|
||||||
setChangeToPanel(panel);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDismissChangeToPanel = () => {
|
|
||||||
setChangeToPanel(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalGroup spacing="md">
|
<VerticalGroup spacing="md">
|
||||||
@@ -72,7 +61,7 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
|||||||
currentPanelId={panel.libraryPanel?.uid}
|
currentPanelId={panel.libraryPanel?.uid}
|
||||||
searchString={searchQuery}
|
searchString={searchQuery}
|
||||||
panelFilter={panelFilter}
|
panelFilter={panelFilter}
|
||||||
onClickCard={onChangeLibraryPanel}
|
onClickCard={setChangeToPanel}
|
||||||
showSecondaryActions
|
showSecondaryActions
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,10 +82,8 @@ export const PanelLibraryOptionsGroup: FC<Props> = ({ panel, searchQuery }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const styles = {
|
||||||
return {
|
libraryPanelsView: css`
|
||||||
libraryPanelsView: css`
|
width: 100%;
|
||||||
width: 100%;
|
`,
|
||||||
`,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user