mirror of https://github.com/grafana/grafana
Alerting: Add a button to try out the new list page (#103855)
* Add user-facing feature toggle functionality for the new alerting list view - Implemented `useFeatureToggle` hook to manage feature toggles using local storage. - Added unit tests for `useFeatureToggle` to verify behavior for various toggle states. - Updated `RuleList` components to utilize the new feature toggle for alerting list view. - Introduced `RuleListPageTitle` component to handle toggling between list views with a badge indicator. * Add tests * Fix imports and remove unused code * Add a new feature flag for list v2 preview button * Hide v2 preview button behind the new feature flag * Update list v2 feature toggle stage * Alerting: List view feature toggle button PR review (#104161) * Add test for undefined feature toggles case * Tweak tests to use test utils and user * Add i18n for toggle button and tweak props spreading * Update translations --------- Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>pull/104268/head
parent
17e4a3b386
commit
512df0091a
|
@ -0,0 +1,90 @@ |
||||
import { setLocalStorageFeatureToggle } from './featureToggles'; |
||||
|
||||
const featureTogglesKey = 'grafana.featureToggles'; |
||||
const storage = new Map<string, string>(); |
||||
|
||||
const mockLocalStorage = { |
||||
getItem: (key: string) => storage.get(key) ?? null, |
||||
setItem: (key: string, value: string) => storage.set(key, value), |
||||
clear: () => storage.clear(), |
||||
}; |
||||
|
||||
Object.defineProperty(window, 'localStorage', { |
||||
value: mockLocalStorage, |
||||
writable: true, |
||||
}); |
||||
|
||||
describe('setLocalStorageFeatureToggle', () => { |
||||
beforeEach(() => { |
||||
storage.clear(); |
||||
}); |
||||
|
||||
it('should set feature toggle to true', () => { |
||||
setLocalStorageFeatureToggle('alertingListViewV2', true); |
||||
expect(storage.get(featureTogglesKey)).toBe('alertingListViewV2=true'); |
||||
}); |
||||
|
||||
it('should set feature toggle to false', () => { |
||||
setLocalStorageFeatureToggle('alertingListViewV2', false); |
||||
expect(storage.get(featureTogglesKey)).toBe('alertingListViewV2=false'); |
||||
}); |
||||
|
||||
it('should remove feature toggle when set to undefined', () => { |
||||
storage.set( |
||||
featureTogglesKey, |
||||
'alertingListViewV2=true,alertingPrometheusRulesPrimary=true,alertingCentralAlertHistory=true' |
||||
); |
||||
|
||||
setLocalStorageFeatureToggle('alertingPrometheusRulesPrimary', undefined); |
||||
expect(storage.get(featureTogglesKey)).toBe('alertingListViewV2=true,alertingCentralAlertHistory=true'); |
||||
}); |
||||
|
||||
it('should not set undefined when no feature toggles are set', () => { |
||||
storage.set(featureTogglesKey, ''); |
||||
|
||||
setLocalStorageFeatureToggle('alertingPrometheusRulesPrimary', undefined); |
||||
expect(storage.get(featureTogglesKey)).toBe(''); |
||||
}); |
||||
|
||||
it('should update only one feature toggle when multiple feature toggles are set', () => { |
||||
storage.set( |
||||
featureTogglesKey, |
||||
'alertingListViewV2=true,alertingPrometheusRulesPrimary=true,alertingCentralAlertHistory=true' |
||||
); |
||||
|
||||
setLocalStorageFeatureToggle('alertingPrometheusRulesPrimary', false); |
||||
expect(storage.get(featureTogglesKey)).toBe( |
||||
'alertingListViewV2=true,alertingPrometheusRulesPrimary=false,alertingCentralAlertHistory=true' |
||||
); |
||||
}); |
||||
|
||||
it('should not rewrite other feature toggles when updating one', () => { |
||||
storage.set( |
||||
featureTogglesKey, |
||||
'alertingListViewV2=true,alertingPrometheusRulesPrimary=1,alertingCentralAlertHistory=false' |
||||
); |
||||
|
||||
setLocalStorageFeatureToggle('alertingListViewV2', false); |
||||
expect(storage.get(featureTogglesKey)).toBe( |
||||
'alertingListViewV2=false,alertingPrometheusRulesPrimary=1,alertingCentralAlertHistory=false' |
||||
); |
||||
}); |
||||
|
||||
it('should add a new toggle when others exist', () => { |
||||
storage.set(featureTogglesKey, 'alertingListViewV2=true'); |
||||
setLocalStorageFeatureToggle('alertingCentralAlertHistory', true); |
||||
expect(storage.get(featureTogglesKey)).toBe('alertingListViewV2=true,alertingCentralAlertHistory=true'); |
||||
}); |
||||
|
||||
it('should remove the only existing toggle', () => { |
||||
storage.set(featureTogglesKey, 'alertingListViewV2=true'); |
||||
setLocalStorageFeatureToggle('alertingListViewV2', undefined); |
||||
expect(storage.get(featureTogglesKey)).toBe(''); |
||||
}); |
||||
|
||||
it('should not change localStorage when attempting to remove a non-existent toggle', () => { |
||||
storage.set(featureTogglesKey, 'alertingListViewV2=true'); |
||||
setLocalStorageFeatureToggle('alertingCentralAlertHistory', undefined); |
||||
expect(storage.get(featureTogglesKey)).toBe('alertingListViewV2=true'); |
||||
}); |
||||
}); |
@ -0,0 +1,129 @@ |
||||
import { render } from 'test/test-utils'; |
||||
import { byRole } from 'testing-library-selector'; |
||||
|
||||
import { reportInteraction } from '@grafana/runtime'; |
||||
|
||||
import { testWithFeatureToggles } from '../test/test-utils'; |
||||
|
||||
import { RuleListPageTitle } from './RuleListPageTitle'; |
||||
|
||||
// Constants
|
||||
const featureTogglesKey = 'grafana.featureToggles'; |
||||
const toggleName = 'alertingListViewV2'; |
||||
|
||||
jest.mock('@grafana/runtime', () => ({ |
||||
...jest.requireActual('@grafana/runtime'), |
||||
reportInteraction: jest.fn(), |
||||
})); |
||||
|
||||
// Mock window.location.reload
|
||||
const mockReload = jest.fn(); |
||||
Object.defineProperty(window, 'location', { |
||||
value: { reload: mockReload }, |
||||
writable: true, |
||||
}); |
||||
|
||||
const ui = { |
||||
title: byRole('heading', { name: 'Alert rules' }), |
||||
enableV2Button: byRole('button', { name: 'Try out the new look!' }), |
||||
disableV2Button: byRole('button', { name: 'Go back to the old look' }), |
||||
}; |
||||
|
||||
// Helper function for rendering the component
|
||||
function renderRuleListPageTitle() { |
||||
// Mock localStorage
|
||||
const storage = new Map<string, string>(); |
||||
const mockLocalStorage = { |
||||
getItem: (key: string) => storage.get(key) ?? null, |
||||
setItem: (key: string, value: string) => storage.set(key, value), |
||||
clear: () => storage.clear(), |
||||
}; |
||||
|
||||
Object.defineProperty(window, 'localStorage', { |
||||
value: mockLocalStorage, |
||||
writable: true, |
||||
}); |
||||
|
||||
const view = render(<RuleListPageTitle title="Alert rules" />); |
||||
|
||||
return { |
||||
...view, |
||||
storage, |
||||
}; |
||||
} |
||||
|
||||
describe('RuleListPageTitle', () => { |
||||
beforeEach(() => { |
||||
jest.clearAllMocks(); |
||||
}); |
||||
|
||||
it('should render the title', () => { |
||||
renderRuleListPageTitle(); |
||||
expect(ui.title.get()).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should not show v2 toggle when alertingListViewV2PreviewToggle feature flag is disabled', () => { |
||||
renderRuleListPageTitle(); |
||||
expect(ui.enableV2Button.query()).not.toBeInTheDocument(); |
||||
expect(ui.disableV2Button.query()).not.toBeInTheDocument(); |
||||
}); |
||||
|
||||
describe('with alertingListViewV2PreviewToggle enabled and alertingListViewV2 disabled', () => { |
||||
testWithFeatureToggles(['alertingListViewV2PreviewToggle']); |
||||
|
||||
it('should show enable v2 button', () => { |
||||
renderRuleListPageTitle(); |
||||
expect(ui.enableV2Button.get()).toBeInTheDocument(); |
||||
expect(ui.disableV2Button.query()).not.toBeInTheDocument(); |
||||
expect(ui.enableV2Button.get()).toHaveAttribute('data-testid', 'alerting-list-view-toggle-v2'); |
||||
}); |
||||
|
||||
it('should enable v2 and reload page when clicked on "Try out the new look!" button', async () => { |
||||
const { user, storage } = renderRuleListPageTitle(); |
||||
|
||||
await user.click(ui.enableV2Button.get()); |
||||
|
||||
expect(storage.get(featureTogglesKey)).toBe(`${toggleName}=true`); |
||||
expect(mockReload).toHaveBeenCalled(); |
||||
}); |
||||
|
||||
it('should report interaction when enabling v2', async () => { |
||||
const { user } = renderRuleListPageTitle(); |
||||
|
||||
await user.click(ui.enableV2Button.get()); |
||||
|
||||
expect(reportInteraction).toHaveBeenCalledWith('alerting.list_view.v2.enabled'); |
||||
}); |
||||
}); |
||||
|
||||
describe('with alertingListViewV2PreviewToggle enabled and alertingListViewV2 enabled', () => { |
||||
testWithFeatureToggles(['alertingListViewV2PreviewToggle', 'alertingListViewV2']); |
||||
|
||||
it('should show disable v2 button', () => { |
||||
renderRuleListPageTitle(); |
||||
expect(ui.disableV2Button.get()).toBeInTheDocument(); |
||||
expect(ui.enableV2Button.query()).not.toBeInTheDocument(); |
||||
expect(ui.disableV2Button.get()).toHaveAttribute('data-testid', 'alerting-list-view-toggle-v1'); |
||||
}); |
||||
|
||||
it('should disable v2 and reload page when clicked on "Go back to the old look" button', async () => { |
||||
const { user, storage } = renderRuleListPageTitle(); |
||||
storage.set(featureTogglesKey, `${toggleName}=true`); |
||||
|
||||
await user.click(ui.disableV2Button.get()); |
||||
|
||||
// When the toggle is set to undefined, it should be removed from localStorage
|
||||
expect(storage.get(featureTogglesKey)).toBe(''); |
||||
expect(mockReload).toHaveBeenCalled(); |
||||
}); |
||||
|
||||
it('should report interaction when disabling v2', async () => { |
||||
const { user, storage } = renderRuleListPageTitle(); |
||||
storage.set(featureTogglesKey, `${toggleName}=true`); |
||||
|
||||
await user.click(ui.disableV2Button.get()); |
||||
|
||||
expect(reportInteraction).toHaveBeenCalledWith('alerting.list_view.v2.disabled'); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,69 @@ |
||||
import { useCallback } from 'react'; |
||||
|
||||
import { config, reportInteraction } from '@grafana/runtime'; |
||||
import { Button, ButtonProps, Stack } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
|
||||
import { setLocalStorageFeatureToggle, shouldUseAlertingListViewV2 } from '../featureToggles'; |
||||
|
||||
export function RuleListPageTitle({ title }: { title: string }) { |
||||
const shouldShowV2Toggle = config.featureToggles.alertingListViewV2PreviewToggle ?? false; |
||||
|
||||
const { listViewV2Enabled, enableListViewV2, disableListViewV2 } = useV2AlertListViewToggle(); |
||||
|
||||
const toggleListView = () => { |
||||
if (listViewV2Enabled) { |
||||
disableListViewV2(); |
||||
reportInteraction('alerting.list_view.v2.disabled'); |
||||
} else { |
||||
enableListViewV2(); |
||||
reportInteraction('alerting.list_view.v2.enabled'); |
||||
} |
||||
window.location.reload(); |
||||
}; |
||||
|
||||
const { text, ...configToUse }: ButtonProps & { text: string; 'data-testid': string } = listViewV2Enabled |
||||
? { |
||||
variant: 'secondary', |
||||
icon: undefined, |
||||
text: t('alerting.rule-list.toggle.go-back-to-old-look', 'Go back to the old look'), |
||||
'data-testid': 'alerting-list-view-toggle-v1', |
||||
} |
||||
: { |
||||
variant: 'primary', |
||||
icon: 'rocket', |
||||
text: t('alerting.rule-list.toggle.try-out-the-new-look', 'Try out the new look!'), |
||||
'data-testid': 'alerting-list-view-toggle-v2', |
||||
}; |
||||
|
||||
return ( |
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={2}> |
||||
<h1>{title}</h1> |
||||
{shouldShowV2Toggle && ( |
||||
<div> |
||||
<Button size="sm" fill="outline" {...configToUse} onClick={toggleListView} className="fs-unmask"> |
||||
{text} |
||||
</Button> |
||||
</div> |
||||
)} |
||||
</Stack> |
||||
); |
||||
} |
||||
|
||||
function useV2AlertListViewToggle() { |
||||
const listViewV2Enabled = shouldUseAlertingListViewV2(); |
||||
|
||||
const enableListViewV2 = useCallback(() => { |
||||
setLocalStorageFeatureToggle('alertingListViewV2', true); |
||||
}, []); |
||||
|
||||
const disableListViewV2 = useCallback(() => { |
||||
setLocalStorageFeatureToggle('alertingListViewV2', undefined); |
||||
}, []); |
||||
|
||||
return { |
||||
listViewV2Enabled, |
||||
enableListViewV2, |
||||
disableListViewV2, |
||||
}; |
||||
} |
Loading…
Reference in new issue