Skip to content

Commit 64f678d

Browse files
acstllclaudeweronikaolejniczakelasticmachinekibanamachine
authored
[Unified Search] DateRangePicker integration (#260163)
## Summary Replaces [`EuiSuperDatePicker`](https://eui.elastic.co/docs/components/forms/date-and-time/super-date-picker/) with the new [`DateRangePicker`](https://ci-artifacts.kibana.dev/storybooks/main/shared_ux/index.html?path=/story/date-time-daterangepicker--playground) from `@kbn/date-range-picker` in `QueryBarTopRow`, only for Discover and Dashboards, behind a feature flag. ~~🔗 [Cloud deployment preview](https://kibana-pr-260163-485d2f.kb.us-west2.gcp.elastic-cloud.com/) (ping me for credentials)~~ Resolves elastic/eui-private#521 <details> <summary>Screenshots</summary> <img width="730" height="644" alt="Screenshot 2026-03-30 at 22 59 23" src="https://github.com/user-attachments/assets/97a11c72-ce6c-43ca-8f8b-f3d5d2c91cd2" /> <img width="730" height="644" alt="Screenshot 2026-03-30 at 22 59 55" src="https://github.com/user-attachments/assets/51ef9e33-4a68-427c-ba46-cfe828e610c1" /> </details> ### Changes >[!NOTE] > Initially this PR introduced a UI setting as a mean for users to roll back to the old picker, this was replaced with a feature flag (off by default) for a controlled/granular rollout 3e87774 - **Added `unifiedSearch.newDateRangePickerEnabled` feature flag**, to roll out the new time picker in steps (defaults to `false`) - Added new `enableDateRangePicker` prop to `SearchBar` and `QueryBarTopRow`, to **scope the new time picker to only Discover and Dashboard** - **Updated tests with a dual approach** in page objects (FTR and Scout) so testing both the old and new time pickers is supported - **Fixed syncing for auto-refresh** in `DateRangePicker` (fdbf42e) >[!TIP] > To test the new time picker locally, override the feature flag in `config/kibana.dev.yml` ```yaml feature_flags.overrides: unifiedSearch.newDateRangePickerEnabled: true ``` ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [x] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Identify risks Risk is low: - `EuiSuperDatePicker` is replaced by `DateRangePicker`, which exposes a very simliar API and the change is largely a swap of implementations behind the same interface - FTR and Scout page objects were updated to cover both the old and new picker paths; the flaky test runner showed no regressions across 5 FTR configs (125 runs) and 15 Scout runs - The new picker is only active when `unifiedSearch.newDateRangePickerEnabled: true` is explicitly set; all users get the existing `EuiSuperDatePicker` until the flag is enabled ## Release note Introduces a new date range picker in Discover and Dashboard, in technical preview behind a feature flag. The new picker lets you type time ranges directly into a text input using flexible formats — combine relative and absolute dates like `8 weeks ago to Apr 7, 2026`, use plain expressions like `last 20 minutes`, or use the new shorthand syntax like `-6mo` for the last 6 months. It supports the same quick presets, absolute dates, recently used ranges, and auto-refresh as before. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Weronika Olejniczak <weronika.olejniczak@elastic.co> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
1 parent 947199a commit 64f678d

27 files changed

Lines changed: 1241 additions & 404 deletions

File tree

src/platform/packages/shared/kbn-scout/src/playwright/page_objects/date_picker.ts

Lines changed: 218 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,38 @@ export class DatePicker {
3232
);
3333
}
3434

35+
/**
36+
* Detects whether the page is using the new DateRangePicker or the legacy
37+
* EuiSuperDatePicker. Not cached because the lazy page object proxy lacks a
38+
* `set` trap, so instance property writes through the proxy are lost.
39+
*
40+
* When a containerLocator is provided, the detection is scoped to that
41+
* container (useful when the only DateRangePicker lives inside a panel).
42+
*/
43+
private async isNewDateRangePicker(containerLocator?: Locator): Promise<boolean> {
44+
try {
45+
await this.getTestSubjLocator('dateRangePickerControlButton', containerLocator).waitFor({
46+
timeout: 5000,
47+
});
48+
return true;
49+
} catch {
50+
return false;
51+
}
52+
}
53+
54+
private getTestSubjLocator(selector: string, containerLocator?: Locator) {
55+
return containerLocator
56+
? containerLocator.getByTestId(selector)
57+
: this.page.testSubj.locator(selector);
58+
}
59+
60+
// ---------------------------------------------------------------------------
61+
// Legacy EuiSuperDatePicker helpers
62+
// ---------------------------------------------------------------------------
63+
3564
private async showStartEndTimes(containerLocator?: Locator) {
3665
const getTestSubjLocator = (selector: string) =>
37-
containerLocator
38-
? containerLocator.getByTestId(selector)
39-
: this.page.testSubj.locator(selector);
66+
this.getTestSubjLocator(selector, containerLocator);
4067
const getLocator = (selector: string) =>
4168
containerLocator ? containerLocator.locator(selector) : this.page.locator(selector);
4269

@@ -49,30 +76,9 @@ export class DatePicker {
4976
await getLocator('div.kbnTypeahead').waitFor({ state: 'hidden' });
5077
}
5178

52-
// Initial check if show/end buttons are visible
5379
const showBtn = getTestSubjLocator('superDatePickerShowDatesButton');
54-
const endBtn = getTestSubjLocator('superDatePickerendDatePopoverButton');
55-
56-
if (
57-
!((await showBtn.isVisible({ timeout: 2000 })) || (await endBtn.isVisible({ timeout: 2000 })))
58-
) {
59-
// Reload loop only if initial fails
60-
await expect
61-
.poll(
62-
async () => {
63-
await this.page.reload();
64-
await getTestSubjLocator('superDatePickerToggleQuickMenuButton').waitFor();
65-
return (await showBtn.isVisible()) || (await endBtn.isVisible());
66-
},
67-
{
68-
timeout: 20000,
69-
intervals: [500], // Retry every 0.5s
70-
}
71-
)
72-
.toBe(true);
73-
}
7480

75-
if (await showBtn.isVisible()) {
81+
if (await showBtn.isVisible({ timeout: 2000 })) {
7682
// Click to show start/end time pickers
7783
await showBtn.click();
7884
await this.page.testSubj.locator('superDatePickerAbsoluteTab').waitFor();
@@ -82,7 +88,16 @@ export class DatePicker {
8288
}
8389
}
8490

85-
async typeAbsoluteRange({
91+
private async openAbsoluteTab() {
92+
// Usually 2 matching elements exist: one visible in the popover and one hidden in the DOM.
93+
const absoluteTab = this.page.testSubj
94+
.locator('superDatePickerAbsoluteTab')
95+
.filter({ visible: true });
96+
await expect(absoluteTab).toHaveCount(1);
97+
await absoluteTab.click();
98+
}
99+
100+
private async typeAbsoluteRangeLegacy({
86101
from,
87102
to,
88103
validateDates = false,
@@ -94,9 +109,7 @@ export class DatePicker {
94109
containerLocator?: Locator;
95110
}) {
96111
const getTestSubjLocator = (selector: string) =>
97-
containerLocator
98-
? containerLocator.getByTestId(selector)
99-
: this.page.testSubj.locator(selector);
112+
this.getTestSubjLocator(selector, containerLocator);
100113

101114
// we start with end date
102115
await getTestSubjLocator('superDatePickerendDatePopoverButton').click();
@@ -107,7 +120,7 @@ export class DatePicker {
107120
await this.page.testSubj.locator('parseAbsoluteDateFormat').click();
108121
await this.page.keyboard.press('Escape');
109122
// and later change start date
110-
await this.page.testSubj.locator('superDatePickerstartDatePopoverButton').click();
123+
await getTestSubjLocator('superDatePickerstartDatePopoverButton').click();
111124
await this.openAbsoluteTab();
112125
const inputTo = this.page.testSubj.locator('superDatePickerAbsoluteDateInput');
113126
await inputTo.clear();
@@ -129,16 +142,104 @@ export class DatePicker {
129142
await getTestSubjLocator('querySubmitButton').click();
130143
}
131144

145+
// ---------------------------------------------------------------------------
146+
// New DateRangePicker helpers
147+
// ---------------------------------------------------------------------------
148+
149+
private async ensurePickerVisible(containerLocator?: Locator) {
150+
const controlButton = this.getTestSubjLocator('dateRangePickerControlButton', containerLocator);
151+
await controlButton.waitFor();
152+
153+
// Close any open suggestion lists that might block the date picker button
154+
const getLocator = (selector: string) =>
155+
containerLocator ? containerLocator.locator(selector) : this.page.locator(selector);
156+
const isSuggestionListVisible = await getLocator('div.kbnTypeahead').isVisible();
157+
if (isSuggestionListVisible) {
158+
await this.getTestSubjLocator('unifiedTabs_tabsBar', containerLocator).click();
159+
await getLocator('div.kbnTypeahead').waitFor({ state: 'hidden' });
160+
}
161+
}
162+
163+
private async openCustomRangePanel(containerLocator?: Locator) {
164+
await this.ensurePickerVisible(containerLocator);
165+
// Click the control button scoped to the container
166+
await this.getTestSubjLocator('dateRangePickerControlButton', containerLocator).click();
167+
// The dialog/popover renders as a portal at the page root, not inside the container,
168+
// so dialog elements must be found at the page level.
169+
await this.page.testSubj.locator('dateRangePickerCustomRangeNavItem').click();
170+
await this.page.testSubj.locator('dateRangePickerCustomRangePanel').waitFor();
171+
}
172+
173+
private async setDatePart(side: 'Start' | 'End', value: string) {
174+
// Dialog elements render as a portal at the page root
175+
await this.page.testSubj.locator(`dateRangePicker${side}AbsoluteTab`).click();
176+
const input = this.page.testSubj.locator(`dateRangePicker${side}AbsoluteInput`);
177+
await input.clear();
178+
await input.fill(value);
179+
}
180+
181+
private async typeAbsoluteRangeNewPicker({
182+
from,
183+
to,
184+
validateDates = false,
185+
containerLocator,
186+
}: {
187+
from: string;
188+
to: string;
189+
validateDates?: boolean;
190+
containerLocator?: Locator;
191+
}) {
192+
// Dialog elements render as a portal at the page root
193+
await this.setDatePart('Start', from);
194+
await this.setDatePart('End', to);
195+
await this.page.testSubj.locator('dateRangePickerCustomRangeApplyButton').click();
196+
197+
if (validateDates) {
198+
const controlButton = this.getTestSubjLocator(
199+
'dateRangePickerControlButton',
200+
containerLocator
201+
);
202+
// Note: data-date-range stores ISO 8601 strings (e.g. "2025-01-01T00:00:00.000Z"),
203+
// not the human-readable format passed as `from`/`to`. We assert visibility only
204+
// to confirm the picker updated without risking a format-mismatch failure.
205+
await expect(
206+
controlButton,
207+
`Date picker should reflect the updated time range`
208+
).toBeVisible();
209+
}
210+
211+
await this.getTestSubjLocator('querySubmitButton', containerLocator).click();
212+
}
213+
214+
// ---------------------------------------------------------------------------
215+
// Public API (dual-path)
216+
// ---------------------------------------------------------------------------
217+
132218
async setCommonlyUsedTime(option: string) {
133-
await this.quickMenuButton.click();
134-
const commonlyUsedOption = this.page.testSubj.locator(`superDatePickerCommonlyUsed_${option}`);
135-
await expect(commonlyUsedOption).toBeVisible();
136-
await commonlyUsedOption.click();
219+
if (await this.isNewDateRangePicker()) {
220+
await this.page.testSubj.locator('dateRangePickerControlButton').click();
221+
await this.page.testSubj.locator('dateRangePickerMainPanel').waitFor();
222+
const presetItem = this.page.testSubj.locator(`dateRangePickerPresetItem-${option}`);
223+
await expect(presetItem).toBeVisible();
224+
await presetItem.click();
225+
} else {
226+
await this.quickMenuButton.click();
227+
const commonlyUsedOption = this.page.testSubj.locator(
228+
`superDatePickerCommonlyUsed_${option}`
229+
);
230+
await expect(commonlyUsedOption).toBeVisible();
231+
await commonlyUsedOption.click();
232+
}
137233
}
138234

139235
async setAbsoluteRange({ from, to }: { from: string; to: string }) {
140-
await this.showStartEndTimes();
141-
await this.typeAbsoluteRange({ from, to, validateDates: true });
236+
if (await this.isNewDateRangePicker()) {
237+
await this.openCustomRangePanel();
238+
await this.typeAbsoluteRangeNewPicker({ from, to, validateDates: true });
239+
} else {
240+
await this.showStartEndTimes();
241+
await this.typeAbsoluteRangeLegacy({ from, to, validateDates: true });
242+
}
142243
}
143244

144245
async setAbsoluteRangeInRootContainer({
@@ -150,43 +251,102 @@ export class DatePicker {
150251
to: string;
151252
containerLocator: Locator;
152253
}) {
153-
await this.showStartEndTimes(containerLocator);
154-
await this.typeAbsoluteRange({ from, to, validateDates: true, containerLocator });
254+
if (await this.isNewDateRangePicker(containerLocator)) {
255+
await this.openCustomRangePanel(containerLocator);
256+
await this.typeAbsoluteRangeNewPicker({
257+
from,
258+
to,
259+
validateDates: true,
260+
containerLocator,
261+
});
262+
} else {
263+
await this.showStartEndTimes(containerLocator);
264+
await this.typeAbsoluteRangeLegacy({
265+
from,
266+
to,
267+
validateDates: true,
268+
containerLocator,
269+
});
270+
}
155271
}
156272

157-
async openAbsoluteTab() {
158-
// usually 2 matching elements exist, one in the popover and one hidden in the DOM
159-
const absoluteTab = this.page.testSubj
160-
.locator('superDatePickerAbsoluteTab')
161-
.filter({ visible: true });
162-
await expect(absoluteTab).toHaveCount(1);
163-
await absoluteTab.click();
273+
/** @deprecated Use {@link setAbsoluteRangeInRootContainer} instead. */
274+
async typeAbsoluteRange({
275+
from,
276+
to,
277+
validateDates = false,
278+
containerLocator,
279+
}: {
280+
from: string;
281+
to: string;
282+
validateDates?: boolean;
283+
containerLocator?: Locator;
284+
}) {
285+
if (await this.isNewDateRangePicker(containerLocator)) {
286+
await this.openCustomRangePanel(containerLocator);
287+
await this.typeAbsoluteRangeNewPicker({ from, to, validateDates, containerLocator });
288+
} else {
289+
await this.showStartEndTimes(containerLocator);
290+
await this.typeAbsoluteRangeLegacy({ from, to, validateDates, containerLocator });
291+
}
164292
}
165293

166294
async getTimeConfig(): Promise<{ start: string; end: string }> {
295+
if (await this.isNewDateRangePicker()) {
296+
const dateRange =
297+
(await this.page.testSubj
298+
.locator('dateRangePickerControlButton')
299+
.getAttribute('data-date-range')) ?? '';
300+
const [start, end] = dateRange.split(' to ');
301+
return { start: start?.trim() ?? '', end: end?.trim() ?? '' };
302+
}
167303
await this.showStartEndTimes();
168304
const start = await this.page.testSubj.innerText('superDatePickerstartDatePopoverButton');
169305
const end = await this.page.testSubj.innerText('superDatePickerendDatePopoverButton');
170306
return { start, end };
171307
}
172308

173309
async startAutoRefresh(interval: number, dateUnit: DateUnitSelector = DateUnitSelector.Seconds) {
174-
await this.quickMenuButton.click();
175-
// Check if refresh is already running
176-
const isPaused = (await this.toggleRefreshButton.getAttribute('aria-checked')) === 'false';
177-
if (isPaused) {
178-
await this.toggleRefreshButton.click();
179-
}
180-
// Set interval
181-
await this.refreshIntervalInput.clear();
182-
await this.refreshIntervalInput.fill(interval.toString());
183-
await this.refreshIntervalUnitSelect.selectOption({ value: dateUnit });
184-
await this.refreshIntervalInput.press('Enter');
310+
if (await this.isNewDateRangePicker()) {
311+
await this.page.testSubj.locator('dateRangePickerControlButton').click();
312+
await this.page.testSubj.locator('dateRangePickerSettingsButton').click();
313+
await this.page.testSubj.locator('dateRangePickerSettingsPanel').waitFor();
314+
315+
const toggle = this.page.testSubj.locator('dateRangePickerAutoRefreshToggle');
316+
const isPaused = (await toggle.getAttribute('aria-checked')) !== 'true';
317+
if (isPaused) {
318+
await toggle.click();
319+
}
185320

186-
await this.quickMenuButton.click();
321+
const countInput = this.page.testSubj.locator('dateRangePickerAutoRefreshIntervalCount');
322+
await countInput.clear();
323+
await countInput.fill(interval.toString());
324+
await this.page.testSubj
325+
.locator('dateRangePickerAutoRefreshIntervalUnit')
326+
.selectOption({ value: dateUnit });
327+
328+
await this.page.keyboard.press('Escape');
329+
} else {
330+
await this.quickMenuButton.click();
331+
const isPaused = (await this.toggleRefreshButton.getAttribute('aria-checked')) === 'false';
332+
if (isPaused) {
333+
await this.toggleRefreshButton.click();
334+
}
335+
await this.refreshIntervalInput.clear();
336+
await this.refreshIntervalInput.fill(interval.toString());
337+
await this.refreshIntervalUnitSelect.selectOption({ value: dateUnit });
338+
await this.refreshIntervalInput.press('Enter');
339+
await this.quickMenuButton.click();
340+
}
187341
}
188342

189343
async waitToBeHidden() {
190-
await this.page.testSubj.locator('superDatePickerAbsoluteTab').waitFor({ state: 'hidden' });
344+
if (await this.isNewDateRangePicker()) {
345+
await this.page.testSubj
346+
.locator('dateRangePickerCustomRangePanel')
347+
.waitFor({ state: 'hidden' });
348+
} else {
349+
await this.page.testSubj.locator('superDatePickerAbsoluteTab').waitFor({ state: 'hidden' });
350+
}
191351
}
192352
}

src/platform/packages/shared/shared-ux/datetime/kbn-date-range-picker/date_range_picker.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ export interface DateRangePickerProps {
147147
timeZone?: string;
148148
/** Fires at the end of each auto-refresh interval while `settings.autoRefresh` exists, is enabled and timer is unpaused. */
149149
onRefresh?: () => void;
150+
/**
151+
* Increment this value whenever an external timer (e.g. the Kibana timefilter) triggers a
152+
* refresh, so the visual countdown resets to stay in sync with actual query cadence.
153+
* `undefined` on first render is ignored.
154+
*/
155+
refreshEpoch?: number;
150156
/**
151157
* Prepends the Kibana server `basePath` to an internal URL path.
152158
* Typically provided as `core.http.basePath.prepend`.

src/platform/packages/shared/shared-ux/datetime/kbn-date-range-picker/date_range_picker_context.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ export function DateRangePickerProvider({
174174
onSettingsChange,
175175
timeZone,
176176
onRefresh,
177+
refreshEpoch,
177178
prependBasePath: prependBasePathProp,
178179
canAccessAdvancedSettings = false,
179180
}: PropsWithChildren<DateRangePickerProps>) {
@@ -260,6 +261,7 @@ export function DateRangePickerProvider({
260261
isPaused: refreshTimerPaused,
261262
intervalMs: settings.autoRefresh?.isEnabled ? settings.autoRefresh.intervalMs : 0,
262263
onRefresh,
264+
refreshEpoch,
263265
});
264266

265267
const toggleAutoRefresh = useCallback(() => {

0 commit comments

Comments
 (0)