chore: more refactor

This commit is contained in:
Hugo Häggmark
2025-12-15 10:13:26 +01:00
parent 88924ee9ac
commit 512f4bc8dc
46 changed files with 2769 additions and 2809 deletions
@@ -29,4 +29,4 @@ export {
export { UserStorage } from '../utils/userStorage';
export { initOpenFeature, evaluateBooleanFlag } from './openFeature';
export { initPluginMetas, setAppPluginMetas } from '../services/plugins';
export { setAppPluginMetas } from '../services/plugins';
@@ -14,7 +14,7 @@ function areAppsInitialized(): boolean {
return Boolean(Object.keys(apps).length);
}
export async function initPluginMetas(): Promise<void> {
async function initPluginMetas(): Promise<void> {
if (appsPromise) {
return appsPromise;
}
@@ -44,15 +44,7 @@ export async function getAppPluginMetas(): Promise<AppPluginConfig[]> {
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> {
export async function getAppPluginMeta(id: string): Promise<AppPluginConfig | undefined> {
if (!areAppsInitialized()) {
await initPluginMetas();
}
@@ -85,3 +77,14 @@ export function useAppPluginMetas(filterByIds: string[] = []): UseAppPluginMetas
return { isAppPluginMetasLoading: loading, error, apps: filtered };
}
export interface UseAppPluginMetaResult {
isAppPluginMetaLoading: boolean;
error: Error | undefined;
app: AppPluginConfig | undefined;
}
export function useAppPluginMeta(filterById: string): UseAppPluginMetaResult {
const { loading, error, value: app } = useAsync(() => getAppPluginMeta(filterById));
return { isAppPluginMetaLoading: loading, error, app };
}
+3 -2
View File
@@ -14,9 +14,10 @@ export const unstable = {};
export {
type AppPluginMetas,
type UseAppPluginMetasResult as UseAppPluginMetasCollectionResult,
type UseAppPluginMetaResult,
type UseAppPluginMetasResult,
getAppPluginMeta,
getAppPluginConfig,
getAppPluginMetas,
useAppPluginMeta,
useAppPluginMetas,
} from './services/plugins';
+3 -17
View File
@@ -44,14 +44,12 @@ import {
} from '@grafana/runtime';
import {
initOpenFeature,
initPluginMetas,
setGetObservablePluginComponents,
setGetObservablePluginLinks,
setPanelDataErrorView,
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';
@@ -101,10 +99,9 @@ import { usePluginComponent } from './features/plugins/extensions/usePluginCompo
import { usePluginComponents } from './features/plugins/extensions/usePluginComponents';
import { usePluginFunctions } from './features/plugins/extensions/usePluginFunctions';
import { usePluginLinks } from './features/plugins/extensions/usePluginLinks';
import { getAppPluginsToAwait, getAppPluginsToPreload } from './features/plugins/extensions/utils';
import { importPanelPlugin, syncGetPanelPlugin } from './features/plugins/importPanelPlugin';
import { initSystemJSHooks } from './features/plugins/loader/systemjsHooks';
import { preloadPlugins } from './features/plugins/pluginPreloader';
import { preloadPluginsToBeAwaited, preloadPluginsToBePreloaded } from './features/plugins/pluginPreloader';
import { QueryRunner } from './features/query/state/QueryRunner';
import { runRequest } from './features/query/state/runRequest';
import { initWindowRuntime } from './features/runtime/init';
@@ -178,13 +175,6 @@ export class GrafanaApp {
// This needs to be done after the `initEchoSrv` since it is being used under the hood.
startMeasure('frontend_app_init');
try {
startMeasure('frontend_app_init_plugins');
await initPluginMetas();
} finally {
stopMeasure('frontend_app_init_plugins');
}
setLocale(config.regionalFormat);
setWeekStart(contextSrv.user.weekStart);
setPanelRenderer(PanelRenderer);
@@ -266,12 +256,8 @@ export class GrafanaApp {
const skipAppPluginsPreload =
config.featureToggles.rendererDisableAppPluginsPreload && contextSrv.user.authenticatedBy === 'render';
if (contextSrv.user.orgRole !== '' && !skipAppPluginsPreload) {
const apps = await getAppPluginMetas();
const appPluginsToAwait = getAppPluginsToAwait(apps);
const appPluginsToPreload = getAppPluginsToPreload(apps);
preloadPlugins(appPluginsToPreload);
await preloadPlugins(appPluginsToAwait);
preloadPluginsToBePreloaded();
await preloadPluginsToBeAwaited();
}
setHelpNavItemHook(useHelpNode);
@@ -1,4 +1,4 @@
import { SupportedPlugin } from '../types/pluginBridges';
import { getIrmIfPresentOrIncidentPluginId } from '../utils/config';
import { alertingApi } from './alertingApi';
@@ -7,18 +7,17 @@ interface IncidentsPluginConfigDto {
isIncidentCreated: boolean;
}
const getProxyApiUrl = (path: string, pluginId: SupportedPlugin) => `/api/plugins/${pluginId}/resources${path}`;
const getProxyApiUrl = (path: string) => `/api/plugins/${getIrmIfPresentOrIncidentPluginId()}/resources${path}`;
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,
}),
export const incidentsApi = alertingApi.injectEndpoints({
endpoints: (build) => ({
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, void>({
query: () => ({
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker'),
data: {},
method: 'POST',
showErrorAlert: false,
}),
}),
});
}),
});
@@ -1,4 +1,4 @@
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { config } from '@grafana/runtime';
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
import { SupportedPlugin } from '../types/pluginBridges';
@@ -7,7 +7,8 @@ import { getProxyApiUrl } from './onCallApi';
describe('getProxyApiUrl', () => {
it('should return URL with IRM plugin ID when IRM plugin is present', () => {
setAppPluginMetas({ [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) });
// eslint-disable-next-line no-restricted-syntax
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
'/api/plugins/grafana-irm-app/resources/alert_receive_channels/'
@@ -15,10 +16,11 @@ describe('getProxyApiUrl', () => {
});
it('should return URL with OnCall plugin ID when IRM plugin is not present', () => {
setAppPluginMetas({
// eslint-disable-next-line no-restricted-syntax
config.apps = {
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
});
};
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
'/api/plugins/grafana-oncall-app/resources/alert_receive_channels/'
@@ -1,7 +1,7 @@
import { FetchError, isFetchError } from '@grafana/runtime';
import { GRAFANA_ONCALL_INTEGRATION_TYPE } from '../components/receivers/grafanaAppReceivers/onCall/onCall';
import { SupportedPlugin } from '../types/pluginBridges';
import { getIrmIfPresentOrOnCallPluginId } from '../utils/config';
import { alertingApi } from './alertingApi';
@@ -38,63 +38,62 @@ export interface OnCallConfigChecks {
is_integration_chatops_connected: boolean;
}
export function getProxyApiUrl(path: string, pluginId: SupportedPlugin) {
return `/api/plugins/${pluginId}/resources${path}`;
export function getProxyApiUrl(path: string) {
return `/api/plugins/${getIrmIfPresentOrOnCallPluginId()}/resources${path}`;
}
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;
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,
},
providesTags: ['OnCallIntegrations'],
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,
}),
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,
}),
}),
});
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
@@ -102,6 +101,8 @@ 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,19 +11,16 @@ interface Props {
url?: string;
}
const pluginId = getIrmIfPresentOrIncidentPluginId();
export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => {
const {
irmConfig: { incidentPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const bridgeURL = createBridgeURL(incidentPluginId, '/incidents/declare', {
const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', {
title,
severity,
url,
});
const { loading: isPluginBridgeLoading, installed, settings } = usePluginBridge(incidentPluginId);
const loading = isIrmConfigLoading || isPluginBridgeLoading;
const { loading, installed, settings } = usePluginBridge(pluginId);
return (
<>
@@ -54,18 +51,13 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P
};
export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }: Props) => {
const {
irmConfig: { incidentPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const bridgeURL = createBridgeURL(incidentPluginId, '/incidents/declare', {
const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', {
title,
severity,
url,
});
const { loading: isPluginBridgeLoading, installed, settings } = usePluginBridge(incidentPluginId);
const loading = isIrmConfigLoading || isPluginBridgeLoading;
const { loading, installed, settings } = usePluginBridge(pluginId);
return (
<>
@@ -12,7 +12,6 @@ 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';
@@ -22,7 +21,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 { SupportedPlugin } from '../../types/pluginBridges';
import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config';
import { enhanceContactPointsWithMetadata } from './utils';
@@ -42,7 +41,7 @@ const {
useGrafanaNotifiersQuery,
useLazyGetAlertmanagerConfigurationQuery,
} = alertmanagerApi;
const { useGrafanaOnCallIntegrationsQuery } = onCallApi;
const {
useListNamespacedReceiverQuery,
useReadNamespacedReceiverQuery,
@@ -62,14 +61,8 @@ const defaultOptions = {
* Otherwise, returns no data
*/
const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
const {
irmConfig: { onCallPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const { installed, loading: isPluginBridgeLoading } = usePluginBridge(onCallPluginId);
const { useGrafanaOnCallIntegrationsQuery } = onCallApi(onCallPluginId);
const { installed, loading } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });
const loading = isIrmConfigLoading || isPluginBridgeLoading;
return useMemo(() => {
if (installed) {
@@ -145,11 +138,9 @@ 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 || isIrmConfigLoading;
const isLoading = onCallResponse.isLoading || alertNotifiers.isLoading || contactPointsListResponse.isLoading;
if (isLoading) {
return {
@@ -169,7 +160,6 @@ export const useGrafanaContactPoints = ({
onCallIntegrations: onCallResponse?.data,
contactPoints: contactPointsListResponse.data || [],
alertmanagerConfiguration: alertmanagerConfigResponse.data,
irmConfig,
});
return {
@@ -182,8 +172,6 @@ export const useGrafanaContactPoints = ({
contactPointsListResponse,
contactPointsStatusResponse,
onCallResponse,
isIrmConfigLoading,
irmConfig,
]);
};
@@ -250,10 +238,9 @@ 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 || isIrmConfigLoading,
skip: skip || !isGrafanaAlertmanager,
fetchStatuses,
fetchPolicies,
});
@@ -267,7 +254,6 @@ export function useContactPointsWithStatus({
notifiers: cloudNotifierTypes,
contactPoints: result.data.alertmanager_config.receivers ?? [],
alertmanagerConfiguration: result.data,
irmConfig,
})
: [],
}),
@@ -5,7 +5,6 @@ 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,
@@ -117,7 +116,6 @@ type EnhanceContactPointsArgs = {
onCallIntegrations?: OnCallIntegrationDTO[] | undefined | null;
contactPoints: Receiver[];
alertmanagerConfiguration?: AlertManagerCortexConfig;
irmConfig: UseIsIrmConfig;
};
/**
@@ -134,7 +132,6 @@ 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(
@@ -165,7 +162,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), irmConfig)
? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration))
: undefined,
};
}),
@@ -1,4 +1,4 @@
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { config } from '@grafana/runtime';
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { pluginMeta, pluginMetaToPluginConfig } from '../../testSetup/plugins';
@@ -67,7 +67,8 @@ describe('filterRulerRulesConfig', () => {
};
it('should filter by namespace', () => {
setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) });
// eslint-disable-next-line no-restricted-syntax
config.apps = { [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) };
const { filteredConfig, someRulesAreSkipped } = filterRulerRulesConfig(mockRulesConfig, 'namespace1');
expect(filteredConfig).toEqual({
@@ -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 { useIrmConfig } from 'app/features/gops/configuration-tracker/irmHooks';
import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config';
import { useAppNotification } from '../../../../../../../core/copy/appNotification';
import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types';
@@ -38,21 +38,17 @@ enum OnCallIntegrationStatus {
}
function useOnCallPluginStatus() {
const {
irmConfig: { onCallPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const {
installed: isOnCallEnabled,
loading: isPluginBridgeLoading,
error: pluginError,
} = usePluginBridge(onCallPluginId);
} = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
const {
data: onCallFeatures = [],
error: onCallFeaturesError,
isLoading: isOnCallFeaturesLoading,
} = onCallApi(onCallPluginId).endpoints.features.useQuery(undefined, { skip: !isOnCallEnabled });
} = onCallApi.endpoints.features.useQuery(undefined, { skip: !isOnCallEnabled });
const integrationStatus = useMemo((): OnCallIntegrationStatus => {
if (!isOnCallEnabled) {
@@ -74,22 +70,19 @@ function useOnCallPluginStatus() {
isOnCallEnabled,
integrationStatus,
isAlertingV2IntegrationEnabled,
isOnCallStatusLoading: isPluginBridgeLoading || isOnCallFeaturesLoading || isIrmConfigLoading,
isOnCallStatusLoading: isPluginBridgeLoading || isOnCallFeaturesLoading,
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(onCallPluginId);
onCallApi;
const [validateIntegrationNameQuery, { isFetching: isValidating }] = useLazyValidateIntegrationNameQuery();
const [createIntegrationMutation] = useCreateIntegrationMutation();
@@ -278,7 +271,7 @@ export function useOnCallIntegration() {
extendOnCallReceivers,
createOnCallIntegrations,
onCallFormValidators,
isLoadingOnCallIntegration: isLoadingOnCallIntegrations || isOnCallStatusLoading || isIrmConfigLoading,
isLoadingOnCallIntegration: isLoadingOnCallIntegrations || isOnCallStatusLoading,
isValidating,
hasOnCallError: Boolean(onCallError) || isIntegrationsQueryError,
};
@@ -1,9 +1,6 @@
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 { SupportedPlugin } from '../../../types/pluginBridges';
import { getIrmIfPresentOrOnCallPluginId, getIsIrmPluginPresent } from '../../../utils/config';
import { createBridgeURL } from '../../PluginBridge';
import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types';
@@ -16,37 +13,38 @@ export interface ReceiverPluginMetadata {
warning?: string;
}
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],
});
const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[getIrmIfPresentOrOnCallPluginId()];
const onCallReceiverTitle = 'Grafana OnCall';
export const onCallReceiverMeta: ReceiverPluginMetadata = {
title: onCallReceiverTitle,
icon: onCallReceiverICon,
};
export function getOnCallMetadata(
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
receiver: GrafanaManagedReceiverConfig,
hasAlertManagerConfigData = true,
irmConfig: UseIsIrmConfig
hasAlertManagerConfigData = true
): ReceiverPluginMetadata {
const pluginName = irmConfig.isIrmPluginPresent ? 'IRM' : 'OnCall';
const pluginId = irmConfig.onCallPluginId;
const pluginName = getIsIrmPluginPresent() ? 'IRM' : 'OnCall';
if (!hasAlertManagerConfigData) {
return onCallReceiverMeta(pluginId);
return onCallReceiverMeta;
}
if (!receiver.settings?.url) {
return onCallReceiverMeta(pluginId);
return onCallReceiverMeta;
}
// oncall status is still loading
if (onCallIntegrations === undefined) {
return onCallReceiverMeta(pluginId);
return onCallReceiverMeta;
}
// indication that onCall is not enabled
if (onCallIntegrations == null) {
return {
...onCallReceiverMeta(pluginId),
...onCallReceiverMeta,
warning: `Grafana ${pluginName} is not installed or is disabled`,
};
}
@@ -56,10 +54,10 @@ export function getOnCallMetadata(
);
return {
...onCallReceiverMeta(pluginId),
...onCallReceiverMeta,
description: matchingOnCallIntegration?.display_name,
externalUrl: matchingOnCallIntegration
? createBridgeURL(pluginId, `/integrations/${matchingOnCallIntegration.value}`)
? createBridgeURL(getIrmIfPresentOrOnCallPluginId(), `/integrations/${matchingOnCallIntegration.value}`)
: undefined,
warning: matchingOnCallIntegration ? undefined : `${pluginName} Integration no longer exists`,
};
@@ -1,5 +1,6 @@
import { type DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams, http } from 'msw';
import { config } from '@grafana/runtime';
import server from '@grafana/test-utils/server';
import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks';
import {
@@ -9,7 +10,10 @@ 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 } from 'app/features/alerting/unified/mocks/server/handlers/plugins';
import {
getDisabledPluginHandler,
getPluginMissingHandler,
} from 'app/features/alerting/unified/mocks/server/handlers/plugins';
import {
ALERTING_API_SERVER_BASE_URL,
getK8sResponse,
@@ -208,6 +212,13 @@ 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) => {
// eslint-disable-next-line no-restricted-syntax
delete config.apps[pluginId];
server.use(getPluginMissingHandler(pluginId));
};
/** Make a plugin respond with `enabled: false`, as if its installed but disabled */
export const disablePlugin = (pluginId: SupportedPlugin) => {
clearPluginSettingsCache(pluginId);
@@ -1,8 +1,7 @@
import { HttpResponse, http } from 'msw';
import { PluginLoadingStrategy, PluginMeta } from '@grafana/data';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { type AppPluginMetas } from '@grafana/runtime/unstable';
import { config } from '@grafana/runtime';
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,10 +11,9 @@ 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) => {
const allPlugins: AppPluginMetas = {};
plugins.reduce((acc, curr) => {
const { id, baseUrl, info, angular } = curr;
acc[id] = {
plugins.forEach(({ id, baseUrl, info, angular }) => {
// eslint-disable-next-line no-restricted-syntax
config.apps[id] = {
id,
path: baseUrl,
preload: true,
@@ -37,9 +35,7 @@ export const getPluginsHandler = (pluginsArray: PluginMeta[] = plugins) => {
},
},
};
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,4 +1,4 @@
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { config } from '@grafana/runtime';
import { RuleWithLocation } from 'app/types/unified-alerting';
import {
RulerAlertingRuleDTO,
@@ -137,7 +137,10 @@ describe('cloneRuleDefinition', () => {
it('Should remove the origin label when cloning data source plugin-provided rules', () => {
// Mock the plugin as installed
setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) });
// eslint-disable-next-line no-restricted-syntax
config.apps = {
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
};
const rule: RulerAlertingRuleDTO = mockRulerAlertingRule({
alert: 'slo-provider-alert',
@@ -172,7 +175,10 @@ describe('cloneRuleDefinition', () => {
});
it('Should remove the origin label when cloning Grafana-managed plugin-provided rules', () => {
setAppPluginMetas({ [SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]) });
// eslint-disable-next-line no-restricted-syntax
config.apps = {
[SupportedPlugin.Slo]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Slo]),
};
const rule: RulerGrafanaRuleDTO = mockRulerGrafanaRule(
{
@@ -1,6 +1,14 @@
import { config } from '@grafana/runtime';
import { checkEvaluationIntervalGlobalLimit } from './config';
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
import { SupportedPlugin } from '../types/pluginBridges';
import {
checkEvaluationIntervalGlobalLimit,
getIrmIfPresentOrIncidentPluginId,
getIrmIfPresentOrOnCallPluginId,
getIsIrmPluginPresent,
} from './config';
describe('checkEvaluationIntervalGlobalLimit', () => {
it('should NOT exceed limit if evaluate every is not valid duration', () => {
@@ -51,3 +59,54 @@ describe('checkEvaluationIntervalGlobalLimit', () => {
expect(exceedsLimit).toBe(false);
});
});
describe('getIsIrmPluginPresent', () => {
it('should return true when IRM plugin is present in config.apps', () => {
// eslint-disable-next-line no-restricted-syntax
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
expect(getIsIrmPluginPresent()).toBe(true);
});
it('should return false when IRM plugin is not present in config.apps', () => {
// eslint-disable-next-line no-restricted-syntax
config.apps = {
[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', () => {
// eslint-disable-next-line no-restricted-syntax
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm);
});
it('should return Incident plugin ID when IRM plugin is not present', () => {
// eslint-disable-next-line no-restricted-syntax
config.apps = {
[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', () => {
// eslint-disable-next-line no-restricted-syntax
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm);
});
it('should return OnCall plugin ID when IRM plugin is not present', () => {
// eslint-disable-next-line no-restricted-syntax
config.apps = {
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
};
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall);
});
});
@@ -1,6 +1,8 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SupportedPlugin } from '../types/pluginBridges';
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
@@ -26,3 +28,16 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit };
}
export function getIsIrmPluginPresent() {
// eslint-disable-next-line no-restricted-syntax
return SupportedPlugin.Irm in config.apps;
}
export function getIrmIfPresentOrIncidentPluginId() {
return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.Incident;
}
export function getIrmIfPresentOrOnCallPluginId() {
return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.OnCall;
}
@@ -1,5 +1,5 @@
import { PluginLoadingStrategy } from '@grafana/data';
import { setAppPluginMetas } from '@grafana/runtime/internal';
import { config } from '@grafana/runtime';
import { RuleGroupIdentifier } from 'app/types/unified-alerting';
import {
@@ -42,7 +42,8 @@ describe('getRuleOrigin', () => {
});
it('returns pluginId when origin label matches expected format and plugin is installed', () => {
setAppPluginMetas({
// eslint-disable-next-line no-restricted-syntax
config.apps = {
installed_plugin: {
id: 'installed_plugin',
version: '',
@@ -65,7 +66,7 @@ describe('getRuleOrigin', () => {
},
},
},
});
};
const rule = mockPromAlertingRule({
labels: { [GRAFANA_ORIGIN_LABEL]: 'plugin/installed_plugin' },
});
@@ -1,7 +1,7 @@
import { capitalize } from 'lodash';
import { AlertState } from '@grafana/data';
import { getAppPluginMeta } from '@grafana/runtime/unstable';
import { config } from '@grafana/runtime';
import {
Alert,
AlertingRule,
@@ -273,7 +273,8 @@ export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): R
}
function isPluginInstalled(pluginId: string) {
return Boolean(getAppPluginMeta(pluginId));
// eslint-disable-next-line no-restricted-syntax
return Boolean(config.apps[pluginId]);
}
export function isPluginProvidedGroup(group: RulerRuleGroupDTO): boolean {
@@ -5,7 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { UserStorage } from '@grafana/runtime/internal';
import { getAppPluginMeta } from '@grafana/runtime/unstable';
import { useAppPluginMeta } from '@grafana/runtime/unstable';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
@@ -28,9 +28,9 @@ export function AdvisorRedirectNotice() {
const styles = useStyles2(getStyles);
const hasAdminRights = contextSrv.hasRole('Admin') || contextSrv.isGrafanaAdmin;
const [showNotice, setShowNotice] = useState(false);
const { app } = useAppPluginMeta('grafana-advisor-app');
const canUseAdvisor =
hasAdminRights && config.featureToggles.grafanaAdvisor && !!getAppPluginMeta('grafana-advisor-app');
const canUseAdvisor = hasAdminRights && config.featureToggles.grafanaAdvisor && !!app;
useEffect(() => {
if (canUseAdvisor) {
@@ -17,9 +17,9 @@ jest.mock('@grafana/llm', () => ({
},
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppPluginMeta: () => ({}),
jest.mock('@grafana/runtime/unstable', () => ({
...jest.requireActual('@grafana/runtime/unstable'),
getAppPluginMeta: () => Promise.resolve({}),
}));
describe('getDashboardChanges', () => {
@@ -70,7 +70,8 @@ let llmHealthCheck: Promise<boolean> | undefined;
* @returns true if the LLM plugin is enabled.
*/
export async function isLLMPluginEnabled(): Promise<boolean> {
if (!getAppPluginMeta('grafana-llm-app')) {
const app = await getAppPluginMeta('grafana-llm-app');
if (!app) {
return false;
}
@@ -1,7 +1,6 @@
import { incidentsApi } from 'app/features/alerting/unified/api/incidentsApi';
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
import { useIrmConfig } from '../irmHooks';
import { getIrmIfPresentOrIncidentPluginId } from 'app/features/alerting/unified/utils/config';
interface IncidentsPluginConfig {
isInstalled: boolean;
@@ -11,18 +10,16 @@ interface IncidentsPluginConfig {
}
export function useGetIncidentPluginConfig(): IncidentsPluginConfig {
const {
irmConfig: { incidentPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge(incidentPluginId);
const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge(
getIrmIfPresentOrIncidentPluginId()
);
const { data: incidentsConfig, isLoading: loadingPluginConfig } =
incidentsApi(incidentPluginId).endpoints.getIncidentsPluginConfig.useQuery();
incidentsApi.endpoints.getIncidentsPluginConfig.useQuery();
return {
isInstalled: incidentPluginInstalled ?? false,
isChatOpsInstalled: incidentsConfig?.isChatOpsInstalled ?? false,
isIncidentCreated: incidentsConfig?.isIncidentCreated ?? false,
isLoading: loadingPluginSettings || loadingPluginConfig || isIrmConfigLoading,
isLoading: loadingPluginSettings || loadingPluginConfig,
};
}
@@ -1,75 +0,0 @@
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,12 +1,14 @@
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 { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
import {
getIrmIfPresentOrIncidentPluginId,
getIrmIfPresentOrOnCallPluginId,
getIsIrmPluginPresent,
} from 'app/features/alerting/unified/utils/config';
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
import { RelativeUrl, createRelativeUrl } from 'app/features/alerting/unified/utils/url';
@@ -109,39 +111,6 @@ 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 },
@@ -150,10 +119,6 @@ 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}`, {
@@ -186,7 +151,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
},
];
if (isIrmPluginPresent) {
if (!getIsIrmPluginPresent()) {
steps = [
...steps,
{
@@ -301,8 +266,8 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
},
{
title: t('gops.use-get-essentials-configuration.essential-content.title.respond', 'Respond'),
description: isIrmPluginPresent ? 'Configure IRM' : 'Configure OnCall and Incident',
steps: isIrmPluginPresent
description: getIsIrmPluginPresent() ? 'Configure IRM' : 'Configure OnCall and Incident',
steps: getIsIrmPluginPresent()
? [
{
title: t(
@@ -336,11 +301,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${incidentPluginId}/integrations/apps/grate.irm.slack`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/apps/grate.irm.slack`,
},
label: t('gops.use-get-essentials-configuration.essential-content.label.connect', 'Connect'),
urlLinkOnDone: {
url: `/a/${incidentPluginId}/integrations/apps/grate.irm.slack`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/apps/grate.irm.slack`,
},
labelOnDone: 'View',
},
@@ -358,11 +323,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${onCallPluginId}/integrations/`,
url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`,
},
label: t('gops.use-get-essentials-configuration.essential-content.label.add', 'Add'),
urlLinkOnDone: {
url: `/a/${onCallPluginId}/integrations/`,
url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`,
},
labelOnDone: 'View',
},
@@ -382,11 +347,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${incidentPluginId}/walkthrough/generate-key`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}/walkthrough/generate-key`,
},
label: t('gops.use-get-essentials-configuration.essential-content.label.initialize', 'Initialize'),
urlLinkOnDone: {
url: `/a/${incidentPluginId}`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}`,
},
labelOnDone: 'View',
},
@@ -404,12 +369,12 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${onCallPluginId}/settings`,
url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`,
queryParams: { tab: 'ChatOps', chatOpsTab: 'Slack' },
},
label: t('gops.use-get-essentials-configuration.essential-content.label.connect', 'Connect'),
urlLinkOnDone: {
url: `/a/${onCallPluginId}/settings`,
url: `/a/${getIrmIfPresentOrOnCallPluginId()}/settings`,
queryParams: { tab: 'ChatOps' },
},
labelOnDone: 'View',
@@ -426,11 +391,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${incidentPluginId}/integrations/grate.slack`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations/grate.slack`,
},
label: t('gops.use-get-essentials-configuration.essential-content.label.connect', 'Connect'),
urlLinkOnDone: {
url: `/a/${incidentPluginId}/integrations`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}/integrations`,
},
},
done: isChatOpsInstalled,
@@ -447,11 +412,11 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${onCallPluginId}/integrations/`,
url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`,
},
label: t('gops.use-get-essentials-configuration.essential-content.label.add', 'Add'),
urlLinkOnDone: {
url: `/a/${onCallPluginId}/integrations/`,
url: `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`,
},
labelOnDone: 'View',
},
@@ -467,7 +432,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
description: '',
steps: [
{
title: isIrmPluginPresent ? 'Send test alert' : 'Send OnCall demo alert via Alerting integration',
title: getIsIrmPluginPresent() ? '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',
@@ -476,7 +441,8 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
'Select integration'
),
options: onCallOptions,
onClickOption: (value) => onIntegrationClick(value, `/a/${onCallPluginId}/integrations/`),
onClickOption: (value) =>
onIntegrationClick(value, `/a/${getIrmIfPresentOrOnCallPluginId()}/integrations/`),
stepNotAvailableText: 'No integrations available',
},
},
@@ -492,7 +458,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
button: {
type: 'openLink',
urlLink: {
url: `/a/${incidentPluginId}`,
url: `/a/${getIrmIfPresentOrIncidentPluginId()}`,
queryParams: { declare: 'new', drill: '1' },
},
label: t('gops.use-get-essentials-configuration.essential-content.label.start-drill', 'Start drill'),
@@ -513,7 +479,7 @@ export function useGetEssentialsConfiguration(): EssentialsConfigurationData {
},
{ stepsDone: 0, totalStepsToDo: 0 }
);
return { essentialContent, stepsDone, totalStepsToDo, isLoading: isLoading || isIrmConfigLoading };
return { essentialContent, stepsDone, totalStepsToDo, isLoading };
}
interface UseConfigurationProps {
dataSourceConfigurationData: DataSourceConfigurationData;
@@ -1,44 +1,29 @@
import { onCallApi } from 'app/features/alerting/unified/api/onCallApi';
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
import { useIrmConfig } from '../irmHooks';
import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config';
export function useGetOnCallIntegrations() {
const {
irmConfig: { onCallPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const { installed: onCallPluginInstalled } = usePluginBridge(onCallPluginId);
const { installed: onCallPluginInstalled } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
const { data: onCallIntegrations } = onCallApi(onCallPluginId).endpoints.grafanaOnCallIntegrations.useQuery(
undefined,
{
skip: !onCallPluginInstalled || isIrmConfigLoading,
refetchOnFocus: true,
refetchOnReconnect: true,
refetchOnMountOrArgChange: true,
}
);
const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, {
skip: !onCallPluginInstalled,
refetchOnFocus: true,
refetchOnReconnect: true,
refetchOnMountOrArgChange: true,
});
return onCallIntegrations ?? [];
}
function useGetOnCallConfigurationChecks() {
const {
irmConfig: { onCallPluginId },
isIrmConfigLoading,
} = useIrmConfig();
const { data: onCallConfigChecks, isLoading } = onCallApi(onCallPluginId).endpoints.onCallConfigChecks.useQuery(
undefined,
{
refetchOnFocus: true,
refetchOnReconnect: true,
refetchOnMountOrArgChange: true,
}
);
const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(undefined, {
refetchOnFocus: true,
refetchOnReconnect: true,
refetchOnMountOrArgChange: true,
});
return {
isLoading: isLoading || isIrmConfigLoading,
isLoading,
onCallConfigChecks: onCallConfigChecks ?? { is_chatops_connected: false, is_integration_chatops_connected: false },
};
}
@@ -448,7 +448,7 @@ describe('AddedComponentsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [] } };
@@ -501,7 +501,7 @@ describe('AddedComponentsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [] } };
@@ -531,7 +531,7 @@ describe('AddedComponentsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [componentConfig] } };
@@ -30,10 +30,10 @@ export class AddedComponentsRegistry extends Registry<
super(options);
}
mapToRegistry(
async mapToRegistry(
registry: RegistryType<AddedComponentRegistryItem[]>,
item: PluginExtensionConfigs<PluginExtensionAddedComponentConfig>
): RegistryType<AddedComponentRegistryItem[]> {
): Promise<RegistryType<AddedComponentRegistryItem[]>> {
const { pluginId, configs } = item;
for (const config of configs) {
@@ -51,7 +51,7 @@ export class AddedComponentsRegistry extends Registry<
if (
pluginId !== 'grafana' &&
isGrafanaDevMode() &&
isAddedComponentMetaInfoMissing(pluginId, config, configLog)
(await isAddedComponentMetaInfoMissing(pluginId, config, configLog))
) {
continue;
}
@@ -640,7 +640,7 @@ describe('addedFunctionsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [] } };
@@ -693,7 +693,7 @@ describe('addedFunctionsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [] } };
@@ -723,7 +723,7 @@ describe('addedFunctionsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [fnConfig] } };
@@ -28,11 +28,12 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
super(options);
}
mapToRegistry(
async mapToRegistry(
registry: RegistryType<AddedFunctionsRegistryItem[]>,
item: PluginExtensionConfigs<PluginExtensionAddedFunctionConfig>
): RegistryType<AddedFunctionsRegistryItem[]> {
): Promise<RegistryType<AddedFunctionsRegistryItem[]>> {
const { pluginId, configs } = item;
for (const config of configs) {
const configLog = this.logger.child({
title: config.title,
@@ -49,7 +50,11 @@ export class AddedFunctionsRegistry extends Registry<AddedFunctionsRegistryItem[
continue;
}
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedFunctionMetaInfoMissing(pluginId, config, configLog)) {
if (
pluginId !== 'grafana' &&
isGrafanaDevMode() &&
(await isAddedFunctionMetaInfoMissing(pluginId, config, configLog))
) {
continue;
}
@@ -624,7 +624,7 @@ describe('AddedLinksRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [] } };
@@ -679,7 +679,7 @@ describe('AddedLinksRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [] } };
@@ -710,7 +710,7 @@ describe('AddedLinksRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [linkConfig] } };
@@ -34,10 +34,10 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
super(options);
}
mapToRegistry(
async mapToRegistry(
registry: RegistryType<AddedLinkRegistryItem[]>,
item: PluginExtensionConfigs<PluginExtensionAddedLinkConfig>
): RegistryType<AddedLinkRegistryItem[]> {
): Promise<RegistryType<AddedLinkRegistryItem[]>> {
const { pluginId, configs } = item;
for (const config of configs) {
@@ -66,7 +66,11 @@ export class AddedLinksRegistry extends Registry<AddedLinkRegistryItem[], Plugin
continue;
}
if (pluginId !== 'grafana' && isGrafanaDevMode() && isAddedLinkMetaInfoMissing(pluginId, config, configLog)) {
if (
pluginId !== 'grafana' &&
isGrafanaDevMode() &&
(await isAddedLinkMetaInfoMissing(pluginId, config, configLog))
) {
continue;
}
@@ -421,7 +421,7 @@ describe('ExposedComponentsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [] } };
@@ -474,7 +474,7 @@ describe('ExposedComponentsRegistry', () => {
};
// Make sure that the meta-info is empty
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [] } };
@@ -503,7 +503,7 @@ describe('ExposedComponentsRegistry', () => {
component: () => React.createElement('div', null, 'Hello World1'),
};
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, exposedComponents: [componentConfig] } };
@@ -30,10 +30,10 @@ export class ExposedComponentsRegistry extends Registry<
super(options);
}
mapToRegistry(
async mapToRegistry(
registry: RegistryType<ExposedComponentRegistryItem>,
{ pluginId, configs }: PluginExtensionConfigs<PluginExtensionExposedComponentConfig>
): RegistryType<ExposedComponentRegistryItem> {
): Promise<RegistryType<ExposedComponentRegistryItem>> {
if (!configs) {
return registry;
}
@@ -65,7 +65,7 @@ export class ExposedComponentsRegistry extends Registry<
if (
pluginId !== 'grafana' &&
isGrafanaDevMode() &&
isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog)
(await isExposedComponentMetaInfoMissing(pluginId, config, pointIdLog))
) {
continue;
}
@@ -1,4 +1,13 @@
import { Observable, ReplaySubject, Subject, distinctUntilChanged, firstValueFrom, map, scan, startWith } from 'rxjs';
import {
Observable,
ReplaySubject,
Subject,
distinctUntilChanged,
firstValueFrom,
map,
mergeScan,
startWith,
} from 'rxjs';
import { ExtensionsLog, log } from '../logs/log';
import { deepFreeze } from '../utils';
@@ -44,7 +53,7 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
this.registrySubject = new ReplaySubject<RegistryType<TRegistryValue>>(1);
this.resultSubject
.pipe(
scan(this.mapToRegistry.bind(this), options.initialState ?? {}),
mergeScan(this.mapToRegistry.bind(this), options.initialState ?? {}),
// Emit an empty registry to start the stream (it is only going to do it once during construction, and then just passes down the values)
startWith(options.initialState ?? {})
)
@@ -55,7 +64,7 @@ export abstract class Registry<TRegistryValue extends object | unknown[] | Recor
abstract mapToRegistry(
registry: RegistryType<TRegistryValue>,
item: PluginExtensionConfigs<TMapType>
): RegistryType<TRegistryValue>;
): Promise<RegistryType<TRegistryValue>>;
register(result: PluginExtensionConfigs<TMapType>): void {
if (this.isReadOnly) {
@@ -1,27 +1,12 @@
import { useAsync } from 'react-use';
import { AppPluginConfig } from '@grafana/data';
import { useAppPluginMetas } from '@grafana/runtime/unstable';
import { PreloadAppPluginsPredicate, preloadPluginsWithPredicate } from '../pluginPreloader';
import { preloadPlugins } from '../pluginPreloader';
export function useLoadAppPlugins(extensionId: string, predicate: PreloadAppPluginsPredicate): { isLoading: boolean } {
const { loading: isLoading } = useAsync(
() => preloadPluginsWithPredicate(extensionId, predicate),
[extensionId, predicate]
);
export type UseLoadAppPluginsPredicate = (apps: AppPluginConfig[], filterById: string) => string[];
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 () => {
if (!filtered.length) {
return;
}
await preloadPlugins(filtered);
}, [filtered]);
return { isLoading: isLoading || isAppPluginMetasLoading || isFilteredLoading };
return { isLoading: isLoading };
}
@@ -500,7 +500,7 @@ describe('usePluginComponents()', () => {
});
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
it('should not validate the extension point meta-info for core plugins', () => {
it('should not validate the extension point meta-info for core plugins', async () => {
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const componentConfig = {
@@ -511,7 +511,7 @@ describe('usePluginComponents()', () => {
};
// The `AddedComponentsRegistry` is validating if the link is registered in the plugin metadata.
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedComponents: [componentConfig] } };
@@ -321,7 +321,7 @@ describe('usePluginFunctions()', () => {
});
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
it('should not validate the extension point meta-info for core plugins', () => {
it('should not validate the extension point meta-info for core plugins', async () => {
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const functionConfig = {
@@ -332,7 +332,7 @@ describe('usePluginFunctions()', () => {
};
// The `AddedFunctionsRegistry` is validating if the function is registered in the plugin metadata.
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedFunctions: [functionConfig] } };
@@ -261,7 +261,7 @@ describe('usePluginLinks()', () => {
});
// It can happen that core Grafana plugins (e.g. traces) reuse core components which implement extension points.
it('should not validate the extension point meta-info for core plugins', () => {
it('should not validate the extension point meta-info for core plugins', async () => {
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const linkConfig = {
@@ -272,7 +272,7 @@ describe('usePluginLinks()', () => {
};
// The `AddedLinksRegistry` is validating if the link is registered in the plugin metadata (config.apps).
const meta = getAppPluginMeta(pluginId);
const meta = await getAppPluginMeta(pluginId);
expect(meta).toBeDefined();
const app = { ...meta!, extensions: { ...meta!.extensions, addedLinks: [linkConfig] } };
File diff suppressed because it is too large Load Diff
@@ -14,11 +14,9 @@ import {
PanelMenuItem,
PluginExtensionAddedLinkConfig,
urlUtil,
PluginExtensionPoints,
ExtensionInfo,
} from '@grafana/data';
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';
@@ -30,11 +28,11 @@ import {
} from 'app/types/events';
import { RestrictedGrafanaApisProvider } from '../components/restrictedGrafanaApis/RestrictedGrafanaApisProvider';
import { PreloadAppPluginsPredicate } from '../pluginPreloader';
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 = '') {
@@ -619,7 +617,7 @@ 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: UseLoadAppPluginsPredicate = (
export const getExtensionPointPluginDependencies: PreloadAppPluginsPredicate = (
apps: AppPluginConfig[],
extensionPointId: string
): string[] => {
@@ -655,7 +653,7 @@ export const getExtensionPointPluginMeta = (
return new Map(
getExtensionPointPluginDependencies(apps, extensionPointId)
.map((pluginId) => {
const app = getAppPluginMeta(pluginId);
const app = apps.find((a) => a.id === pluginId);
// if the plugin does not exist or does not expose any components or links to the extension point, return undefined
if (
!app ||
@@ -680,7 +678,7 @@ export const getExtensionPointPluginMeta = (
// 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: UseLoadAppPluginsPredicate = (
export const getExposedComponentPluginDependencies: PreloadAppPluginsPredicate = (
apps: AppPluginConfig[],
exposedComponentId: string
) => {
@@ -720,28 +718,3 @@ export const getAppPluginDependencies = (
.filter((id) => id !== pluginId)
);
};
// Returns a list of app plugins that has to be loaded before core Grafana could finish the initialization.
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 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 = (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(
apps,
PluginExtensionPoints.DashboardPanelMenu
);
const awaitedPluginIds = getAppPluginsToAwait(apps).map((app) => app.id);
const isNotAwaited = (app: AppPluginConfig) => !awaitedPluginIds.includes(app.id);
return apps.filter((app) => {
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
});
};
File diff suppressed because it is too large Load Diff
@@ -146,13 +146,13 @@ export const isExposedComponentDependencyMissing = (id: string, pluginContext: P
return !exposedComponentsDependencies || !exposedComponentsDependencies.includes(id);
};
export const isAddedLinkMetaInfoMissing = (
export const isAddedLinkMetaInfoMissing = async (
pluginId: string,
metaInfo: PluginExtensionAddedLinkConfig,
log: ExtensionsLog
) => {
const logPrefix = 'Could not register link extension. Reason:';
const app = getAppPluginMeta(pluginId);
const app = await getAppPluginMeta(pluginId);
const pluginJsonMetaInfo = app ? app.extensions.addedLinks.filter(({ title }) => title === metaInfo.title) : null;
if (!app) {
@@ -178,13 +178,13 @@ export const isAddedLinkMetaInfoMissing = (
return false;
};
export const isAddedFunctionMetaInfoMissing = (
export const isAddedFunctionMetaInfoMissing = async (
pluginId: string,
metaInfo: PluginExtensionAddedFunctionConfig,
log: ExtensionsLog
) => {
const logPrefix = 'Could not register function extension. Reason:';
const app = getAppPluginMeta(pluginId);
const app = await getAppPluginMeta(pluginId);
const pluginJsonMetaInfo = app ? app.extensions.addedFunctions.filter(({ title }) => title === metaInfo.title) : null;
if (!app) {
@@ -210,13 +210,13 @@ export const isAddedFunctionMetaInfoMissing = (
return false;
};
export const isAddedComponentMetaInfoMissing = (
export const isAddedComponentMetaInfoMissing = async (
pluginId: string,
metaInfo: PluginExtensionAddedComponentConfig,
log: ExtensionsLog
) => {
const logPrefix = 'Could not register component extension. Reason:';
const app = getAppPluginMeta(pluginId);
const app = await getAppPluginMeta(pluginId);
const pluginJsonMetaInfo = app
? app.extensions.addedComponents.filter(({ title }) => title === metaInfo.title)
: null;
@@ -244,13 +244,13 @@ export const isAddedComponentMetaInfoMissing = (
return false;
};
export const isExposedComponentMetaInfoMissing = (
export const isExposedComponentMetaInfoMissing = async (
pluginId: string,
metaInfo: PluginExtensionExposedComponentConfig,
log: ExtensionsLog
) => {
const logPrefix = 'Could not register exposed component extension. Reason:';
const app = getAppPluginMeta(pluginId);
const app = await getAppPluginMeta(pluginId);
const pluginJsonMetaInfo = app ? app.extensions.exposedComponents.filter(({ id }) => id === metaInfo.id) : null;
if (!app) {
+61 -5
View File
@@ -1,12 +1,15 @@
import type {
AppPluginConfig,
PluginExtensionAddedLinkConfig,
PluginExtensionExposedComponentConfig,
PluginExtensionAddedComponentConfig,
import {
type AppPluginConfig,
type PluginExtensionAddedLinkConfig,
type PluginExtensionExposedComponentConfig,
type PluginExtensionAddedComponentConfig,
PluginExtensionPoints,
} from '@grafana/data';
import { getAppPluginMetas } from '@grafana/runtime/unstable';
import { contextSrv } from 'app/core/services/context_srv';
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
import { getExtensionPointPluginDependencies } from './extensions/utils';
import { pluginImporter } from './importer/pluginImporter';
export type PluginPreloadResult = {
@@ -23,6 +26,59 @@ export const clearPreloadedPluginsCache = () => {
preloadPromises.clear();
};
function getAppPluginIdsToAwait() {
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 pluginIds;
}
function isNotAwaited(app: AppPluginConfig) {
return !getAppPluginIdsToAwait().includes(app.id);
}
export async function preloadPluginsToBeAwaited() {
const apps = await getAppPluginMetas();
const awaited = getAppPluginIdsToAwait();
const filtered = apps.filter((app) => awaited.includes(app.id));
preloadPlugins(filtered);
}
export async function preloadPluginsToBePreloaded() {
const apps = await getAppPluginMetas();
// 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(
apps,
PluginExtensionPoints.DashboardPanelMenu
);
const filtered = apps.filter((app) => {
return isNotAwaited(app) && (app.preload || dashboardPanelMenuPluginIds.includes(app.id));
});
preloadPlugins(filtered);
}
export type PreloadAppPluginsPredicate = (apps: AppPluginConfig[], extensionId: string) => string[];
const noop: PreloadAppPluginsPredicate = () => [];
export async function preloadPluginsWithPredicate(extensionId: string, predicate: PreloadAppPluginsPredicate = noop) {
const apps = await getAppPluginMetas();
const filteredIds = predicate(apps, extensionId);
const filtered = apps.filter((app) => filteredIds.includes(app.id));
if (!filtered.length) {
return;
}
preloadPlugins(filtered);
}
export async function preloadPlugins(apps: AppPluginConfig[] = []) {
// Create preload promises for each app, reusing existing promises if already loading
const promises = apps.map((app) => {
@@ -1,6 +1,6 @@
import { PluginType, patchArrayVectorProrotypeMethods } from '@grafana/data';
import { config } from '@grafana/runtime';
import { getAppPluginConfig } from '@grafana/runtime/unstable';
import { getAppPluginMeta } from '@grafana/runtime/unstable';
import { transformPluginSourceForCDN } from '../cdn/utils';
import { resolvePluginUrlWithCache } from '../loader/pluginInfoCache';
@@ -139,7 +139,7 @@ export async function getPluginLoadData(pluginId: string): Promise<SandboxPlugin
//find it in apps
//the information inside the apps object is more limited
const app = await getAppPluginConfig(pluginId);
const app = await getAppPluginMeta(pluginId);
if (!app) {
throw new Error(`Could not find plugin ${pluginId}`);
}