Time Range: Using relative time takes timezone into account (#74013)

* account for timezone when using relative times
Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com>
Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: tskarhed <1438972+tskarhed@users.noreply.github.com>

* keep as it was originally for now

* add e2e test for relative time zone overrides

* empty commit to add coauthors

Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com>
Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: tskarhed <1438972+tskarhed@users.noreply.github.com>

* fix types

* switch to using table panel in e2e test

* use regex for partial text match

* actually go to the dashboard...

* use include.text

* check for visibility first

* try waiting on backend request to complete

* CI driven development is fun

* make sure we're waiting for both data query calls

* open dashboard instead

* kick drone

* Revert "open dashboard instead"

This reverts commit bab9c77c4d.

* check timezone second

* refactor to avoid detached elements

---------

Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com>
Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: tskarhed <1438972+tskarhed@users.noreply.github.com>
pull/74321/head
Ashley Harrison 2 years ago committed by GitHub
parent 1922f4c9a1
commit e2724f39d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 179
      devenv/dev-dashboards/scenarios/relative_time_zone_support.json
  2. 7
      devenv/jsonnet/dev-dashboards.libsonnet
  3. 121
      e2e/dashboards-suite/dashboard-time-zone.spec.ts
  4. 14
      packages/grafana-data/src/datetime/moment_wrapper.ts
  5. 4
      packages/grafana-e2e-selectors/src/selectors/components.ts
  6. 1
      packages/grafana-e2e/src/flows/index.ts
  7. 3
      packages/grafana-ui/src/components/Table/Table.tsx
  8. 7
      packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
  9. 2
      public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx
  10. 8
      public/app/features/dashboard/utils/panel.ts

@ -0,0 +1,179 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 7271,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "10.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1
}
],
"title": "Panel in timezone",
"type": "table"
},
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true
},
"pluginVersion": "10.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "gdev-testdata"
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 1
}
],
"timeFrom": "now/d",
"title": "Panel with relative time override",
"type": "table"
}
],
"refresh": "",
"schemaVersion": 38,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-3h",
"to": "now"
},
"timepicker": {},
"timezone": "Asia/Tokyo",
"title": "Panel Tests - Relative time zone support",
"uid": "d41dbaa2-a39e-4536-ab2b-caca52f1a9c8",
"version": 7,
"weekStart": ""
}

@ -548,6 +548,13 @@ local dashboard = grafana.dashboard;
id: 0, id: 0,
} }
}, },
dashboard.new('relative_time_zone_support', import '../dev-dashboards/scenarios/relative_time_zone_support.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('reuse', import '../dev-dashboards/transforms/reuse.json') + dashboard.new('reuse', import '../dev-dashboards/transforms/reuse.json') +
resource.addMetadata('folder', 'dev-dashboards') + resource.addMetadata('folder', 'dev-dashboards') +
{ {

@ -85,6 +85,127 @@ e2e.scenario({
}, },
}); });
e2e.scenario({
describeName: 'Dashboard time zone support',
itName: 'Tests relative timezone support and overrides',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
// Open dashboard
e2e.flows.openDashboard({
uid: 'd41dbaa2-a39e-4536-ab2b-caca52f1a9c8',
});
e2e().intercept('/api/ds/query*').as('dataQuery');
// Switch to Browser timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'Browser',
});
// Need to wait for 2 calls as there's 2 panels
e2e().wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in Browser timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
e2e().wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
// Test Tokyo timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'Asia/Tokyo',
});
// Need to wait for 2 calls as there's 2 panels
e2e().wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in Tokyo timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
e2e().wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
// Test LA timezone
e2e.flows.setTimeRange({
from: 'now-6h',
to: 'now',
zone: 'America/Los_Angeles',
});
// Need to wait for 2 calls as there's 2 panels
e2e().wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
// Today so far, still in LA timezone
e2e.flows.setTimeRange({
from: 'now/d',
to: 'now',
});
// Need to wait for 2 calls as there's 2 panels
e2e().wait(['@dataQuery', '@dataQuery']);
e2e.components.Panels.Panel.title('Panel with relative time override')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
e2e.components.Panels.Panel.title('Panel in timezone')
.should('be.visible')
.within(() => {
e2e().contains('[role="row"]', '00:00:00').should('be.visible');
});
},
});
const isTimeCorrect = (inUtc: string, inTz: string, offset: number): boolean => { const isTimeCorrect = (inUtc: string, inTz: string, offset: number): boolean => {
if (inUtc === inTz) { if (inUtc === inTz) {
// we need to catch issues when timezone isn't changed for some reason like https://github.com/grafana/grafana/issues/35504 // we need to catch issues when timezone isn't changed for some reason like https://github.com/grafana/grafana/issues/35504

@ -130,6 +130,20 @@ export const dateTimeForTimeZone = (
return toUtc(input, formatInput); return toUtc(input, formatInput);
} }
if (timezone && timezone !== 'browser') {
let result: moment.Moment;
if (typeof input === 'string' && formatInput) {
result = moment.tz(input, formatInput, timezone);
} else {
result = moment.tz(input, timezone);
}
if (isDateTime(result)) {
return result;
}
}
return dateTime(input, formatInput); return dateTime(input, formatInput);
}; };

@ -112,6 +112,7 @@ export const Components = {
Table: { Table: {
header: 'table header', header: 'table header',
footer: 'table-footer', footer: 'table-footer',
body: 'data-testid table body',
}, },
}, },
}, },
@ -425,4 +426,7 @@ export const Components = {
annotationsTypeInput: 'annotations-type-input', annotationsTypeInput: 'annotations-type-input',
annotationsChoosePanelInput: 'choose-panels-input', annotationsChoosePanelInput: 'choose-panels-input',
}, },
Tooltip: {
container: 'data-testid tooltip',
},
}; };

@ -11,6 +11,7 @@ export * from './openPanelMenuItem';
export * from './revertAllChanges'; export * from './revertAllChanges';
export * from './saveDashboard'; export * from './saveDashboard';
export * from './selectOption'; export * from './selectOption';
export * from './setTimeRange';
export * from './importDashboard'; export * from './importDashboard';
export * from './importDashboards'; export * from './importDashboards';
export * from './userPreferences'; export * from './userPreferences';

@ -13,6 +13,7 @@ import {
import { VariableSizeList } from 'react-window'; import { VariableSizeList } from 'react-window';
import { Field, FieldType, ReducerID } from '@grafana/data'; import { Field, FieldType, ReducerID } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { TableCellHeight } from '@grafana/schema'; import { TableCellHeight } from '@grafana/schema';
import { useTheme2 } from '../../themes'; import { useTheme2 } from '../../themes';
@ -330,7 +331,7 @@ export const Table = memo((props: Props) => {
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} /> <HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
)} )}
{itemCount > 0 ? ( {itemCount > 0 ? (
<div ref={variableSizeListScrollbarRef}> <div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}> <CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}>
<VariableSizeList <VariableSizeList
// This component needs an unmount/remount when row height or page changes // This component needs an unmount/remount when row height or page changes

@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from 'react';
import { usePopperTooltip } from 'react-popper-tooltip'; import { usePopperTooltip } from 'react-popper-tooltip';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes/ThemeContext'; import { useStyles2 } from '../../themes/ThemeContext';
import { buildTooltipTheme } from '../../utils/tooltipUtils'; import { buildTooltipTheme } from '../../utils/tooltipUtils';
@ -76,7 +77,11 @@ export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>(
})} })}
{visible && ( {visible && (
<Portal> <Portal>
<div ref={setTooltipRef} {...getTooltipProps({ className: style.container })}> <div
data-testid={selectors.components.Tooltip.container}
ref={setTooltipRef}
{...getTooltipProps({ className: style.container })}
>
<div {...getArrowProps({ className: style.arrow })} /> <div {...getArrowProps({ className: style.arrow })} />
{typeof content === 'string' && content} {typeof content === 'string' && content}
{React.isValidElement(content) && React.cloneElement(content)} {React.isValidElement(content) && React.cloneElement(content)}

@ -129,7 +129,7 @@ describe('TimePickerWithHistory', () => {
it('Should display handle timezones correctly', async () => { it('Should display handle timezones correctly', async () => {
const timeRange = getDefaultTimeRange(); const timeRange = getDefaultTimeRange();
render(<TimePickerWithHistory value={timeRange} {...props} {...{ timeZone: 'Eastern/Pacific' }} />); render(<TimePickerWithHistory value={timeRange} {...props} {...{ timeZone: 'Asia/Tokyo' }} />);
await userEvent.click(screen.getByLabelText(/Time range selected/)); await userEvent.click(screen.getByLabelText(/Time range selected/));
await clearAndType(getFromField(), '2022-12-10 00:00:00'); await clearAndType(getFromField(), '2022-12-10 00:00:00');

@ -1,6 +1,6 @@
import { isString as _isString } from 'lodash'; import { isString as _isString } from 'lodash';
import { TimeRange, AppEvents, rangeUtil, dateMath, PanelModel as IPanelModel } from '@grafana/data'; import { TimeRange, AppEvents, rangeUtil, dateMath, PanelModel as IPanelModel, dateTimeAsMoment } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import config from 'app/core/config'; import config from 'app/core/config';
@ -126,11 +126,13 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange)
} }
if (_isString(timeRange.raw.from)) { if (_isString(timeRange.raw.from)) {
const timeFromDate = dateMath.parse(timeFromInfo.from)!; const fromTimezone = dateTimeAsMoment(timeRange.from).tz();
const toTimezone = dateTimeAsMoment(timeRange.to).tz();
const timeFromDate = dateMath.parse(timeFromInfo.from, undefined, fromTimezone)!;
newTimeData.timeInfo = timeFromInfo.display; newTimeData.timeInfo = timeFromInfo.display;
newTimeData.timeRange = { newTimeData.timeRange = {
from: timeFromDate, from: timeFromDate,
to: dateMath.parse(timeFromInfo.to)!, to: dateMath.parse(timeFromInfo.to, undefined, toTimezone)!,
raw: { raw: {
from: timeFromInfo.from, from: timeFromInfo.from,
to: timeFromInfo.to, to: timeFromInfo.to,

Loading…
Cancel
Save