115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import { createSelector } from '@reduxjs/toolkit';
|
|
import { debounce } from 'lodash';
|
|
|
|
import { PluginError, PluginType, unEscapeStringFromRegex } from '@grafana/data';
|
|
import { reportInteraction } from '@grafana/runtime';
|
|
|
|
import { filterByKeyword, isPluginUpdatable } from '../helpers';
|
|
import { RequestStatus, PluginCatalogStoreState } from '../types';
|
|
|
|
import { pluginsAdapter } from './reducer';
|
|
|
|
export const selectRoot = (state: PluginCatalogStoreState) => state.plugins;
|
|
|
|
export const selectItems = createSelector(selectRoot, ({ items }) => items);
|
|
|
|
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
|
|
|
|
const debouncedTrackSearch = debounce((count) => {
|
|
reportInteraction('plugins_search', {
|
|
resultsCount: count,
|
|
creator_team: 'grafana_plugins_catalog',
|
|
schema_version: '1.0.0',
|
|
});
|
|
}, 300);
|
|
|
|
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;
|
|
|
|
// (Optional, only applied if set)
|
|
type?: PluginType;
|
|
|
|
// (Optional, only applied if set)
|
|
isInstalled?: boolean;
|
|
|
|
// (Optional, only applied if set)
|
|
isEnterprise?: boolean;
|
|
|
|
// (Optional, only applied if set)
|
|
hasUpdate?: boolean;
|
|
};
|
|
|
|
export const selectPlugins = (filters: PluginFilters) =>
|
|
createSelector(selectAll, (plugins) => {
|
|
const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';
|
|
// Fuzzy search does not consider plugin type filter
|
|
const filteredPluginIds = keyword !== '' ? filterByKeyword(plugins, keyword) : null;
|
|
|
|
// Filters are applied here
|
|
const filteredPlugins = plugins.filter((plugin) => {
|
|
if (keyword && filteredPluginIds == null) {
|
|
return false;
|
|
}
|
|
|
|
if (keyword && !filteredPluginIds?.includes(plugin.id)) {
|
|
return false;
|
|
}
|
|
|
|
if (filters.type && plugin.type !== filters.type) {
|
|
return false;
|
|
}
|
|
|
|
if (filters.isInstalled !== undefined && plugin.isInstalled !== filters.isInstalled) {
|
|
return false;
|
|
}
|
|
|
|
if (filters.isEnterprise !== undefined && plugin.isEnterprise !== filters.isEnterprise) {
|
|
return false;
|
|
}
|
|
|
|
if (filters.hasUpdate !== undefined && (plugin.hasUpdate !== filters.hasUpdate || !isPluginUpdatable(plugin))) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (keyword) {
|
|
debouncedTrackSearch(filteredPlugins.length);
|
|
}
|
|
|
|
return filteredPlugins;
|
|
});
|
|
|
|
export const selectPluginErrors = (filterByPluginType?: PluginType) =>
|
|
createSelector(selectAll, (plugins) => {
|
|
const pluginErrors: PluginError[] = [];
|
|
for (const plugin of plugins) {
|
|
if (plugin.error && (!filterByPluginType || plugin.type === filterByPluginType)) {
|
|
pluginErrors.push({
|
|
pluginId: plugin.id,
|
|
errorCode: plugin.error,
|
|
pluginType: plugin.type,
|
|
});
|
|
}
|
|
}
|
|
return pluginErrors;
|
|
});
|
|
|
|
// The following selectors are used to get information about the outstanding or completed plugins-related network requests.
|
|
export const selectRequest = (actionType: string) =>
|
|
createSelector(selectRoot, ({ requests = {} }) => requests[actionType]);
|
|
|
|
export const selectIsRequestPending = (actionType: string) =>
|
|
createSelector(selectRequest(actionType), (request) => request?.status === RequestStatus.Pending);
|
|
|
|
export const selectRequestError = (actionType: string) =>
|
|
createSelector(selectRequest(actionType), (request) =>
|
|
request?.status === RequestStatus.Rejected ? request?.error : null
|
|
);
|
|
|
|
export const selectIsRequestNotFetched = (actionType: string) =>
|
|
createSelector(selectRequest(actionType), (request) => request === undefined);
|