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