Files
grafana/public/app/features/plugins/extensions/usePluginComponent.tsx
Levente Balogh 77f84e494d Plugin Extensions: Add error boundaries (#107515)
* feat(grafana-data): expose PluginContext

This is aimed to be used in the `PluginErrorBoundary` (which is a class component, and cannot use the hook.)

* feat(PluginErrorBoundary): add an error boundary for plugins

* feat(ExtensionsErrorBoundary): add an error boundary for extensions)

* feat(Extensions/Utils): wrap components with error boundaries

* feat(Plugins): wrap root plugin page with an error boundary

* fix: Fallback component should always be visible for onClick() modals

* review: use object arguments instead of positional ones for `renderWithPluginContext()`

* review: update `wrapWithPluginContext()` to receive args as an object

* refactor(AppChromeExtensionPoint): remove the error boundary

We have an error boundary on the extensions-framework level now

* refactor(ExtensionSidebar): remove the ErrorBoundary from the extensions

This is handled on the extensions-framework level now.

* test(ExtensionSidebar): add tests

* chore: translation extraction

* chore: prettier formatting

* fix(PluginErrorBoundary): remove unnecessary type casting
2025-07-07 14:51:04 +02:00

66 lines
2.3 KiB
TypeScript

import { useMemo } from 'react';
import { useObservable } from 'react-use';
import { usePluginContext } from '@grafana/data';
import { UsePluginComponentResult } from '@grafana/runtime';
import { useExposedComponentsRegistry } from './ExtensionRegistriesContext';
import * as errors from './errors';
import { log } from './logs/log';
import { useLoadAppPlugins } from './useLoadAppPlugins';
import { getExposedComponentPluginDependencies, isGrafanaDevMode, wrapWithPluginContext } from './utils';
import { isExposedComponentDependencyMissing } from './validators';
// Returns a component exposed by a plugin.
// (Exposed components can be defined in plugins by calling .exposeComponent() on the AppPlugin instance.)
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
const registry = useExposedComponentsRegistry();
const registryState = useObservable(registry.asObservable());
const pluginContext = usePluginContext();
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExposedComponentPluginDependencies(id));
return useMemo(() => {
// For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana.
const enableRestrictions = isGrafanaDevMode() && pluginContext;
if (isLoadingAppPlugins) {
return {
isLoading: true,
component: null,
};
}
if (!registryState?.[id]) {
return {
isLoading: false,
component: null,
};
}
const registryItem = registryState[id];
const componentLog = log.child({
title: registryItem.title,
description: registryItem.description ?? '',
pluginId: registryItem.pluginId,
});
if (enableRestrictions && isExposedComponentDependencyMissing(id, pluginContext)) {
componentLog.error(errors.EXPOSED_COMPONENT_DEPENDENCY_MISSING);
return {
isLoading: false,
component: null,
};
}
return {
isLoading: false,
component: wrapWithPluginContext({
pluginId: registryItem.pluginId,
extensionTitle: registryItem.title,
Component: registryItem.component,
log: componentLog,
}),
};
}, [id, pluginContext, registryState, isLoadingAppPlugins]);
}