From e2724f39d9dbcec34e524c68f2b0aefebe00eb0e Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Mon, 4 Sep 2023 10:38:35 +0100 Subject: [PATCH] Time Range: Using relative time takes timezone into account (#74013) * account for timezone when using relative times Co-authored-by: eledobleefe Co-authored-by: Joao Silva Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Co-authored-by: joshhunt 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 Co-authored-by: Joao Silva Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Co-authored-by: joshhunt 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 bab9c77c4d6a776e77ccc692110112ebb1f2d12c. * check timezone second * refactor to avoid detached elements --------- Co-authored-by: eledobleefe Co-authored-by: Joao Silva Co-authored-by: L-M-K-B <48948963+L-M-K-B@users.noreply.github.com> Co-authored-by: joshhunt Co-authored-by: tskarhed <1438972+tskarhed@users.noreply.github.com> --- .../scenarios/relative_time_zone_support.json | 179 ++++++++++++++++++ devenv/jsonnet/dev-dashboards.libsonnet | 7 + .../dashboard-time-zone.spec.ts | 121 ++++++++++++ .../src/datetime/moment_wrapper.ts | 14 ++ .../src/selectors/components.ts | 4 + packages/grafana-e2e/src/flows/index.ts | 1 + .../grafana-ui/src/components/Table/Table.tsx | 3 +- .../src/components/Tooltip/Tooltip.tsx | 7 +- .../TimePicker/TimePickerWithHistory.test.tsx | 2 +- public/app/features/dashboard/utils/panel.ts | 8 +- 10 files changed, 340 insertions(+), 6 deletions(-) create mode 100644 devenv/dev-dashboards/scenarios/relative_time_zone_support.json diff --git a/devenv/dev-dashboards/scenarios/relative_time_zone_support.json b/devenv/dev-dashboards/scenarios/relative_time_zone_support.json new file mode 100644 index 00000000000..5fcf0e10597 --- /dev/null +++ b/devenv/dev-dashboards/scenarios/relative_time_zone_support.json @@ -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": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index bd946d2f062..56259f979bf 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -548,6 +548,13 @@ local dashboard = grafana.dashboard; 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') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/e2e/dashboards-suite/dashboard-time-zone.spec.ts b/e2e/dashboards-suite/dashboard-time-zone.spec.ts index 08686be2cbc..accdeb5991d 100644 --- a/e2e/dashboards-suite/dashboard-time-zone.spec.ts +++ b/e2e/dashboards-suite/dashboard-time-zone.spec.ts @@ -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 => { if (inUtc === inTz) { // we need to catch issues when timezone isn't changed for some reason like https://github.com/grafana/grafana/issues/35504 diff --git a/packages/grafana-data/src/datetime/moment_wrapper.ts b/packages/grafana-data/src/datetime/moment_wrapper.ts index 73f5850dde5..33f53fb0a17 100644 --- a/packages/grafana-data/src/datetime/moment_wrapper.ts +++ b/packages/grafana-data/src/datetime/moment_wrapper.ts @@ -130,6 +130,20 @@ export const dateTimeForTimeZone = ( 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); }; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index adef35cd7c4..318446188a9 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -112,6 +112,7 @@ export const Components = { Table: { header: 'table header', footer: 'table-footer', + body: 'data-testid table body', }, }, }, @@ -425,4 +426,7 @@ export const Components = { annotationsTypeInput: 'annotations-type-input', annotationsChoosePanelInput: 'choose-panels-input', }, + Tooltip: { + container: 'data-testid tooltip', + }, }; diff --git a/packages/grafana-e2e/src/flows/index.ts b/packages/grafana-e2e/src/flows/index.ts index e699542788f..e7fbfb3ce82 100644 --- a/packages/grafana-e2e/src/flows/index.ts +++ b/packages/grafana-e2e/src/flows/index.ts @@ -11,6 +11,7 @@ export * from './openPanelMenuItem'; export * from './revertAllChanges'; export * from './saveDashboard'; export * from './selectOption'; +export * from './setTimeRange'; export * from './importDashboard'; export * from './importDashboards'; export * from './userPreferences'; diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 7c7bef084a1..1f1ddf6f160 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -13,6 +13,7 @@ import { import { VariableSizeList } from 'react-window'; import { Field, FieldType, ReducerID } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { TableCellHeight } from '@grafana/schema'; import { useTheme2 } from '../../themes'; @@ -330,7 +331,7 @@ export const Table = memo((props: Props) => { )} {itemCount > 0 ? ( -
+
( })} {visible && ( -
+
{typeof content === 'string' && content} {React.isValidElement(content) && React.cloneElement(content)} diff --git a/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx b/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx index c946efe0547..87ab11d13c8 100644 --- a/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx +++ b/public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx @@ -129,7 +129,7 @@ describe('TimePickerWithHistory', () => { it('Should display handle timezones correctly', async () => { const timeRange = getDefaultTimeRange(); - render(); + render(); await userEvent.click(screen.getByLabelText(/Time range selected/)); await clearAndType(getFromField(), '2022-12-10 00:00:00'); diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index 38156c1c115..cd178de9a4d 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -1,6 +1,6 @@ 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 appEvents from 'app/core/app_events'; import config from 'app/core/config'; @@ -126,11 +126,13 @@ export function applyPanelTimeOverrides(panel: PanelModel, timeRange: TimeRange) } 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.timeRange = { from: timeFromDate, - to: dateMath.parse(timeFromInfo.to)!, + to: dateMath.parse(timeFromInfo.to, undefined, toTimezone)!, raw: { from: timeFromInfo.from, to: timeFromInfo.to,