Connections: Show core datasource plugins as well (#67815)

fix: add a new hook & selector that can show core plugins by default
This commit is contained in:
Levente Balogh 2023-05-05 09:38:18 +02:00 committed by GitHub
parent e7cbe0276e
commit 21459c7c97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 133 additions and 102 deletions

View File

@ -4,7 +4,7 @@ import React, { useMemo, useState } from 'react';
import { PluginType } from '@grafana/data';
import { useStyles2, LoadingPlaceholder } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { useGetAllWithFilters } from 'app/features/plugins/admin/state/hooks';
import { useGetAll } from 'app/features/plugins/admin/state/hooks';
import { AccessControlAction } from 'app/types';
import { ROUTES } from '../../constants';
@ -38,10 +38,9 @@ export function AddNewConnection() {
setSearchTerm(e.currentTarget.value.toLowerCase());
};
const { isLoading, error, plugins } = useGetAllWithFilters({
query: searchTerm,
filterBy: '',
filterByType: PluginType.datasource,
const { isLoading, error, plugins } = useGetAll({
keyword: searchTerm,
type: PluginType.datasource,
});
const cardGridItems = useMemo(

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { ReactElement } from 'react';
import { useLocation } from 'react-router-dom';
import { SelectableValue, GrafanaTheme2 } from '@grafana/data';
import { SelectableValue, GrafanaTheme2, PluginType } from '@grafana/data';
import { config, locationSearchToObject } from '@grafana/runtime';
import { LoadingPlaceholder, Select, RadioButtonGroup, useStyles2, Tooltip, Field } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
@ -16,7 +16,7 @@ import { PluginList } from '../components/PluginList';
import { SearchField } from '../components/SearchField';
import { Sorters } from '../helpers';
import { useHistory } from '../hooks/useHistory';
import { useGetAllWithFilters, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks';
import { useGetAll, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks';
import { PluginListDisplayMode } from '../types';
export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null {
@ -27,16 +27,19 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
const styles = useStyles2(getStyles);
const history = useHistory();
const remotePluginsAvailable = useIsRemotePluginsAvailable();
const query = (locationSearch.q as string) || '';
const keyword = (locationSearch.q as string) || '';
const filterBy = (locationSearch.filterBy as string) || 'installed';
const filterByType = (locationSearch.filterByType as string) || 'all';
const filterByType = (locationSearch.filterByType as PluginType | 'all') || 'all';
const sortBy = (locationSearch.sortBy as Sorters) || Sorters.nameAsc;
const { isLoading, error, plugins } = useGetAllWithFilters({
query,
filterBy,
filterByType,
sortBy,
});
const { isLoading, error, plugins } = useGetAll(
{
keyword,
type: filterByType !== 'all' ? filterByType : undefined,
isInstalled: filterBy === 'installed' ? true : undefined,
isCore: filterBy === 'installed' ? undefined : false, // We only would like to show core plugins when the user filters to installed plugins
},
sortBy
);
const filterByOptions = [
{ value: 'all', label: 'All' },
{ value: 'installed', label: 'Installed' },
@ -81,7 +84,7 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
<Page.Contents>
<HorizontalGroup wrap>
<Field label="Search">
<SearchField value={query} onSearch={onSearch} />
<SearchField value={keyword} onSearch={onSearch} />
</Field>
<HorizontalGroup wrap className={styles.actionBar}>
{/* Filter by type */}

View File

@ -1,4 +1,4 @@
import { useEffect } from 'react';
import { useEffect, useMemo } from 'react';
import { PluginError } from '@grafana/data';
import { useDispatch, useSelector } from 'app/types';
@ -9,48 +9,31 @@ import { CatalogPlugin, PluginListDisplayMode } from '../types';
import { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions';
import { setDisplayMode } from './reducer';
import {
find,
selectAll,
selectPlugins,
selectById,
selectIsRequestPending,
selectRequestError,
selectIsRequestNotFetched,
selectDisplayMode,
selectPluginErrors,
type PluginFilters,
} from './selectors';
type Filters = {
query?: string; // Note: this will be an escaped regex string as it comes from `FilterInput`
filterBy?: string;
filterByType?: string;
sortBy?: Sorters;
};
export const useGetAllWithFilters = ({
query = '',
filterBy = 'installed',
filterByType = 'all',
sortBy = Sorters.nameAsc,
}: Filters) => {
export const useGetAll = (filters: PluginFilters, sortBy: Sorters = Sorters.nameAsc) => {
useFetchAll();
const filtered = useSelector(find(query, filterBy, filterByType));
const selector = useMemo(() => selectPlugins(filters), [filters]);
const plugins = useSelector(selector);
const { isLoading, error } = useFetchStatus();
const sortedAndFiltered = sortPlugins(filtered, sortBy);
const sortedPlugins = sortPlugins(plugins, sortBy);
return {
isLoading,
error,
plugins: sortedAndFiltered,
plugins: sortedPlugins,
};
};
export const useGetAll = (): CatalogPlugin[] => {
useFetchAll();
return useSelector(selectAll);
};
export const useGetSingle = (id: string): CatalogPlugin | undefined => {
useFetchAll();
useFetchDetails(id);

View File

@ -3,64 +3,100 @@ import { configureStore } from 'app/store/configureStore';
import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
import { find } from './selectors';
import { selectPlugins } from './selectors';
describe('Plugins Selectors', () => {
describe('find()', () => {
describe('selectPlugins()', () => {
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 }),
getCatalogPluginMock({
id: 'plugin-1',
name: 'Plugin 1',
isInstalled: true,
type: PluginType.datasource,
isCore: true,
}),
getCatalogPluginMock({
id: 'plugin-2',
name: 'Plugin 2',
isInstalled: true,
type: PluginType.datasource,
isCore: true,
}),
getCatalogPluginMock({
id: 'plugin-3',
name: 'Plugin 3',
isInstalled: true,
type: PluginType.panel,
isCore: false,
}),
getCatalogPluginMock({
id: 'plugin-4',
name: 'Plugin 4',
isInstalled: false,
type: PluginType.panel,
isCore: false,
}),
getCatalogPluginMock({
id: 'plugin-5',
name: 'Plugin 5',
isInstalled: true,
type: PluginType.app,
isCore: false,
}),
]),
});
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());
const results = selectPlugins({})(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());
it('should be possible to search only by the "keyword"', () => {
const results = selectPlugins({ keyword: 'Plugin 3' })(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());
const results = selectPlugins({ type: PluginType.panel })(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());
it('should be possible to search for core plugins', () => {
const results = selectPlugins({ isCore: true })(store.getState());
expect(results).toHaveLength(2);
expect(results.map(({ name }) => name)).toEqual(['Plugin 1', 'Plugin 2']);
});
it('should be possible to exclude core plugins from the search', () => {
const results = selectPlugins({ isCore: false })(store.getState());
expect(results).toHaveLength(3);
expect(results.map(({ name }) => name)).toEqual(['Plugin 3', 'Plugin 4', 'Plugin 5']);
});
it('should be possible to only search for installed plugins', () => {
const results = selectPlugins({ isInstalled: true })(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 only search for not yet installed plugins', () => {
const results = selectPlugins({ isInstalled: false })(store.getState());
expect(results).toHaveLength(1);
expect(results.map(({ name }) => name)).toEqual(['Plugin 4']);
});
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());
const results = selectPlugins({ keyword: '2', type: PluginType.datasource })(store.getState());
expect(results).toHaveLength(1);
expect(results[0].name).toBe('Plugin 2');

View File

@ -1,8 +1,8 @@
import { createSelector } from '@reduxjs/toolkit';
import { PluginError, PluginErrorCode, unEscapeStringFromRegex } from '@grafana/data';
import { PluginError, PluginErrorCode, PluginType, unEscapeStringFromRegex } from '@grafana/data';
import { RequestStatus, PluginCatalogStoreState, CatalogPlugin } from '../types';
import { RequestStatus, PluginCatalogStoreState } from '../types';
import { pluginsAdapter } from './reducer';
@ -14,44 +14,54 @@ export const selectDisplayMode = createSelector(selectRoot, ({ settings }) => se
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
const findByState = (state: string) =>
createSelector(selectAll, (plugins) =>
plugins.filter((plugin) => (state === 'installed' ? plugin.isInstalled : !plugin.isCore))
);
export type PluginFilters = {
// Searches for a string in certain fields (e.g. "name" or "orgName")
// (Note: this will be an escaped regex string as it comes from `FilterInput`)
keyword?: string;
type PluginFilters = {
state: string;
type: string;
// (Optional, only applied if set)
type?: PluginType;
// (Optional, only applied if set)
isCore?: boolean;
// (Optional, only applied if set)
isInstalled?: boolean;
// (Optional, only applied if set)
isEnterprise?: boolean;
};
const findPluginsByFilters = (filters: PluginFilters) =>
createSelector(findByState(filters.state), (plugins) =>
plugins.filter((plugin) => filters.type === 'all' || plugin.type === filters.type)
);
export const selectPlugins = (filters: PluginFilters) =>
createSelector(selectAll, (plugins) => {
const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';
const findByKeyword = (plugins: CatalogPlugin[], query: string) => {
if (query === '') {
return plugins;
}
return plugins.filter((plugin) => {
const fieldsToSearchIn = [plugin.name, plugin.orgName].filter(Boolean).map((f) => f.toLowerCase());
return plugins.filter((plugin) => {
const fields: String[] = [];
if (plugin.name) {
fields.push(plugin.name.toLowerCase());
}
if (keyword && !fieldsToSearchIn.some((f) => f.includes(keyword))) {
return false;
}
if (plugin.orgName) {
fields.push(plugin.orgName.toLowerCase());
}
if (filters.type && plugin.type !== filters.type) {
return false;
}
return fields.some((f) => f.includes(unEscapeStringFromRegex(query).toLowerCase()));
if (filters.isInstalled !== undefined && plugin.isInstalled !== filters.isInstalled) {
return false;
}
if (filters.isCore !== undefined && plugin.isCore !== filters.isCore) {
return false;
}
if (filters.isEnterprise !== undefined && plugin.isEnterprise !== filters.isEnterprise) {
return false;
}
return true;
});
});
};
export const find = (searchBy: string, filterBy: string, filterByType: string) =>
createSelector(findPluginsByFilters({ state: filterBy, type: filterByType }), (filteredPlugins) =>
findByKeyword(filteredPlugins, searchBy)
);
export const selectPluginErrors = createSelector(selectAll, (plugins) =>
plugins