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>
This commit is contained in:
co-authored by
Tom Ratcliffe
parent
17e4a3b386
commit
512df0091a
@@ -11,8 +11,6 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
AppName: "alerting",
|
||||
Group: "notifications.alerting.grafana.app",
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"xorm.io/builder"
|
||||
"github.com/grafana/grafana/pkg/util/xorm/core"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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(',');
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
isLoading={false}
|
||||
renderTitle={(title) => <RuleListPageTitle title={title} />}
|
||||
actions={
|
||||
hasAlertRulesCreated && (
|
||||
<Stack gap={1}>
|
||||
|
||||
@@ -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 (
|
||||
<AlertingPageWrapper navId="alert-list" isLoading={false} actions={<RuleListActions />}>
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
renderTitle={(title) => <RuleListPageTitle title={title} />}
|
||||
isLoading={false}
|
||||
actions={<RuleListActions />}
|
||||
>
|
||||
<RuleList />
|
||||
</AlertingPageWrapper>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user