diff --git a/.betterer.results b/.betterer.results index 449693d7e46..e7f154cafba 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5922,20 +5922,23 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Do not use any type assertions.", "4"], + [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Do not use any type assertions.", "6"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"], - [0, 0, 0, "Do not use any type assertions.", "12"], - [0, 0, 0, "Unexpected any. Specify a different type.", "13"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Unexpected any. Specify a different type.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"], + [0, 0, 0, "Unexpected any. Specify a different type.", "12"], + [0, 0, 0, "Do not use any type assertions.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"], - [0, 0, 0, "Unexpected any. Specify a different type.", "15"], - [0, 0, 0, "Unexpected any. Specify a different type.", "16"] + [0, 0, 0, "Do not use any type assertions.", "15"], + [0, 0, 0, "Unexpected any. Specify a different type.", "16"], + [0, 0, 0, "Unexpected any. Specify a different type.", "17"], + [0, 0, 0, "Unexpected any. Specify a different type.", "18"], + [0, 0, 0, "Unexpected any. Specify a different type.", "19"] ], "public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] diff --git a/e2e/various-suite/graph-auto-migrate.spec.ts b/e2e/various-suite/graph-auto-migrate.spec.ts new file mode 100644 index 00000000000..319ba5bec25 --- /dev/null +++ b/e2e/various-suite/graph-auto-migrate.spec.ts @@ -0,0 +1,52 @@ +import { e2e } from '@grafana/e2e'; +const DASHBOARD_ID = 'XMjIZPmik'; +const DASHBOARD_NAME = 'Panel Tests - Graph Time Regions'; + +e2e.scenario({ + describeName: 'Auto-migrate graph panel', + itName: 'Annotation markers exist for time regions', + addScenarioDataSource: false, + addScenarioDashBoard: false, + skipScenario: false, + scenario: () => { + e2e.flows.openDashboard({ uid: DASHBOARD_ID }); + e2e().contains(DASHBOARD_NAME).should('be.visible'); + cy.contains('uplot-main-div').should('not.exist'); + + e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { '__feature.autoMigrateOldPanels': true } }); + + e2e().wait(1000); + + e2e.components.Panels.Panel.title('Business Hours') + .should('exist') + .within(() => { + e2e.pages.Dashboard.Annotations.marker().should('exist'); + }); + + e2e.components.Panels.Panel.title("Sunday's 20-23") + .should('exist') + .within(() => { + e2e.pages.Dashboard.Annotations.marker().should('exist'); + }); + + e2e.components.Panels.Panel.title('Each day of week') + .should('exist') + .within(() => { + e2e.pages.Dashboard.Annotations.marker().should('exist'); + }); + + e2e.pages.Dashboard.wrapper().children().children('.scrollbar-view').scrollTo('bottom'); + + e2e.components.Panels.Panel.title('05:00') + .should('exist') + .within(() => { + e2e.pages.Dashboard.Annotations.marker().should('exist'); + }); + + e2e.components.Panels.Panel.title('From 22:00 to 00:30 (crossing midnight)') + .should('exist') + .within(() => { + e2e.pages.Dashboard.Annotations.marker().should('exist'); + }); + }, +}); diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 95cd03e5b25..d1c611cc547 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -49,6 +49,7 @@ export const Pages = { }, Dashboard: { url: (uid: string) => `/d/${uid}`, + wrapper: 'data-testid dashboard-page-wrapper', DashNav: { /** * @deprecated use navV2 from Grafana 8.3 instead diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 9bcaaddc3d0..1b89e4692f8 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -237,7 +237,7 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) { if (key.startsWith('__feature.')) { const featureToggles = config.featureToggles as Record; const featureName = key.substring(10); - const toggleState = value === 'true'; + const toggleState = value === 'true' || value === ''; // browser rewrites true as '' if (toggleState !== featureToggles[key]) { featureToggles[featureName] = toggleState; console.log(`Setting feature toggle ${featureName} = ${toggleState}`); diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index df666b44ed9..fd4c79749d8 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css'; import React, { useLayoutEffect } from 'react'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { CustomScrollbar, useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; @@ -50,7 +51,7 @@ export const Page: PageType = ({ }, [navModel, pageNav, chrome, layout]); return ( -
+
{layout === PageLayoutType.Standard && (
diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index c8d91f352f8..959dafeabaa 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -9,7 +9,7 @@ Migrate

-

Some features like colored time regions and negative transforms are not supported in the new panel yet.

+

Some features are not supported in the new panel yet.

{ let prevFieldConfig: FieldConfigSource; + let dashboard: DashboardModel; beforeEach(() => { prevFieldConfig = { defaults: {}, overrides: [], }; + + dashboard = createDashboardModelFixture({ + id: 74, + version: 7, + annotations: {}, + links: [], + panels: [], + }); + + getDashboardSrv().setCurrent(dashboard); }); it('simple bars', () => { @@ -82,6 +97,36 @@ describe('Graph Migrations', () => { expect(panel.fieldConfig.overrides[1].matcher.id).toBe(FieldMatcherID.byRegexp); }); + describe('time regions', () => { + test('should migrate', () => { + const old = { + angular: { + timeRegions: [ + { + colorMode: 'red', + fill: true, + fillColor: 'rgba(234, 112, 112, 0.12)', + fromDayOfWeek: 1, + line: true, + lineColor: 'rgba(237, 46, 24, 0.60)', + op: 'time', + }, + ], + }, + }; + + const panel = { datasource: { type: 'datasource', uid: 'gdev-testdata' } } as PanelModel; + dashboard.panels.push(new PanelModelState(panel)); + panel.options = graphPanelChangedHandler(panel, 'graph', old, prevFieldConfig); + expect(dashboard.panels).toHaveLength(1); + expect(dashboard.annotations.list).toHaveLength(2); // built-in + time region + expect( + dashboard.annotations.list.filter((annotation) => annotation.target?.queryType === GrafanaQueryType.TimeRegions) + ).toHaveLength(1); + expect(panel).toMatchSnapshot(); + }); + }); + describe('legend', () => { test('without values', () => { const old = { diff --git a/public/app/plugins/panel/timeseries/migrations.ts b/public/app/plugins/panel/timeseries/migrations.ts index 9f1c774a94c..c1465405a44 100644 --- a/public/app/plugins/panel/timeseries/migrations.ts +++ b/public/app/plugins/panel/timeseries/migrations.ts @@ -31,12 +31,19 @@ import { StackingMode, SortOrder, GraphTransform, + AnnotationQuery, ComparisonOperation, } from '@grafana/schema'; +import { TimeRegionConfig } from 'app/core/utils/timeRegions'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types'; import { defaultGraphConfig } from './config'; import { PanelOptions } from './panelcfg.gen'; +let dashboardRefreshDebouncer: ReturnType | null = null; + /** * This is called when the panel changes from another panel */ @@ -48,10 +55,25 @@ export const graphPanelChangedHandler: PanelTypeChangedHandler = ( ) => { // Changing from angular/flot panel to react/uPlot if (prevPluginId === 'graph' && prevOptions.angular) { - const { fieldConfig, options } = graphToTimeseriesOptions({ + const { fieldConfig, options, annotations } = graphToTimeseriesOptions({ ...prevOptions.angular, fieldConfig: prevFieldConfig, + panel: panel, }); + + const dashboard = getDashboardSrv().getCurrent(); + if (dashboard && annotations?.length > 0) { + dashboard.annotations.list = [...dashboard.annotations.list, ...annotations]; + + // Trigger a full dashboard refresh when annotations change + if (dashboardRefreshDebouncer == null) { + dashboardRefreshDebouncer = setTimeout(() => { + dashboardRefreshDebouncer = null; + getTimeSrv().refreshTimeModel(); + }); + } + } + panel.fieldConfig = fieldConfig; // Mutates the incoming panel panel.alert = prevOptions.angular.alert; return options; @@ -63,7 +85,13 @@ export const graphPanelChangedHandler: PanelTypeChangedHandler = ( return {}; }; -export function graphToTimeseriesOptions(angular: any): { fieldConfig: FieldConfigSource; options: PanelOptions } { +export function graphToTimeseriesOptions(angular: any): { + fieldConfig: FieldConfigSource; + options: PanelOptions; + annotations: AnnotationQuery[]; +} { + let annotations: AnnotationQuery[] = []; + const overrides: ConfigOverrideRule[] = angular.fieldConfig?.overrides ?? []; const yaxes = angular.yaxes ?? []; let y1 = getFieldConfigFromOldAxis(yaxes[0]); @@ -362,6 +390,55 @@ export function graphToTimeseriesOptions(angular: any): { fieldConfig: FieldConf } } + // timeRegions migration + if (angular.timeRegions?.length) { + let regions: any[] = angular.timeRegions.map((old: GraphTimeRegionConfig, idx: number) => ({ + name: `T${idx + 1}`, + color: old.colorMode !== 'custom' ? old.colorMode : old.fillColor, + line: old.line, + fill: old.fill, + fromDayOfWeek: old.fromDayOfWeek, + toDayOfWeek: old.toDayOfWeek, + from: old.from, + to: old.to, + })); + + regions.forEach((region: GraphTimeRegionConfig, idx: number) => { + const anno: AnnotationQuery = { + datasource: { + type: 'datasource', + uid: 'grafana', + }, + enable: true, + hide: true, // don't show the toggle at the top of the dashboard + filter: { + exclude: false, + ids: [angular.panel.id], + }, + iconColor: region.fillColor ?? (region as any).color, + name: `T${idx + 1}`, + target: { + queryType: GrafanaQueryType.TimeRegions, + refId: 'Anno', + timeRegion: { + fromDayOfWeek: region.fromDayOfWeek, + toDayOfWeek: region.toDayOfWeek, + from: region.from, + to: region.to, + timezone: 'utc', // graph panel was always UTC + }, + }, + }; + + if (region.fill) { + annotations.push(anno); + } else if (region.line) { + anno.iconColor = region.lineColor ?? 'white'; + annotations.push(anno); + } + }); + } + const tooltipConfig = angular.tooltip; if (tooltipConfig) { if (tooltipConfig.shared !== undefined) { @@ -479,9 +556,18 @@ export function graphToTimeseriesOptions(angular: any): { fieldConfig: FieldConf overrides, }, options, + annotations, }; } +interface GraphTimeRegionConfig extends TimeRegionConfig { + colorMode: string; + fill: boolean; + fillColor: string; + line: boolean; + lineColor: string; +} + function getThresholdColor(threshold: AngularThreshold): string { if (threshold.colorMode === 'critical') { return 'red';