feat(dynamic command palette api): moving the code to command palette folder

This commit is contained in:
kozhuhds
2025-11-21 16:04:59 +01:00
parent f987a33870
commit 688c51a306
16 changed files with 148 additions and 1197 deletions
+31 -10
View File
@@ -23,10 +23,11 @@ temp_data_lifetime = 24h
logs = data/log
# Directory where grafana will automatically scan and look for plugins
plugins = data/plugins
plugins = ../asserts-app-plugin
# folder that contains provisioning config files that grafana will apply on startup and while running.
provisioning = conf/provisioning
provisioning = ../asserts-app-plugin/provisioning
# Directories that are permitted to contain local repositories.
# This is a list. Each entry is delimited by a pipe (|). No leading or trailing spaces are supported.
@@ -69,7 +70,7 @@ router_logging = false
static_root_path = public
# enable gzip
enable_gzip = false
enable_gzip = true
# https certs & key file
cert_file =
@@ -394,10 +395,10 @@ disable_ip_address_login_protection = true
cookie_secure = false
# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled"
cookie_samesite = lax
cookie_samesite = disabled
# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
allow_embedding = false
allow_embedding = true
# Set to true if you want to enable http strict transport security (HSTS) response header.
# HSTS tells browsers that the site should only be accessed using HTTPS.
@@ -675,7 +676,7 @@ id_response_header_namespaces = user api-key service-account
# Enables the use of managed service accounts for plugin authentication
# This feature currently **only supports single-organization deployments**
managed_service_accounts_enabled = false
managed_service_accounts_enabled = true
#################################### Passwordless Auth ###########################
[auth.passwordless]
@@ -1885,6 +1886,7 @@ renderer_token = -
# Concurrent render request limit affects when the /render HTTP endpoint is used. Rendering many images at the same time can overload the server,
# which this setting can help protect against by only allowing a certain amount of concurrent requests.
concurrent_render_request_limit = 30
# Determines the lifetime of the render key used by the image renderer to access and render Grafana.
# This setting should be expressed as a duration. Examples: 10s (seconds), 5m (minutes), 2h (hours).
# Default is 5m. This should be more than enough for most deployments.
@@ -1906,7 +1908,7 @@ disable_sanitize_html = false
enable_alpha = false
app_tls_skip_verify_insecure = false
# Enter a comma-separated list of plugin identifiers to identify plugins to load even if they are unsigned. Plugins with modified signatures are never loaded.
allow_loading_unsigned_plugins =
allow_loading_unsigned_plugins = grafana-asserts-app,grafana-pyroscope-app,grafana-lokiexplore-app,grafana-assistant-app
# Enable or disable installing / uninstalling / updating plugins directly from within Grafana.
plugin_admin_enabled = true
plugin_admin_external_manage_enabled = false
@@ -1926,11 +1928,11 @@ disable_plugins =
forward_host_env_vars =
# Comma separated list of plugin ids to install as part of the startup process.
# These will be installed, by default, asynchronously (in the background) while starting Grafana.
preinstall =
preinstall =
# Comma separated list of plugin ids to install before the startup process
# These will be installed before starting Grafana. Useful when used with provisioning.
preinstall_sync =
# Disables preinstall feature. It has the same effect as setting preinstall to an empty list.
; preinstall_sync = grafana-assistant-app
# Disables preinstall feature. It has the same effect as setting preinstall to an empty list.
preinstall_disabled = false
# Update strategy for plugins.
# Available options: "latest", "minor"
@@ -2039,6 +2041,25 @@ grpc_port =
license_path =
[feature_toggles]
dockedMegaMenu = true
accessControlOnCall = true
idForwarding = true
externalServiceAccounts = true
assertsOnboarding = true
assertsOnboardingV2 = true
relabelRulesUi = true
workbenchAi = true
externalEntityRings = true
# appO11yInAsserts = true
extensionSidebar = true
grafanaAssistantLoop = true
dashboardNewLayouts = true
dashboardScene = true
dashboardSceneForViewers = true
kubernetesDashboards = true
kubernetesDashboardsAPI = true
workbenchAiAssistant = true
# there are currently two ways to enable feature toggles in the `grafana.ini`.
# you can either pass an array of feature you want to enable to the `enable` field or
# configure each toggle by setting the name of the toggle to true/false. Toggles set to true/false
@@ -16,11 +16,8 @@ import { KBarSearch } from './KBarSearch';
import { ResultItem } from './ResultItem';
import { useSearchResults } from './actions/dashboardActions';
import { useRegisterRecentScopesActions, useRegisterScopesActions } from './actions/scopeActions';
import {
useRegisterDynamicActions,
useRegisterRecentDashboardsActions,
useRegisterStaticActions,
} from './actions/useActions';
import { useRegisterRecentDashboardsActions, useRegisterStaticActions } from './actions/useActions';
import { useDynamicExtensionResults } from './actions/useDynamicExtensionActions';
import { CommandPaletteAction } from './types';
import { useMatches } from './useMatches';
@@ -51,12 +48,13 @@ function CommandPaletteContents() {
useRegisterRecentDashboardsActions();
useRegisterRecentScopesActions();
// Register dynamic actions from plugins based on search query
const { isLoading: isDynamicLoading } = useRegisterDynamicActions(searchQuery);
const queryToggle = useCallback(() => query.toggle(), [query]);
const { scopesRow } = useRegisterScopesActions(searchQuery, queryToggle, currentRootActionId);
// Fetch dynamic results from plugins - these bypass kbar's fuzzy filtering
// since they're already filtered by the plugin's searchProvider
const { results: dynamicResults, isLoading: isDynamicLoading } = useDynamicExtensionResults(searchQuery);
// This searches dashboards and folders it shows only if we are not in some specific category (and there is no
// dashboards category right now, so if any category is selected, we don't show these).
// Normally we register actions with kbar, and it knows not to show actions which are under a different parent than is
@@ -95,7 +93,11 @@ function CommandPaletteContents() {
</div>
{scopesRow ? <div className={styles.searchContainer}>{scopesRow}</div> : null}
<div className={styles.resultsContainer}>
<RenderResults isFetchingSearchResults={isFetchingSearchResults} searchResults={searchResults} />
<RenderResults
isFetchingSearchResults={isFetchingSearchResults}
searchResults={searchResults}
dynamicResults={dynamicResults}
/>
</div>
</div>
</FocusScope>
@@ -138,9 +140,10 @@ function AncestorBreadcrumbs() {
interface RenderResultsProps {
isFetchingSearchResults: boolean;
searchResults: CommandPaletteAction[];
dynamicResults: Array<{ section: string; items: ActionImpl[] }>;
}
const RenderResults = ({ isFetchingSearchResults, searchResults }: RenderResultsProps) => {
const RenderResults = ({ isFetchingSearchResults, searchResults, dynamicResults }: RenderResultsProps) => {
const { results: kbarResults, rootActionId } = useMatches();
const lateralSpace = getCommandPalettePosition();
const styles = useStyles2(getSearchStyles, lateralSpace);
@@ -166,6 +169,16 @@ const RenderResults = ({ isFetchingSearchResults, searchResults }: RenderResults
const items = useMemo(() => {
const results = [...kbarResults];
// Add dynamic results from plugins (already filtered by searchProvider)
dynamicResults.forEach(({ section, items }) => {
if (items.length > 0) {
results.push(section);
results.push(...items);
}
});
// Add dashboard and folder search results
if (folderResultItems.length > 0) {
results.push(foldersSectionTitle);
results.push(...folderResultItems);
@@ -175,7 +188,14 @@ const RenderResults = ({ isFetchingSearchResults, searchResults }: RenderResults
results.push(...dashboardResultItems);
}
return results;
}, [kbarResults, dashboardsSectionTitle, dashboardResultItems, foldersSectionTitle, folderResultItems]);
}, [
kbarResults,
dynamicResults,
dashboardsSectionTitle,
dashboardResultItems,
foldersSectionTitle,
folderResultItems,
]);
const showEmptyState = !isFetchingSearchResults && items.length === 0;
useEffect(() => {
@@ -5,7 +5,6 @@ import { CommandPaletteAction } from '../types';
import { getRecentDashboardActions } from './dashboardActions';
import { useStaticActions } from './staticActions';
import { useDynamicExtensionActions } from './useDynamicExtensionActions';
import useExtensionActions from './useExtensionActions';
/**
@@ -34,15 +33,3 @@ export function useRegisterRecentDashboardsActions() {
useRegisterActions(recentDashboardActions, [recentDashboardActions]);
}
/**
* Register dynamic actions based on search query
* These are fetched from plugins that registered dynamic search providers
*/
export function useRegisterDynamicActions(searchQuery: string) {
const { actions: dynamicActions, isLoading } = useDynamicExtensionActions(searchQuery);
useRegisterActions(dynamicActions, [dynamicActions]);
return { isLoading };
}
@@ -1,3 +1,4 @@
import { ActionImpl } from 'kbar';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useDebounce } from 'react-use';
@@ -9,8 +10,8 @@ import {
import { appEvents } from 'app/core/app_events';
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events';
import { commandPaletteDynamicRegistry } from '../../plugins/extensions/registry/setup';
import { createOpenModalFunction } from '../../plugins/extensions/utils';
import { commandPaletteDynamicRegistry, CommandPaletteDynamicSearchResult } from '../CommandPaletteDynamicRegistry';
import { CommandPaletteAction } from '../types';
import { EXTENSIONS_PRIORITY } from '../values';
@@ -19,8 +20,18 @@ interface DynamicResultWithAction extends CommandPaletteDynamicResult {
onSelect?: CommandPaletteDynamicResultAction;
}
export function useDynamicExtensionActions(searchQuery: string): {
actions: CommandPaletteAction[];
export interface DynamicExtensionResultGroup {
section: string;
items: ActionImpl[];
}
/**
* Fetches dynamic results from plugin extensions without registering them with kbar.
* This allows the results to bypass kbar's fuzzy filtering since they're already
* filtered by the plugin's searchProvider function.
*/
export function useDynamicExtensionResults(searchQuery: string): {
results: DynamicExtensionResultGroup[];
isLoading: boolean;
} {
const [dynamicResults, setDynamicResults] = useState<DynamicResultWithAction[]>([]);
@@ -65,15 +76,15 @@ export function useDynamicExtensionActions(searchQuery: string): {
// Execute search across all registered providers
commandPaletteDynamicRegistry
.search(context)
.then((resultsMap) => {
.then((resultsMap: Map<string, CommandPaletteDynamicSearchResult>) => {
if (abortController.signal.aborted) {
return;
}
const allResults: DynamicResultWithAction[] = [];
resultsMap.forEach(({ items, config }) => {
items.forEach((item) => {
resultsMap.forEach(({ items, config }: CommandPaletteDynamicSearchResult) => {
items.forEach((item: CommandPaletteDynamicResult) => {
allResults.push({
...item,
pluginId: config.pluginId,
@@ -86,7 +97,7 @@ export function useDynamicExtensionActions(searchQuery: string): {
setDynamicResults(allResults);
setIsLoading(false);
})
.catch((error) => {
.catch((error: unknown) => {
if (!abortController.signal.aborted) {
console.error('[CommandPalette] Dynamic search failed:', error);
setDynamicResults([]);
@@ -100,55 +111,74 @@ export function useDynamicExtensionActions(searchQuery: string): {
};
}, [debouncedSearchQuery]);
const actions: CommandPaletteAction[] = useMemo(() => {
return dynamicResults.map((result) => ({
id: `dynamic-${result.pluginId}-${result.id}`,
name: result.title,
section: result.section ?? 'Dynamic Results',
subtitle: result.description,
priority: EXTENSIONS_PRIORITY - 0.5, // Slightly lower than static extensions
keywords: result.keywords?.join(' '),
perform: () => {
if (result.onSelect) {
const extensionPointId = 'grafana/commandpalette/action';
result.onSelect(result, {
context: { searchQuery: debouncedSearchQuery },
extensionPointId,
openModal: createOpenModalFunction({
pluginId: result.pluginId,
title: result.title,
description: result.description,
// Group results by section and convert to ActionImpl objects
const results: DynamicExtensionResultGroup[] = useMemo(() => {
const groups = new Map<string, CommandPaletteAction[]>();
dynamicResults.forEach((result) => {
const section = result.section ?? 'Dynamic Results';
if (!groups.has(section)) {
groups.set(section, []);
}
const action: CommandPaletteAction = {
id: `dynamic-${result.pluginId}-${result.id}`,
name: result.title,
section,
subtitle: result.description,
priority: EXTENSIONS_PRIORITY - 0.5,
keywords: result.keywords?.join(' '),
perform: () => {
if (result.onSelect) {
const extensionPointId = 'grafana/commandpalette/action';
result.onSelect(result, {
context: { searchQuery: debouncedSearchQuery },
extensionPointId,
path: result.path,
category: result.section,
}),
openSidebar: (componentTitle, context) => {
appEvents.publish(
new OpenExtensionSidebarEvent({
props: context,
pluginId: result.pluginId,
componentTitle,
})
);
},
closeSidebar: () => {
appEvents.publish(new CloseExtensionSidebarEvent());
},
toggleSidebar: (componentTitle, context) => {
appEvents.publish(
new ToggleExtensionSidebarEvent({
props: context,
pluginId: result.pluginId,
componentTitle,
})
);
},
});
}
},
url: result.path,
openModal: createOpenModalFunction({
pluginId: result.pluginId,
title: result.title,
description: result.description,
extensionPointId,
path: result.path,
category: result.section,
}),
openSidebar: (componentTitle, context) => {
appEvents.publish(
new OpenExtensionSidebarEvent({
props: context,
pluginId: result.pluginId,
componentTitle,
})
);
},
closeSidebar: () => {
appEvents.publish(new CloseExtensionSidebarEvent());
},
toggleSidebar: (componentTitle, context) => {
appEvents.publish(
new ToggleExtensionSidebarEvent({
props: context,
pluginId: result.pluginId,
componentTitle,
})
);
},
});
}
},
url: result.path,
};
groups.get(section)!.push(action);
});
// Convert to array of groups with ActionImpl objects
return Array.from(groups.entries()).map(([section, actions]) => ({
section,
items: actions.map((action) => new ActionImpl(action, { store: {} })),
}));
}, [dynamicResults, debouncedSearchQuery]);
return { actions, isLoading };
return { results, isLoading };
}
@@ -14,7 +14,6 @@ import { ExtensionRegistriesProvider } from '../extensions/ExtensionRegistriesCo
import { AddedComponentsRegistry } from '../extensions/registry/AddedComponentsRegistry';
import { AddedFunctionsRegistry } from '../extensions/registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from '../extensions/registry/AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from '../extensions/registry/CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from '../extensions/registry/ExposedComponentsRegistry';
import { pluginImporter } from '../importer/pluginImporter';
import { getPluginSettings } from '../pluginSettings';
@@ -94,7 +93,6 @@ function renderUnderRouter(page = '') {
exposedComponentsRegistry: new ExposedComponentsRegistry(),
addedLinksRegistry: new AddedLinksRegistry(),
addedFunctionsRegistry: new AddedFunctionsRegistry(),
commandPaletteDynamicRegistry: new CommandPaletteDynamicRegistry(),
};
const pagePath = page ? `/${page}` : '';
const route = {
@@ -31,7 +31,6 @@ import {
useAddedComponentsRegistry,
useExposedComponentsRegistry,
useAddedFunctionsRegistry,
useCommandPaletteDynamicRegistry,
} from '../extensions/ExtensionRegistriesContext';
import { pluginImporter } from '../importer/pluginImporter';
import { getPluginSettings } from '../pluginSettings';
@@ -66,7 +65,6 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
const addedComponentsRegistry = useAddedComponentsRegistry();
const exposedComponentsRegistry = useExposedComponentsRegistry();
const addedFunctionsRegistry = useAddedFunctionsRegistry();
const commandPaletteDynamicRegistry = useCommandPaletteDynamicRegistry();
const location = useLocation();
const [state, dispatch] = useReducer(stateSlice.reducer, initialState);
const currentUrl = config.appSubUrl + location.pathname + location.search;
@@ -126,7 +124,6 @@ export function AppRootPage({ pluginId, pluginNavSection }: Props) {
addedComponentsRegistry: addedComponentsRegistry.readOnly(),
exposedComponentsRegistry: exposedComponentsRegistry.readOnly(),
addedFunctionsRegistry: addedFunctionsRegistry.readOnly(),
commandPaletteDynamicRegistry: commandPaletteDynamicRegistry.readOnly(),
}}
>
<plugin.root
@@ -3,7 +3,6 @@ import { PropsWithChildren, createContext, useContext } from 'react';
import { AddedComponentsRegistry } from 'app/features/plugins/extensions/registry/AddedComponentsRegistry';
import { AddedFunctionsRegistry } from 'app/features/plugins/extensions/registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from 'app/features/plugins/extensions/registry/AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from 'app/features/plugins/extensions/registry/CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from 'app/features/plugins/extensions/registry/ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './registry/types';
@@ -17,7 +16,6 @@ export const AddedLinksRegistryContext = createContext<AddedLinksRegistry | unde
export const AddedComponentsRegistryContext = createContext<AddedComponentsRegistry | undefined>(undefined);
export const AddedFunctionsRegistryContext = createContext<AddedFunctionsRegistry | undefined>(undefined);
export const ExposedComponentsRegistryContext = createContext<ExposedComponentsRegistry | undefined>(undefined);
export const CommandPaletteDynamicRegistryContext = createContext<CommandPaletteDynamicRegistry | undefined>(undefined);
export function useAddedLinksRegistry(): AddedLinksRegistry {
const context = useContext(AddedLinksRegistryContext);
@@ -51,14 +49,6 @@ export function useExposedComponentsRegistry(): ExposedComponentsRegistry {
return context;
}
export function useCommandPaletteDynamicRegistry(): CommandPaletteDynamicRegistry {
const context = useContext(CommandPaletteDynamicRegistryContext);
if (!context) {
throw new Error('No `CommandPaletteDynamicRegistryContext` found.');
}
return context;
}
export const ExtensionRegistriesProvider = ({
registries,
children,
@@ -68,9 +58,7 @@ export const ExtensionRegistriesProvider = ({
<AddedComponentsRegistryContext.Provider value={registries.addedComponentsRegistry}>
<AddedFunctionsRegistryContext.Provider value={registries.addedFunctionsRegistry}>
<ExposedComponentsRegistryContext.Provider value={registries.exposedComponentsRegistry}>
<CommandPaletteDynamicRegistryContext.Provider value={registries.commandPaletteDynamicRegistry}>
{children}
</CommandPaletteDynamicRegistryContext.Provider>
{children}
</ExposedComponentsRegistryContext.Provider>
</AddedFunctionsRegistryContext.Provider>
</AddedComponentsRegistryContext.Provider>
@@ -1,922 +0,0 @@
import { firstValueFrom } from 'rxjs';
import { PluginExtensionCommandPaletteContext } from '@grafana/data';
import { log } from '../logs/log';
import { resetLogMock } from '../logs/testUtils';
import { isGrafanaDevMode } from '../utils';
import { CommandPaletteDynamicRegistry } from './CommandPaletteDynamicRegistry';
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
jest.mock('../utils', () => ({
...jest.requireActual('../utils'),
isGrafanaDevMode: jest.fn().mockReturnValue(false),
}));
jest.mock('../logs/log', () => {
const { createLogMock } = jest.requireActual('../logs/testUtils');
const original = jest.requireActual('../logs/log');
return {
...original,
log: createLogMock(),
};
});
describe('CommandPaletteDynamicRegistry', () => {
const pluginId = 'test-plugin';
beforeEach(() => {
resetLogMock(log);
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
});
describe('Registry Management', () => {
it('should return empty registry when no extensions registered', async () => {
const registry = new CommandPaletteDynamicRegistry();
const observable = registry.asObservable();
const state = await firstValueFrom(observable);
expect(state).toEqual({});
});
it('should be possible to register command palette dynamic providers', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const state = await registry.getState();
expect(state).toEqual({
[`${pluginId}/Test Provider`]: [
{
pluginId,
config: {
title: 'Test Provider',
searchProvider: mockSearchProvider,
category: pluginId,
minQueryLength: 2,
debounceMs: 300,
},
},
],
});
});
it('should apply default values for optional config properties', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const state = await registry.getState();
const item = state[`${pluginId}/Test Provider`][0];
expect(item.config.category).toBe(pluginId);
expect(item.config.minQueryLength).toBe(2);
expect(item.config.debounceMs).toBe(300);
});
it('should preserve custom config values when provided', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Custom Provider',
searchProvider: mockSearchProvider,
category: 'Custom Category',
minQueryLength: 5,
debounceMs: 500,
},
],
});
const state = await registry.getState();
const item = state[`${pluginId}/Custom Provider`][0];
expect(item.config.category).toBe('Custom Category');
expect(item.config.minQueryLength).toBe(5);
expect(item.config.debounceMs).toBe(500);
});
it('should register multiple providers from the same plugin', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider1 = jest.fn().mockResolvedValue([]);
const mockSearchProvider2 = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Provider 1',
searchProvider: mockSearchProvider1,
},
{
title: 'Provider 2',
searchProvider: mockSearchProvider2,
},
],
});
const state = await registry.getState();
expect(Object.keys(state)).toHaveLength(2);
expect(state[`${pluginId}/Provider 1`]).toBeDefined();
expect(state[`${pluginId}/Provider 2`]).toBeDefined();
});
it('should notify subscribers when the registry changes', async () => {
const registry = new CommandPaletteDynamicRegistry();
const observable = registry.asObservable();
const subscribeCallback = jest.fn();
observable.subscribe(subscribeCallback);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: jest.fn().mockResolvedValue([]),
},
],
});
expect(subscribeCallback).toHaveBeenCalledTimes(2); // initial empty state + registration
});
it('should not be possible to register on a read-only registry', async () => {
const registry = new CommandPaletteDynamicRegistry();
const readOnlyRegistry = new CommandPaletteDynamicRegistry({
registrySubject: registry['registrySubject'],
});
expect(() => {
readOnlyRegistry.register({
pluginId,
configs: [
{
title: 'Test',
searchProvider: jest.fn(),
},
],
});
}).toThrow(MSG_CANNOT_REGISTER_READ_ONLY);
});
it('should create a read-only version of the registry', async () => {
const registry = new CommandPaletteDynamicRegistry();
const readOnlyRegistry = registry.readOnly();
expect(() => {
readOnlyRegistry.register({
pluginId,
configs: [
{
title: 'Test',
searchProvider: jest.fn(),
},
],
});
}).toThrow(MSG_CANNOT_REGISTER_READ_ONLY);
const currentState = await readOnlyRegistry.getState();
expect(Object.keys(currentState)).toHaveLength(0);
});
it('should pass down fresh registrations to the read-only version of the registry', async () => {
const registry = new CommandPaletteDynamicRegistry();
const readOnlyRegistry = registry.readOnly();
const subscribeCallback = jest.fn();
let readOnlyState;
// Should have no providers registered in the beginning
readOnlyState = await readOnlyRegistry.getState();
expect(Object.keys(readOnlyState)).toHaveLength(0);
readOnlyRegistry.asObservable().subscribe(subscribeCallback);
// Register a provider to the original (writable) registry
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: jest.fn().mockResolvedValue([]),
},
],
});
// The read-only registry should have received the new provider
readOnlyState = await readOnlyRegistry.getState();
expect(Object.keys(readOnlyState)).toHaveLength(1);
expect(subscribeCallback).toHaveBeenCalledTimes(2); // initial empty + registration
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual([`${pluginId}/Test Provider`]);
});
});
describe('Config Validation', () => {
it('should not register provider without title', async () => {
const registry = new CommandPaletteDynamicRegistry();
registry.register({
pluginId,
configs: [
{
// @ts-ignore - testing invalid config
title: '',
searchProvider: jest.fn(),
},
],
});
const state = await registry.getState();
expect(Object.keys(state)).toHaveLength(0);
expect(log.error).toHaveBeenCalled();
});
it('should not register provider with non-string title', async () => {
const registry = new CommandPaletteDynamicRegistry();
registry.register({
pluginId,
configs: [
{
// @ts-ignore - testing invalid config
title: 123,
searchProvider: jest.fn(),
},
],
});
const state = await registry.getState();
expect(Object.keys(state)).toHaveLength(0);
expect(log.error).toHaveBeenCalled();
});
it('should not register provider without searchProvider', async () => {
const registry = new CommandPaletteDynamicRegistry();
registry.register({
pluginId,
configs: [
{
title: 'Test',
// @ts-ignore - testing invalid config
searchProvider: undefined,
},
],
});
const state = await registry.getState();
expect(Object.keys(state)).toHaveLength(0);
expect(log.error).toHaveBeenCalled();
});
it('should not register provider with non-function searchProvider', async () => {
const registry = new CommandPaletteDynamicRegistry();
registry.register({
pluginId,
configs: [
{
title: 'Test',
// @ts-ignore - testing invalid config
searchProvider: 'not-a-function',
},
],
});
const state = await registry.getState();
expect(Object.keys(state)).toHaveLength(0);
expect(log.error).toHaveBeenCalled();
});
it('should log provider registration in dev mode', async () => {
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
const registry = new CommandPaletteDynamicRegistry();
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: jest.fn().mockResolvedValue([]),
},
],
});
await registry.getState();
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Registered provider: test-plugin/Test Provider')
);
consoleLogSpy.mockRestore();
});
});
describe('Search Functionality', () => {
it('should execute search across all registered providers', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider1 = jest.fn().mockResolvedValue([{ id: 'result1', title: 'Result 1' }]);
const mockSearchProvider2 = jest.fn().mockResolvedValue([{ id: 'result2', title: 'Result 2' }]);
registry.register({
pluginId: 'plugin1',
configs: [
{
title: 'Provider 1',
searchProvider: mockSearchProvider1,
},
],
});
registry.register({
pluginId: 'plugin2',
configs: [
{
title: 'Provider 2',
searchProvider: mockSearchProvider2,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test query',
};
const results = await registry.search(context);
expect(mockSearchProvider1).toHaveBeenCalledWith(context);
expect(mockSearchProvider2).toHaveBeenCalledWith(context);
expect(results.size).toBe(2);
});
it('should pass context with AbortSignal to search providers', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const abortController = new AbortController();
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
signal: abortController.signal,
};
await registry.search(context);
expect(mockSearchProvider).toHaveBeenCalledWith(
expect.objectContaining({
searchQuery: 'test',
signal: expect.any(Object),
})
);
});
it('should not search when query is below minimum length', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
minQueryLength: 3,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'ab', // Only 2 characters
};
await registry.search(context);
expect(mockSearchProvider).not.toHaveBeenCalled();
});
it('should search when query meets minimum length', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
minQueryLength: 2,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'ab', // Exactly 2 characters
};
await registry.search(context);
expect(mockSearchProvider).toHaveBeenCalled();
});
it('should skip inactive providers', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
const isActiveFn = jest.fn().mockReturnValue(false);
registry.register({
pluginId,
configs: [
{
title: 'Inactive Provider',
searchProvider: mockSearchProvider,
isActive: isActiveFn,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
await registry.search(context);
expect(isActiveFn).toHaveBeenCalledWith(context);
expect(mockSearchProvider).not.toHaveBeenCalled();
});
it('should execute search for active providers', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
const isActiveFn = jest.fn().mockReturnValue(true);
registry.register({
pluginId,
configs: [
{
title: 'Active Provider',
searchProvider: mockSearchProvider,
isActive: isActiveFn,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
await registry.search(context);
expect(isActiveFn).toHaveBeenCalledWith(context);
expect(mockSearchProvider).toHaveBeenCalled();
});
it('should limit results to 5 items per provider', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
{ id: '1', title: 'Result 1' },
{ id: '2', title: 'Result 2' },
{ id: '3', title: 'Result 3' },
{ id: '4', title: 'Result 4' },
{ id: '5', title: 'Result 5' },
{ id: '6', title: 'Result 6' },
{ id: '7', title: 'Result 7' },
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.items).toHaveLength(5);
expect(searchResult?.items[4].id).toBe('5');
});
});
describe('Result Validation', () => {
it('should filter out results without id', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
{ id: 'valid', title: 'Valid Result' },
// @ts-ignore - testing invalid result
{ title: 'Invalid Result' },
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.items).toHaveLength(1);
expect(searchResult?.items[0].id).toBe('valid');
expect(log.warning).toHaveBeenCalled();
});
it('should filter out results with non-string id', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
{ id: 'valid', title: 'Valid Result' },
// @ts-ignore - testing invalid result
{ id: 123, title: 'Invalid Result' },
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.items).toHaveLength(1);
expect(searchResult?.items[0].id).toBe('valid');
expect(log.warning).toHaveBeenCalled();
});
it('should filter out results without title', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
{ id: 'valid', title: 'Valid Result' },
// @ts-ignore - testing invalid result
{ id: 'invalid' },
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.items).toHaveLength(1);
expect(searchResult?.items[0].id).toBe('valid');
expect(log.warning).toHaveBeenCalled();
});
it('should filter out results with non-string title', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
{ id: 'valid', title: 'Valid Result' },
// @ts-ignore - testing invalid result
{ id: 'invalid', title: 123 },
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.items).toHaveLength(1);
expect(searchResult?.items[0].id).toBe('valid');
expect(log.warning).toHaveBeenCalled();
});
it('should not include provider in results if no valid items', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
// @ts-ignore - testing invalid results
{ id: 'invalid' }, // missing title
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
expect(results.size).toBe(0);
});
it('should warn if provider returns non-array', async () => {
const registry = new CommandPaletteDynamicRegistry();
// @ts-ignore - testing invalid return value
const mockSearchProvider = jest.fn().mockResolvedValue('not-an-array');
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
expect(results.size).toBe(0);
expect(log.warning).toHaveBeenCalled();
});
});
describe('Error Handling', () => {
it('should handle search provider errors gracefully', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockRejectedValue(new Error('Search failed'));
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
expect(results.size).toBe(0);
expect(log.error).toHaveBeenCalledWith(
expect.stringContaining('Search failed'),
expect.objectContaining({
error: 'Error: Search failed',
})
);
});
it('should not log AbortErrors', async () => {
const registry = new CommandPaletteDynamicRegistry();
const abortError = new Error('Aborted');
abortError.name = 'AbortError';
const mockSearchProvider = jest.fn().mockRejectedValue(abortError);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
await registry.search(context);
expect(log.error).not.toHaveBeenCalled();
});
it('should continue searching other providers if one fails', async () => {
const registry = new CommandPaletteDynamicRegistry();
const failingProvider = jest.fn().mockRejectedValue(new Error('Failed'));
const successProvider = jest.fn().mockResolvedValue([{ id: 'success', title: 'Success Result' }]);
registry.register({
pluginId: 'failing-plugin',
configs: [
{
title: 'Failing Provider',
searchProvider: failingProvider,
},
],
});
registry.register({
pluginId: 'success-plugin',
configs: [
{
title: 'Success Provider',
searchProvider: successProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
expect(results.size).toBe(1);
expect(results.get('success-plugin/Success Provider')).toBeDefined();
});
});
describe('Search Context', () => {
it('should handle empty search query', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
minQueryLength: 0,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: '',
};
await registry.search(context);
expect(mockSearchProvider).toHaveBeenCalled();
});
it('should handle undefined search query', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
minQueryLength: 0,
},
],
});
const context: PluginExtensionCommandPaletteContext = {};
await registry.search(context);
expect(mockSearchProvider).toHaveBeenCalled();
});
});
describe('Result Structure', () => {
it('should return results with correct structure', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([
{
id: 'test-id',
title: 'Test Title',
description: 'Test Description',
path: '/test/path',
keywords: ['test', 'keyword'],
section: 'Test Section',
data: { custom: 'data' },
},
]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.items[0]).toEqual({
id: 'test-id',
title: 'Test Title',
description: 'Test Description',
path: '/test/path',
keywords: ['test', 'keyword'],
section: 'Test Section',
data: { custom: 'data' },
});
});
it('should include config information in search result', async () => {
const registry = new CommandPaletteDynamicRegistry();
const mockSearchProvider = jest.fn().mockResolvedValue([{ id: 'test', title: 'Test' }]);
registry.register({
pluginId,
configs: [
{
title: 'Test Provider',
searchProvider: mockSearchProvider,
category: 'Test Category',
},
],
});
const context: PluginExtensionCommandPaletteContext = {
searchQuery: 'test',
};
const results = await registry.search(context);
const searchResult = results.get(`${pluginId}/Test Provider`);
expect(searchResult?.config.pluginId).toBe(pluginId);
expect(searchResult?.config.config.title).toBe('Test Provider');
expect(searchResult?.config.config.category).toBe('Test Category');
});
});
});
@@ -1,155 +0,0 @@
import { ReplaySubject } from 'rxjs';
import {
PluginExtensionCommandPaletteDynamicConfig,
CommandPaletteDynamicResult,
PluginExtensionCommandPaletteContext,
} from '@grafana/data';
import { isGrafanaDevMode } from '../utils';
import { PluginExtensionConfigs, Registry, RegistryType } from './Registry';
const logPrefix = '[CommandPaletteDynamic]';
export interface CommandPaletteDynamicRegistryItem {
pluginId: string;
config: PluginExtensionCommandPaletteDynamicConfig;
}
export interface CommandPaletteDynamicSearchResult {
items: CommandPaletteDynamicResult[];
config: CommandPaletteDynamicRegistryItem;
}
export class CommandPaletteDynamicRegistry extends Registry<
CommandPaletteDynamicRegistryItem[],
PluginExtensionCommandPaletteDynamicConfig
> {
constructor(
options: {
registrySubject?: ReplaySubject<RegistryType<CommandPaletteDynamicRegistryItem[]>>;
initialState?: RegistryType<CommandPaletteDynamicRegistryItem[]>;
} = {}
) {
super(options);
}
mapToRegistry(
registry: RegistryType<CommandPaletteDynamicRegistryItem[]>,
item: PluginExtensionConfigs<PluginExtensionCommandPaletteDynamicConfig>
): RegistryType<CommandPaletteDynamicRegistryItem[]> {
const { pluginId, configs } = item;
for (const config of configs) {
const { title, searchProvider, category } = config;
if (!title || typeof title !== 'string') {
this.logger.error(`${logPrefix} Plugin ${pluginId}: title is required and must be a string`);
continue;
}
if (!searchProvider || typeof searchProvider !== 'function') {
this.logger.error(`${logPrefix} Plugin ${pluginId}: searchProvider must be a function`);
continue;
}
const providerId = `${pluginId}/${title}`;
if (!(providerId in registry)) {
registry[providerId] = [];
}
registry[providerId].push({
pluginId,
config: {
...config,
category: category ?? pluginId,
minQueryLength: config.minQueryLength ?? 2,
debounceMs: config.debounceMs ?? 300,
},
});
if (isGrafanaDevMode()) {
console.log(`${logPrefix} Registered provider: ${providerId}`);
}
}
return registry;
}
/**
* Execute a search across all registered providers
*/
async search(context: PluginExtensionCommandPaletteContext): Promise<Map<string, CommandPaletteDynamicSearchResult>> {
const registry = await this.getState();
const results = new Map<string, CommandPaletteDynamicSearchResult>();
const searchQuery = context.searchQuery ?? '';
const searchPromises = Object.entries(registry).map(async ([providerId, registryItems]) => {
if (!Array.isArray(registryItems) || registryItems.length === 0) {
return;
}
const item = registryItems[0]; // Take first config per provider
const { config } = item;
// Check minimum query length
if (searchQuery.length < (config.minQueryLength ?? 2)) {
return;
}
// Check if provider is active
if (config.isActive && !config.isActive(context)) {
return;
}
try {
const items = await config.searchProvider(context);
// Validate results
if (!Array.isArray(items)) {
this.logger.warning(`${logPrefix} Provider ${providerId} did not return an array`);
return;
}
// Validate and filter items
const validItems = items
.filter((item) => {
if (!item.id || typeof item.id !== 'string') {
this.logger.warning(`${logPrefix} Provider ${providerId}: result missing id`);
return false;
}
if (!item.title || typeof item.title !== 'string') {
this.logger.warning(`${logPrefix} Provider ${providerId}: result missing title`);
return false;
}
return true;
})
.slice(0, 5); // Limit to 5 items maximum
if (validItems.length > 0) {
results.set(providerId, { items: validItems, config: item });
}
} catch (error) {
// Don't log AbortErrors as they are expected
if (error instanceof Error && error.name === 'AbortError') {
return;
}
this.logger.error(`${logPrefix} Search failed for ${providerId}`, { error: String(error) });
}
});
await Promise.all(searchPromises);
return results;
}
/**
* Returns a read-only version of the registry.
*/
readOnly() {
return new CommandPaletteDynamicRegistry({
registrySubject: this.registrySubject,
});
}
}
@@ -7,7 +7,6 @@ import { getCoreExtensionConfigurations } from '../getCoreExtensionConfiguration
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
import { AddedLinksRegistry } from './AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from './CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './types';
@@ -15,13 +14,11 @@ export const addedComponentsRegistry = new AddedComponentsRegistry();
export const exposedComponentsRegistry = new ExposedComponentsRegistry();
export const addedLinksRegistry = new AddedLinksRegistry();
export const addedFunctionsRegistry = new AddedFunctionsRegistry();
export const commandPaletteDynamicRegistry = new CommandPaletteDynamicRegistry();
export const pluginExtensionRegistries: PluginExtensionRegistries = {
addedComponentsRegistry,
exposedComponentsRegistry,
addedLinksRegistry,
addedFunctionsRegistry,
commandPaletteDynamicRegistry,
};
// Registering core extension links
@@ -1,7 +1,6 @@
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
import { AddedFunctionsRegistry } from './AddedFunctionsRegistry';
import { AddedLinksRegistry } from './AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from './CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from './ExposedComponentsRegistry';
export type PluginExtensionRegistries = {
@@ -9,5 +8,4 @@ export type PluginExtensionRegistries = {
exposedComponentsRegistry: ExposedComponentsRegistry;
addedFunctionsRegistry: AddedFunctionsRegistry;
addedLinksRegistry: AddedLinksRegistry;
commandPaletteDynamicRegistry: CommandPaletteDynamicRegistry;
};
@@ -9,7 +9,6 @@ import { resetLogMock } from './logs/testUtils';
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from './registry/CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './registry/types';
import { useLoadAppPlugins } from './useLoadAppPlugins';
@@ -94,7 +93,6 @@ describe('usePluginComponent()', () => {
exposedComponentsRegistry: new ExposedComponentsRegistry(),
addedLinksRegistry: new AddedLinksRegistry(),
addedFunctionsRegistry: new AddedFunctionsRegistry(),
commandPaletteDynamicRegistry: new CommandPaletteDynamicRegistry(),
};
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
@@ -17,7 +17,6 @@ import { resetLogMock } from './logs/testUtils';
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from './registry/CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './registry/types';
import { useLoadAppPlugins } from './useLoadAppPlugins';
@@ -75,7 +74,6 @@ describe('usePluginComponents()', () => {
exposedComponentsRegistry: new ExposedComponentsRegistry(),
addedLinksRegistry: new AddedLinksRegistry(),
addedFunctionsRegistry: new AddedFunctionsRegistry(),
commandPaletteDynamicRegistry: new CommandPaletteDynamicRegistry(),
};
pluginMeta = {
@@ -16,7 +16,6 @@ import { resetLogMock } from './logs/testUtils';
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from './registry/CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './registry/types';
import { useLoadAppPlugins } from './useLoadAppPlugins';
@@ -68,7 +67,6 @@ describe('usePluginFunctions()', () => {
exposedComponentsRegistry: new ExposedComponentsRegistry(),
addedLinksRegistry: new AddedLinksRegistry(),
addedFunctionsRegistry: new AddedFunctionsRegistry(),
commandPaletteDynamicRegistry: new CommandPaletteDynamicRegistry(),
};
resetLogMock(log);
@@ -16,7 +16,6 @@ import { resetLogMock } from './logs/testUtils';
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
import { AddedFunctionsRegistry } from './registry/AddedFunctionsRegistry';
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
import { CommandPaletteDynamicRegistry } from './registry/CommandPaletteDynamicRegistry';
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './registry/types';
import { useLoadAppPlugins } from './useLoadAppPlugins';
@@ -68,7 +67,6 @@ describe('usePluginLinks()', () => {
exposedComponentsRegistry: new ExposedComponentsRegistry(),
addedLinksRegistry: new AddedLinksRegistry(),
addedFunctionsRegistry: new AddedFunctionsRegistry(),
commandPaletteDynamicRegistry: new CommandPaletteDynamicRegistry(),
};
resetLogMock(log);
@@ -13,6 +13,7 @@ import {
throwIfAngular,
} from '@grafana/data';
import { config } from '@grafana/runtime';
import { commandPaletteDynamicRegistry } from 'app/features/commandPalette/CommandPaletteDynamicRegistry';
import { GenericDataSourcePlugin } from 'app/features/datasources/types';
import { getPanelPluginLoadError } from 'app/features/panel/components/PanelPluginError';
@@ -20,7 +21,6 @@ import {
addedComponentsRegistry,
addedFunctionsRegistry,
addedLinksRegistry,
commandPaletteDynamicRegistry,
exposedComponentsRegistry,
} from '../extensions/registry/setup';
import { pluginsLogger } from '../utils';