chore: refactoring to async

This commit is contained in:
Hugo Häggmark
2025-12-12 12:10:28 +01:00
parent 75e08a20f6
commit 5e3c7ad0c1
36 changed files with 522 additions and 469 deletions
+1 -1
View File
@@ -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<string, AppPluginConfigGrafanaData> = {};
auth: AuthSettings = {};
minRefreshInterval = '';
@@ -1,41 +0,0 @@
import { cloneDeep } from 'lodash';
import { AppPluginConfig } from '@grafana/data';
import { config } from '../config';
export type AppPluginMetas = Record<string, AppPluginConfig>;
let apps: AppPluginMetas = {};
export async function initPluginMetas(): Promise<void> {
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 };
}
@@ -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<string, AppPluginConfig>;
let apps: AppPluginMetas = {};
let appsPromise: Promise<void> | undefined = undefined;
function areAppsInitialized(): boolean {
return Boolean(Object.keys(apps).length);
}
export async function initPluginMetas(): Promise<void> {
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<AppPluginConfig[]> {
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<AppPluginConfig | undefined> {
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 };
}
+8 -1
View File
@@ -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';
+4 -2
View File
@@ -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);
@@ -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
@@ -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<IncidentsPluginConfigDto, void>({
query: () => ({
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker'),
data: {},
method: 'POST',
showErrorAlert: false,
export const incidentsApi = (pluginId: SupportedPlugin) =>
alertingApi.injectEndpoints({
endpoints: (build) => ({
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, void>({
query: () => ({
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker', pluginId),
data: {},
method: 'POST',
showErrorAlert: false,
}),
}),
}),
}),
});
});
@@ -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<OnCallIntegrationDTO[], void>({
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<OnCallIntegrationDTO[], void>({
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<boolean, string>({
query: (name) => ({
url: getProxyApiUrl('/alert_receive_channels/validate_name/'),
params: { verbal_name: name },
showErrorAlert: false,
validateIntegrationName: build.query<boolean, string>({
query: (name) => ({
url: getProxyApiUrl('/alert_receive_channels/validate_name/', pluginId),
params: { verbal_name: name },
showErrorAlert: false,
}),
}),
createIntegration: build.mutation<NewOnCallIntegrationDTO, CreateIntegrationDTO>({
query: (integration) => ({
url: getProxyApiUrl('/alert_receive_channels/', pluginId),
data: integration,
method: 'POST',
showErrorAlert: true,
}),
invalidatesTags: ['OnCallIntegrations'],
}),
features: build.query<OnCallFeature[], void>({
query: () => ({
url: getProxyApiUrl('/features/', pluginId),
showErrorAlert: false,
}),
}),
onCallConfigChecks: build.query<OnCallConfigChecks, void>({
query: () => ({
url: getProxyApiUrl('/organization/config-checks/', pluginId),
showErrorAlert: false,
}),
}),
}),
createIntegration: build.mutation<NewOnCallIntegrationDTO, CreateIntegrationDTO>({
query: (integration) => ({
url: getProxyApiUrl('/alert_receive_channels/'),
data: integration,
method: 'POST',
showErrorAlert: true,
}),
invalidatesTags: ['OnCallIntegrations'],
}),
features: build.query<OnCallFeature[], void>({
query: () => ({
url: getProxyApiUrl('/features/'),
showErrorAlert: false,
}),
}),
onCallConfigChecks: build.query<OnCallConfigChecks, void>({
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;
}
@@ -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 (
<>
@@ -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,
})
: [],
}),
@@ -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,
};
}),
@@ -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,
};
@@ -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`,
};
@@ -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);
@@ -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);
@@ -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);
});
});
@@ -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;
}
@@ -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,
};
}
@@ -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);
});
});
});
@@ -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;
@@ -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 },
};
}
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 () => {
@@ -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 };
}
@@ -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', () => {
@@ -15,7 +15,7 @@ import { isExposedComponentDependencyMissing } from './validators';
export function usePluginComponent<Props extends object = {}>(id: string): UsePluginComponentResult<Props> {
const registryItem = useExposedComponentRegistrySlice<Props>(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.
@@ -21,7 +21,7 @@ export function usePluginComponents<Props extends object = {}>({
}: UsePluginComponentsOptions): UsePluginComponentsResult<Props> {
const registryItems = useAddedComponentsRegistrySlice<Props>(extensionPointId);
const pluginContext = usePluginContext();
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(getExtensionPointPluginDependencies(extensionPointId));
const { isLoading: isLoadingAppPlugins } = useLoadAppPlugins(extensionPointId, getExtensionPointPluginDependencies);
return useMemo(() => {
const { result } = validateExtensionPoint({ extensionPointId, pluginContext, isLoadingAppPlugins });
@@ -15,8 +15,7 @@ export function usePluginFunctions<Signature>({
}: UsePluginFunctionsOptions): UsePluginFunctionsResult<Signature> {
const registryItems = useAddedFunctionsRegistrySlice<Signature>(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 });
@@ -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({
@@ -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: [] } },
@@ -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));
});
};
@@ -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', () => {
@@ -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<SandboxPluginMeta> {
// 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,
};
}
@@ -31,7 +31,7 @@ const pluginLogCache: Record<string, boolean> = {};
export async function importPluginModuleInSandbox({ pluginId }: { pluginId: string }): Promise<System.Module> {
patchWebAPIs();
try {
const pluginMeta = getPluginLoadData(pluginId);
const pluginMeta = await getPluginLoadData(pluginId);
if (!pluginImportCache.has(pluginId)) {
pluginImportCache.set(pluginId, doImportPluginModuleInSandbox(pluginMeta));
}