Skip to content

Commit cf647f6

Browse files
authored
Merge branch 'main' into 14421-feature-workflow-error-trigger-reference-implementation
2 parents 1c7b4f2 + 2676084 commit cf647f6

57 files changed

Lines changed: 2124 additions & 416 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/platform/packages/private/kbn-esql-editor/src/editor_visor/index.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
EuiButtonIcon,
1313
EuiFlexGroup,
1414
EuiFlexItem,
15+
EuiIconTip,
1516
useEuiTheme,
1617
type EuiComboBoxOptionOption,
1718
} from '@elastic/eui';
@@ -57,6 +58,10 @@ const closeButtonAriaLabel = i18n.translate('esqlEditor.visor.closeButtonAriaLab
5758
defaultMessage: 'Close quick search visor',
5859
});
5960

61+
const techPreviewTooltip = i18n.translate('esqlEditor.visor.techPreviewTooltip', {
62+
defaultMessage: 'Technical preview',
63+
});
64+
6065
export function QuickSearchVisor({
6166
query,
6267
isSpaceReduced,
@@ -250,6 +255,9 @@ export function QuickSearchVisor({
250255
>
251256
{isNlToEsqlEnabled && (
252257
<>
258+
<EuiFlexItem grow={false} css={styles.techPreviewIcon}>
259+
<EuiIconTip type="beaker" size="s" color="subdued" content={techPreviewTooltip} />
260+
</EuiFlexItem>
253261
<EuiFlexItem grow={false} css={styles.modeSelectWrapper}>
254262
<ModeSelector onModeChange={onModeChange} />
255263
</EuiFlexItem>

src/platform/packages/private/kbn-esql-editor/src/editor_visor/visor.styles.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,14 @@ export const visorStyles = (
117117
}
118118
}
119119
`,
120+
techPreviewIcon: css`
121+
padding-left: ${euiTheme.size.s};
122+
flex-shrink: 0;
123+
flex-grow: 0;
124+
display: flex;
125+
align-items: center;
126+
`,
120127
modeSelectWrapper: css`
121-
padding-left: ${euiTheme.size.xs};
122128
flex-shrink: 0;
123129
flex-grow: 0;
124130
width: ${modeSelectWidth}px;

src/platform/packages/shared/kbn-esql-language/src/language/autocomplete/__tests__/helpers.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,8 +387,7 @@ export function createCustomCallbackMocks(
387387
getTimeseriesIndices: jest.fn(async () => ({ indices: timeseriesIndices })),
388388
getViews: jest.fn(async () => ({ views })),
389389
getEditorExtensions: jest.fn(async (queryString: string) => {
390-
// from * is called in the empty state
391-
if (queryString.includes('logs*') || queryString === 'from *') {
390+
if (queryString.includes('logs*') || queryString.includes('a_index')) {
392391
return {
393392
recommendedQueries: editorExtensions.recommendedQueries,
394393
recommendedFields: editorExtensions.recommendedFields,

src/platform/packages/shared/kbn-esql-language/src/language/autocomplete/autocomplete.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,9 @@ export async function suggest(
158158
innerText,
159159
resourceRetriever
160160
);
161-
const editorExtensions = (await resourceRetriever?.getEditorExtensions?.('from *')) ?? {
161+
const editorExtensions = (await resourceRetriever?.getEditorExtensions?.(
162+
fromCommand + ' '
163+
)) ?? {
162164
recommendedQueries: [],
163165
};
164166
const recommendedQueriesSuggestionsFromExtensions = mapRecommendedQueriesFromExtensions(

src/platform/packages/shared/shared-ux/ai-components/ai_button/src/ai_button_base.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const resolvedIconType = (iconType: AiButtonIconType): IconType =>
2121
iconType === 'aiAssistantLogo' ? AiAssistantLogo : iconType;
2222

2323
// Per design: only xs uses small icon; s and m both use medium icon.
24-
const getSyncedIconSize = (size?: string): 's' | 'm' => (size === 'xs' ? 's' : 'm');
24+
const getSyncedIconSize = (size?: 'xs' | 's' | 'm') => (size === 'xs' ? 's' : 'm');
2525

2626
export const AiButtonBase = (props: AiButtonProps) => {
2727
const variant: AiButtonVariant = props.variant ?? 'base';
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React from 'react';
11+
import { fireEvent, screen, within } from '@testing-library/react';
12+
import { renderWithEuiTheme } from '@kbn/test-jest-helpers';
13+
14+
import type { DateRangePickerProps } from '../date_range_picker';
15+
import { DateRangePickerProvider, useDateRangePickerContext } from '../date_range_picker_context';
16+
import {
17+
DateRangePickerPanel,
18+
DateRangePickerPanelNavigationProvider,
19+
useDateRangePickerPanelNavigation,
20+
} from '../date_range_picker_panel_navigation';
21+
import { customTimeRangePanelTexts } from '../translations';
22+
import { CustomTimeRangePanel } from './custom_time_range_panel';
23+
24+
interface RenderPanelProps {
25+
defaultValue?: string;
26+
onChange?: DateRangePickerProps['onChange'];
27+
onPresetSave?: DateRangePickerProps['onPresetSave'];
28+
}
29+
30+
const OpenCustomPanelButton = () => {
31+
const { navigateTo } = useDateRangePickerPanelNavigation();
32+
33+
return (
34+
<button
35+
type="button"
36+
data-test-subj="openCustomTimeRangePanelButton"
37+
onClick={() => navigateTo(CustomTimeRangePanel.PANEL_ID)}
38+
>
39+
Open custom panel
40+
</button>
41+
);
42+
};
43+
44+
/** Reads the current context text value so tests can assert what the input would show. */
45+
const CurrentTextProbe = () => {
46+
const { text } = useDateRangePickerContext();
47+
return <output data-test-subj="currentDateRangeText">{text}</output>;
48+
};
49+
50+
/**
51+
* Simulates the main input changing (e.g. the user typing). Fires onChange which
52+
* calls setText, exercising the input→panel sync path without needing to render the
53+
* full input field.
54+
*/
55+
const TextSetterProbe = () => {
56+
const { setText } = useDateRangePickerContext();
57+
return (
58+
<input aria-label="Set picker text" defaultValue="" onChange={(e) => setText(e.target.value)} />
59+
);
60+
};
61+
62+
const TestHarness = ({
63+
defaultValue = '-15m',
64+
onChange = () => {},
65+
onPresetSave,
66+
}: RenderPanelProps) => {
67+
return (
68+
<DateRangePickerProvider
69+
defaultValue={defaultValue}
70+
onChange={onChange}
71+
onPresetSave={onPresetSave}
72+
>
73+
<DateRangePickerPanelNavigationProvider defaultPanelId="main" panelDescriptors={[]}>
74+
<CurrentTextProbe />
75+
<TextSetterProbe />
76+
<DateRangePickerPanel id="main">
77+
<OpenCustomPanelButton />
78+
</DateRangePickerPanel>
79+
<DateRangePickerPanel id={CustomTimeRangePanel.PANEL_ID}>
80+
<div role="dialog">
81+
<CustomTimeRangePanel />
82+
</div>
83+
</DateRangePickerPanel>
84+
</DateRangePickerPanelNavigationProvider>
85+
</DateRangePickerProvider>
86+
);
87+
};
88+
89+
const renderCustomTimeRangePanel = (props?: RenderPanelProps) => {
90+
renderWithEuiTheme(<TestHarness {...props} />);
91+
};
92+
93+
const openCustomPanel = () => {
94+
fireEvent.click(screen.getByTestId('openCustomTimeRangePanelButton'));
95+
};
96+
97+
const getFieldset = (name: string) => {
98+
const groups = screen.getAllByRole('group', { name });
99+
return groups[0];
100+
};
101+
const getStartFieldset = () => getFieldset('Start date');
102+
const getEndFieldset = () => getFieldset('End date');
103+
104+
describe('CustomTimeRangePanel', () => {
105+
describe('initial state', () => {
106+
it('derives start/end picker state from the current time range', () => {
107+
renderCustomTimeRangePanel({ defaultValue: '-15m' });
108+
openCustomPanel();
109+
110+
expect(within(getStartFieldset()).getByLabelText('Count')).toHaveValue(15);
111+
expect(
112+
within(getEndFieldset()).getByText(customTimeRangePanelTexts.nowEndHelpText)
113+
).toBeInTheDocument();
114+
});
115+
});
116+
117+
describe('tab switching', () => {
118+
it('switches from Relative to Absolute and shows a text input', () => {
119+
renderCustomTimeRangePanel();
120+
openCustomPanel();
121+
122+
const startFieldset = getStartFieldset();
123+
fireEvent.click(within(startFieldset).getByText('Absolute'));
124+
125+
expect(within(startFieldset).getByLabelText('Start date absolute date')).toBeInTheDocument();
126+
});
127+
128+
it('switches from Relative to Now and shows side-specific help text', () => {
129+
renderCustomTimeRangePanel();
130+
openCustomPanel();
131+
132+
fireEvent.click(within(getStartFieldset()).getByText('Now'));
133+
expect(
134+
within(getStartFieldset()).getByText(customTimeRangePanelTexts.nowStartHelpText)
135+
).toBeInTheDocument();
136+
137+
fireEvent.click(within(getEndFieldset()).getByText('Now'));
138+
expect(
139+
within(getEndFieldset()).getByText(customTimeRangePanelTexts.nowEndHelpText)
140+
).toBeInTheDocument();
141+
});
142+
});
143+
144+
describe('absolute text editing', () => {
145+
it('does not clobber the other date part when typing', () => {
146+
renderCustomTimeRangePanel();
147+
openCustomPanel();
148+
149+
fireEvent.click(within(getStartFieldset()).getByText('Absolute'));
150+
const absInput = within(getStartFieldset()).getByLabelText('Start date absolute date');
151+
fireEvent.change(absInput, { target: { value: 'Jan 1 2025, 00:00' } });
152+
153+
expect(
154+
within(getEndFieldset()).getByText(customTimeRangePanelTexts.nowEndHelpText)
155+
).toBeInTheDocument();
156+
});
157+
});
158+
159+
describe('validation', () => {
160+
it('shows end-before-start error and disables Apply when end < start', () => {
161+
renderCustomTimeRangePanel({ defaultValue: '2025-06-01 to 2025-01-01' });
162+
openCustomPanel();
163+
164+
const dialog = screen.getByRole('dialog');
165+
expect(
166+
within(dialog).getByText(customTimeRangePanelTexts.endBeforeStartError)
167+
).toBeInTheDocument();
168+
169+
const applyButton = within(dialog).getByRole('button', { name: 'Apply' });
170+
expect(applyButton).toBeDisabled();
171+
});
172+
});
173+
174+
describe('shorthand display', () => {
175+
it('shows a shorthand value for a valid relative range', () => {
176+
renderCustomTimeRangePanel({ defaultValue: '-15m' });
177+
openCustomPanel();
178+
179+
const shorthandInput = screen.getByLabelText('Shorthand');
180+
expect(shorthandInput).toHaveValue('-15m');
181+
});
182+
183+
it('shows "(not available)" when the range is invalid', () => {
184+
renderCustomTimeRangePanel({ defaultValue: '2025-06-01 to 2025-01-01' });
185+
openCustomPanel();
186+
187+
const shorthandInput = screen.getByLabelText('Shorthand');
188+
expect(shorthandInput).toHaveValue(customTimeRangePanelTexts.notAvailable);
189+
});
190+
});
191+
192+
describe('input text sync', () => {
193+
it('strips the "now" prefix from relative dates when the panel drives the input', () => {
194+
// Start with the full date-math form to confirm it gets normalised to shorthand.
195+
renderCustomTimeRangePanel({ defaultValue: 'now-30m to now' });
196+
openCustomPanel();
197+
198+
// Trigger a panel-driven change so Effect B fires and calls setText.
199+
fireEvent.change(within(getStartFieldset()).getByLabelText('Count'), {
200+
target: { value: '15' },
201+
});
202+
203+
expect(screen.getByTestId('currentDateRangeText')).toHaveTextContent('-15m to now');
204+
});
205+
206+
it('emits the literal "now" in the input for the Now type', () => {
207+
// Default: start=RELATIVE 15m, end=NOW. Switching start to Now produces "now to now".
208+
renderCustomTimeRangePanel({ defaultValue: '-15m' });
209+
openCustomPanel();
210+
211+
fireEvent.click(within(getStartFieldset()).getByText('Now'));
212+
213+
expect(screen.getByTestId('currentDateRangeText')).toHaveTextContent('now to now');
214+
});
215+
216+
it('updates the panel UI when the input text changes to a valid range', () => {
217+
renderCustomTimeRangePanel({ defaultValue: '-15m' });
218+
openCustomPanel();
219+
220+
fireEvent.change(screen.getByLabelText('Set picker text'), {
221+
target: { value: 'now-30m to now' },
222+
});
223+
224+
expect(within(getStartFieldset()).getByLabelText('Count')).toHaveValue(30);
225+
expect(
226+
within(getEndFieldset()).getByText(customTimeRangePanelTexts.nowEndHelpText)
227+
).toBeInTheDocument();
228+
});
229+
230+
it('does not reset the panel when the input becomes partial or unparseable', () => {
231+
// '2025-01-01 to now' → start=ABSOLUTE "Jan 1 2025, 00:00", end=NOW.
232+
renderCustomTimeRangePanel({ defaultValue: '2025-01-01 to now' });
233+
openCustomPanel();
234+
235+
const startAbsInput = within(getStartFieldset()).getByLabelText('Start date absolute date');
236+
expect(startAbsInput).toHaveValue('Jan 1 2025, 00:00');
237+
238+
// Simulate the user clearing/partially typing in the main input.
239+
fireEvent.change(screen.getByLabelText('Set picker text'), {
240+
target: { value: '2025-01-01 to' },
241+
});
242+
243+
// Panel state must not have been clobbered by a fallback timestamp.
244+
expect(startAbsInput).toHaveValue('Jan 1 2025, 00:00');
245+
expect(
246+
within(getEndFieldset()).getByText(customTimeRangePanelTexts.nowEndHelpText)
247+
).toBeInTheDocument();
248+
});
249+
});
250+
251+
describe('save as preset', () => {
252+
it('does not show the checkbox when onPresetSave is not provided', () => {
253+
renderCustomTimeRangePanel();
254+
openCustomPanel();
255+
256+
expect(screen.queryByLabelText('Save as preset')).not.toBeInTheDocument();
257+
});
258+
259+
it('calls onPresetSave with the correct bounds and label when Apply is clicked', () => {
260+
const onPresetSave = jest.fn();
261+
renderCustomTimeRangePanel({ onPresetSave });
262+
openCustomPanel();
263+
264+
const dialog = screen.getByRole('dialog');
265+
fireEvent.click(within(dialog).getByLabelText('Save as preset'));
266+
fireEvent.click(within(dialog).getByRole('button', { name: 'Apply' }));
267+
268+
expect(onPresetSave).toHaveBeenCalledWith({
269+
start: 'now-15m',
270+
end: 'now',
271+
label: '-15m to now',
272+
});
273+
});
274+
});
275+
276+
describe('go back', () => {
277+
it('restores original input text when going back without applying', () => {
278+
renderCustomTimeRangePanel({ defaultValue: '-15m' });
279+
openCustomPanel();
280+
281+
const countInput = within(getStartFieldset()).getByLabelText('Count');
282+
fireEvent.change(countInput, { target: { value: '30' } });
283+
284+
const dialog = screen.getByRole('dialog');
285+
fireEvent.click(within(dialog).getByText('Custom time range'));
286+
287+
expect(screen.getByTestId('currentDateRangeText')).toHaveTextContent('-15m');
288+
});
289+
});
290+
});

0 commit comments

Comments
 (0)