diff --git a/apps/alerting/notifications/pkg/apis/alerting_manifest.go b/apps/alerting/notifications/pkg/apis/alerting_manifest.go index 9e40415b859..9a97a47f78a 100644 --- a/apps/alerting/notifications/pkg/apis/alerting_manifest.go +++ b/apps/alerting/notifications/pkg/apis/alerting_manifest.go @@ -11,8 +11,6 @@ import ( "github.com/grafana/grafana-app-sdk/app" ) -var () - var appManifestData = app.ManifestData{ AppName: "alerting", Group: "notifications.alerting.grafana.app", diff --git a/apps/alerting/notifications/pkg/apis/receiver/v0alpha1/receiver_schema_gen.go b/apps/alerting/notifications/pkg/apis/receiver/v0alpha1/receiver_schema_gen.go index 48b5d72fbd9..d159d0d9cb9 100644 --- a/apps/alerting/notifications/pkg/apis/receiver/v0alpha1/receiver_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/receiver/v0alpha1/receiver_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaReceiver = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &Receiver{}, &ReceiverList{}, resource.WithKind("Receiver"), - resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("receivers"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*Receiver) diff --git a/apps/alerting/notifications/pkg/apis/templategroup/v0alpha1/templategroup_schema_gen.go b/apps/alerting/notifications/pkg/apis/templategroup/v0alpha1/templategroup_schema_gen.go index 256fbab3116..073e8eb9058 100644 --- a/apps/alerting/notifications/pkg/apis/templategroup/v0alpha1/templategroup_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/templategroup/v0alpha1/templategroup_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaTemplateGroup = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TemplateGroup{}, &TemplateGroupList{}, resource.WithKind("TemplateGroup"), - resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("templategroups"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.title", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*TemplateGroup) diff --git a/apps/alerting/notifications/pkg/apis/timeinterval/v0alpha1/timeinterval_schema_gen.go b/apps/alerting/notifications/pkg/apis/timeinterval/v0alpha1/timeinterval_schema_gen.go index 627e02a9572..af8ff6454a5 100644 --- a/apps/alerting/notifications/pkg/apis/timeinterval/v0alpha1/timeinterval_schema_gen.go +++ b/apps/alerting/notifications/pkg/apis/timeinterval/v0alpha1/timeinterval_schema_gen.go @@ -13,7 +13,7 @@ import ( // schema is unexported to prevent accidental overwrites var ( schemaTimeInterval = resource.NewSimpleSchema("notifications.alerting.grafana.app", "v0alpha1", &TimeInterval{}, &TimeIntervalList{}, resource.WithKind("TimeInterval"), - resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{resource.SelectableField{ + resource.WithPlural("timeintervals"), resource.WithScope(resource.NamespacedScope), resource.WithSelectableFields([]resource.SelectableField{{ FieldSelector: "spec.name", FieldValueFunc: func(o resource.Object) (string, error) { cast, ok := o.(*TimeInterval) diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 2bb29c441a8..6709960a27c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1026,4 +1026,8 @@ export interface FeatureToggles { * Enables auto-updating of users installed plugins */ pluginsAutoUpdate?: boolean; + /** + * Enables the alerting list view v2 preview toggle + */ + alertingListViewV2PreviewToggle?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2ea014cbb0c..820d571f435 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1026,7 +1026,7 @@ var ( { Name: "alertingListViewV2", Description: "Enables the new alert list view design", - Stage: FeatureStageExperimental, + Stage: FeatureStagePrivatePreview, Owner: grafanaAlertingSquad, FrontendOnly: true, }, @@ -1767,6 +1767,13 @@ var ( FrontendOnly: false, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "alertingListViewV2PreviewToggle", + Description: "Enables the alerting list view v2 preview toggle", + FrontendOnly: true, + Stage: FeatureStagePrivatePreview, + Owner: grafanaAlertingSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index f7481981212..491dd8c4ef9 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -132,7 +132,7 @@ grafanaManagedRecordingRules,experimental,@grafana/alerting-squad,false,false,fa queryLibrary,experimental,@grafana/grafana-frontend-platform,false,false,false logsExploreTableDefaultVisualization,experimental,@grafana/observability-logs,false,false,true newDashboardSharingComponent,GA,@grafana/sharing-squad,false,false,true -alertingListViewV2,experimental,@grafana/alerting-squad,false,false,true +alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false alertingCentralAlertHistory,experimental,@grafana/alerting-squad,false,false,true @@ -231,3 +231,4 @@ unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true logsPanelControls,preview,@grafana/observability-logs,false,false,true metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false +alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 09378c3e93c..301e473665a 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -934,4 +934,8 @@ const ( // FlagPluginsAutoUpdate // Enables auto-updating of users installed plugins FlagPluginsAutoUpdate = "pluginsAutoUpdate" + + // FlagAlertingListViewV2PreviewToggle + // Enables the alerting list view v2 preview toggle + FlagAlertingListViewV2PreviewToggle = "alertingListViewV2PreviewToggle" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 71587efe07c..62ae788337d 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -224,12 +224,31 @@ { "metadata": { "name": "alertingListViewV2", - "resourceVersion": "1743693517832", - "creationTimestamp": "2024-05-24T14:40:49Z" + "resourceVersion": "1744700823766", + "creationTimestamp": "2024-05-24T14:40:49Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-15 07:07:03.766981 +0000 UTC" + } }, "spec": { "description": "Enables the new alert list view design", - "stage": "experimental", + "stage": "privatePreview", + "codeowner": "@grafana/alerting-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "alertingListViewV2PreviewToggle", + "resourceVersion": "1744700823766", + "creationTimestamp": "2025-04-14T13:28:02Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-04-15 07:07:03.766981 +0000 UTC" + } + }, + "spec": { + "description": "Enables the alerting list view v2 preview toggle", + "stage": "privatePreview", "codeowner": "@grafana/alerting-squad", "frontend": true } diff --git a/pkg/util/xorm/dialect_spanner.go b/pkg/util/xorm/dialect_spanner.go index a5da535e983..fbc361ce6b8 100644 --- a/pkg/util/xorm/dialect_spanner.go +++ b/pkg/util/xorm/dialect_spanner.go @@ -11,8 +11,8 @@ import ( spannerclient "cloud.google.com/go/spanner" _ "github.com/googleapis/go-sql-spanner" spannerdriver "github.com/googleapis/go-sql-spanner" - "google.golang.org/grpc/codes" "github.com/grafana/grafana/pkg/util/xorm/core" + "google.golang.org/grpc/codes" ) func init() { diff --git a/pkg/util/xorm/dialect_sqlite3.go b/pkg/util/xorm/dialect_sqlite3.go index 4c5a593c9c4..381a38bb490 100644 --- a/pkg/util/xorm/dialect_sqlite3.go +++ b/pkg/util/xorm/dialect_sqlite3.go @@ -11,8 +11,8 @@ import ( "regexp" "strings" - sqlite "github.com/mattn/go-sqlite3" "github.com/grafana/grafana/pkg/util/xorm/core" + sqlite "github.com/mattn/go-sqlite3" ) var ( diff --git a/pkg/util/xorm/engine.go b/pkg/util/xorm/engine.go index 47413fa8c0d..64c4172939d 100644 --- a/pkg/util/xorm/engine.go +++ b/pkg/util/xorm/engine.go @@ -15,8 +15,8 @@ import ( "sync" "time" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) // Engine is the major struct of xorm, it means a database manager. diff --git a/pkg/util/xorm/engine_cond.go b/pkg/util/xorm/engine_cond.go index 2f04b637c74..425f6903d31 100644 --- a/pkg/util/xorm/engine_cond.go +++ b/pkg/util/xorm/engine_cond.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) func (engine *Engine) buildConds(table *core.Table, bean any, diff --git a/pkg/util/xorm/session_exist.go b/pkg/util/xorm/session_exist.go index 4a713197dff..fa7fa6131cc 100644 --- a/pkg/util/xorm/session_exist.go +++ b/pkg/util/xorm/session_exist.go @@ -9,8 +9,8 @@ import ( "fmt" "reflect" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) // Exist returns true if the record exist otherwise return false diff --git a/pkg/util/xorm/session_find.go b/pkg/util/xorm/session_find.go index 2f94848d594..e10e743eb2d 100644 --- a/pkg/util/xorm/session_find.go +++ b/pkg/util/xorm/session_find.go @@ -9,8 +9,8 @@ import ( "reflect" "strings" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) const ( diff --git a/pkg/util/xorm/session_insert.go b/pkg/util/xorm/session_insert.go index 3bad1b0a60f..f640e25bd03 100644 --- a/pkg/util/xorm/session_insert.go +++ b/pkg/util/xorm/session_insert.go @@ -12,8 +12,8 @@ import ( "strconv" "strings" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) // ErrNoElementsOnSlice represents an error there is no element when insert diff --git a/pkg/util/xorm/session_query.go b/pkg/util/xorm/session_query.go index ef03f2c14b8..9f15837dcea 100644 --- a/pkg/util/xorm/session_query.go +++ b/pkg/util/xorm/session_query.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) func (session *Session) genQuerySQL(sqlOrArgs ...interface{}) (string, []interface{}, error) { diff --git a/pkg/util/xorm/session_raw.go b/pkg/util/xorm/session_raw.go index 29b432172c0..9b87ce31886 100644 --- a/pkg/util/xorm/session_raw.go +++ b/pkg/util/xorm/session_raw.go @@ -9,8 +9,8 @@ import ( "reflect" "time" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) func (session *Session) queryPreprocess(sqlStr *string, paramStr ...any) { diff --git a/pkg/util/xorm/session_update.go b/pkg/util/xorm/session_update.go index 1f5e6d6c158..439a76fc45e 100644 --- a/pkg/util/xorm/session_update.go +++ b/pkg/util/xorm/session_update.go @@ -10,8 +10,8 @@ import ( "reflect" "strings" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) // Update records, bean's non-empty fields are updated contents, diff --git a/pkg/util/xorm/statement.go b/pkg/util/xorm/statement.go index af865b32b3d..5924352fd78 100644 --- a/pkg/util/xorm/statement.go +++ b/pkg/util/xorm/statement.go @@ -11,8 +11,8 @@ import ( "strings" "time" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) // Statement save all the sql info for executing SQL diff --git a/pkg/util/xorm/statement_args.go b/pkg/util/xorm/statement_args.go index b27437c2a58..0ff285c0328 100644 --- a/pkg/util/xorm/statement_args.go +++ b/pkg/util/xorm/statement_args.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "xorm.io/builder" "github.com/grafana/grafana/pkg/util/xorm/core" + "xorm.io/builder" ) func quoteNeeded(a any) bool { diff --git a/public/app/features/alerting/unified/featureToggles.test.ts b/public/app/features/alerting/unified/featureToggles.test.ts new file mode 100644 index 00000000000..0665b102226 --- /dev/null +++ b/public/app/features/alerting/unified/featureToggles.test.ts @@ -0,0 +1,90 @@ +import { setLocalStorageFeatureToggle } from './featureToggles'; + +const featureTogglesKey = 'grafana.featureToggles'; +const storage = new Map(); + +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'); + }); +}); diff --git a/public/app/features/alerting/unified/featureToggles.ts b/public/app/features/alerting/unified/featureToggles.ts index 5cd667c6638..1d4ab503836 100644 --- a/public/app/features/alerting/unified/featureToggles.ts +++ b/public/app/features/alerting/unified/featureToggles.ts @@ -1,3 +1,4 @@ +import { FeatureToggles } from '@grafana/data'; import { config } from '@grafana/runtime'; import { isAdmin } from './utils/misc'; @@ -14,3 +15,42 @@ export const shouldAllowRecoveringDeletedRules = () => export const shouldAllowPermanentlyDeletingRules = () => (shouldAllowRecoveringDeletedRules() && config.featureToggles.alertingRulePermanentlyDelete) ?? false; + +export function setLocalStorageFeatureToggle(featureName: keyof FeatureToggles, value: boolean | undefined) { + const featureToggles = localStorage.getItem('grafana.featureToggles') ?? ''; + + const newToggles = updateFeatureToggle(featureToggles, featureName, value); + localStorage.setItem('grafana.featureToggles', newToggles); +} + +function updateFeatureToggle( + featureToggles: string | undefined, + featureName: string, + value: boolean | undefined +): string { + if (!featureToggles) { + if (value !== undefined) { + return `${featureName}=${value}`; + } + return ''; + } + + const parts = featureToggles.split(','); + const featurePrefix = `${featureName}=`; + const featureIndex = parts.findIndex((part) => part.startsWith(featurePrefix)); + + if (featureIndex !== -1) { + if (value === undefined) { + // Remove the feature + parts.splice(featureIndex, 1); + } else { + // Update the feature value + parts[featureIndex] = `${featureName}=${value}`; + } + } else if (value !== undefined) { + // Add new feature + parts.push(`${featureName}=${value}`); + } + + return parts.join(','); +} diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx index d9fd63482ea..cfab28a41bc 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v1.tsx @@ -29,6 +29,8 @@ import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants'; import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames } from '../utils/datasource'; import { createRelativeUrl } from '../utils/url'; +import { RuleListPageTitle } from './RuleListPageTitle'; + const VIEWS = { groups: RuleListGroupView, state: RuleListStateView, @@ -123,6 +125,7 @@ const RuleListV1 = () => { } actions={ hasAlertRulesCreated && ( diff --git a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx index 3d79cb1e783..921cce7fa75 100644 --- a/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx +++ b/public/app/features/alerting/unified/rule-list/RuleList.v2.tsx @@ -12,6 +12,7 @@ import { useURLSearchParams } from '../hooks/useURLSearchParams'; import { FilterView } from './FilterView'; import { GroupedView } from './GroupedView'; +import { RuleListPageTitle } from './RuleListPageTitle'; function RuleList() { const [queryParams] = useURLSearchParams(); @@ -86,7 +87,12 @@ export function RuleListActions() { export default function RuleListPage() { return ( - }> + } + isLoading={false} + actions={} + > ); diff --git a/public/app/features/alerting/unified/rule-list/RuleListPageTitle.test.tsx b/public/app/features/alerting/unified/rule-list/RuleListPageTitle.test.tsx new file mode 100644 index 00000000000..c4fe4479579 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/RuleListPageTitle.test.tsx @@ -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(); + 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(); + + 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'); + }); + }); +}); diff --git a/public/app/features/alerting/unified/rule-list/RuleListPageTitle.tsx b/public/app/features/alerting/unified/rule-list/RuleListPageTitle.tsx new file mode 100644 index 00000000000..fb7c349671c --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/RuleListPageTitle.tsx @@ -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 ( + +

{title}

+ {shouldShowV2Toggle && ( +
+ +
+ )} +
+ ); +} + +function useV2AlertListViewToggle() { + const listViewV2Enabled = shouldUseAlertingListViewV2(); + + const enableListViewV2 = useCallback(() => { + setLocalStorageFeatureToggle('alertingListViewV2', true); + }, []); + + const disableListViewV2 = useCallback(() => { + setLocalStorageFeatureToggle('alertingListViewV2', undefined); + }, []); + + return { + listViewV2Enabled, + enableListViewV2, + disableListViewV2, + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 5edbf69c780..be1570c399e 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2004,6 +2004,10 @@ "title": "Alert rules" }, "rulerrule-loading-error": "Failed to load the rule", + "toggle": { + "go-back-to-old-look": "Go back to the old look", + "try-out-the-new-look": "Try out the new look!" + }, "unknown-rule-type": "Unknown rule type" }, "rule-list-errors": {