[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:
co-authored by
Levente Balogh
parent
2ae5c3dd78
commit
907c378cdf
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user