Dashboards: Conserve timestamp on time range copy-paste across timezones (#109769)

* fix(timepicker): preserve UTC timestamp on copy-paste across timezones

* test: add UTC copy/paste timezone conversion test

* Update packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Remove duplicate mockClipboard clear

* Extract utility functions for formatting and converting time ranges

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Alikamran Rzayev
2025-08-29 16:03:34 +02:00
committed by GitHub
co-authored by Copilot
parent a746f6e121
commit be3fa041a5
6 changed files with 91 additions and 20 deletions
@@ -505,3 +505,14 @@ export function relativeToTimeRange(relativeTimeRange: RelativeTimeRange, now: D
raw: { from, to },
};
}
/**
* @internal
* Returns a RawTimeRange that has been converted so that from and to are strings
*/
export function formatRawTimeRange(range: RawTimeRange): RawTimeRange {
return {
from: isDateTime(range.from) ? range.from.toISOString() : range.from,
to: isDateTime(range.to) ? range.to.toISOString() : range.to,
};
}
@@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { dateTimeParse, FeatureToggles, systemDateFormats, TimeRange } from '@grafana/data';
@@ -66,6 +66,8 @@ function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc') {
describe('TimeRangeForm', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
mockClipboard.writeText.mockClear();
mockClipboard.readText.mockClear();
user = userEvent.setup();
Object.defineProperty(global.navigator, 'clipboard', {
value: mockClipboard,
@@ -117,6 +119,37 @@ describe('TimeRangeForm', () => {
expect(getByLabelText('To')).toHaveValue('2021-06-19 19:59:00');
});
it('copy in UTC then paste into different timezone should convert times', async () => {
const sourceRange: TimeRange = {
from: defaultTimeRange.from,
to: defaultTimeRange.to,
raw: {
from: defaultTimeRange.from,
to: defaultTimeRange.to,
},
};
const source = setup(sourceRange);
let written = '';
mockClipboard.writeText.mockImplementation((text: string) => {
written = text;
return Promise.resolve();
});
await user.click(within(source.container).getByTestId('data-testid TimePicker copy button'));
const target = setup(undefined, 'America/New_York');
mockClipboard.readText.mockResolvedValue(written);
const targetPasteButton = within(target.container).getByTestId('data-testid TimePicker paste button');
await user.click(targetPasteButton);
expect(within(target.container).getByLabelText('From')).toHaveValue('2021-06-16 20:00:00');
expect(within(target.container).getByLabelText('To')).toHaveValue('2021-06-19 19:59:00');
});
describe('when common format are entered', () => {
it('parses those dates in the current timezone', async () => {
setup();
@@ -114,8 +114,9 @@ export const TimeRangeContent = (props: Props) => {
};
const onCopy = () => {
const raw: RawTimeRange = { from: from.value, to: to.value };
navigator.clipboard.writeText(JSON.stringify(raw));
const rawSource: RawTimeRange = value.raw;
const clipboardPayload = rangeUtil.formatRawTimeRange(rawSource);
navigator.clipboard.writeText(JSON.stringify(clipboardPayload));
};
const onPaste = async () => {