[v10.0.x] Connections: Show core datasource plugins as well (#67886)

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

fix: add a new hook & selector that can show core plugins by default
(cherry picked from commit 21459c7c97)

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
This commit is contained in:
Grot (@grafanabot)
2023-05-05 08:02:26 +00:00
committed by GitHub
co-authored by Levente Balogh
parent 2ae5c3dd78
commit 907c378cdf
5 changed files with 133 additions and 102 deletions
@@ -4,7 +4,7 @@ import React, { useMemo, useState } from 'react';
import { PluginType } from '@grafana/data'; import { PluginType } from '@grafana/data';
import { useStyles2, LoadingPlaceholder } from '@grafana/ui'; import { useStyles2, LoadingPlaceholder } from '@grafana/ui';
import { contextSrv } from 'app/core/core'; 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 { AccessControlAction } from 'app/types';
import { ROUTES } from '../../constants'; import { ROUTES } from '../../constants';
@@ -38,10 +38,9 @@ export function AddNewConnection() {
setSearchTerm(e.currentTarget.value.toLowerCase()); setSearchTerm(e.currentTarget.value.toLowerCase());
}; };
const { isLoading, error, plugins } = useGetAllWithFilters({ const { isLoading, error, plugins } = useGetAll({
query: searchTerm, keyword: searchTerm,
filterBy: '', type: PluginType.datasource,
filterByType: PluginType.datasource,
}); });
const cardGridItems = useMemo( const cardGridItems = useMemo(
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { useLocation } from 'react-router-dom'; 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 { config, locationSearchToObject } from '@grafana/runtime';
import { LoadingPlaceholder, Select, RadioButtonGroup, useStyles2, Tooltip, Field } from '@grafana/ui'; import { LoadingPlaceholder, Select, RadioButtonGroup, useStyles2, Tooltip, Field } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
@@ -16,7 +16,7 @@ import { PluginList } from '../components/PluginList';
import { SearchField } from '../components/SearchField'; import { SearchField } from '../components/SearchField';
import { Sorters } from '../helpers'; import { Sorters } from '../helpers';
import { useHistory } from '../hooks/useHistory'; import { useHistory } from '../hooks/useHistory';
import { useGetAllWithFilters, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks'; import { useGetAll, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks';
import { PluginListDisplayMode } from '../types'; import { PluginListDisplayMode } from '../types';
export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null { export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null {
@@ -27,16 +27,19 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const history = useHistory(); const history = useHistory();
const remotePluginsAvailable = useIsRemotePluginsAvailable(); const remotePluginsAvailable = useIsRemotePluginsAvailable();
const query = (locationSearch.q as string) || ''; const keyword = (locationSearch.q as string) || '';
const filterBy = (locationSearch.filterBy as string) || 'installed'; 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 sortBy = (locationSearch.sortBy as Sorters) || Sorters.nameAsc;
const { isLoading, error, plugins } = useGetAllWithFilters({ const { isLoading, error, plugins } = useGetAll(
query, {
filterBy, keyword,
filterByType, type: filterByType !== 'all' ? filterByType : undefined,
sortBy, 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 = [ const filterByOptions = [
{ value: 'all', label: 'All' }, { value: 'all', label: 'All' },
{ value: 'installed', label: 'Installed' }, { value: 'installed', label: 'Installed' },
@@ -81,7 +84,7 @@ export default function Browse({ route }: GrafanaRouteComponentProps): ReactElem
<Page.Contents> <Page.Contents>
<HorizontalGroup wrap> <HorizontalGroup wrap>
<Field label="Search"> <Field label="Search">
<SearchField value={query} onSearch={onSearch} /> <SearchField value={keyword} onSearch={onSearch} />
</Field> </Field>
<HorizontalGroup wrap className={styles.actionBar}> <HorizontalGroup wrap className={styles.actionBar}>
{/* Filter by type */} {/* Filter by type */}
@@ -1,4 +1,4 @@
import { useEffect } from 'react'; import { useEffect, useMemo } from 'react';
import { PluginError } from '@grafana/data'; import { PluginError } from '@grafana/data';
import { useDispatch, useSelector } from 'app/types'; 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 { fetchAll, fetchDetails, fetchRemotePlugins, install, uninstall, fetchAllLocal, unsetInstall } from './actions';
import { setDisplayMode } from './reducer'; import { setDisplayMode } from './reducer';
import { import {
find, selectPlugins,
selectAll,
selectById, selectById,
selectIsRequestPending, selectIsRequestPending,
selectRequestError, selectRequestError,
selectIsRequestNotFetched, selectIsRequestNotFetched,
selectDisplayMode, selectDisplayMode,
selectPluginErrors, selectPluginErrors,
type PluginFilters,
} from './selectors'; } from './selectors';
type Filters = { export const useGetAll = (filters: PluginFilters, sortBy: Sorters = Sorters.nameAsc) => {
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) => {
useFetchAll(); useFetchAll();
const filtered = useSelector(find(query, filterBy, filterByType)); const selector = useMemo(() => selectPlugins(filters), [filters]);
const plugins = useSelector(selector);
const { isLoading, error } = useFetchStatus(); const { isLoading, error } = useFetchStatus();
const sortedAndFiltered = sortPlugins(filtered, sortBy); const sortedPlugins = sortPlugins(plugins, sortBy);
return { return {
isLoading, isLoading,
error, error,
plugins: sortedAndFiltered, plugins: sortedPlugins,
}; };
}; };
export const useGetAll = (): CatalogPlugin[] => {
useFetchAll();
return useSelector(selectAll);
};
export const useGetSingle = (id: string): CatalogPlugin | undefined => { export const useGetSingle = (id: string): CatalogPlugin | undefined => {
useFetchAll(); useFetchAll();
useFetchDetails(id); useFetchDetails(id);
@@ -3,64 +3,100 @@ import { configureStore } from 'app/store/configureStore';
import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__'; import { getCatalogPluginMock, getPluginsStateMock } from '../__mocks__';
import { find } from './selectors'; import { selectPlugins } from './selectors';
describe('Plugins Selectors', () => { describe('Plugins Selectors', () => {
describe('find()', () => { describe('selectPlugins()', () => {
const store = configureStore({ const store = configureStore({
plugins: getPluginsStateMock([ plugins: getPluginsStateMock([
getCatalogPluginMock({ id: 'plugin-1', name: 'Plugin 1', isInstalled: true, type: PluginType.datasource }), getCatalogPluginMock({
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: true, type: PluginType.datasource }), id: 'plugin-1',
getCatalogPluginMock({ id: 'plugin-3', name: 'Plugin 3', isInstalled: true, type: PluginType.panel }), name: 'Plugin 1',
getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: false, type: PluginType.panel }), isInstalled: true,
getCatalogPluginMock({ id: 'plugin-5', name: 'Plugin 5', isInstalled: true, type: PluginType.app }), 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', () => { it('should return all plugins if there are no filters', () => {
const query = ''; const results = selectPlugins({})(store.getState());
const filterBy = 'all';
const filterByType = 'all';
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(5); expect(results).toHaveLength(5);
}); });
it('should be possible to search only by the "query"', () => { it('should be possible to search only by the "keyword"', () => {
const query = 'Plugin 3'; const results = selectPlugins({ keyword: 'Plugin 3' })(store.getState());
const filterBy = 'all';
const filterByType = 'all';
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(1); expect(results).toHaveLength(1);
expect(results[0].name).toBe('Plugin 3'); expect(results[0].name).toBe('Plugin 3');
}); });
it('should be possible to search by plugin type', () => { it('should be possible to search by plugin type', () => {
const query = ''; const results = selectPlugins({ type: PluginType.panel })(store.getState());
const filterBy = 'all';
const filterByType = PluginType.panel;
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(2); expect(results).toHaveLength(2);
expect(results.map(({ name }) => name)).toEqual(['Plugin 3', 'Plugin 4']); expect(results.map(({ name }) => name)).toEqual(['Plugin 3', 'Plugin 4']);
}); });
it('should be possible to search by plugin state (installed / all)', () => { it('should be possible to search for core plugins', () => {
const query = ''; const results = selectPlugins({ isCore: true })(store.getState());
const filterBy = 'installed';
const filterByType = 'all'; expect(results).toHaveLength(2);
const results = find(query, filterBy, filterByType)(store.getState()); 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).toHaveLength(4);
expect(results.map(({ name }) => name)).toEqual(['Plugin 1', 'Plugin 2', 'Plugin 3', 'Plugin 5']); 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', () => { it('should be possible to search by multiple filters', () => {
const query = '2'; const results = selectPlugins({ keyword: '2', type: PluginType.datasource })(store.getState());
const filterBy = 'all';
const filterByType = PluginType.datasource;
const results = find(query, filterBy, filterByType)(store.getState());
expect(results).toHaveLength(1); expect(results).toHaveLength(1);
expect(results[0].name).toBe('Plugin 2'); expect(results[0].name).toBe('Plugin 2');
@@ -1,8 +1,8 @@
import { createSelector } from '@reduxjs/toolkit'; 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'; import { pluginsAdapter } from './reducer';
@@ -14,44 +14,54 @@ export const selectDisplayMode = createSelector(selectRoot, ({ settings }) => se
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems); export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
const findByState = (state: string) => export type PluginFilters = {
createSelector(selectAll, (plugins) => // Searches for a string in certain fields (e.g. "name" or "orgName")
plugins.filter((plugin) => (state === 'installed' ? plugin.isInstalled : !plugin.isCore)) // (Note: this will be an escaped regex string as it comes from `FilterInput`)
); keyword?: string;
type PluginFilters = { // (Optional, only applied if set)
state: string; type?: PluginType;
type: string;
// (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) => export const selectPlugins = (filters: PluginFilters) =>
createSelector(findByState(filters.state), (plugins) => createSelector(selectAll, (plugins) => {
plugins.filter((plugin) => filters.type === 'all' || plugin.type === filters.type) const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';
);
const findByKeyword = (plugins: CatalogPlugin[], query: string) => { return plugins.filter((plugin) => {
if (query === '') { const fieldsToSearchIn = [plugin.name, plugin.orgName].filter(Boolean).map((f) => f.toLowerCase());
return plugins;
}
return plugins.filter((plugin) => { if (keyword && !fieldsToSearchIn.some((f) => f.includes(keyword))) {
const fields: String[] = []; return false;
if (plugin.name) { }
fields.push(plugin.name.toLowerCase());
}
if (plugin.orgName) { if (filters.type && plugin.type !== filters.type) {
fields.push(plugin.orgName.toLowerCase()); 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) => export const selectPluginErrors = createSelector(selectAll, (plugins) =>
plugins plugins