Plugins: Fix plugin catalog filtering (#66663)

* fix: make the filters work together

* tests: add more tests
This commit is contained in:
Levente Balogh
2023-04-18 08:15:35 +02:00
committed by GitHub
parent 58e3b3a90e
commit c22c31ed13
5 changed files with 136 additions and 28 deletions

View File

@@ -20,6 +20,7 @@ const useDebounceWithoutFirstRender = (callBack: () => any, delay = 0, deps: Rea
isFirstRender.current = false;
return;
}
console.log('--------- DEBUOUNCE');
return callBack();
},
delay,
@@ -42,6 +43,7 @@ export const SearchField = ({ value, onSearch }: Props) => {
}}
placeholder="Search Grafana plugins"
onChange={(value) => {
console.log('--------- ONCHANGE', value);
setQuery(value);
}}
width={46}

View File

@@ -207,6 +207,43 @@ describe('Browse list of plugins', () => {
expect(queryByText('Plugin 2')).not.toBeInTheDocument();
expect(queryByText('Plugin 3')).not.toBeInTheDocument();
});
it('should be possible to filter plugins by type', async () => {
const { queryByText } = renderBrowse('/plugins?filterByType=datasource&filterBy=all', [
getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.app }),
getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.datasource }),
]);
await waitFor(() => expect(queryByText('Plugin 3')).toBeInTheDocument());
// Other plugin types shouldn't be shown
expect(queryByText('Plugin 1')).not.toBeInTheDocument();
expect(queryByText('Plugin 2')).not.toBeInTheDocument();
});
it('should be possible to filter plugins both by type and a keyword', async () => {
const { queryByText } = renderBrowse('/plugins?filterByType=datasource&filterBy=all&q=Foo', [
getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.datasource }),
getCatalogPluginMock({ id: 'plugin-3', name: 'Foo plugin', type: PluginType.datasource }),
]);
await waitFor(() => expect(queryByText('Foo plugin')).toBeInTheDocument());
// Other plugin types shouldn't be shown
expect(queryByText('Plugin 1')).not.toBeInTheDocument();
expect(queryByText('Plugin 2')).not.toBeInTheDocument();
});
it('should list all available plugins if the keyword is empty', async () => {
const { queryByText } = renderBrowse('/plugins?filterBy=all&q=', [
getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', type: PluginType.app }),
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', type: PluginType.panel }),
getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', type: PluginType.datasource }),
]);
// We did not filter for any specific plugin type, so all plugins should be shown
await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
expect(queryByText('Plugin 2')).toBeInTheDocument();
expect(queryByText('Plugin 3')).toBeInTheDocument();
});
});
describe('when sorting', () => {

View File

@@ -55,7 +55,7 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
};
const onSearch = (q: string) => {
history.push({ query: { filterBy: 'all', filterByType: 'all', q } });
history.push({ query: { filterBy, filterByType, q } });
};
// How should we handle errors?

View File

@@ -0,0 +1,69 @@
import { PluginType } from '@grafana/data';
import { configureStore } from 'app/store/configureStore';
import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
import { find } from './selectors';
describe('Plugins Selectors', () => {
describe('find()', () => {
const store = configureStore({
plugins: getPluginsStateMock([
getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true, type: PluginType.datasource }),
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: true, type: PluginType.datasource }),
getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true, type: PluginType.panel }),
getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: false, type: PluginType.panel }),
getCatalogPluginMock({ id: 'plugin-5', name: 'Plugin 5', isInstalled: true, type: PluginType.app }),
]),
});
it('should return all plugins if there are no filters', () => {
const query = '';
const filterBy = 'all';
const filterByType = 'all';
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(5);
});
it('should be possible to search only by the "query"', () => {
const query = 'Plugin 3';
const filterBy = 'all';
const filterByType = 'all';
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(1);
expect(results[0].name).toBe('Plugin 3');
});
it('should be possible to search by plugin type', () => {
const query = '';
const filterBy = 'all';
const filterByType = PluginType.panel;
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(2);
expect(results.map(({ name }) => name)).toEqual(['Plugin 3', 'Plugin 4']);
});
it('should be possible to search by plugin state (installed / all)', () => {
const query = '';
const filterBy = 'installed';
const filterByType = 'all';
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(4);
expect(results.map(({ name }) => name)).toEqual(['Plugin 1', 'Plugin 2', 'Plugin 3', 'Plugin 5']);
});
it('should be possible to search by multiple filters', () => {
const query = '2';
const filterBy = 'all';
const filterByType = PluginType.datasource;
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(1);
expect(results[0].name).toBe('Plugin 2');
});
});
});

View File

@@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit';
import { PluginError, PluginErrorCode, unEscapeStringFromRegex } from '@grafana/data';
import { RequestStatus, PluginCatalogStoreState } from '../types';
import { RequestStatus, PluginCatalogStoreState, CatalogPlugin } from '../types';
import { pluginsAdapter } from './reducer';
@@ -14,43 +14,43 @@ export const selectDisplayMode = createSelector(selectRoot, ({ settings }) => se
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
const selectInstalled = (filterBy: string) =>
const findByState = (state: string) =>
createSelector(selectAll, (plugins) =>
plugins.filter((plugin) => (filterBy === 'installed' ? plugin.isInstalled : !plugin.isCore))
plugins.filter((plugin) => (state === 'installed' ? plugin.isInstalled : !plugin.isCore))
);
const findByInstallAndType = (filterBy: string, filterByType: string) =>
createSelector(selectInstalled(filterBy), (plugins) =>
plugins.filter((plugin) => filterByType === 'all' || plugin.type === filterByType)
type PluginFilters = {
state: string;
type: string;
};
const findPluginsByFilters = (filters: PluginFilters) =>
createSelector(findByState(filters.state), (plugins) =>
plugins.filter((plugin) => filters.type === 'all' || plugin.type === filters.type)
);
const findByKeyword = (searchBy: string) =>
createSelector(selectAll, (plugins) => {
if (searchBy === '') {
return [];
const findByKeyword = (plugins: CatalogPlugin[], query: string) => {
if (query === '') {
return plugins;
}
return plugins.filter((plugin) => {
const fields: String[] = [];
if (plugin.name) {
fields.push(plugin.name.toLowerCase());
}
return plugins.filter((plugin) => {
const fields: String[] = [];
if (plugin.name) {
fields.push(plugin.name.toLowerCase());
}
if (plugin.orgName) {
fields.push(plugin.orgName.toLowerCase());
}
if (plugin.orgName) {
fields.push(plugin.orgName.toLowerCase());
}
return fields.some((f) => f.includes(unEscapeStringFromRegex(searchBy).toLowerCase()));
});
return fields.some((f) => f.includes(unEscapeStringFromRegex(query).toLowerCase()));
});
};
export const find = (searchBy: string, filterBy: string, filterByType: string) =>
createSelector(
findByInstallAndType(filterBy, filterByType),
findByKeyword(searchBy),
(filteredPlugins, searchedPlugins) => {
return searchBy === '' ? filteredPlugins : searchedPlugins;
}
createSelector(findPluginsByFilters({ state: filterBy, type: filterByType }), (filteredPlugins) =>
findByKeyword(filteredPlugins, searchBy)
);
export const selectPluginErrors = createSelector(selectAll, (plugins) =>