From 05338838b04461c8df5ce89938173e9c0f0ca89e Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Thu, 10 Apr 2025 22:04:12 +0200 Subject: [PATCH] Sidecar: Remove experimental Sidecar implementation (#103786) * Sidecar: remove `appSidecar` feature toggle * Sidecar: Remove sidecar implementation * lint --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 4 - .../services/SidecarContext_EXPERIMENTAL.ts | 49 --- .../SidecarService_EXPERIMENTAL.test.ts | 152 -------- .../services/SidecarService_EXPERIMENTAL.ts | 342 ------------------ .../grafana-runtime/src/services/index.ts | 2 - pkg/services/featuremgmt/registry.go | 6 - pkg/services/featuremgmt/toggles-gitlog.csv | 1 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 12 - public/app/AppWrapper.tsx | 40 +- public/app/routes/RoutesWrapper.tsx | 127 +------ public/locales/en-US/grafana.json | 3 - 14 files changed, 16 insertions(+), 728 deletions(-) delete mode 100644 packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts delete mode 100644 packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts delete mode 100644 packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 2b27329a8af..51df92d1ac0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -190,7 +190,6 @@ Experimental features might be changed or removed without prior notice. | `exploreLogsAggregatedMetrics` | Used in Logs Drilldown to query by aggregated metrics | | `exploreLogsLimitedTimeRange` | Used in Logs Drilldown to limit the time range | | `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | | `rolePickerDrawer` | Enables the new role picker drawer design | | `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | | `timeRangeProvider` | Enables time pickers sync | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 70bdc1a2911..154c070f860 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -725,10 +725,6 @@ export interface FeatureToggles { */ appPlatformGrpcClientAuth?: boolean; /** - * Enable the app sidecar feature that allows rendering 2 apps at the same time - */ - appSidecar?: boolean; - /** * Enable the groupsync extension for managing Group Attribute Sync feature */ groupAttributeSync?: boolean; diff --git a/packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts b/packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts deleted file mode 100644 index 05f4579d0ba..00000000000 --- a/packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createContext, useContext } from 'react'; -import { useObservable } from 'react-use'; - -import { SidecarService_EXPERIMENTAL, sidecarServiceSingleton_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL'; - -export const SidecarContext_EXPERIMENTAL = createContext( - sidecarServiceSingleton_EXPERIMENTAL -); - -/** - * This is the main way to interact with the sidecar service inside a react context. It provides a wrapper around the - * service props so that even though they are observables we just pass actual values to the components. - * - * @experimental - */ -export function useSidecar_EXPERIMENTAL() { - // As the sidecar service functionality is behind feature flag this does not need to be for now - const service = useContext(SidecarContext_EXPERIMENTAL); - - if (!service) { - throw new Error('No SidecarContext found'); - } - - const initialContext = useObservable(service.initialContextObservable, service.initialContext); - const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId); - const locationService = service.getLocationService(); - - return { - activePluginId, - initialContext, - locationService, - // TODO: currently this allows anybody to open any app, in the future we should probably scope this to the - // current app but that means we will need to incorporate this better into the plugin platform APIs which - // we will do once the functionality is reasonably stable - openApp: (pluginId: string, context?: unknown) => { - return service.openApp(pluginId, context); - }, - openAppV2: (pluginId: string, path?: string) => { - return service.openAppV2(pluginId, path); - }, - openAppV3: (options: { pluginId: string; path?: string; follow?: boolean }) => { - return service.openAppV3(options); - }, - closeApp: () => service.closeApp(), - isAppOpened: (pluginId: string) => { - return service.isAppOpened(pluginId); - }, - }; -} diff --git a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts b/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts deleted file mode 100644 index 6e6c7a0d1a8..00000000000 --- a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as H from 'history'; - -import { config } from '../config'; - -import { HistoryWrapper } from './LocationService'; -import { SidecarService_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL'; - -function setup() { - const mainLocationService = new HistoryWrapper(H.createMemoryHistory({ initialEntries: ['/explore'] })); - const sidecarService = new SidecarService_EXPERIMENTAL(mainLocationService); - return { - mainLocationService, - sidecarService, - }; -} - -describe('SidecarService_EXPERIMENTAL', () => { - beforeAll(() => { - config.featureToggles.appSidecar = true; - }); - - afterAll(() => { - config.featureToggles.appSidecar = false; - }); - - it('has the correct state after opening and closing an app', () => { - const { sidecarService } = setup(); - sidecarService.openApp('pluginId', { filter: 'test' }); - - expect(sidecarService.activePluginId).toBe('pluginId'); - expect(sidecarService.initialContext).toMatchObject({ filter: 'test' }); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId'); - - sidecarService.closeApp(); - expect(sidecarService.activePluginId).toBe(undefined); - expect(sidecarService.initialContext).toBe(undefined); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/'); - }); - - it('has the correct state after opening and closing an app v2', () => { - const { sidecarService } = setup(); - sidecarService.openAppV2('pluginId', '/test'); - - expect(sidecarService.activePluginId).toBe('pluginId'); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId/test'); - - sidecarService.closeApp(); - expect(sidecarService.activePluginId).toBe(undefined); - expect(sidecarService.initialContext).toBe(undefined); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/'); - }); - - it('has the correct state after opening and closing an app v3', () => { - const { sidecarService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', path: '/test' }); - - expect(sidecarService.activePluginId).toBe('pluginId'); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId/test'); - - sidecarService.closeApp(); - expect(sidecarService.activePluginId).toBe(undefined); - expect(sidecarService.initialContext).toBe(undefined); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/'); - }); - - it('reports correct opened state', () => { - const { sidecarService } = setup(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - - sidecarService.openApp('pluginId'); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - - sidecarService.closeApp(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('reports correct opened state v2', () => { - const { sidecarService } = setup(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - - sidecarService.openAppV2('pluginId'); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - - sidecarService.closeApp(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('reports correct opened state v3', () => { - const { sidecarService } = setup(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - - sidecarService.openAppV3({ pluginId: 'pluginId' }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - - sidecarService.closeApp(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('autocloses on not allowed routes', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId' }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/config'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('autocloses on when changing route', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId' }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/a/other-app'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('does not autocloses when set to follow', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/a/other-app'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - }); - - it('autocloses on not allowed routes when set to follow', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/config'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('autocloses on not allowed routes when set to follow', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/config'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('opens sidecar even if starting route is not allowed and then it changes', () => { - const mainLocationService = new HistoryWrapper(H.createMemoryHistory({ initialEntries: ['/login'] })); - const sidecarService = new SidecarService_EXPERIMENTAL(mainLocationService); - mainLocationService.push('/explore'); - - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - }); -}); diff --git a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts b/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts deleted file mode 100644 index 8e00fbb42e0..00000000000 --- a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts +++ /dev/null @@ -1,342 +0,0 @@ -import * as H from 'history'; -import { pick } from 'lodash'; -import { BehaviorSubject, map, Observable } from 'rxjs'; - -import { reportInteraction } from '../analytics/utils'; -import { config } from '../config'; - -import { HistoryWrapper, locationService as mainLocationService, LocationService } from './LocationService'; - -// Only allow sidecar to be opened on these routes. It does not seem to make sense to keep the sidecar opened on -// config/admin pages for example. -// At this moment let's be restrictive about where the sidecar can show and add more routes if there is a need. -const ALLOW_ROUTES = [ - /(^\/d\/)/, // dashboards - /^\/explore/, // explore + explore metrics - /^\/a\/[^\/]+/, // app plugins - /^\/alerting/, -]; - -/** - * This is a service that handles state and operation of a sidecar feature (sideview to render a second app in grafana). - * At this moment this is highly experimental and if used should be understood to break easily with newer versions. - * None of this functionality works without a feature toggle `appSidecar` being enabled. - * - * Right now this being in a single service is more of a practical tradeoff for easier isolation in the future these - * APIs may be integrated into other services or features like app extensions, plugin system etc. - * - * @experimental - */ -export class SidecarService_EXPERIMENTAL { - private _initialContext: BehaviorSubject; - - private sidecarLocationService: LocationService; - private mainLocationService: LocationService; - - // If true we don't close the sidecar when user navigates to another app or part of Grafana from where the sidecar - // was opened. - private follow = false; - - // Keep track of where the sidecar was originally opened for autoclose behaviour. - private mainLocationWhenOpened: string | undefined; - - private mainOnAllowedRoute = false; - - constructor(mainLocationService: LocationService) { - this._initialContext = new BehaviorSubject(undefined); - this.mainLocationService = mainLocationService; - this.sidecarLocationService = new HistoryWrapper( - createLocationStorageHistory({ storageKey: 'grafana.sidecar.history' }) - ); - this.handleMainLocationChanges(); - } - - private assertFeatureEnabled() { - if (!config.featureToggles.appSidecar) { - console.warn('The `appSidecar` feature toggle is not enabled, doing nothing.'); - return false; - } - - return true; - } - - private updateMainLocationWhenOpened() { - const pathname = this.mainLocationService.getLocation().pathname; - for (const route of ALLOW_ROUTES) { - const match = pathname.match(route)?.[0]; - if (match) { - this.mainLocationWhenOpened = match; - return; - } - } - } - - /** - * Every time the main location changes we check if we should keep the sidecar open or close it based on list - * of allowed routes and also based on the follow flag when opening the app. - */ - private handleMainLocationChanges() { - this.mainOnAllowedRoute = ALLOW_ROUTES.some((prefix) => - this.mainLocationService.getLocation().pathname.match(prefix) - ); - - this.mainLocationService.getLocationObservable().subscribe((location) => { - this.mainOnAllowedRoute = ALLOW_ROUTES.some((prefix) => location.pathname.match(prefix)); - - if (!this.activePluginId) { - return; - } - - if (!this.mainOnAllowedRoute) { - this.closeApp(); - return; - } - - // We check if we moved to some other app or part of grafana from where we opened the sidecar. - const isTheSameLocation = Boolean( - this.mainLocationWhenOpened && location.pathname.startsWith(this.mainLocationWhenOpened) - ); - - if (!(isTheSameLocation || this.follow)) { - this.closeApp(); - } - }); - } - - /** - * Get current app id of the app in sidecar. This is most probably provisional. In the future - * this should be driven by URL addressing so that routing for the apps don't change. Useful just internally - * to decide which app to render. - * - * @experimental - */ - get activePluginIdObservable() { - return this.sidecarLocationService.getLocationObservable().pipe( - map((val) => { - return getPluginIdFromUrl(val?.pathname || ''); - }) - ); - } - - /** - * Get initial context which is whatever data was passed when calling the 'openApp' function. This is meant as - * a way for the app to initialize it's state based on some context that is passed to it from the primary app. - * - * @experimental - */ - get initialContextObservable() { - return this._initialContext.asObservable(); - } - - // Get the current value of the subject, this is needed if we want the value immediately. For example if used in - // hook in react with useObservable first render would return undefined even if the behaviourSubject has some - // value which will be emitted in the next tick and thus next rerender. - get initialContext() { - return this._initialContext.getValue(); - } - - /** - * @experimental - */ - get activePluginId() { - return getPluginIdFromUrl(this.sidecarLocationService.getLocation().pathname); - } - - getLocationService() { - return this.sidecarLocationService; - } - - /** - * Opens an app in a sidecar. You can also pass some context object that will be then available to the app. - * @deprecated - * @experimental - */ - openApp(pluginId: string, context?: unknown) { - if (!(this.assertFeatureEnabled() && this.mainOnAllowedRoute)) { - return; - } - this._initialContext.next(context); - this.openAppV3({ pluginId, follow: false }); - } - - /** - * Opens an app in a sidecar. You can also relative path inside the app to open. - * @deprecated - * @experimental - */ - openAppV2(pluginId: string, path?: string) { - this.openAppV3({ pluginId, path, follow: false }); - } - - /** - * Opens an app in a sidecar. You can also relative path inside the app to open. - * @param options.pluginId Plugin ID of the app to open - * @param options.path Relative path inside the app to open - * @param options.follow If true, the sidecar will stay open even if the main location change to another app or - * Grafana section - * - * @experimental - */ - openAppV3(options: { pluginId: string; path?: string; follow?: boolean }) { - if (!(this.assertFeatureEnabled() && this.mainOnAllowedRoute)) { - return; - } - - this.follow = options.follow || false; - - this.updateMainLocationWhenOpened(); - this.sidecarLocationService.push({ pathname: `/a/${options.pluginId}${options.path || ''}` }); - reportInteraction('sidecar_service_open_app', { pluginId: options.pluginId, follow: options.follow }); - } - - /** - * @experimental - */ - closeApp() { - if (!this.assertFeatureEnabled()) { - return; - } - - this.follow = false; - this.mainLocationWhenOpened = undefined; - this._initialContext.next(undefined); - this.sidecarLocationService.replace({ pathname: '/' }); - - reportInteraction('sidecar_service_close_app'); - } - - /** - * This is mainly useful inside an app extensions which are executed outside the main app context but can work - * differently depending on whether their app is currently rendered or not. - * - * This is also true only in case a sidecar is opened. In other cases, just to check if a single app is opened - * probably does not make sense. - * - * This means these are the states and the result of this function: - * Single app is opened: false (may seem strange from considering the function name, but the main point of - * this is to recognize when the app needs to do specific alteration in context of running next to second app) - * 2 apps are opened and pluginId is the one in the main window: true - * 2 apps are opened and pluginId is the one in the sidecar window: true - * 2 apps are opened and pluginId is not one of those: false - * - * @experimental - */ - isAppOpened(pluginId: string) { - if (!this.assertFeatureEnabled()) { - return false; - } - - const result = !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId)); - reportInteraction('sidecar_service_is_app_opened', { pluginId, isOpened: result }); - return result; - } -} - -const pluginIdUrlRegex = /a\/([^\/]+)/; -function getPluginIdFromUrl(url: string) { - return url.match(pluginIdUrlRegex)?.[1]; -} - -// The app plugin that is "open" in the main Grafana view -function getMainAppPluginId() { - // TODO: not great but we have to get a handle on the other locationService used for the main view and easiest way - // right now is through this global singleton - const { pathname } = mainLocationService.getLocation(); - - // A naive way to sort of simulate core features being an app and having an appID - let mainApp = getPluginIdFromUrl(pathname); - if (!mainApp && pathname.match(/\/explore/)) { - mainApp = 'explore'; - } - - if (!mainApp && pathname.match(/\/d\//)) { - mainApp = 'dashboards'; - } - - return mainApp || 'unknown'; -} - -type LocalStorageHistoryOptions = { - storageKey: string; -}; - -interface LocationStorageHistory extends H.MemoryHistory { - getLocationObservable(): Observable; -} - -/** - * Simple wrapper over the memory history that persists the location in the localStorage. - * - * @param options - */ -function createLocationStorageHistory(options: LocalStorageHistoryOptions): LocationStorageHistory { - const storedLocation = localStorage.getItem(options.storageKey); - const initialEntry = storedLocation ? JSON.parse(storedLocation) : '/'; - const locationSubject = new BehaviorSubject(initialEntry); - const memoryHistory = H.createMemoryHistory({ initialEntries: [initialEntry] }); - - let currentLocation = memoryHistory.location; - - function maybeUpdateLocation() { - if (memoryHistory.location !== currentLocation) { - localStorage.setItem( - options.storageKey, - JSON.stringify(pick(memoryHistory.location, 'pathname', 'search', 'hash')) - ); - currentLocation = memoryHistory.location; - locationSubject.next(memoryHistory.location); - } - } - - // This creates a sort of proxy over the memory location just to add the localStorage persistence and the location - // observer. We could achieve the same effect by a listener but that would create a memory leak as there would be no - // reasonable way to unsubcribe the listener later on. - // Another issue is that react router for some reason does not care about proper `this` binding and just calls these - // as normal functions. So if this were to be a class we would still need to bind each of these methods to the - // instance so at that moment this just seems easier. - return { - ...memoryHistory, - // Getter aren't destructured as getter but as values, so they have to be still here even though we are not - // modifying them. - get index() { - return memoryHistory.index; - }, - get entries() { - return memoryHistory.entries; - }, - get length() { - return memoryHistory.length; - }, - get action() { - return memoryHistory.action; - }, - get location() { - return memoryHistory.location; - }, - push(location: H.Path | H.LocationDescriptor, state?: H.LocationState) { - memoryHistory.push(location, state); - maybeUpdateLocation(); - }, - replace(location: H.Path | H.LocationDescriptor, state?: H.LocationState) { - memoryHistory.replace(location, state); - maybeUpdateLocation(); - }, - go(n: number) { - memoryHistory.go(n); - maybeUpdateLocation(); - }, - goBack() { - memoryHistory.goBack(); - maybeUpdateLocation(); - }, - goForward() { - memoryHistory.goForward(); - maybeUpdateLocation(); - }, - getLocationObservable() { - return locationSubject.asObservable(); - }, - }; -} - -export const sidecarServiceSingleton_EXPERIMENTAL = new SidecarService_EXPERIMENTAL(mainLocationService); diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index f1bf983ba1b..e9d2eeac852 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -6,8 +6,6 @@ export * from './templateSrv'; export * from './live'; export * from './LocationService'; export * from './appEvents'; -export * from './SidecarService_EXPERIMENTAL'; -export * from './SidecarContext_EXPERIMENTAL'; export { setPluginComponentHook, diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ccd67927245..568a33f60ac 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1244,12 +1244,6 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, - { - Name: "appSidecar", - Description: "Enable the app sidecar feature that allows rendering 2 apps at the same time", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendPlatformSquad, - }, { Name: "groupAttributeSync", Description: "Enable the groupsync extension for managing Group Attribute Sync feature", diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 2d7324de32e..4e787933b8b 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -355,7 +355,6 @@ exploreLogsLimitedTimeRange,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa976 exploreLogsShardSplitting,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa9766a995b5711,Sven Grossmann newFiltersUI,2024-08-30T12:48:13Z,,00ae49a61adff11e5fbac3341941b35185d9e8d9,Sergej-Vlasov appPlatformAccessTokens,2024-09-05T16:18:44Z,2024-10-14T10:47:18Z,d5ebaa0ef92edecfb9511dc7c8080be25f8cc7e7,Claudiu Dragalina-Paraipan -appSidecar,2024-09-09T12:45:05Z,,5e2ac24890906e5070323d87730dd78a4f885963,Andrej Ocenas vizActions,2024-09-09T14:11:55Z,,af48d3db1eb2d8681843f5997e50fea5e5ea3096,Adela Almasan groupAttributeSync,2024-09-09T15:29:43Z,,6ded6a8872204a818b3795dc733cc5fe5db066a0,Aaron Godin alertingFilterV2,2024-09-11T11:29:26Z,,90ee52e8d9c14237f8a57b622c0def7512e657cd,Gilles De Mey diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d7af7ac12bc..432f331d2f2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -161,7 +161,6 @@ exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,fals exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true homeSetupGuide,experimental,@grafana/growth-and-onboarding,false,false,true appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false -appSidecar,experimental,@grafana/grafana-frontend-platform,false,false,false groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true improvedExternalSessionHandling,preview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5fa99bec225..4b23d0076e6 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -655,10 +655,6 @@ const ( // Enables the gRPC client to authenticate with the App Platform by using ID & access tokens FlagAppPlatformGrpcClientAuth = "appPlatformGrpcClientAuth" - // FlagAppSidecar - // Enable the app sidecar feature that allows rendering 2 apps at the same time - FlagAppSidecar = "appSidecar" - // FlagGroupAttributeSync // Enable the groupsync extension for managing Group Attribute Sync feature FlagGroupAttributeSync = "groupAttributeSync" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 65737a0b3c0..542851adf3a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -479,18 +479,6 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "appSidecar", - "resourceVersion": "1743693517832", - "creationTimestamp": "2024-09-09T12:45:05Z" - }, - "spec": { - "description": "Enable the app sidecar feature that allows rendering 2 apps at the same time", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" - } - }, { "metadata": { "name": "assetSriChecks", diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index fca1a0004e5..e3a8e59b257 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -4,13 +4,7 @@ import CacheProvider from 'react-inlinesvg/provider'; import { Provider } from 'react-redux'; import { Route, Routes } from 'react-router-dom-v5-compat'; -import { - config, - navigationLogger, - reportInteraction, - SidecarContext_EXPERIMENTAL, - sidecarServiceSingleton_EXPERIMENTAL, -} from '@grafana/runtime'; +import { config, navigationLogger, reportInteraction } from '@grafana/runtime'; import { ErrorBoundaryAlert, PortalContainer, TimeRangeProvider } from '@grafana/ui'; import { getAppRoutes } from 'app/routes/routes'; import { store } from 'app/store/store'; @@ -26,7 +20,7 @@ import { LiveConnectionWarning } from './features/live/LiveConnectionWarning'; import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext'; import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup'; import { ScopesContextProvider } from './features/scopes/ScopesContextProvider'; -import { ExperimentalSplitPaneRouterWrapper, RouterWrapper } from './routes/RoutesWrapper'; +import { RouterWrapper } from './routes/RoutesWrapper'; interface AppWrapperProps { app: GrafanaApp; @@ -125,24 +119,18 @@ export class AppWrapper extends Component { options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }} > - - - - - -
- {config.featureToggles.appSidecar ? ( - - ) : ( - - )} - - -
-
-
-
-
+ + + + +
+ + + +
+
+
+
diff --git a/public/app/routes/RoutesWrapper.tsx b/public/app/routes/RoutesWrapper.tsx index f0b0502519f..21a74256d74 100644 --- a/public/app/routes/RoutesWrapper.tsx +++ b/public/app/routes/RoutesWrapper.tsx @@ -1,22 +1,13 @@ -import { css } from '@emotion/css'; import { ComponentType, ReactNode } from 'react'; -// eslint-disable-next-line no-restricted-imports import { Router } from 'react-router-dom'; import { CompatRouter } from 'react-router-dom-v5-compat'; -import { GrafanaTheme2 } from '@grafana/data'; -import { - locationService, - LocationServiceProvider, - useChromeHeaderHeight, - useSidecar_EXPERIMENTAL, -} from '@grafana/runtime'; -import { GlobalStyles, IconButton, ModalRoot, Stack, useSplitter, useStyles2 } from '@grafana/ui'; +import { locationService, LocationServiceProvider } from '@grafana/runtime'; +import { ModalRoot, Stack } from '@grafana/ui'; import { AppChrome } from '../core/components/AppChrome/AppChrome'; import { AppNotificationList } from '../core/components/AppNotifications/AppNotificationList'; import { ModalsContextProvider } from '../core/context/ModalsContextProvider'; -import { t } from '../core/internationalization'; import { QueriesDrawerContextProvider } from '../features/explore/QueriesDrawer/QueriesDrawerContext'; function ExtraProviders(props: { children: ReactNode; providers: Array> }) { @@ -60,117 +51,3 @@ export function RouterWrapper(props: RouterWrapperProps) { ); } - -/** - * Renders both the main app tree and a secondary sidecar app tree to show 2 apps at the same time in a resizable split - * view. - * @param props - * @constructor - */ -export function ExperimentalSplitPaneRouterWrapper(props: RouterWrapperProps) { - const { closeApp, locationService, activePluginId } = useSidecar_EXPERIMENTAL(); - - let { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ - direction: 'row', - initialSize: 0.6, - dragPosition: 'end', - handleSize: 'sm', - }); - - // The style changes allow the resizing to be more flexible and not constrained by the content dimensions. In the - // future this could be a switch in the useSplitter but for now it's here until this feature is more final. - function alterStyles(props: T): T { - return { - ...props, - style: { ...props.style, overflow: 'auto', minWidth: 'unset', minHeight: 'unset' }, - }; - } - primaryProps = alterStyles(primaryProps); - secondaryProps = alterStyles(secondaryProps); - - const headerHeight = useChromeHeaderHeight(); - const styles = useStyles2(getStyles, headerHeight); - - // Right now we consider only app plugin to be opened here but in the future we might want to just open any kind - // of url and so this should check whether there is a location in the sidecar locationService. - const sidecarOpen = Boolean(activePluginId); - - return ( - // Why do we need these 2 wrappers here? We want for one app case to render very similar as if there was no split - // wrapper at all but the split wrapper needs to have these wrappers to attach its container props to. At the same - // time we don't want to rerender the main app when going from 2 apps render to single app render which would happen - // if we removed the wrappers. So the solution is to keep those 2 divs but make them no actually do anything in - // case we are rendering a single app. -
-
- -
- {/* Sidecar */} - {sidecarOpen && ( - <> -
-
- - - - -
-
- closeApp()} - /> -
-
- {/*We don't render anything other than app plugin but we want to keep the same routing layout so*/} - {/*there are is no difference with matching relative routes between main and sidecar view.*/} - {props.routes} -
-
-
-
-
-
- - )} -
- ); -} - -const getStyles = (theme: GrafanaTheme2, headerHeight: number | undefined) => { - return { - secondAppChrome: css({ - label: 'secondAppChrome', - display: 'flex', - height: '100%', - width: '100%', - paddingTop: headerHeight || 0, - flexGrow: 1, - flexDirection: 'column', - }), - - secondAppToolbar: css({ - label: 'secondAppToolbar', - display: 'flex', - justifyContent: 'flex-end', - }), - - secondAppWrapper: css({ - label: 'secondAppWrapper', - overflow: 'auto', - flex: '1', - }), - - // This is basically the same as grafana-app class. This means the 2 additional wrapper divs that are in between - // grafana-app div and the main app component don't actually change anything in the layout. - dummyWrapper: css({ - label: 'dummyWrapper', - display: 'flex', - height: '100vh', - flexDirection: 'column', - }), - }; -}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 55989b22263..c2ecec89b32 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7135,9 +7135,6 @@ "reload-button": "Reload", "title": "Unable to find application file" }, - "routes-wrapper": { - "close-aria-label": "Close" - }, "sandbox": { "test-stuff-page": { "application-notifications-toasts-testing": "Application notifications (toasts) testing",