From 5e3c7ad0c10a7b61d3df7153757639a6380e8729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 12 Dec 2025 12:10:28 +0100 Subject: [PATCH] chore: refactoring to async --- packages/grafana-runtime/src/config.ts | 2 +- .../grafana-runtime/src/services/plugins.ts | 41 ------- .../grafana-runtime/src/services/plugins.tsx | 87 +++++++++++++++ packages/grafana-runtime/src/unstable.ts | 9 +- public/app/app.ts | 6 +- .../ExtensionSidebarProvider.tsx | 9 +- .../alerting/unified/api/incidentsApi.ts | 25 +++-- .../alerting/unified/api/onCallApi.ts | 105 +++++++++--------- .../bridges/DeclareIncidentButton.tsx | 22 ++-- .../contact-points/useContactPoints.ts | 24 +++- .../components/contact-points/utils.ts | 5 +- .../onCall/useOnCallIntegration.tsx | 21 ++-- .../useReceiversMetadata.ts | 34 +++--- .../unified/mocks/server/configure.ts | 15 +-- .../unified/mocks/server/handlers/plugins.ts | 50 ++++----- .../alerting/unified/utils/config.test.ts | 56 +--------- .../features/alerting/unified/utils/config.ts | 15 --- .../configuration-tracker/incidents/hooks.ts | 15 ++- .../configuration-tracker/irmHooks.test.tsx | 75 +++++++++++++ .../{irmHooks.ts => irmHooks.tsx} | 84 +++++++++----- .../configuration-tracker/onCall/hooks.ts | 43 ++++--- .../registry/AddedComponentsRegistry.test.ts | 5 +- .../registry/AddedFunctionsRegistry.test.ts | 5 +- .../registry/AddedLinksRegistry.test.ts | 5 +- .../ExposedComponentsRegistry.test.ts | 5 +- .../plugins/extensions/useLoadAppPlugins.tsx | 24 ++-- .../extensions/usePluginComponent.test.tsx | 4 +- .../plugins/extensions/usePluginComponent.tsx | 2 +- .../extensions/usePluginComponents.tsx | 2 +- .../plugins/extensions/usePluginFunctions.tsx | 3 +- .../plugins/extensions/usePluginLinks.tsx | 2 +- .../plugins/extensions/utils.test.tsx | 99 +---------------- .../app/features/plugins/extensions/utils.tsx | 57 ++++++---- .../plugins/extensions/validators.test.tsx | 10 +- .../features/plugins/sandbox/codeLoader.ts | 23 ++-- .../plugins/sandbox/sandboxPluginLoader.ts | 2 +- 36 files changed, 522 insertions(+), 469 deletions(-) delete mode 100644 packages/grafana-runtime/src/services/plugins.ts create mode 100644 packages/grafana-runtime/src/services/plugins.tsx create mode 100644 public/app/features/gops/configuration-tracker/irmHooks.test.tsx rename public/app/features/gops/configuration-tracker/{irmHooks.ts => irmHooks.tsx} (88%) diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 61ec79d23eb..589a2ae0a64 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -86,7 +86,7 @@ export class GrafanaBootConfig { snapshotEnabled = true; datasources: { [str: string]: DataSourceInstanceSettings } = {}; panels: { [key: string]: PanelPluginMeta } = {}; - /** @deprecated it will be removed in a future release, use getAppPluginMetas() function instead */ + /** @deprecated it will be removed in a future release, use getAppPluginMetas function or useAppPluginMetas hook instead */ apps: Record = {}; auth: AuthSettings = {}; minRefreshInterval = ''; diff --git a/packages/grafana-runtime/src/services/plugins.ts b/packages/grafana-runtime/src/services/plugins.ts deleted file mode 100644 index 99706312fcd..00000000000 --- a/packages/grafana-runtime/src/services/plugins.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { cloneDeep } from 'lodash'; - -import { AppPluginConfig } from '@grafana/data'; - -import { config } from '../config'; - -export type AppPluginMetas = Record; - -let apps: AppPluginMetas = {}; - -export async function initPluginMetas(): Promise { - if (config.featureToggles.useMTPlugins) { - // add loading app configs from MT API here - apps = {}; - return; - } - - // eslint-disable-next-line no-restricted-syntax - apps = config.apps; -} - -export function getAppPluginMetas(): AppPluginMetas { - return cloneDeep(apps); -} - -export function getAppPluginMeta(id: string): AppPluginConfig | undefined { - if (!apps[id]) { - return undefined; - } - - return cloneDeep(apps[id]); -} - -export function setAppPluginMetas(override: AppPluginMetas) { - // We allow overriding apps in tests - if (override && process.env.NODE_ENV !== 'test') { - throw new Error('setAppPluginMetas() function can only be called from tests.'); - } - - apps = { ...override }; -} diff --git a/packages/grafana-runtime/src/services/plugins.tsx b/packages/grafana-runtime/src/services/plugins.tsx new file mode 100644 index 00000000000..cb7294e3c24 --- /dev/null +++ b/packages/grafana-runtime/src/services/plugins.tsx @@ -0,0 +1,87 @@ +import { cloneDeep } from 'lodash'; +import { useAsync } from 'react-use'; + +import { AppPluginConfig } from '@grafana/data'; + +import { config } from '../config'; + +export type AppPluginMetas = Record; + +let apps: AppPluginMetas = {}; +let appsPromise: Promise | undefined = undefined; + +function areAppsInitialized(): boolean { + return Boolean(Object.keys(apps).length); +} + +export async function initPluginMetas(): Promise { + if (appsPromise) { + return appsPromise; + } + + appsPromise = new Promise((resolve) => { + if (config.featureToggles.useMTPlugins) { + // add loading app configs from MT API here + apps = {}; + resolve(); + return; + } + + // eslint-disable-next-line no-restricted-syntax + apps = config.apps; + resolve(); + return; + }); + + return appsPromise; +} + +export async function getAppPluginMetas(): Promise { + if (!areAppsInitialized()) { + await initPluginMetas(); + } + + return Object.values(cloneDeep(apps)); +} + +export function getAppPluginMeta(id: string): AppPluginConfig | undefined { + if (!apps[id]) { + return undefined; + } + + return cloneDeep(apps[id]); +} + +export async function getAppPluginConfig(id: string): Promise { + if (!areAppsInitialized()) { + await initPluginMetas(); + } + + if (!apps[id]) { + return undefined; + } + + return cloneDeep(apps[id]); +} + +export function setAppPluginMetas(override: AppPluginMetas) { + // We allow overriding apps in tests + if (override && process.env.NODE_ENV !== 'test') { + throw new Error('setAppPluginMetas() function can only be called from tests.'); + } + + apps = { ...override }; +} + +export interface UseAppPluginMetasResult { + isAppPluginMetasLoading: boolean; + error: Error | undefined; + apps: AppPluginConfig[]; +} + +export function useAppPluginMetas(filterByIds: string[] = []): UseAppPluginMetasResult { + const { loading, error, value: apps = [] } = useAsync(getAppPluginMetas); + const filtered = apps.filter((app) => filterByIds.includes(app.id)); + + return { isAppPluginMetasLoading: loading, error, apps: filtered }; +} diff --git a/packages/grafana-runtime/src/unstable.ts b/packages/grafana-runtime/src/unstable.ts index d9bd77cdb3a..d18189a7c57 100644 --- a/packages/grafana-runtime/src/unstable.ts +++ b/packages/grafana-runtime/src/unstable.ts @@ -12,4 +12,11 @@ // This is a dummy export so typescript doesn't error importing an "empty module" export const unstable = {}; -export { getAppPluginMetas, getAppPluginMeta, type AppPluginMetas } from './services/plugins'; +export { + type AppPluginMetas, + type UseAppPluginMetasResult as UseAppPluginMetasCollectionResult, + getAppPluginMeta, + getAppPluginConfig, + getAppPluginMetas, + useAppPluginMetas, +} from './services/plugins'; diff --git a/public/app/app.ts b/public/app/app.ts index 76153fd663e..ef367fa0616 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -51,6 +51,7 @@ import { setPanelRenderer, setPluginPage, } from '@grafana/runtime/internal'; +import { getAppPluginMetas } from '@grafana/runtime/unstable'; import { loadResources as loadScenesResources, sceneUtils } from '@grafana/scenes'; import config, { updateConfig } from 'app/core/config'; import { getStandardTransformers } from 'app/features/transformers/standardTransformers'; @@ -265,8 +266,9 @@ export class GrafanaApp { const skipAppPluginsPreload = config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render'; if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) { - const appPluginsToAwait = getAppPluginsToAwait(); - const appPluginsToPreload = getAppPluginsToPreload(); + const apps = await getAppPluginMetas(); + const appPluginsToAwait = getAppPluginsToAwait(apps); + const appPluginsToPreload = getAppPluginsToPreload(apps); preloadPlugins(appPluginsToPreload); await preloadPlugins(appPluginsToAwait); diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx index 51d73d2e8e5..dcae6f9319b 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx @@ -3,6 +3,7 @@ import { useLocalStorage } from 'react-use'; import { PluginExtensionPoints, store } from '@grafana/data'; import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime'; +import { useAppPluginMetas } from '@grafana/runtime/unstable'; import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils'; import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent, ToggleExtensionSidebarEvent } from 'app/types/events'; @@ -90,19 +91,21 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo // that means, a plugin would need to register both, a link and a component to // `grafana/extension-sidebar/v0-alpha` and the link's `configure` method would control // whether the component is rendered or not - const { links, isLoading } = usePluginLinks({ + const { links, isLoading: isPluginLinksLoading } = usePluginLinks({ extensionPointId: PluginExtensionPoints.ExtensionSidebar, context: { path: currentPath, }, }); + const { apps, isAppPluginMetasLoading: isAppPluginConfigsLoading } = useAppPluginMetas(); + const isLoading = isPluginLinksLoading || isAppPluginConfigsLoading; // get all components for this extension point, but only for the permitted plugins // if the extension sidebar is not enabled, we will return an empty map const availableComponents = useMemo( () => new Map( - Array.from(getExtensionPointPluginMeta(PluginExtensionPoints.ExtensionSidebar).entries()).filter( + Array.from(getExtensionPointPluginMeta(apps, PluginExtensionPoints.ExtensionSidebar).entries()).filter( ([pluginId, pluginMeta]) => PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) && links.some( @@ -112,7 +115,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo ) ) ), - [links] + [links, apps] ); // check if the stored docked component is still available diff --git a/public/app/features/alerting/unified/api/incidentsApi.ts b/public/app/features/alerting/unified/api/incidentsApi.ts index 737529c18d4..a5cf3525869 100644 --- a/public/app/features/alerting/unified/api/incidentsApi.ts +++ b/public/app/features/alerting/unified/api/incidentsApi.ts @@ -1,4 +1,4 @@ -import { getIrmIfPresentOrIncidentPluginId } from '../utils/config'; +import { SupportedPlugin } from '../types/pluginBridges'; import { alertingApi } from './alertingApi'; @@ -7,17 +7,18 @@ interface IncidentsPluginConfigDto { isIncidentCreated: boolean; } -const getProxyApiUrl = (path: string) => `/api/plugins/${getIrmIfPresentOrIncidentPluginId()}/resources${path}`; +const getProxyApiUrl = (path: string, pluginId: SupportedPlugin) => `/api/plugins/${pluginId}/resources${path}`; -export const incidentsApi = alertingApi.injectEndpoints({ - endpoints: (build) => ({ - getIncidentsPluginConfig: build.query({ - query: () => ({ - url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker'), - data: {}, - method: 'POST', - showErrorAlert: false, +export const incidentsApi = (pluginId: SupportedPlugin) => + alertingApi.injectEndpoints({ + endpoints: (build) => ({ + getIncidentsPluginConfig: build.query({ + query: () => ({ + url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker', pluginId), + data: {}, + method: 'POST', + showErrorAlert: false, + }), }), }), - }), -}); + }); diff --git a/public/app/features/alerting/unified/api/onCallApi.ts b/public/app/features/alerting/unified/api/onCallApi.ts index 0e35ae2f1b6..af88a2365c6 100644 --- a/public/app/features/alerting/unified/api/onCallApi.ts +++ b/public/app/features/alerting/unified/api/onCallApi.ts @@ -1,7 +1,7 @@ import { FetchError, isFetchError } from '@grafana/runtime'; import { GRAFANA_ONCALL_INTEGRATION_TYPE } from '../components/receivers/grafanaAppReceivers/onCall/onCall'; -import { getIrmIfPresentOrOnCallPluginId } from '../utils/config'; +import { SupportedPlugin } from '../types/pluginBridges'; import { alertingApi } from './alertingApi'; @@ -38,62 +38,63 @@ export interface OnCallConfigChecks { is_integration_chatops_connected: boolean; } -export function getProxyApiUrl(path: string) { - return `/api/plugins/${getIrmIfPresentOrOnCallPluginId()}/resources${path}`; +export function getProxyApiUrl(path: string, pluginId: SupportedPlugin) { + return `/api/plugins/${pluginId}/resources${path}`; } -export const onCallApi = alertingApi.injectEndpoints({ - endpoints: (build) => ({ - grafanaOnCallIntegrations: build.query({ - query: () => ({ - url: getProxyApiUrl('/alert_receive_channels/'), - // legacy_grafana_alerting is necessary for OnCall. - // We do NOT need to differentiate between these two on our side - params: { - filters: true, - integration: [GRAFANA_ONCALL_INTEGRATION_TYPE, 'legacy_grafana_alerting'], - skip_pagination: true, +export const onCallApi = (pluginId: SupportedPlugin) => + alertingApi.injectEndpoints({ + endpoints: (build) => ({ + grafanaOnCallIntegrations: build.query({ + query: () => ({ + url: getProxyApiUrl('/alert_receive_channels/', pluginId), + // legacy_grafana_alerting is necessary for OnCall. + // We do NOT need to differentiate between these two on our side + params: { + filters: true, + integration: [GRAFANA_ONCALL_INTEGRATION_TYPE, 'legacy_grafana_alerting'], + skip_pagination: true, + }, + showErrorAlert: false, + }), + transformResponse: (response: AlertReceiveChannelsResult) => { + if (isPaginatedResponse(response)) { + return response.results; + } + return response; }, - showErrorAlert: false, + providesTags: ['OnCallIntegrations'], }), - transformResponse: (response: AlertReceiveChannelsResult) => { - if (isPaginatedResponse(response)) { - return response.results; - } - return response; - }, - providesTags: ['OnCallIntegrations'], - }), - validateIntegrationName: build.query({ - query: (name) => ({ - url: getProxyApiUrl('/alert_receive_channels/validate_name/'), - params: { verbal_name: name }, - showErrorAlert: false, + validateIntegrationName: build.query({ + query: (name) => ({ + url: getProxyApiUrl('/alert_receive_channels/validate_name/', pluginId), + params: { verbal_name: name }, + showErrorAlert: false, + }), + }), + createIntegration: build.mutation({ + query: (integration) => ({ + url: getProxyApiUrl('/alert_receive_channels/', pluginId), + data: integration, + method: 'POST', + showErrorAlert: true, + }), + invalidatesTags: ['OnCallIntegrations'], + }), + features: build.query({ + query: () => ({ + url: getProxyApiUrl('/features/', pluginId), + showErrorAlert: false, + }), + }), + onCallConfigChecks: build.query({ + query: () => ({ + url: getProxyApiUrl('/organization/config-checks/', pluginId), + showErrorAlert: false, + }), }), }), - createIntegration: build.mutation({ - query: (integration) => ({ - url: getProxyApiUrl('/alert_receive_channels/'), - data: integration, - method: 'POST', - showErrorAlert: true, - }), - invalidatesTags: ['OnCallIntegrations'], - }), - features: build.query({ - query: () => ({ - url: getProxyApiUrl('/features/'), - showErrorAlert: false, - }), - }), - onCallConfigChecks: build.query({ - query: () => ({ - url: getProxyApiUrl('/organization/config-checks/'), - showErrorAlert: false, - }), - }), - }), -}); + }); function isPaginatedResponse( response: AlertReceiveChannelsResult @@ -101,8 +102,6 @@ function isPaginatedResponse( return 'results' in response && Array.isArray(response.results); } -export const { useGrafanaOnCallIntegrationsQuery } = onCallApi; - export function isOnCallFetchError(error: unknown): error is FetchError<{ detail: string }> { return isFetchError(error) && 'detail' in error.data; } diff --git a/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx b/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx index bb2220b82a1..92c508e33fa 100644 --- a/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx +++ b/public/app/features/alerting/unified/components/bridges/DeclareIncidentButton.tsx @@ -1,8 +1,8 @@ import { Trans, t } from '@grafana/i18n'; import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui'; +import { useIrmConfig } from 'app/features/gops/configuration-tracker/irmHooks'; import { usePluginBridge } from '../../hooks/usePluginBridge'; -import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config'; import { createBridgeURL } from '../PluginBridge'; interface Props { @@ -11,16 +11,19 @@ interface Props { url?: string; } -const pluginId = getIrmIfPresentOrIncidentPluginId(); - export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => { - const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', { + const { + irmConfig: { incidentPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); + const bridgeURL = createBridgeURL(incidentPluginId, '/incidents/declare', { title, severity, url, }); - const { loading, installed, settings } = usePluginBridge(pluginId); + const { loading: isPluginBridgeLoading, installed, settings } = usePluginBridge(incidentPluginId); + const loading = isIrmConfigLoading || isPluginBridgeLoading; return ( <> @@ -51,13 +54,18 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P }; export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }: Props) => { - const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', { + const { + irmConfig: { incidentPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); + const bridgeURL = createBridgeURL(incidentPluginId, '/incidents/declare', { title, severity, url, }); - const { loading, installed, settings } = usePluginBridge(pluginId); + const { loading: isPluginBridgeLoading, installed, settings } = usePluginBridge(incidentPluginId); + const loading = isIrmConfigLoading || isPluginBridgeLoading; return ( <> diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts index 6627d5f69d2..a2b75f611cc 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.ts @@ -12,6 +12,7 @@ import { BaseAlertmanagerArgs, Skippable } from 'app/features/alerting/unified/t import { cloudNotifierTypes } from 'app/features/alerting/unified/utils/cloud-alertmanager-notifier-types'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { isK8sEntityProvisioned, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; +import { useIrmConfig } from 'app/features/gops/configuration-tracker/irmHooks'; import { GrafanaManagedContactPoint, Receiver } from 'app/plugins/datasource/alertmanager/types'; import { getAPINamespace } from '../../../../../api/utils'; @@ -21,7 +22,7 @@ import { useAsync } from '../../hooks/useAsync'; import { usePluginBridge } from '../../hooks/usePluginBridge'; import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig'; import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers'; -import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config'; +import { SupportedPlugin } from '../../types/pluginBridges'; import { enhanceContactPointsWithMetadata } from './utils'; @@ -41,7 +42,7 @@ const { useGrafanaNotifiersQuery, useLazyGetAlertmanagerConfigurationQuery, } = alertmanagerApi; -const { useGrafanaOnCallIntegrationsQuery } = onCallApi; + const { useListNamespacedReceiverQuery, useReadNamespacedReceiverQuery, @@ -61,8 +62,14 @@ const defaultOptions = { * Otherwise, returns no data */ const useOnCallIntegrations = ({ skip }: Skippable = {}) => { - const { installed, loading } = usePluginBridge(getIrmIfPresentOrOnCallPluginId()); + const { + irmConfig: { onCallPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); + const { installed, loading: isPluginBridgeLoading } = usePluginBridge(onCallPluginId); + const { useGrafanaOnCallIntegrationsQuery } = onCallApi(onCallPluginId); const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed }); + const loading = isIrmConfigLoading || isPluginBridgeLoading; return useMemo(() => { if (installed) { @@ -138,9 +145,11 @@ export const useGrafanaContactPoints = ({ const alertmanagerConfigResponse = useGetAlertmanagerConfigurationQuery(GRAFANA_RULES_SOURCE_NAME, { skip: skip || !fetchPolicies, }); + const { irmConfig, isIrmConfigLoading } = useIrmConfig(); return useMemo(() => { - const isLoading = onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading; + const isLoading = + onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading || isIrmConfigLoading; if (isLoading) { return { @@ -160,6 +169,7 @@ export const useGrafanaContactPoints = ({ onCallIntegrations: onCallResponse?.data, contactPoints: contactPointsListResponse.data || [], alertmanagerConfiguration: alertmanagerConfigResponse.data, + irmConfig, }); return { @@ -172,6 +182,8 @@ export const useGrafanaContactPoints = ({ contactPointsListResponse, contactPointsStatusResponse, onCallResponse, + isIrmConfigLoading, + irmConfig, ]); }; @@ -238,9 +250,10 @@ export function useContactPointsWithStatus({ fetchPolicies, skip, }: GrafanaFetchOptions & BaseAlertmanagerArgs & Skippable) { + const { irmConfig, isIrmConfigLoading } = useIrmConfig(); const isGrafanaAlertmanager = alertmanager === GRAFANA_RULES_SOURCE_NAME; const grafanaResponse = useGrafanaContactPoints({ - skip: skip || !isGrafanaAlertmanager, + skip: skip || !isGrafanaAlertmanager || isIrmConfigLoading, fetchStatuses, fetchPolicies, }); @@ -254,6 +267,7 @@ export function useContactPointsWithStatus({ notifiers: cloudNotifierTypes, contactPoints: result.data.alertmanager_config.receivers ?? [], alertmanagerConfiguration: result.data, + irmConfig, }) : [], }), diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index d2cc43901c1..bd3d1521532 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -5,6 +5,7 @@ import { computeInheritedTree } from '@grafana/alerting'; import { t } from '@grafana/i18n'; import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/features/alerting/unified/types/alerting'; import { canAdminEntity, shouldUseK8sApi } from 'app/features/alerting/unified/utils/k8s/utils'; +import { UseIsIrmConfig } from 'app/features/gops/configuration-tracker/irmHooks'; import { AlertManagerCortexConfig, GrafanaManagedContactPoint, @@ -116,6 +117,7 @@ type EnhanceContactPointsArgs = { onCallIntegrations?: OnCallIntegrationDTO[] | undefined | null; contactPoints: Receiver[]; alertmanagerConfiguration?: AlertManagerCortexConfig; + irmConfig: UseIsIrmConfig; }; /** @@ -132,6 +134,7 @@ export function enhanceContactPointsWithMetadata({ onCallIntegrations, contactPoints, alertmanagerConfiguration, + irmConfig, }: EnhanceContactPointsArgs): ContactPointWithMetadata[] { // compute the entire inherited tree before finding what notification policies are using a particular contact point const fullyInheritedTree = computeInheritedTree( @@ -162,7 +165,7 @@ export function enhanceContactPointsWithMetadata({ [RECEIVER_META_KEY]: getNotifierMetadata(notifiers, receiver), // if OnCall plugin is installed, we'll add it to the receiver's plugin metadata [RECEIVER_PLUGIN_META_KEY]: isOnCallReceiver - ? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration)) + ? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration), irmConfig) : undefined, }; }), diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx index 6f962f36805..664dc00f4cd 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/onCall/useOnCallIntegration.tsx @@ -6,7 +6,7 @@ import { t } from '@grafana/i18n'; import { isFetchError } from '@grafana/runtime'; import { Badge } from '@grafana/ui'; import { NotifierDTO } from 'app/features/alerting/unified/types/alerting'; -import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config'; +import { useIrmConfig } from 'app/features/gops/configuration-tracker/irmHooks'; import { useAppNotification } from '../../../../../../../core/copy/appNotification'; import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types'; @@ -38,17 +38,21 @@ enum OnCallIntegrationStatus { } function useOnCallPluginStatus() { + const { + irmConfig: { onCallPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); const { installed: isOnCallEnabled, loading: isPluginBridgeLoading, error: pluginError, - } = usePluginBridge(getIrmIfPresentOrOnCallPluginId()); + } = usePluginBridge(onCallPluginId); const { data: onCallFeatures = [], error: onCallFeaturesError, isLoading: isOnCallFeaturesLoading, - } = onCallApi.endpoints.features.useQuery(undefined, { skip: !isOnCallEnabled }); + } = onCallApi(onCallPluginId).endpoints.features.useQuery(undefined, { skip: !isOnCallEnabled }); const integrationStatus = useMemo((): OnCallIntegrationStatus => { if (!isOnCallEnabled) { @@ -70,19 +74,22 @@ function useOnCallPluginStatus() { isOnCallEnabled, integrationStatus, isAlertingV2IntegrationEnabled, - isOnCallStatusLoading: isPluginBridgeLoading || isOnCallFeaturesLoading, + isOnCallStatusLoading: isPluginBridgeLoading || isOnCallFeaturesLoading || isIrmConfigLoading, onCallError: pluginError ?? onCallFeaturesError, }; } export function useOnCallIntegration() { const notifyApp = useAppNotification(); - + const { + irmConfig: { onCallPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); const { isOnCallEnabled, integrationStatus, isAlertingV2IntegrationEnabled, isOnCallStatusLoading, onCallError } = useOnCallPluginStatus(); const { useCreateIntegrationMutation, useGrafanaOnCallIntegrationsQuery, useLazyValidateIntegrationNameQuery } = - onCallApi; + onCallApi(onCallPluginId); const [validateIntegrationNameQuery, { isFetching: isValidating }] = useLazyValidateIntegrationNameQuery(); const [createIntegrationMutation] = useCreateIntegrationMutation(); @@ -271,7 +278,7 @@ export function useOnCallIntegration() { extendOnCallReceivers, createOnCallIntegrations, onCallFormValidators, - isLoadingOnCallIntegration: isLoadingOnCallIntegrations || isOnCallStatusLoading, + isLoadingOnCallIntegration: isLoadingOnCallIntegrations || isOnCallStatusLoading || isIrmConfigLoading, isValidating, hasOnCallError: Boolean(onCallError) || isIntegrationsQueryError, }; diff --git a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts index 404e86e24c7..c9d5efbdcc8 100644 --- a/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts +++ b/public/app/features/alerting/unified/components/receivers/grafanaAppReceivers/useReceiversMetadata.ts @@ -1,6 +1,9 @@ +import { t } from '@grafana/i18n'; +import { UseIsIrmConfig } from 'app/features/gops/configuration-tracker/irmHooks'; + import { GrafanaManagedReceiverConfig } from '../../../../../../plugins/datasource/alertmanager/types'; import { OnCallIntegrationDTO } from '../../../api/onCallApi'; -import { getIrmIfPresentOrOnCallPluginId, getIsIrmPluginPresent } from '../../../utils/config'; +import { SupportedPlugin } from '../../../types/pluginBridges'; import { createBridgeURL } from '../../PluginBridge'; import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types'; @@ -13,38 +16,37 @@ export interface ReceiverPluginMetadata { warning?: string; } -const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[getIrmIfPresentOrOnCallPluginId()]; -const onCallReceiverTitle = 'Grafana OnCall'; - -export const onCallReceiverMeta: ReceiverPluginMetadata = { - title: onCallReceiverTitle, - icon: onCallReceiverICon, -}; +export const onCallReceiverMeta = (pluginId: SupportedPlugin): ReceiverPluginMetadata => ({ + title: t('alerting.on-call-receiver-meta.title.grafana-on-call', 'Grafana OnCall'), + icon: GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[pluginId], +}); export function getOnCallMetadata( onCallIntegrations: OnCallIntegrationDTO[] | undefined | null, receiver: GrafanaManagedReceiverConfig, - hasAlertManagerConfigData = true + hasAlertManagerConfigData = true, + irmConfig: UseIsIrmConfig ): ReceiverPluginMetadata { - const pluginName = getIsIrmPluginPresent() ? 'IRM' : 'OnCall'; + const pluginName = irmConfig.isIrmPluginPresent ? 'IRM' : 'OnCall'; + const pluginId = irmConfig.onCallPluginId; if (!hasAlertManagerConfigData) { - return onCallReceiverMeta; + return onCallReceiverMeta(pluginId); } if (!receiver.settings?.url) { - return onCallReceiverMeta; + return onCallReceiverMeta(pluginId); } // oncall status is still loading if (onCallIntegrations === undefined) { - return onCallReceiverMeta; + return onCallReceiverMeta(pluginId); } // indication that onCall is not enabled if (onCallIntegrations == null) { return { - ...onCallReceiverMeta, + ...onCallReceiverMeta(pluginId), warning: `Grafana ${pluginName} is not installed or is disabled`, }; } @@ -54,10 +56,10 @@ export function getOnCallMetadata( ); return { - ...onCallReceiverMeta, + ...onCallReceiverMeta(pluginId), description: matchingOnCallIntegration?.display_name, externalUrl: matchingOnCallIntegration - ? createBridgeURL(getIrmIfPresentOrOnCallPluginId(), `/integrations/${matchingOnCallIntegration.value}`) + ? createBridgeURL(pluginId, `/integrations/${matchingOnCallIntegration.value}`) : undefined, warning: matchingOnCallIntegration ? undefined : `${pluginName} Integration no longer exists`, }; diff --git a/public/app/features/alerting/unified/mocks/server/configure.ts b/public/app/features/alerting/unified/mocks/server/configure.ts index a98330212c4..1ab9aa08dab 100644 --- a/public/app/features/alerting/unified/mocks/server/configure.ts +++ b/public/app/features/alerting/unified/mocks/server/configure.ts @@ -1,7 +1,5 @@ import { type DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams, http } from 'msw'; -import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMetas } from '@grafana/runtime/unstable'; import server from '@grafana/test-utils/server'; import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks'; import { @@ -11,10 +9,7 @@ import { } from 'app/features/alerting/unified/mocks/server/handlers/alertmanagers'; import { getFolderHandler } from 'app/features/alerting/unified/mocks/server/handlers/folders'; import { listNamespacedTimeIntervalHandler } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s'; -import { - getDisabledPluginHandler, - getPluginMissingHandler, -} from 'app/features/alerting/unified/mocks/server/handlers/plugins'; +import { getDisabledPluginHandler } from 'app/features/alerting/unified/mocks/server/handlers/plugins'; import { ALERTING_API_SERVER_BASE_URL, getK8sResponse, @@ -213,14 +208,6 @@ export function setGrafanaPromRules(groups: GrafanaPromRuleGroupDTO[]) { server.use(http.get(`/api/prometheus/grafana/api/v1/rules`, paginatedHandlerFor(groups))); } -/** Make a given plugin ID respond with a 404, as if it isn't installed at all */ -export const removePlugin = (pluginId: string) => { - const apps = getAppPluginMetas(); - delete apps[pluginId]; - setAppPluginMetas(apps); - server.use(getPluginMissingHandler(pluginId)); -}; - /** Make a plugin respond with `enabled: false`, as if its installed but disabled */ export const disablePlugin = (pluginId: SupportedPlugin) => { clearPluginSettingsCache(pluginId); diff --git a/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts b/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts index ffad134e674..cac81cf9eb6 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/plugins.ts @@ -2,7 +2,7 @@ import { HttpResponse, http } from 'msw'; import { PluginLoadingStrategy, PluginMeta } from '@grafana/data'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMetas } from '@grafana/runtime/unstable'; +import { type AppPluginMetas } from '@grafana/runtime/unstable'; import { plugins } from 'app/features/alerting/unified/testSetup/plugins'; const PLUGIN_NOT_FOUND_RESPONSE = { message: 'Plugin not found, no installed plugin with that id' }; @@ -12,34 +12,34 @@ const PLUGIN_NOT_FOUND_RESPONSE = { message: 'Plugin not found, no installed plu * config side effects that are expected to come along with this API behaviour */ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => { - plugins.forEach(({ id, baseUrl, info, angular }) => { - const apps = getAppPluginMetas(); - setAppPluginMetas({ - ...apps, - [id]: { - id, - path: baseUrl, - preload: true, - version: info.version, - angular: angular ?? { detected: false, hideDeprecation: false }, - loadingStrategy: PluginLoadingStrategy.script, + const allPlugins: AppPluginMetas = {}; + plugins.reduce((acc, curr) => { + const { id, baseUrl, info, angular } = curr; + acc[id] = { + id, + path: baseUrl, + preload: true, + version: info.version, + angular: angular ?? { detected: false, hideDeprecation: false }, + loadingStrategy: PluginLoadingStrategy.script, + extensions: { + addedLinks: [], + addedComponents: [], + extensionPoints: [], + exposedComponents: [], + addedFunctions: [], + }, + dependencies: { + grafanaVersion: '', + plugins: [], extensions: { - addedLinks: [], - addedComponents: [], - extensionPoints: [], exposedComponents: [], - addedFunctions: [], - }, - dependencies: { - grafanaVersion: '', - plugins: [], - extensions: { - exposedComponents: [], - }, }, }, - }); - }); + }; + return acc; + }, allPlugins); + setAppPluginMetas(allPlugins); return http.get<{ pluginId: string }>(`/api/plugins/:pluginId/settings`, ({ params: { pluginId } }) => { const matchingPlugin = pluginsArray.find((plugin) => plugin.id === pluginId); diff --git a/public/app/features/alerting/unified/utils/config.test.ts b/public/app/features/alerting/unified/utils/config.test.ts index fe462d71210..cab4615eb7a 100644 --- a/public/app/features/alerting/unified/utils/config.test.ts +++ b/public/app/features/alerting/unified/utils/config.test.ts @@ -1,15 +1,6 @@ import { config } from '@grafana/runtime'; -import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins'; -import { SupportedPlugin } from '../types/pluginBridges'; - -import { - checkEvaluationIntervalGlobalLimit, - getIrmIfPresentOrIncidentPluginId, - getIrmIfPresentOrOnCallPluginId, - getIsIrmPluginPresent, -} from './config'; +import { checkEvaluationIntervalGlobalLimit } from './config'; describe('checkEvaluationIntervalGlobalLimit', () => { it('should NOT exceed limit if evaluate every is not valid duration', () => { @@ -60,48 +51,3 @@ describe('checkEvaluationIntervalGlobalLimit', () => { expect(exceedsLimit).toBe(false); }); }); - -describe('getIsIrmPluginPresent', () => { - it('should return true when IRM plugin is present in config.apps', () => { - setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); - expect(getIsIrmPluginPresent()).toBe(true); - }); - - it('should return false when IRM plugin is not present in config.apps', () => { - setAppPluginMetas({ - [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), - [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }); - expect(getIsIrmPluginPresent()).toBe(false); - }); -}); - -describe('getIrmIfPresentOrIncidentPluginId', () => { - it('should return IRM plugin ID when IRM plugin is present', () => { - setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); - expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm); - }); - - it('should return Incident plugin ID when IRM plugin is not present', () => { - setAppPluginMetas({ - [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), - [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }); - expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident); - }); -}); - -describe('getIrmIfPresentOrOnCallPluginId', () => { - it('should return IRM plugin ID when IRM plugin is present', () => { - setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); - expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm); - }); - - it('should return OnCall plugin ID when IRM plugin is not present', () => { - setAppPluginMetas({ - [SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]), - [SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]), - }); - expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall); - }); -}); diff --git a/public/app/features/alerting/unified/utils/config.ts b/public/app/features/alerting/unified/utils/config.ts index 5f00e0616c3..2183ebd5ce1 100644 --- a/public/app/features/alerting/unified/utils/config.ts +++ b/public/app/features/alerting/unified/utils/config.ts @@ -1,8 +1,5 @@ import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { getAppPluginMetas } from '@grafana/runtime/unstable'; - -import { SupportedPlugin } from '../types/pluginBridges'; import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time'; @@ -29,15 +26,3 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit }; } - -export function getIsIrmPluginPresent() { - return SupportedPlugin.Irm in getAppPluginMetas(); -} - -export function getIrmIfPresentOrIncidentPluginId() { - return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.Incident; -} - -export function getIrmIfPresentOrOnCallPluginId() { - return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.OnCall; -} diff --git a/public/app/features/gops/configuration-tracker/incidents/hooks.ts b/public/app/features/gops/configuration-tracker/incidents/hooks.ts index 748c21b4b11..23dde9bb709 100644 --- a/public/app/features/gops/configuration-tracker/incidents/hooks.ts +++ b/public/app/features/gops/configuration-tracker/incidents/hooks.ts @@ -1,6 +1,7 @@ import { incidentsApi } from 'app/features/alerting/unified/api/incidentsApi'; import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge'; -import { getIrmIfPresentOrIncidentPluginId } from 'app/features/alerting/unified/utils/config'; + +import { useIrmConfig } from '../irmHooks'; interface IncidentsPluginConfig { isInstalled: boolean; @@ -10,16 +11,18 @@ interface IncidentsPluginConfig { } export function useGetIncidentPluginConfig(): IncidentsPluginConfig { - const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge( - getIrmIfPresentOrIncidentPluginId() - ); + const { + irmConfig: { incidentPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); + const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge(incidentPluginId); const { data: incidentsConfig, isLoading: loadingPluginConfig } = - incidentsApi.endpoints.getIncidentsPluginConfig.useQuery(); + incidentsApi(incidentPluginId).endpoints.getIncidentsPluginConfig.useQuery(); return { isInstalled: incidentPluginInstalled ?? false, isChatOpsInstalled: incidentsConfig?.isChatOpsInstalled ?? false, isIncidentCreated: incidentsConfig?.isIncidentCreated ?? false, - isLoading: loadingPluginSettings || loadingPluginConfig, + isLoading: loadingPluginSettings || loadingPluginConfig || isIrmConfigLoading, }; } diff --git a/public/app/features/gops/configuration-tracker/irmHooks.test.tsx b/public/app/features/gops/configuration-tracker/irmHooks.test.tsx new file mode 100644 index 00000000000..ae490fc9c6e --- /dev/null +++ b/public/app/features/gops/configuration-tracker/irmHooks.test.tsx @@ -0,0 +1,75 @@ +import { renderHook, waitFor } from '@testing-library/react'; + +import { setAppPluginMetas } from '@grafana/runtime/internal'; +import { pluginMeta, pluginMetaToPluginConfig } from 'app/features/alerting/unified/testSetup/plugins'; +import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; + +import { useIrmConfig } from './irmHooks'; + +describe('useIrmConfig', () => { + it('should return default values during load', async () => { + const { result } = renderHook(() => useIrmConfig()); + + expect(result.current.isIrmConfigLoading).toBe(true); + expect(result.current.irmConfig).toEqual({ + isIrmPluginPresent: false, + incidentPluginId: SupportedPlugin.Incident, + onCallPluginId: SupportedPlugin.OnCall, + }); + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + }); + + describe('when IRM plugin does not exists in apps', () => { + beforeEach(() => { + setAppPluginMetas({}); + }); + + it('isIrmPluginPresent should be false', async () => { + const { result } = renderHook(() => useIrmConfig()); + + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + expect(result.current.irmConfig.isIrmPluginPresent).toBe(false); + }); + + it('incidentPluginId should be Incident plugin ID', async () => { + const { result } = renderHook(() => useIrmConfig()); + + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + expect(result.current.irmConfig.incidentPluginId).toBe(SupportedPlugin.Incident); + }); + + it('onCallPluginId should be OnCall plugin ID', async () => { + const { result } = renderHook(() => useIrmConfig()); + + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + expect(result.current.irmConfig.onCallPluginId).toBe(SupportedPlugin.OnCall); + }); + }); + + describe('when IRM plugin exists in apps', () => { + beforeEach(() => { + setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) }); + }); + + it('isIrmPluginPresent should be true', async () => { + const { result } = renderHook(() => useIrmConfig()); + + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + expect(result.current.irmConfig.isIrmPluginPresent).toBe(true); + }); + + it('incidentPluginId should be IRM plugin ID', async () => { + const { result } = renderHook(() => useIrmConfig()); + + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + expect(result.current.irmConfig.incidentPluginId).toBe(SupportedPlugin.Irm); + }); + + it('onCallPluginId should be IRM plugin ID', async () => { + const { result } = renderHook(() => useIrmConfig()); + + await waitFor(() => expect(result.current.isIrmConfigLoading).toBe(false)); + expect(result.current.irmConfig.onCallPluginId).toBe(SupportedPlugin.Irm); + }); + }); +}); diff --git a/public/app/features/gops/configuration-tracker/irmHooks.ts b/public/app/features/gops/configuration-tracker/irmHooks.tsx similarity index 88% rename from public/app/features/gops/configuration-tracker/irmHooks.ts rename to public/app/features/gops/configuration-tracker/irmHooks.tsx index e65ccf44652..736e23637b2 100644 --- a/public/app/features/gops/configuration-tracker/irmHooks.ts +++ b/public/app/features/gops/configuration-tracker/irmHooks.tsx @@ -1,14 +1,12 @@ import { useMemo } from 'react'; +import { useAsync } from 'react-use'; import { t } from '@grafana/i18n'; import { locationService } from '@grafana/runtime'; +import { getAppPluginConfig } from '@grafana/runtime/unstable'; import { useGrafanaContactPoints } from 'app/features/alerting/unified/components/contact-points/useContactPoints'; import { useNotificationPolicyRoute } from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute'; -import { - getIrmIfPresentOrIncidentPluginId, - getIrmIfPresentOrOnCallPluginId, - getIsIrmPluginPresent, -} from 'app/features/alerting/unified/utils/config'; +import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { RelativeUrl, createRelativeUrl } from 'app/features/alerting/unified/utils/url'; @@ -111,6 +109,39 @@ function useGetConfigurationForApps() { }; } +export interface UseIsIrmConfig { + isIrmPluginPresent: boolean; + incidentPluginId: SupportedPlugin; + onCallPluginId: SupportedPlugin; +} +export interface UseIsIrmConfigResult { + isIrmConfigLoading: boolean; + irmConfig: UseIsIrmConfig; +} + +export function useIrmConfig(): UseIsIrmConfigResult { + const { loading, value: irmConfig } = useAsync(async () => { + const app = await getAppPluginConfig(SupportedPlugin.Irm); + const isIrmPluginPresent = Boolean(app); + const incidentPluginId = isIrmPluginPresent ? SupportedPlugin.Irm : SupportedPlugin.Incident; + const onCallPluginId = isIrmPluginPresent ? SupportedPlugin.Irm : SupportedPlugin.OnCall; + return { isIrmPluginPresent, incidentPluginId, onCallPluginId }; + }); + + if (!irmConfig) { + return { + isIrmConfigLoading: loading, + irmConfig: { + isIrmPluginPresent: false, + incidentPluginId: SupportedPlugin.Incident, + onCallPluginId: SupportedPlugin.OnCall, + }, + }; + } + + return { isIrmConfigLoading: loading, irmConfig }; +} + export function useGetEssentialsConfiguration(): EssentialsConfigurationData { const { alerting: { contactPoints, defaultContactpoint, isCreateAlertRuleDone }, @@ -119,6 +150,10 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { slo: { hasSlo, hasSloWithAlert }, isLoading, } = useGetConfigurationForApps(); + const { + irmConfig: { incidentPluginId, isIrmPluginPresent, onCallPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); function onIntegrationClick(integrationId: string, url: RelativeUrl) { const urlToGoWithIntegration = createRelativeUrl(`${url} + ${integrationId}`, { @@ -151,7 +186,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { }, ]; - if (!getIsIrmPluginPresent()) { + if (isIrmPluginPresent) { steps = [ ...steps, { @@ -266,8 +301,8 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { }, { title: t('gops.use-get-essentials-configuration.essential-content.title.respond', 'Respond'), - description: getIsIrmPluginPresent() ? 'Configure IRM' : 'Configure OnCall and Incident', - steps: getIsIrmPluginPresent() + description: isIrmPluginPresent ? 'Configure IRM' : 'Configure OnCall and Incident', + steps: isIrmPluginPresent ? [ { title: t( @@ -301,11 +336,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/apps/grate.irm.slack`, + url: `/a/${incidentPluginId}/integrations/apps/grate.irm.slack`, }, label: t('gops.use-get-essentials-configuration.essential-content.label.connect', 'Connect'), urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/apps/grate.irm.slack`, + url: `/a/${incidentPluginId}/integrations/apps/grate.irm.slack`, }, labelOnDone: 'View', }, @@ -323,11 +358,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + url: `/a/${onCallPluginId}/integrations/`, }, label: t('gops.use-get-essentials-configuration.essential-content.label.add', 'Add'), urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + url: `/a/${onCallPluginId}/integrations/`, }, labelOnDone: 'View', }, @@ -347,11 +382,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/walkthrough/generate-key`, + url: `/a/${incidentPluginId}/walkthrough/generate-key`, }, label: t('gops.use-get-essentials-configuration.essential-content.label.initialize', 'Initialize'), urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}`, + url: `/a/${incidentPluginId}`, }, labelOnDone: 'View', }, @@ -369,12 +404,12 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, + url: `/a/${onCallPluginId}/settings`, queryParams: { tab: 'ChatOps', chatOpsTab: 'Slack' }, }, label: t('gops.use-get-essentials-configuration.essential-content.label.connect', 'Connect'), urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`, + url: `/a/${onCallPluginId}/settings`, queryParams: { tab: 'ChatOps' }, }, labelOnDone: 'View', @@ -391,11 +426,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`, + url: `/a/${incidentPluginId}/integrations/grate.slack`, }, label: t('gops.use-get-essentials-configuration.essential-content.label.connect', 'Connect'), urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations`, + url: `/a/${incidentPluginId}/integrations`, }, }, done: isChatOpsInstalled, @@ -412,11 +447,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + url: `/a/${onCallPluginId}/integrations/`, }, label: t('gops.use-get-essentials-configuration.essential-content.label.add', 'Add'), urlLinkOnDone: { - url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`, + url: `/a/${onCallPluginId}/integrations/`, }, labelOnDone: 'View', }, @@ -432,7 +467,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { description: '', steps: [ { - title: getIsIrmPluginPresent() ? 'Send test alert' : 'Send OnCall demo alert via Alerting integration', + title: isIrmPluginPresent ? 'Send test alert' : 'Send OnCall demo alert via Alerting integration', description: 'In the integration page, click Send demo alert, to review your notification', button: { type: 'dropDown', @@ -441,8 +476,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { 'Select integration' ), options: onCallOptions, - onClickOption: (value) => - onIntegrationClick(value, `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`), + onClickOption: (value) => onIntegrationClick(value, `/a/${onCallPluginId}/integrations/`), stepNotAvailableText: 'No integrations available', }, }, @@ -458,7 +492,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { button: { type: 'openLink', urlLink: { - url: `/a/${getIrmIfPresentOrIncidentPluginId()}`, + url: `/a/${incidentPluginId}`, queryParams: { declare: 'new', drill: '1' }, }, label: t('gops.use-get-essentials-configuration.essential-content.label.start-drill', 'Start drill'), @@ -479,7 +513,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData { }, { stepsDone: 0, totalStepsToDo: 0 } ); - return { essentialContent, stepsDone, totalStepsToDo, isLoading }; + return { essentialContent, stepsDone, totalStepsToDo, isLoading: isLoading || isIrmConfigLoading }; } interface UseConfigurationProps { dataSourceConfigurationData: DataSourceConfigurationData; diff --git a/public/app/features/gops/configuration-tracker/onCall/hooks.ts b/public/app/features/gops/configuration-tracker/onCall/hooks.ts index fe7fb50c056..30793ea8c56 100644 --- a/public/app/features/gops/configuration-tracker/onCall/hooks.ts +++ b/public/app/features/gops/configuration-tracker/onCall/hooks.ts @@ -1,29 +1,44 @@ import { onCallApi } from 'app/features/alerting/unified/api/onCallApi'; import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge'; -import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config'; + +import { useIrmConfig } from '../irmHooks'; export function useGetOnCallIntegrations() { - const { installed: onCallPluginInstalled } = usePluginBridge(getIrmIfPresentOrOnCallPluginId()); + const { + irmConfig: { onCallPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); + const { installed: onCallPluginInstalled } = usePluginBridge(onCallPluginId); - const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, { - skip: !onCallPluginInstalled, - refetchOnFocus: true, - refetchOnReconnect: true, - refetchOnMountOrArgChange: true, - }); + const { data: onCallIntegrations } = onCallApi(onCallPluginId).endpoints.grafanaOnCallIntegrations.useQuery( + undefined, + { + skip: !onCallPluginInstalled || isIrmConfigLoading, + refetchOnFocus: true, + refetchOnReconnect: true, + refetchOnMountOrArgChange: true, + } + ); return onCallIntegrations ?? []; } function useGetOnCallConfigurationChecks() { - const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(undefined, { - refetchOnFocus: true, - refetchOnReconnect: true, - refetchOnMountOrArgChange: true, - }); + const { + irmConfig: { onCallPluginId }, + isIrmConfigLoading, + } = useIrmConfig(); + const { data: onCallConfigChecks, isLoading } = onCallApi(onCallPluginId).endpoints.onCallConfigChecks.useQuery( + undefined, + { + refetchOnFocus: true, + refetchOnReconnect: true, + refetchOnMountOrArgChange: true, + } + ); return { - isLoading, + isLoading: isLoading || isIrmConfigLoading, onCallConfigChecks: onCallConfigChecks ?? { is_chatops_connected: false, is_integration_chatops_connected: false }, }; } diff --git a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts index 916503fccdd..99d41775c0c 100644 --- a/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.test.ts @@ -3,7 +3,7 @@ import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime/unstable'; +import { getAppPluginMeta } from '@grafana/runtime/unstable'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -31,7 +31,6 @@ jest.mock('../logs/log', () => { }); describe('AddedComponentsRegistry', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -66,7 +65,7 @@ describe('AddedComponentsRegistry', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return empty registry when no extensions registered', async () => { diff --git a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts index dc099c3abf0..461ad0a0be0 100644 --- a/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedFunctionsRegistry.test.ts @@ -2,7 +2,7 @@ import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime/unstable'; +import { getAppPluginMeta } from '@grafana/runtime/unstable'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -30,7 +30,6 @@ jest.mock('../logs/log', () => { }); describe('addedFunctionsRegistry', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -65,7 +64,7 @@ describe('addedFunctionsRegistry', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return empty registry when no extensions registered', async () => { diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts index 64406f7d976..3d02c534476 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.test.ts @@ -2,7 +2,7 @@ import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime/unstable'; +import { getAppPluginMeta } from '@grafana/runtime/unstable'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -30,7 +30,6 @@ jest.mock('../logs/log', () => { }); describe('AddedLinksRegistry', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -65,7 +64,7 @@ describe('AddedLinksRegistry', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return empty registry when no extensions registered', async () => { diff --git a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts index cb0fc24d88a..00f303b5ae3 100644 --- a/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts +++ b/public/app/features/plugins/extensions/registry/ExposedComponentsRegistry.test.ts @@ -3,7 +3,7 @@ import { firstValueFrom, take } from 'rxjs'; import { PluginLoadingStrategy } from '@grafana/data'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime/unstable'; +import { getAppPluginMeta } from '@grafana/runtime/unstable'; import { log } from '../logs/log'; import { resetLogMock } from '../logs/testUtils'; @@ -31,7 +31,6 @@ jest.mock('../logs/log', () => { }); describe('ExposedComponentsRegistry', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'grafana-basic-app'; const appPluginConfig = { id: pluginId, @@ -66,7 +65,7 @@ describe('ExposedComponentsRegistry', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return empty registry when no exposed components have been registered', async () => { diff --git a/public/app/features/plugins/extensions/useLoadAppPlugins.tsx b/public/app/features/plugins/extensions/useLoadAppPlugins.tsx index ce013713906..1e38a321ecb 100644 --- a/public/app/features/plugins/extensions/useLoadAppPlugins.tsx +++ b/public/app/features/plugins/extensions/useLoadAppPlugins.tsx @@ -1,19 +1,27 @@ import { useAsync } from 'react-use'; +import { AppPluginConfig } from '@grafana/data'; +import { useAppPluginMetas } from '@grafana/runtime/unstable'; + import { preloadPlugins } from '../pluginPreloader'; -import { getAppPluginConfigs } from './utils'; +export type UseLoadAppPluginsPredicate = (apps: AppPluginConfig[], filterById: string) => string[]; -export function useLoadAppPlugins(pluginIds: string[] = []): { isLoading: boolean } { +const noop: UseLoadAppPluginsPredicate = () => []; + +export function useLoadAppPlugins( + filterById: string, + predicate: UseLoadAppPluginsPredicate = noop +): { isLoading: boolean } { + const { isAppPluginMetasLoading, apps } = useAppPluginMetas(); + const { isAppPluginMetasLoading: isFilteredLoading, apps: filtered } = useAppPluginMetas(predicate(apps, filterById)); const { loading: isLoading } = useAsync(async () => { - const appConfigs = getAppPluginConfigs(pluginIds); - - if (!appConfigs.length) { + if (!filtered.length) { return; } - await preloadPlugins(appConfigs); - }); + await preloadPlugins(filtered); + }, [filtered]); - return { isLoading }; + return { isLoading: isLoading || isAppPluginMetasLoading || isFilteredLoading }; } diff --git a/public/app/features/plugins/extensions/usePluginComponent.test.tsx b/public/app/features/plugins/extensions/usePluginComponent.test.tsx index 75e212161e8..657cdae04c0 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.test.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.test.tsx @@ -4,7 +4,6 @@ import type { JSX } from 'react'; import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data'; import { config } from '@grafana/runtime'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMetas } from '@grafana/runtime/unstable'; import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext'; import { log } from './logs/log'; @@ -53,7 +52,6 @@ describe('usePluginComponent()', () => { let registries: PluginExtensionRegistries; let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element; let pluginMeta: PluginMeta; - const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const exposedComponentId = `${pluginId}/exposed-component/v1`; const exposedComponentConfig = { @@ -145,7 +143,7 @@ describe('usePluginComponent()', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return null if there are no component exposed for the id', () => { diff --git a/public/app/features/plugins/extensions/usePluginComponent.tsx b/public/app/features/plugins/extensions/usePluginComponent.tsx index c85eabb0441..3b255d5a1bb 100644 --- a/public/app/features/plugins/extensions/usePluginComponent.tsx +++ b/public/app/features/plugins/extensions/usePluginComponent.tsx @@ -15,7 +15,7 @@ import { isExposedComponentDependencyMissing } from './validators'; export function usePluginComponent(id: string): UsePluginComponentResult { const registryItem = useExposedComponentRegistrySlice(id); const pluginContext = usePluginContext(); - const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExposedComponentPluginDependencies(id)); + const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(id, getExposedComponentPluginDependencies); return useMemo(() => { // For backwards compatibility we don't enable restrictions in production or when the hook is used in core Grafana. diff --git a/public/app/features/plugins/extensions/usePluginComponents.tsx b/public/app/features/plugins/extensions/usePluginComponents.tsx index c800bf08a31..14c600a7724 100644 --- a/public/app/features/plugins/extensions/usePluginComponents.tsx +++ b/public/app/features/plugins/extensions/usePluginComponents.tsx @@ -21,7 +21,7 @@ export function usePluginComponents({ }: UsePluginComponentsOptions): UsePluginComponentsResult { const registryItems = useAddedComponentsRegistrySlice(extensionPointId); const pluginContext = usePluginContext(); - const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId)); + const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies); return useMemo(() => { const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins }); diff --git a/public/app/features/plugins/extensions/usePluginFunctions.tsx b/public/app/features/plugins/extensions/usePluginFunctions.tsx index b0c065d7aaa..a2656a7c088 100644 --- a/public/app/features/plugins/extensions/usePluginFunctions.tsx +++ b/public/app/features/plugins/extensions/usePluginFunctions.tsx @@ -15,8 +15,7 @@ export function usePluginFunctions({ }: UsePluginFunctionsOptions): UsePluginFunctionsResult { const registryItems = useAddedFunctionsRegistrySlice(extensionPointId); const pluginContext = usePluginContext(); - const deps = getExtensionPointPluginDependencies(extensionPointId); - const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(deps); + const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies); return useMemo(() => { const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins }); diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index 70518ab9450..05d077125db 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -24,7 +24,7 @@ export function usePluginLinks({ }: UsePluginLinksOptions): UsePluginLinksResult { const registryItems = useAddedLinksRegistrySlice(extensionPointId); const pluginContext = usePluginContext(); - const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId)); + const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies); return useMemo(() => { const { result, pointLog } = validateExtensionPoint({ diff --git a/public/app/features/plugins/extensions/utils.test.tsx b/public/app/features/plugins/extensions/utils.test.tsx index 6d192561086..b7c3cf96da2 100644 --- a/public/app/features/plugins/extensions/utils.test.tsx +++ b/public/app/features/plugins/extensions/utils.test.tsx @@ -1,10 +1,9 @@ import { render, screen, waitFor } from '@testing-library/react'; import { type Unsubscribable } from 'rxjs'; -import { dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data'; -import { config, AppPluginConfig } from '@grafana/runtime'; +import { type AppPluginConfig, dateTime, usePluginContext, PluginLoadingStrategy } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime/unstable'; import { appEvents } from 'app/core/app_events'; import { ShowModalReactEvent } from 'app/types/events'; @@ -18,7 +17,6 @@ import { wrapWithPluginContext, getExtensionPointPluginDependencies, getExposedComponentPluginDependencies, - getAppPluginConfigs, getAppPluginIdFromExposedComponentId, getAppPluginDependencies, getExtensionPointPluginMeta, @@ -998,79 +996,6 @@ describe('Plugin Extensions / Utils', () => { }); }); - describe('getAppPluginConfigs()', () => { - const originalApps = getAppPluginMetas(); - const genereicAppPluginConfig = { - path: '', - version: '', - preload: false, - angular: { - detected: false, - hideDeprecation: false, - }, - loadingStrategy: PluginLoadingStrategy.fetch, - dependencies: { - grafanaVersion: '8.0.0', - plugins: [], - extensions: { - exposedComponents: [], - }, - }, - extensions: { - addedLinks: [], - addedComponents: [], - addedFunctions: [], - exposedComponents: [], - extensionPoints: [], - }, - }; - - afterEach(() => { - setAppPluginMetas(originalApps); - }); - - test('should return the app plugin configs based on the provided plugin ids', () => { - setAppPluginMetas({ - 'myorg-first-app': { - ...genereicAppPluginConfig, - id: 'myorg-first-app', - }, - 'myorg-second-app': { - ...genereicAppPluginConfig, - id: 'myorg-second-app', - }, - 'myorg-third-app': { - ...genereicAppPluginConfig, - id: 'myorg-third-app', - }, - }); - - expect(getAppPluginConfigs(['myorg-first-app', 'myorg-third-app'])).toEqual([ - getAppPluginMeta('myorg-first-app'), - getAppPluginMeta('myorg-third-app'), - ]); - }); - - test('should simply ignore the app plugin ids that do not belong to a config', () => { - setAppPluginMetas({ - 'myorg-first-app': { - ...genereicAppPluginConfig, - id: 'myorg-first-app', - }, - 'myorg-second-app': { - ...genereicAppPluginConfig, - id: 'myorg-second-app', - }, - 'myorg-third-app': { - ...genereicAppPluginConfig, - id: 'myorg-third-app', - }, - }); - - expect(getAppPluginConfigs(['myorg-first-app', 'unknown-app-id'])).toEqual([getAppPluginMeta('myorg-first-app')]); - }); - }); - describe('getAppPluginIdFromExposedComponentId()', () => { test('should return the app plugin id from an extension point id', () => { expect(getAppPluginIdFromExposedComponentId('myorg-extensions-app/component/v1')).toBe('myorg-extensions-app'); @@ -1078,7 +1003,6 @@ describe('Plugin Extensions / Utils', () => { }); describe('getExtensionPointPluginDependencies()', () => { - const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1104,10 +1028,6 @@ describe('Plugin Extensions / Utils', () => { }, }; - afterEach(() => { - setAppPluginMetas(originalApps); - }); - test('should return the app plugin ids that register extensions to a link extension point', () => { const extensionPointId = 'myorg-first-app/link/v1'; @@ -1292,7 +1212,6 @@ describe('Plugin Extensions / Utils', () => { }); describe('getExposedComponentPluginDependencies()', () => { - const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1318,10 +1237,6 @@ describe('Plugin Extensions / Utils', () => { }, }; - afterEach(() => { - setAppPluginMetas(originalApps); - }); - test('should only return the app plugin id that exposes the component, if that component does not depend on anything', () => { const exposedComponentId = 'myorg-second-app/component/v1'; @@ -1438,7 +1353,6 @@ describe('Plugin Extensions / Utils', () => { }); describe('getAppPluginDependencies()', () => { - const originalApps = getAppPluginMetas(); const genereicAppPluginConfig = { path: '', version: '', @@ -1464,10 +1378,6 @@ describe('Plugin Extensions / Utils', () => { }, }; - afterEach(() => { - setAppPluginMetas(originalApps); - }); - test('should not end up in an infinite loop if there are circular dependencies', () => { setAppPluginMetas({ 'myorg-first-app': { @@ -1528,7 +1438,6 @@ describe('Plugin Extensions / Utils', () => { }); describe('getExtensionPointPluginMeta()', () => { - const originalApps = getAppPluginMetas(); const mockExtensionPointId = 'test-extension-point'; const mockApp1: AppPluginConfig = { id: 'app1', @@ -1586,10 +1495,6 @@ describe('Plugin Extensions / Utils', () => { setAppPluginMetas({}); }); - afterEach(() => { - setAppPluginMetas(originalApps); - }); - it('should return empty map when no plugins have extensions for the point', () => { setAppPluginMetas({ app1: { ...mockApp1, extensions: { ...mockApp1.extensions, addedComponents: [], addedLinks: [] } }, diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 5a9796d2f18..bec0029eae5 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import { useAsync } from 'react-use'; import { + type AppPluginConfig, type PluginExtensionEventHelpers, type PluginExtensionOpenModalOptions, isDateTime, @@ -16,8 +17,8 @@ import { PluginExtensionPoints, ExtensionInfo, } from '@grafana/data'; -import { reportInteraction, config, AppPluginConfig } from '@grafana/runtime'; -import { getAppPluginMeta, getAppPluginMetas } from '@grafana/runtime/unstable'; +import { reportInteraction, config } from '@grafana/runtime'; +import { getAppPluginMeta } from '@grafana/runtime/unstable'; import { Modal } from '@grafana/ui'; import { appEvents } from 'app/core/app_events'; import { getPluginSettings } from 'app/features/plugins/pluginSettings'; @@ -33,6 +34,7 @@ import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaAp import { ExtensionErrorBoundary } from './ExtensionErrorBoundary'; import { ExtensionsLog, log as baseLog } from './logs/log'; import { AddedLinkRegistryItem } from './registry/AddedLinksRegistry'; +import { UseLoadAppPluginsPredicate } from './useLoadAppPlugins'; import { assertIsNotPromise, assertStringProps, isPromise } from './validators'; export function handleErrorsInFn(fn: Function, errorMessagePrefix = '') { @@ -607,9 +609,6 @@ export function getLinkExtensionPathWithTracking(pluginId: string, path: string, // Can be set with the `GF_DEFAULT_APP_MODE` environment variable export const isGrafanaDevMode = () => config.buildInfo.env === 'development'; -export const getAppPluginConfigs = (pluginIds: string[] = []) => - Object.values(getAppPluginMetas()).filter((app) => pluginIds.includes(app.id)); - export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) => { return exposedComponentId.split('/')[0]; }; @@ -617,8 +616,11 @@ export const getAppPluginIdFromExposedComponentId = (exposedComponentId: string) // Returns a list of app plugin ids that are registering extensions to this extension point. // (These plugins are necessary to be loaded to use the extension point.) // (The function also returns the plugin ids that the plugins - that extend the extension point - depend on.) -export const getExtensionPointPluginDependencies = (extensionPointId: string): string[] => { - return Object.values(getAppPluginMetas()) +export const getExtensionPointPluginDependencies: UseLoadAppPluginsPredicate = ( + apps: AppPluginConfig[], + extensionPointId: string +): string[] => { + return apps .filter( (app) => app.extensions.addedLinks.some((link) => link.targets.includes(extensionPointId)) || @@ -626,7 +628,7 @@ export const getExtensionPointPluginDependencies = (extensionPointId: string): s ) .map((app) => app.id) .reduce((acc: string[], id: string) => { - return [...acc, id, ...getAppPluginDependencies(id)]; + return [...acc, id, ...getAppPluginDependencies(apps, id)]; }, []); }; @@ -643,9 +645,12 @@ export type ExtensionPointPluginMeta = Map< * @param extensionPointId - The id of the extension point. * @returns A map of plugin ids and their addedComponents and addedLinks to the extension point. */ -export const getExtensionPointPluginMeta = (extensionPointId: string): ExtensionPointPluginMeta => { +export const getExtensionPointPluginMeta = ( + apps: AppPluginConfig[], + extensionPointId: string +): ExtensionPointPluginMeta => { return new Map( - getExtensionPointPluginDependencies(extensionPointId) + getExtensionPointPluginDependencies(apps, extensionPointId) .map((pluginId) => { const app = getAppPluginMeta(pluginId); // if the plugin does not exist or does not expose any components or links to the extension point, return undefined @@ -672,19 +677,26 @@ export const getExtensionPointPluginMeta = (extensionPointId: string): Extension // Returns a list of app plugin ids that are necessary to be loaded to use the exposed component. // (It is first the plugin that exposes the component, and then the ones that it depends on.) -export const getExposedComponentPluginDependencies = (exposedComponentId: string) => { +export const getExposedComponentPluginDependencies: UseLoadAppPluginsPredicate = ( + apps: AppPluginConfig[], + exposedComponentId: string +) => { const pluginId = getAppPluginIdFromExposedComponentId(exposedComponentId); return [pluginId].reduce((acc: string[], pluginId: string) => { - return [...acc, pluginId, ...getAppPluginDependencies(pluginId)]; + return [...acc, pluginId, ...getAppPluginDependencies(apps, pluginId)]; }, []); }; // Returns a list of app plugin ids that are necessary to be loaded, based on the `dependencies.extensions` // metadata field. (For example the plugins that expose components that the app depends on.) // Heads up! This is a recursive function. -export const getAppPluginDependencies = (pluginId: string, visited: string[] = []): string[] => { - const app = getAppPluginMeta(pluginId); +export const getAppPluginDependencies = ( + apps: AppPluginConfig[], + pluginId: string, + visited: string[] = [] +): string[] => { + const app = apps.find((a) => a.id === pluginId); if (!app) { return []; } @@ -699,7 +711,7 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [ return ( pluginIdDependencies .reduce((acc, _pluginId) => { - return [...acc, ...getAppPluginDependencies(_pluginId, [...visited, pluginId])]; + return [...acc, ...getAppPluginDependencies(apps, _pluginId, [...visited, pluginId])]; }, pluginIdDependencies) // We don't want the plugin to "depend on itself" .filter((id) => id !== pluginId) @@ -707,23 +719,26 @@ export const getAppPluginDependencies = (pluginId: string, visited: string[] = [ }; // Returns a list of app plugins that has to be loaded before core Grafana could finish the initialization. -export const getAppPluginsToAwait = () => { +export const getAppPluginsToAwait = (apps: AppPluginConfig[]) => { const pluginIds = [ // The "cloud-home-app" is registering banners once it's loaded, and this can cause a rerender in the AppChrome if it's loaded after the Grafana app init. 'cloud-home-app', ]; - return Object.values(getAppPluginMetas()).filter((app) => pluginIds.includes(app.id)); + return apps.filter((app) => pluginIds.includes(app.id)); }; // Returns a list of app plugins that has to be preloaded in parallel with the core Grafana initialization. -export const getAppPluginsToPreload = () => { +export const getAppPluginsToPreload = (apps: AppPluginConfig[]) => { // The DashboardPanelMenu extension point is using the `getPluginExtensions()` API in scenes at the moment, which means that it cannot yet benefit from dynamic plugin loading. - const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies(PluginExtensionPoints.DashboardPanelMenu); - const awaitedPluginIds = getAppPluginsToAwait().map((app) => app.id); + const dashboardPanelMenuPluginIds = getExtensionPointPluginDependencies( + apps, + PluginExtensionPoints.DashboardPanelMenu + ); + const awaitedPluginIds = getAppPluginsToAwait(apps).map((app) => app.id); const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id); - return Object.values(getAppPluginMetas()).filter((app) => { + return apps.filter((app) => { return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id)); }); }; diff --git a/public/app/features/plugins/extensions/validators.test.tsx b/public/app/features/plugins/extensions/validators.test.tsx index b0807aecc6e..f768efcc087 100644 --- a/public/app/features/plugins/extensions/validators.test.tsx +++ b/public/app/features/plugins/extensions/validators.test.tsx @@ -8,7 +8,6 @@ import { PluginType, } from '@grafana/data'; import { setAppPluginMetas } from '@grafana/runtime/internal'; -import { getAppPluginMetas } from '@grafana/runtime/unstable'; import { createLogMock } from './logs/testUtils'; import { @@ -224,7 +223,6 @@ describe('Plugin Extension Validators', () => { }); describe('isAddedLinkMetaInfoMissing()', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const appPluginConfig = { id: pluginId, @@ -262,7 +260,7 @@ describe('Plugin Extension Validators', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { @@ -373,7 +371,6 @@ describe('Plugin Extension Validators', () => { }); describe('isAddedComponentMetaInfoMissing()', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const appPluginConfig = { id: pluginId, @@ -412,7 +409,7 @@ describe('Plugin Extension Validators', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { @@ -522,7 +519,6 @@ describe('Plugin Extension Validators', () => { }); describe('isExposedComponentMetaInfoMissing()', () => { - const originalApps = getAppPluginMetas(); const pluginId = 'myorg-extensions-app'; const appPluginConfig = { id: pluginId, @@ -561,7 +557,7 @@ describe('Plugin Extension Validators', () => { }); afterEach(() => { - setAppPluginMetas(originalApps); + setAppPluginMetas({}); }); it('should return FALSE if the meta-info in the plugin.json is correct', () => { diff --git a/public/app/features/plugins/sandbox/codeLoader.ts b/public/app/features/plugins/sandbox/codeLoader.ts index 6502b9c6db5..10706658655 100644 --- a/public/app/features/plugins/sandbox/codeLoader.ts +++ b/public/app/features/plugins/sandbox/codeLoader.ts @@ -1,6 +1,6 @@ import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { getAppPluginMetas } from '@grafana/runtime/unstable'; +import { getAppPluginConfig } from '@grafana/runtime/unstable'; import { transformPluginSourceForCDN } from '../cdn/utils'; import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache'; @@ -122,7 +122,7 @@ export function patchSandboxEnvironmentPrototype(sandboxEnvironment: SandboxEnvi ); } -export function getPluginLoadData(pluginId: string): SandboxPluginMeta { +export async function getPluginLoadData(pluginId: string): Promise { // find it in datasources for (const datasource of Object.values(config.datasources)) { if (datasource.type === pluginId) { @@ -139,16 +139,15 @@ export function getPluginLoadData(pluginId: string): SandboxPluginMeta { //find it in apps //the information inside the apps object is more limited - for (const app of Object.values(getAppPluginMetas())) { - if (app.id === pluginId) { - return { - id: pluginId, - type: PluginType.app, - module: app.path, - moduleHash: app.moduleHash, - }; - } + const app = await getAppPluginConfig(pluginId); + if (!app) { + throw new Error(`Could not find plugin ${pluginId}`); } - throw new Error(`Could not find plugin ${pluginId}`); + return { + id: pluginId, + type: PluginType.app, + module: app.path, + moduleHash: app.moduleHash, + }; } diff --git a/public/app/features/plugins/sandbox/sandboxPluginLoader.ts b/public/app/features/plugins/sandbox/sandboxPluginLoader.ts index 5b62216ff63..09f407eae7e 100644 --- a/public/app/features/plugins/sandbox/sandboxPluginLoader.ts +++ b/public/app/features/plugins/sandbox/sandboxPluginLoader.ts @@ -31,7 +31,7 @@ const pluginLogCache: Record = {}; export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise { patchWebAPIs(); try { - const pluginMeta = getPluginLoadData(pluginId); + const pluginMeta = await getPluginLoadData(pluginId); if (!pluginImportCache.has(pluginId)) { pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta)); }