Timeseries: Time regions migration (#66998)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Adela Almasan 2023-04-26 21:28:58 -05:00 committed by GitHub
parent 5c4ecf7a86
commit 2beee35567
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 234 additions and 15 deletions

View File

@ -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"]

View File

@ -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');
});
},
});

View File

@ -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

View File

@ -237,7 +237,7 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) {
if (key.startsWith('__feature.')) {
const featureToggles = config.featureToggles as Record<string, boolean>;
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}`);

View File

@ -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 (
<div className={cx(styles.wrapper, className)} {...otherProps}>
<div className={cx(styles.wrapper, className)} {...otherProps} data-testid={selectors.pages.Dashboard.wrapper}>
{layout === PageLayoutType.Standard && (
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className={styles.pageInner}>

View File

@ -9,7 +9,7 @@
Migrate
</button>
</p>
<p>Some features like colored time regions and negative transforms are not supported in the new panel yet.</p>
<p>Some features are not supported in the new panel yet.</p>
</div>
<gf-form-switch

View File

@ -521,6 +521,37 @@ exports[`Graph Migrations stepped line 1`] = `
}
`;
exports[`Graph Migrations time regions should migrate 1`] = `
{
"alert": undefined,
"datasource": {
"type": "datasource",
"uid": "gdev-testdata",
},
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "points",
"spanNulls": false,
},
},
"overrides": [],
},
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true,
},
"tooltip": {
"mode": "single",
"sort": "none",
},
},
}
`;
exports[`Graph Migrations transforms should preserve "constant" transform 1`] = `
{
"defaults": {

View File

@ -2,17 +2,32 @@ import { cloneDeep } from 'lodash';
import { PanelModel, FieldConfigSource, FieldMatcherID, ReducerID } from '@grafana/data';
import { TooltipDisplayMode, SortOrder } from '@grafana/schema';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel, PanelModel as PanelModelState } from 'app/features/dashboard/state';
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { graphPanelChangedHandler } from './migrations';
describe('Graph Migrations', () => {
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 = {

View File

@ -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<typeof setTimeout> | 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<GrafanaQuery> = {
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';