mirror of
https://github.com/grafana/grafana.git
synced 2025-02-12 16:45:43 -06:00
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:
parent
e7cbe0276e
commit
21459c7c97
@ -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(
|
||||
|
@ -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 */}
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user