From 29fa6dfc8de0a758f9f37f000c923f0806c3b629 Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:46:54 -0500 Subject: [PATCH] ExploreMetrics: Add toggle to enable routing to externalized Explore Metrics app plugin (#99481) * feat: toggle for explore metrics app plugin * chore: put app navigation settings behind feature toggle * chore: require restart * feat: context-driven applinks toggling * fix: toggle ownership * docs: note restart requirement * refactor: prefer global feature toggle check * chore: undo rm newlines * fix: minimize changes * fix: app id, `chromeless`, sidebar ordering --- .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 11 ++++- pkg/services/featuremgmt/toggles_gen.csv | 3 +- pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 24 +++++++-- pkg/services/navtree/navtreeimpl/applinks.go | 10 ++-- pkg/services/navtree/navtreeimpl/navtree.go | 2 +- public/app/features/plugins/routes.tsx | 49 +++++++++++++------ public/app/routes/routes.tsx | 16 ++++-- 9 files changed, 91 insertions(+), 29 deletions(-) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 24f2244ac83..71fa78c0d67 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -251,6 +251,7 @@ export interface FeatureToggles { queryLibraryDashboards?: boolean; grafanaAdvisor?: boolean; elasticsearchImprovedParsing?: boolean; + exploreMetricsUseExternalAppPlugin?: boolean; datasourceConnectionsTab?: boolean; fetchRulesUsingPost?: boolean; alertingAlertmanagerExtraDedupStage?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 7571ff6a37f..3e737bb1f45 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -912,7 +912,7 @@ var ( Stage: FeatureStageGeneralAvailability, Expression: "true", // enabled by default FrontendOnly: true, - Owner: grafanaDashboardsSquad, + Owner: grafanaObservabilityMetricsSquad, }, { Name: "alertingSimplifiedRouting", @@ -1742,6 +1742,15 @@ var ( Stage: FeatureStageExperimental, Owner: awsDatasourcesSquad, }, + { + Name: "exploreMetricsUseExternalAppPlugin", + Description: "Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin", + Stage: FeatureStageExperimental, + Owner: grafanaObservabilityMetricsSquad, + FrontendOnly: true, + RequiresRestart: true, + HideFromDocs: true, + }, { Name: "datasourceConnectionsTab", Description: "Shows defined connections for a data source in the plugins detail page", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 250833a78a3..3b653f76b4e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -120,7 +120,7 @@ pdfTables,preview,@grafana/sharing-squad,false,false,false ssoSettingsApi,GA,@grafana/identity-access-team,false,false,false canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true logsInfiniteScrolling,GA,@grafana/observability-logs,false,false,true -exploreMetrics,GA,@grafana/dashboards-squad,false,false,true +exploreMetrics,GA,@grafana/observability-metrics,false,false,true alertingSimplifiedRouting,GA,@grafana/alerting-squad,false,false,false logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false @@ -232,6 +232,7 @@ ABTestFeatureToggleB,experimental,@grafana/sharing-squad,false,false,false queryLibraryDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false grafanaAdvisor,experimental,@grafana/plugins-platform-backend,false,false,false elasticsearchImprovedParsing,experimental,@grafana/aws-datasources,false,false,false +exploreMetricsUseExternalAppPlugin,experimental,@grafana/observability-metrics,false,true,true datasourceConnectionsTab,experimental,@grafana/plugins-platform-backend,false,false,true fetchRulesUsingPost,experimental,@grafana/alerting-squad,false,false,false alertingAlertmanagerExtraDedupStage,experimental,@grafana/alerting-squad,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 556275ed908..3a5db419907 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -939,6 +939,10 @@ const ( // Enables less memory intensive Elasticsearch result parsing FlagElasticsearchImprovedParsing = "elasticsearchImprovedParsing" + // FlagExploreMetricsUseExternalAppPlugin + // Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin + FlagExploreMetricsUseExternalAppPlugin = "exploreMetricsUseExternalAppPlugin" + // FlagDatasourceConnectionsTab // Shows defined connections for a data source in the plugins detail page FlagDatasourceConnectionsTab = "datasourceConnectionsTab" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 573ae344f02..e39d9ef150e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -1529,16 +1529,16 @@ { "metadata": { "name": "exploreMetrics", - "resourceVersion": "1720021873452", + "resourceVersion": "1737658563230", "creationTimestamp": "2024-04-09T18:15:18Z", "annotations": { - "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" + "grafana.app/updatedTimestamp": "2025-01-23 18:56:03.23086 +0000 UTC" } }, "spec": { "description": "Enables the new Explore Metrics core app", "stage": "GA", - "codeowner": "@grafana/dashboards-squad", + "codeowner": "@grafana/observability-metrics", "frontend": true, "expression": "true" } @@ -1557,6 +1557,24 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "exploreMetricsUseExternalAppPlugin", + "resourceVersion": "1738596266973", + "creationTimestamp": "2025-01-21T23:24:50Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-02-03 15:24:26.973231 +0000 UTC" + } + }, + "spec": { + "description": "Use the externalized Metrics Drilldown (formerly known as Explore Metrics) app plugin", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "requiresRestart": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "expressionParser", diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index 8fc88a5ed80..cf78f67e1d4 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -286,9 +286,9 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-k8s-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 1, Text: "Kubernetes"}, "grafana-dbo11y-app": {SectionID: navtree.NavIDInfrastructure, SortWeight: 2, Text: "Databases"}, "grafana-app-observability-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightApplication, Text: "Application", Icon: "graph-bar"}, - "grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Logs"}, - "grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Traces"}, - "grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Profiles"}, + "grafana-lokiexplore-app": {SectionID: navtree.NavIDExplore, SortWeight: 2, Text: "Logs"}, + "grafana-exploretraces-app": {SectionID: navtree.NavIDExplore, SortWeight: 3, Text: "Traces"}, + "grafana-pyroscope-app": {SectionID: navtree.NavIDExplore, SortWeight: 4, Text: "Profiles"}, "grafana-kowalski-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightFrontend, Text: "Frontend", Icon: "frontend-observability"}, "grafana-synthetic-monitoring-app": {SectionID: navtree.NavIDTestingAndSynthetics, SortWeight: 2, Text: "Synthetics"}, "grafana-irm-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 1, Text: "IRM"}, @@ -309,6 +309,10 @@ func (s *ServiceImpl) readNavigationSettings() { "grafana-csp-app": {SectionID: navtree.NavIDRoot, SortWeight: navtree.WeightCloudServiceProviders, Icon: "cloud"}, } + if s.features.IsEnabledGlobally(featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { + s.navigationAppConfig["grafana-metricsdrilldown-app"] = NavigationAppConfig{SectionID: navtree.NavIDExplore, SortWeight: 1, Text: "Metrics"} + } + s.navigationAppPathConfig = map[string]NavigationAppConfig{ "/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2}, } diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index c67ee1bbfe8..1f461db67f3 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -562,7 +562,7 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n func (s *ServiceImpl) buildExploreNavLinks(c *contextmodel.ReqContext) []*navtree.NavLink { exploreChildNavs := []*navtree.NavLink{} - if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) { + if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetrics) && !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagExploreMetricsUseExternalAppPlugin) { exploreChildNavs = append(exploreChildNavs, &navtree.NavLink{ Text: "Metrics", SubTitle: "Queryless exploration of your metrics", diff --git a/public/app/features/plugins/routes.tsx b/public/app/features/plugins/routes.tsx index 46cc470549d..1504f6e2e48 100644 --- a/public/app/features/plugins/routes.tsx +++ b/public/app/features/plugins/routes.tsx @@ -4,26 +4,28 @@ import { getRootSectionForNode } from 'app/core/selectors/navModel'; import AppRootPage from 'app/features/plugins/components/AppRootPage'; import { getState } from 'app/store/store'; +const isPluginNavModelItem = (model: NavModelItem): model is PluginNavModelItem => 'pluginId' in model && 'id' in model; +const isStandalonePluginPage = (id: string) => id.startsWith('standalone-plugin-page-/'); + +function getPathForNavItem(navItem: PluginNavModelItem) { + const pluginNavSection = getRootSectionForNode(navItem); + const appPluginUrl = `/a/${navItem.pluginId}`; + const path = isStandalonePluginPage(navItem.id) ? navItem.url || appPluginUrl : appPluginUrl; // Only standalone pages can use core URLs, otherwise we fall back to "/a/:pluginId" + const isSensitive = isStandalonePluginPage(navItem.id) && !navItem.url?.startsWith('/a/'); // Have case-sensitive URLs only for standalone pages that have custom URLs + + return { + path: `${path}/*`, + sensitive: isSensitive, + component: () => , + }; +} + export function getAppPluginRoutes(): RouteDescriptor[] { const state = getState(); const { navIndex } = state; - const isStandalonePluginPage = (id: string) => id.startsWith('standalone-plugin-page-/'); - const isPluginNavModelItem = (model: NavModelItem): model is PluginNavModelItem => - 'pluginId' in model && 'id' in model; const explicitAppPluginRoutes = Object.values(navIndex) .filter(isPluginNavModelItem) - .map((navItem) => { - const pluginNavSection = getRootSectionForNode(navItem); - const appPluginUrl = `/a/${navItem.pluginId}`; - const path = isStandalonePluginPage(navItem.id) ? navItem.url || appPluginUrl : appPluginUrl; // Only standalone pages can use core URLs, otherwise we fall back to "/a/:pluginId" - const isSensitive = isStandalonePluginPage(navItem.id) && !navItem.url?.startsWith('/a/'); // Have case-sensitive URLs only for standalone pages that have custom URLs - - return { - path: `${path}/*`, - sensitive: isSensitive, - component: () => , - }; - }); + .map(getPathForNavItem); return [ ...explicitAppPluginRoutes, @@ -36,6 +38,23 @@ export function getAppPluginRoutes(): RouteDescriptor[] { ]; } +export function getRouteForAppPlugin(pluginId: string): RouteDescriptor { + const state = getState(); + const { navIndex } = state; + const navItem = Object.values(navIndex) + .filter(isPluginNavModelItem) + .find((navItem) => navItem.pluginId === pluginId); + + if (!navItem) { + return { + path: '/a/:pluginId/*', + component: () => , + }; + } + + return getPathForNavItem(navItem); +} + interface PluginNavModelItem extends Omit { pluginId: string; id: string; diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 8a59242f820..ddadc2f40b7 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -15,7 +15,7 @@ import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/ import { DATASOURCES_ROUTES } from 'app/features/datasources/constants'; import { ConfigureIRM } from 'app/features/gops/configuration-tracker/components/ConfigureIRM'; import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; -import { getAppPluginRoutes } from 'app/features/plugins/routes'; +import { getAppPluginRoutes, getRouteForAppPlugin } from 'app/features/plugins/routes'; import { getProfileRoutes } from 'app/features/profile/routes'; import { AccessControlAction, DashboardRoutes } from 'app/types'; @@ -515,11 +515,17 @@ export function getAppRoutes(): RouteDescriptor[] { }, config.featureToggles.exploreMetrics && { path: '/explore/metrics/*', - chromeless: false, roles: () => contextSrv.evaluatePermission([AccessControlAction.DataSourcesExplore]), - component: SafeDynamicImport( - () => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage') - ), + ...(config.featureToggles.exploreMetricsUseExternalAppPlugin + ? { + component: getRouteForAppPlugin('grafana-metricsdrilldown-app').component, + } + : { + chromeless: false, + component: SafeDynamicImport( + () => import(/* webpackChunkName: "DataTrailsPage"*/ 'app/features/trails/DataTrailsPage') + ), + }), }, { path: '/bookmarks',