mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Timeseries: Time regions migration (#66998)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
5c4ecf7a86
commit
2beee35567
@ -5922,20 +5922,23 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[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.", "1"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
[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.", "3"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
[0, 0, 0, "Do not use any type assertions.", "6"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "8"],
|
[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.", "9"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "12"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
[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.", "14"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
|
[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.", "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": [
|
"public/app/plugins/panel/timeseries/plugins/ExemplarMarker.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
|
52
e2e/various-suite/graph-auto-migrate.spec.ts
Normal file
52
e2e/various-suite/graph-auto-migrate.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
@ -49,6 +49,7 @@ export const Pages = {
|
|||||||
},
|
},
|
||||||
Dashboard: {
|
Dashboard: {
|
||||||
url: (uid: string) => `/d/${uid}`,
|
url: (uid: string) => `/d/${uid}`,
|
||||||
|
wrapper: 'data-testid dashboard-page-wrapper',
|
||||||
DashNav: {
|
DashNav: {
|
||||||
/**
|
/**
|
||||||
* @deprecated use navV2 from Grafana 8.3 instead
|
* @deprecated use navV2 from Grafana 8.3 instead
|
||||||
|
@ -237,7 +237,7 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) {
|
|||||||
if (key.startsWith('__feature.')) {
|
if (key.startsWith('__feature.')) {
|
||||||
const featureToggles = config.featureToggles as Record<string, boolean>;
|
const featureToggles = config.featureToggles as Record<string, boolean>;
|
||||||
const featureName = key.substring(10);
|
const featureName = key.substring(10);
|
||||||
const toggleState = value === 'true';
|
const toggleState = value === 'true' || value === ''; // browser rewrites true as ''
|
||||||
if (toggleState !== featureToggles[key]) {
|
if (toggleState !== featureToggles[key]) {
|
||||||
featureToggles[featureName] = toggleState;
|
featureToggles[featureName] = toggleState;
|
||||||
console.log(`Setting feature toggle ${featureName} = ${toggleState}`);
|
console.log(`Setting feature toggle ${featureName} = ${toggleState}`);
|
||||||
|
@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css';
|
|||||||
import React, { useLayoutEffect } from 'react';
|
import React, { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ export const Page: PageType = ({
|
|||||||
}, [navModel, pageNav, chrome, layout]);
|
}, [navModel, pageNav, chrome, layout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
<div className={cx(styles.wrapper, className)} {...otherProps} data-testid={selectors.pages.Dashboard.wrapper}>
|
||||||
{layout === PageLayoutType.Standard && (
|
{layout === PageLayoutType.Standard && (
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||||
<div className={styles.pageInner}>
|
<div className={styles.pageInner}>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
Migrate
|
Migrate
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
|
||||||
<gf-form-switch
|
<gf-form-switch
|
||||||
|
@ -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`] = `
|
exports[`Graph Migrations transforms should preserve "constant" transform 1`] = `
|
||||||
{
|
{
|
||||||
"defaults": {
|
"defaults": {
|
||||||
|
@ -2,17 +2,32 @@ import { cloneDeep } from 'lodash';
|
|||||||
|
|
||||||
import { PanelModel, FieldConfigSource, FieldMatcherID, ReducerID } from '@grafana/data';
|
import { PanelModel, FieldConfigSource, FieldMatcherID, ReducerID } from '@grafana/data';
|
||||||
import { TooltipDisplayMode, SortOrder } from '@grafana/schema';
|
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';
|
import { graphPanelChangedHandler } from './migrations';
|
||||||
|
|
||||||
describe('Graph Migrations', () => {
|
describe('Graph Migrations', () => {
|
||||||
let prevFieldConfig: FieldConfigSource;
|
let prevFieldConfig: FieldConfigSource;
|
||||||
|
let dashboard: DashboardModel;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
prevFieldConfig = {
|
prevFieldConfig = {
|
||||||
defaults: {},
|
defaults: {},
|
||||||
overrides: [],
|
overrides: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dashboard = createDashboardModelFixture({
|
||||||
|
id: 74,
|
||||||
|
version: 7,
|
||||||
|
annotations: {},
|
||||||
|
links: [],
|
||||||
|
panels: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
getDashboardSrv().setCurrent(dashboard);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('simple bars', () => {
|
it('simple bars', () => {
|
||||||
@ -82,6 +97,36 @@ describe('Graph Migrations', () => {
|
|||||||
expect(panel.fieldConfig.overrides[1].matcher.id).toBe(FieldMatcherID.byRegexp);
|
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', () => {
|
describe('legend', () => {
|
||||||
test('without values', () => {
|
test('without values', () => {
|
||||||
const old = {
|
const old = {
|
||||||
|
@ -31,12 +31,19 @@ import {
|
|||||||
StackingMode,
|
StackingMode,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
GraphTransform,
|
GraphTransform,
|
||||||
|
AnnotationQuery,
|
||||||
ComparisonOperation,
|
ComparisonOperation,
|
||||||
} from '@grafana/schema';
|
} 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 { defaultGraphConfig } from './config';
|
||||||
import { PanelOptions } from './panelcfg.gen';
|
import { PanelOptions } from './panelcfg.gen';
|
||||||
|
|
||||||
|
let dashboardRefreshDebouncer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is called when the panel changes from another panel
|
* 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
|
// Changing from angular/flot panel to react/uPlot
|
||||||
if (prevPluginId === 'graph' && prevOptions.angular) {
|
if (prevPluginId === 'graph' && prevOptions.angular) {
|
||||||
const { fieldConfig, options } = graphToTimeseriesOptions({
|
const { fieldConfig, options, annotations } = graphToTimeseriesOptions({
|
||||||
...prevOptions.angular,
|
...prevOptions.angular,
|
||||||
fieldConfig: prevFieldConfig,
|
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.fieldConfig = fieldConfig; // Mutates the incoming panel
|
||||||
panel.alert = prevOptions.angular.alert;
|
panel.alert = prevOptions.angular.alert;
|
||||||
return options;
|
return options;
|
||||||
@ -63,7 +85,13 @@ export const graphPanelChangedHandler: PanelTypeChangedHandler = (
|
|||||||
return {};
|
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 overrides: ConfigOverrideRule[] = angular.fieldConfig?.overrides ?? [];
|
||||||
const yaxes = angular.yaxes ?? [];
|
const yaxes = angular.yaxes ?? [];
|
||||||
let y1 = getFieldConfigFromOldAxis(yaxes[0]);
|
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;
|
const tooltipConfig = angular.tooltip;
|
||||||
if (tooltipConfig) {
|
if (tooltipConfig) {
|
||||||
if (tooltipConfig.shared !== undefined) {
|
if (tooltipConfig.shared !== undefined) {
|
||||||
@ -479,9 +556,18 @@ export function graphToTimeseriesOptions(angular: any): { fieldConfig: FieldConf
|
|||||||
overrides,
|
overrides,
|
||||||
},
|
},
|
||||||
options,
|
options,
|
||||||
|
annotations,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GraphTimeRegionConfig extends TimeRegionConfig {
|
||||||
|
colorMode: string;
|
||||||
|
fill: boolean;
|
||||||
|
fillColor: string;
|
||||||
|
line: boolean;
|
||||||
|
lineColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
function getThresholdColor(threshold: AngularThreshold): string {
|
function getThresholdColor(threshold: AngularThreshold): string {
|
||||||
if (threshold.colorMode === 'critical') {
|
if (threshold.colorMode === 'critical') {
|
||||||
return 'red';
|
return 'red';
|
||||||
|
Loading…
Reference in New Issue
Block a user