ExtensionSidebar: Remove feature flag and enable by default (#109906)

* ExtensionSidebar: Remove feature flag and enable by default

* ExtensionSidebar: Remove `isEnabled`

* ExtensionSidebar: Lint

* ExtensionSidebar: Lint

* ExtensionSidebar: Remove more FF

* i dont know why, but okay
This commit is contained in:
Sven Grossmann 2025-09-01 12:14:17 +02:00 committed by GitHub
parent d31e682345
commit b6d7374b25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 26 additions and 151 deletions

View File

@ -907,10 +907,6 @@ export interface FeatureToggles {
*/
unifiedStorageGrpcConnectionPool?: boolean;
/**
* Enables the extension sidebar
*/
extensionSidebar?: boolean;
/**
* Enables UI functionality to permanently delete alert rules
* @default true
*/

View File

@ -1563,13 +1563,6 @@ var (
HideFromAdminPage: true,
HideFromDocs: true,
},
{
Name: "extensionSidebar",
Description: "Enables the extension sidebar",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaObservabilityLogsSquad,
},
{
Name: "alertingRulePermanentlyDelete",
Description: "Enables UI functionality to permanently delete alert rules",

View File

@ -203,7 +203,6 @@ unifiedStorageHistoryPruner,GA,@grafana/search-and-storage,false,false,false
azureMonitorLogsBuilderEditor,preview,@grafana/partner-datasources,false,false,false
localeFormatPreference,preview,@grafana/grafana-frontend-platform,false,false,false
unifiedStorageGrpcConnectionPool,experimental,@grafana/search-and-storage,false,false,false
extensionSidebar,experimental,@grafana/observability-logs,false,false,true
alertingRulePermanentlyDelete,GA,@grafana/alerting-squad,false,false,true
alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true
multiTenantTempCredentials,experimental,@grafana/aws-datasources,false,false,false

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
203 azureMonitorLogsBuilderEditor preview @grafana/partner-datasources false false false
204 localeFormatPreference preview @grafana/grafana-frontend-platform false false false
205 unifiedStorageGrpcConnectionPool experimental @grafana/search-and-storage false false false
extensionSidebar experimental @grafana/observability-logs false false true
206 alertingRulePermanentlyDelete GA @grafana/alerting-squad false false true
207 alertingRuleRecoverDeleted GA @grafana/alerting-squad false false true
208 multiTenantTempCredentials experimental @grafana/aws-datasources false false false

View File

@ -823,10 +823,6 @@ const (
// Enables the unified storage grpc connection pool
FlagUnifiedStorageGrpcConnectionPool = "unifiedStorageGrpcConnectionPool"
// FlagExtensionSidebar
// Enables the extension sidebar
FlagExtensionSidebar = "extensionSidebar"
// FlagAlertingRulePermanentlyDelete
// Enables UI functionality to permanently delete alert rules
FlagAlertingRulePermanentlyDelete = "alertingRulePermanentlyDelete"

View File

@ -1322,7 +1322,8 @@
"metadata": {
"name": "extensionSidebar",
"resourceVersion": "1753448760331",
"creationTimestamp": "2025-04-03T10:16:35Z"
"creationTimestamp": "2025-04-03T10:16:35Z",
"deletionTimestamp": "2025-08-20T11:59:44Z"
},
"spec": {
"description": "Enables the extension sidebar",

View File

@ -105,9 +105,6 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
};
const MaybeTimeRangeProvider = config.featureToggles.timeRangeProvider ? TimeRangeProvider : Fragment;
const MaybeExtensionSidebarProvider = config.featureToggles.extensionSidebar
? ExtensionSidebarContextProvider
: Fragment;
return (
<Provider store={store}>
@ -122,7 +119,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
<MaybeTimeRangeProvider>
<ScopesContextProvider>
<ExtensionRegistriesProvider registries={pluginExtensionRegistries}>
<MaybeExtensionSidebarProvider>
<ExtensionSidebarContextProvider>
<UNSAFE_PortalProvider getContainer={getPortalContainer}>
<GlobalStylesWrapper />
<div className="grafana-app">
@ -131,7 +128,7 @@ export class AppWrapper extends Component<AppWrapperProps, AppWrapperState> {
<PortalContainer />
</div>
</UNSAFE_PortalProvider>
</MaybeExtensionSidebarProvider>
</ExtensionSidebarContextProvider>
</ExtensionRegistriesProvider>
</ScopesContextProvider>
</MaybeTimeRangeProvider>

View File

@ -33,7 +33,6 @@ export function AppChrome({ children }: Props) {
const { chrome } = useGrafana();
const {
isOpen: isExtensionSidebarOpen,
isEnabled: isExtensionSidebarEnabled,
extensionSidebarWidth,
setExtensionSidebarWidth,
} = useExtensionSidebarContext();
@ -138,7 +137,7 @@ export function AppChrome({ children }: Props) {
>
{children}
</main>
{!state.chromeless && isExtensionSidebarEnabled && isExtensionSidebarOpen && (
{!state.chromeless && isExtensionSidebarOpen && (
<Resizable
className={styles.sidebarContainer}
defaultSize={{ width: extensionSidebarWidth }}

View File

@ -44,7 +44,6 @@ const addedComponentConfigMock: ExtensionInfo = {
const extensionSidebarContextMock: ExtensionSidebarContextType = {
dockedComponentId: getComponentIdFromComponentMeta(pluginId, addedComponentConfigMock),
isEnabled: true,
props: {},
isOpen: true,
setDockedComponentId: jest.fn(),
@ -71,25 +70,9 @@ describe('ExtensionSidebar', () => {
config.buildInfo.env = originalEnv;
});
it('should render nothing when the extension sidebar is not enabled', () => {
mockUseExtensionSidebarContext.mockReturnValue({
...extensionSidebarContextMock,
isEnabled: false,
});
mockUsePluginComponents.mockReturnValue({
components: [createComponentWithMeta(addedComponentRegistryItemMock, extensionPointId)],
isLoading: false,
});
const { container } = render(<ExtensionSidebar />);
expect(container.firstChild).toBeNull();
});
it('should render nothing when the extension sidebar is enabled but no component is docked', () => {
mockUseExtensionSidebarContext.mockReturnValue({
...extensionSidebarContextMock,
isEnabled: true,
dockedComponentId: undefined,
});
@ -105,7 +88,6 @@ describe('ExtensionSidebar', () => {
it('should render nothing when the extension sidebar is enabled but the component docked is not found in the available components', () => {
mockUseExtensionSidebarContext.mockReturnValue({
...extensionSidebarContextMock,
isEnabled: true,
dockedComponentId: 'test-component-id-not-found',
});
@ -121,7 +103,6 @@ describe('ExtensionSidebar', () => {
it('should render nothing when the extension sidebar is enabled but the component docked is not found in the available components', () => {
mockUseExtensionSidebarContext.mockReturnValue({
...extensionSidebarContextMock,
isEnabled: true,
dockedComponentId: 'test-component-id-not-found',
});
@ -137,7 +118,6 @@ describe('ExtensionSidebar', () => {
it('should render nothing when components are loading', () => {
mockUseExtensionSidebarContext.mockReturnValue({
...extensionSidebarContextMock,
isEnabled: true,
});
mockUsePluginComponents.mockReturnValue({
@ -152,7 +132,6 @@ describe('ExtensionSidebar', () => {
it('should render the component when all conditions are met', () => {
mockUseExtensionSidebarContext.mockReturnValue({
...extensionSidebarContextMock,
isEnabled: true,
});
mockUsePluginComponents.mockReturnValue({

View File

@ -16,12 +16,12 @@ type ExtensionSidebarComponentProps = {
export function ExtensionSidebar() {
const styles = getStyles(useTheme2());
const { dockedComponentId, isEnabled, props = {} } = useExtensionSidebarContext();
const { dockedComponentId, props = {} } = useExtensionSidebarContext();
const { components, isLoading } = usePluginComponents<ExtensionSidebarComponentProps>({
extensionPointId: PluginExtensionPoints.ExtensionSidebar,
});
if (isLoading || !dockedComponentId || !isEnabled) {
if (isLoading || !dockedComponentId) {
return null;
}

View File

@ -1,7 +1,7 @@
import { render, screen, act } from '@testing-library/react';
import { store, EventBusSrv, EventBus } from '@grafana/data';
import { config, getAppEvents, setAppEvents, locationService } from '@grafana/runtime';
import { getAppEvents, setAppEvents, locationService } from '@grafana/runtime';
import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { OpenExtensionSidebarEvent, CloseExtensionSidebarEvent } from 'app/types/events';
@ -43,13 +43,6 @@ jest.mock('app/features/plugins/extensions/utils', () => ({
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
...jest.requireActual('@grafana/runtime').config,
featureToggles: {
...jest.requireActual('@grafana/runtime').config.featureToggles,
extensionSidebar: true,
},
},
locationService: {
getLocation: jest.fn().mockReturnValue({ pathname: '/test-path' }),
getLocationObservable: jest.fn(),
@ -83,8 +76,6 @@ describe('ExtensionSidebarProvider', () => {
getExtensionPointPluginMetaMock.mockReturnValue(new Map([[mockPluginMeta.pluginId, mockPluginMeta]]));
jest.replaceProperty(config.featureToggles, 'extensionSidebar', true);
locationObservableMock = {
subscribe: jest.fn((callback) => {
locationObservableMock.callback = callback;
@ -113,7 +104,6 @@ describe('ExtensionSidebarProvider', () => {
<div data-testid="docked-component-id">{context.dockedComponentId || 'undefined'}</div>
<div data-testid="available-components-size">{context.availableComponents.size}</div>
<div data-testid="plugin-ids">{Array.from(context.availableComponents.keys()).join(', ')}</div>
<div data-testid="is-enabled">{context.isEnabled.toString()}</div>
</div>
);
};
@ -128,20 +118,6 @@ describe('ExtensionSidebarProvider', () => {
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
expect(screen.getByTestId('docked-component-id')).toHaveTextContent('undefined');
expect(screen.getByTestId('available-components-size')).toHaveTextContent('1');
expect(screen.getByTestId('is-enabled')).toHaveTextContent('true');
});
it('should have empty available components when feature toggle is disabled', () => {
jest.replaceProperty(config.featureToggles, 'extensionSidebar', false);
render(
<ExtensionSidebarContextProvider>
<TestComponent />
</ExtensionSidebarContextProvider>
);
expect(screen.getByTestId('is-enabled')).toHaveTextContent('false');
expect(screen.getByTestId('available-components-size')).toHaveTextContent('0');
});
it('should load docked component from storage if available', () => {
@ -158,22 +134,6 @@ describe('ExtensionSidebarProvider', () => {
expect(screen.getByTestId('docked-component-id')).toHaveTextContent(componentId);
});
it('should not load docked component from storage if feature toggle is disabled', () => {
jest.replaceProperty(config.featureToggles, 'extensionSidebar', false);
const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent);
(store.get as jest.Mock).mockReturnValue(componentId);
render(
<ExtensionSidebarContextProvider>
<TestComponent />
</ExtensionSidebarContextProvider>
);
expect(screen.getByTestId('is-open')).toHaveTextContent('false');
expect(screen.getByTestId('docked-component-id')).toHaveTextContent('undefined');
});
it('should update storage when docked component changes', () => {
const componentId = getComponentIdFromComponentMeta(mockPluginMeta.pluginId, mockComponent);
@ -260,18 +220,6 @@ describe('ExtensionSidebarProvider', () => {
expect(subscribeSpy).toHaveBeenCalledWith(CloseExtensionSidebarEvent, expect.any(Function));
});
it('should not subscribe to OpenExtensionSidebarEvent or CloseExtensionSidebarEvent when feature is disabled', () => {
jest.replaceProperty(config.featureToggles, 'extensionSidebar', false);
render(
<ExtensionSidebarContextProvider>
<TestComponent />
</ExtensionSidebarContextProvider>
);
expect(subscribeSpy).not.toHaveBeenCalled();
});
it('should set dockedComponentId and props when receiving a valid OpenExtensionSidebarEvent', () => {
const TestComponentWithProps = () => {
const context = useExtensionSidebarContext();

View File

@ -2,7 +2,7 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useState,
import { useLocalStorage } from 'react-use';
import { PluginExtensionPoints, store, type ExtensionInfo } from '@grafana/data';
import { config, getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime';
import { getAppEvents, reportInteraction, usePluginLinks, locationService } from '@grafana/runtime';
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { CloseExtensionSidebarEvent, OpenExtensionSidebarEvent } from 'app/types/events';
@ -18,10 +18,6 @@ const PERMITTED_EXTENSION_SIDEBAR_PLUGINS = [
];
export type ExtensionSidebarContextType = {
/**
* Whether the extension sidebar is enabled.
*/
isEnabled: boolean;
/**
* Whether the extension sidebar is open.
*/
@ -51,7 +47,6 @@ export type ExtensionSidebarContextType = {
};
export const ExtensionSidebarContext = createContext<ExtensionSidebarContextType>({
isEnabled: !!config.featureToggles.extensionSidebar,
isOpen: false,
dockedComponentId: undefined,
setDockedComponentId: () => {},
@ -99,31 +94,22 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
},
});
const isEnabled = !!config.featureToggles.extensionSidebar;
// get all components for this extension point, but only for the permitted plugins
// if the extension sidebar is not enabled, we will return an empty map
const availableComponents = useMemo(
() =>
isEnabled
? new Map(
Array.from(getExtensionPointPluginMeta(PluginExtensionPoints.ExtensionSidebar).entries()).filter(
([pluginId, pluginMeta]) =>
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
links.some(
(link) =>
link.pluginId === pluginId &&
pluginMeta.addedComponents.some((component) => component.title === link.title)
)
new Map(
Array.from(getExtensionPointPluginMeta(PluginExtensionPoints.ExtensionSidebar).entries()).filter(
([pluginId, pluginMeta]) =>
PERMITTED_EXTENSION_SIDEBAR_PLUGINS.includes(pluginId) &&
links.some(
(link) =>
link.pluginId === pluginId &&
pluginMeta.addedComponents.some((component) => component.title === link.title)
)
)
: new Map<
string,
{
readonly addedComponents: ExtensionInfo[];
readonly addedLinks: ExtensionInfo[];
}
>(),
[isEnabled, links]
)
),
[links]
);
// check if the stored docked component is still available
@ -164,10 +150,6 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
);
useEffect(() => {
if (!isEnabled) {
return;
}
// handler to open the extension sidebar from plugins. this is done with the `helpers.openSidebar` function
const openSidebarHandler = (event: OpenExtensionSidebarEvent) => {
if (
@ -195,7 +177,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
openSubscription.unsubscribe();
closeSubscription.unsubscribe();
};
}, [isEnabled, setDockedComponentWithProps, availableComponents]);
}, [setDockedComponentWithProps, availableComponents]);
// update the stored docked component id when it changes
useEffect(() => {
@ -226,8 +208,7 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
return (
<ExtensionSidebarContext.Provider
value={{
isEnabled,
isOpen: isEnabled && dockedComponentId !== undefined,
isOpen: dockedComponentId !== undefined,
dockedComponentId,
setDockedComponentId: (componentId) => setDockedComponentWithProps(componentId, undefined),
availableComponents,

View File

@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { EventBusSrv, store } from '@grafana/data';
import { config, setAppEvents, usePluginLinks } from '@grafana/runtime';
import { setAppEvents, usePluginLinks } from '@grafana/runtime';
import { getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { ExtensionSidebarContextProvider, useExtensionSidebarContext } from './ExtensionSidebarProvider';
@ -26,13 +26,6 @@ jest.mock('@grafana/data', () => ({
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
...jest.requireActual('@grafana/runtime').config,
featureToggles: {
...jest.requireActual('@grafana/runtime').config.featureToggles,
extensionSidebar: true,
},
},
usePluginLinks: jest.fn().mockImplementation(() => ({
links: [
{
@ -81,7 +74,6 @@ describe('ExtensionToolbarItem', () => {
(store.get as jest.Mock).mockClear();
(store.set as jest.Mock).mockClear();
(store.delete as jest.Mock).mockClear();
jest.replaceProperty(config.featureToggles, 'extensionSidebar', true);
setAppEvents(new EventBusSrv());
});
@ -89,12 +81,6 @@ describe('ExtensionToolbarItem', () => {
jest.clearAllMocks();
});
it('should not render when feature toggle is disabled', () => {
jest.replaceProperty(config.featureToggles, 'extensionSidebar', false);
setup();
expect(screen.queryByRole('button')).not.toBeInTheDocument();
});
it('should not render when no components are available', () => {
(getExtensionPointPluginMeta as jest.Mock).mockReturnValue(new Map());
setup();

View File

@ -13,9 +13,9 @@ import { ExtensionToolbarItemButton } from './ExtensionToolbarItemButton';
type ComponentWithPluginId = ExtensionInfo & { pluginId: string };
export function ExtensionToolbarItem() {
const { availableComponents, dockedComponentId, setDockedComponentId, isEnabled } = useExtensionSidebarContext();
const { availableComponents, dockedComponentId, setDockedComponentId } = useExtensionSidebarContext();
if (!isEnabled || availableComponents.size === 0) {
if (availableComponents.size === 0) {
return null;
}

View File

@ -104,7 +104,7 @@ export const SingleTopBar = memo(function SingleTopBar({
</Dropdown>
)}
<NavToolbarSeparator />
{config.featureToggles.extensionSidebar && !isSmallScreen && <ExtensionToolbarItem />}
{!isSmallScreen && <ExtensionToolbarItem />}
{!showToolbarLevel && actions}
{!contextSrv.user.isSignedIn && <SignInLink />}
<InviteUserButton />