diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 08ede7759dbaa9..7f2a8e986b5d43 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -21781,7 +21781,6 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "{title} を検索", "xpack.uptime.integrationLink.missingDataMessage": "この統合に必要なデータが見つかりませんでした。", "xpack.uptime.kueryBar.indexPatternMissingWarningMessage": "インデックスパターンの取得中にエラーが発生しました。", - "xpack.uptime.kueryBar.searchPlaceholder": "モニター ID、名前、プロトコルタイプなどを検索…", "xpack.uptime.locationAvailabilityViewToggleLegend": "トグルを表示", "xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。", "xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 071f2f51cc7cbb..1eee5f0d2f4dc1 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -21831,7 +21831,6 @@ "xpack.uptime.filterPopout.searchMessage.ariaLabel": "搜索 {title}", "xpack.uptime.integrationLink.missingDataMessage": "未找到此集成的所需数据。", "xpack.uptime.kueryBar.indexPatternMissingWarningMessage": "检索索引模式时出错。", - "xpack.uptime.kueryBar.searchPlaceholder": "搜索监测 ID、名称和协议类型......", "xpack.uptime.locationAvailabilityViewToggleLegend": "视图切换", "xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。", "xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。", diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 0c8ff6d3f1ed6f..880bc0f92ddf6d 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -63,3 +63,5 @@ export enum CERT_STATUS { EXPIRED = 'EXPIRED', TOO_OLD = 'TOO_OLD', } + +export const KQL_SYNTAX_LOCAL_STORAGE = 'xpack.uptime.kql.syntax'; diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts index d5eb67fcdcde15..fc2cd42500d6c1 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor/state.ts @@ -96,6 +96,7 @@ export const FetchMonitorStatesQueryArgsType = t.intersection([ pagination: t.string, filters: t.string, statusFilter: t.string, + query: t.string, }), t.type({ dateRangeStart: t.string, diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts index 0e5a23ddaa83f2..a843eae29efe39 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/histogram.ts @@ -23,6 +23,7 @@ export interface GetPingHistogramParams { filters?: string; monitorId?: string; bucketSize?: string; + query?: string; } export interface HistogramResult { diff --git a/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx index 9de642b82ee0fa..6330f14aa63a30 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ping_histogram/ping_histogram_container.tsx @@ -21,6 +21,7 @@ interface Props { const Container: React.FC = ({ height }) => { const { + query, absoluteDateRangeStart, absoluteDateRangeEnd, dateRangeStart: dateStart, @@ -37,8 +38,8 @@ const Container: React.FC = ({ height }) => { const { loading, pingHistogram: data } = useSelector(selectPingHistogram); useEffect(() => { - dispatch(getPingHistogram({ monitorId, dateStart, dateEnd, filters: esKuery })); - }, [dateStart, dateEnd, monitorId, lastRefresh, esKuery, dispatch]); + dispatch(getPingHistogram.get({ monitorId, dateStart, dateEnd, query, filters: esKuery })); + }, [dateStart, dateEnd, monitorId, lastRefresh, esKuery, dispatch, query]); return ( = ({ ); useEffect(() => { dispatch( - getSnapshotCountAction({ dateRangeStart: 'now-24h', dateRangeEnd: 'now', filters: esKuery }) + getSnapshotCountAction.get({ + dateRangeStart: 'now-24h', + dateRangeEnd: 'now', + filters: esKuery, + }) ); }, [dispatch, esKuery]); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx index 919f831d2d33b8..7db3659564ce26 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/kuery_bar.tsx @@ -68,7 +68,7 @@ export function KueryBar({ let currentRequestCheck: string; const [getUrlParams, updateUrlParams] = useUrlParams(); - const { search: kuery } = getUrlParams(); + const { search: kuery, query } = getUrlParams(); useEffect(() => { updateSearchText(kuery); @@ -155,7 +155,7 @@ export function KueryBar({ dataTestSubj={dataTestSubj} disabled={indexPatternMissing} isLoading={isLoadingSuggestions || loading} - initialValue={defaultKuery || kuery} + initialValue={defaultKuery || kuery || query} onChange={onChange} onSubmit={onSubmit} suggestions={state.suggestions.slice(0, suggestionLimit)} diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx new file mode 100644 index 00000000000000..2e7dfe990e9c10 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { render } from '../../../../../lib/helper/rtl_helpers'; +import { SearchType } from './search_type'; + +describe('Kuery bar search type', () => { + it('can change from simple to kq;', () => { + let kqlSyntax = false; + const setKqlSyntax = jest.fn((val: boolean) => { + kqlSyntax = val; + }); + + const { getByTestId } = render( + + ); + + // open popover to change + fireEvent.click(getByTestId('syntaxChangeToKql')); + + // change syntax + fireEvent.click(getByTestId('toggleKqlSyntax')); + + expect(setKqlSyntax).toHaveBeenCalledWith(true); + expect(setKqlSyntax).toHaveBeenCalledTimes(1); + }); + + it('can change from kql to simple;', () => { + let kqlSyntax = false; + const setKqlSyntax = jest.fn((val: boolean) => { + kqlSyntax = val; + }); + + const { getByTestId } = render( + + ); + + fireEvent.click(getByTestId('syntaxChangeToKql')); + + fireEvent.click(getByTestId('toggleKqlSyntax')); + + expect(setKqlSyntax).toHaveBeenCalledWith(true); + expect(setKqlSyntax).toHaveBeenCalledTimes(1); + }); + + it('clears the query on change to kql', () => { + const setKqlSyntax = jest.fn(); + + const { history } = render(, { + url: '/app/uptime?query=test', + }); + + expect(history?.location.search).toBe(''); + }); + + it('clears the search param on change to simple syntax', () => { + const setKqlSyntax = jest.fn(); + + const { history } = render(, { + url: '/app/uptime?search=test', + }); + + expect(history?.location.search).toBe(''); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx new file mode 100644 index 00000000000000..af539e1c361a15 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/search_type/search_type.tsx @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { + EuiPopover, + EuiFormRow, + EuiSwitch, + EuiButtonEmpty, + EuiPopoverTitle, + EuiText, + EuiSpacer, + EuiLink, + EuiButtonIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { useUrlParams } from '../../../../../hooks'; +import { + CHANGE_SEARCH_BAR_SYNTAX, + CHANGE_SEARCH_BAR_SYNTAX_SIMPLE, + SYNTAX_OPTIONS_LABEL, +} from '../translations'; + +const BoxesVerticalIcon = euiStyled(EuiButtonIcon)` + padding: 10px 8px 0 8px; + border-radius: 0; + height: 38px; + width: 32px; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; + padding-top: 8px; + padding-bottom: 8px; + cursor: pointer; +`; + +interface Props { + kqlSyntax: boolean; + setKqlSyntax: (val: boolean) => void; +} + +export const SearchType = ({ kqlSyntax, setKqlSyntax }: Props) => { + const { + services: { docLinks }, + } = useKibana(); + + const [getUrlParams, updateUrlParams] = useUrlParams(); + + const { query, search } = getUrlParams(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = () => setIsPopoverOpen((prevState) => !prevState); + + const closePopover = () => setIsPopoverOpen(false); + + useEffect(() => { + if (kqlSyntax && query) { + updateUrlParams({ query: '' }); + } + + if (!kqlSyntax && search) { + updateUrlParams({ search: '' }); + } + }, [kqlSyntax, query, search, updateUrlParams]); + + const button = kqlSyntax ? ( + + KQL + + ) : ( + + ); + + return ( + +
+ {SYNTAX_OPTIONS_LABEL} + +

+ +

+
+ + + setKqlSyntax(!kqlSyntax)} + data-test-subj="toggleKqlSyntax" + /> + +
+
+ ); +}; + +const KqlDescription = ({ href }: { href: string }) => { + return ( + + {KIBANA_QUERY_LANGUAGE} + + ), + searchField: Monitor Name, ID, Url, + }} + /> + ); +}; + +const KIBANA_QUERY_LANGUAGE = i18n.translate('xpack.uptime.query.queryBar.kqlFullLanguageName', { + defaultMessage: 'Kibana Query Language', +}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/translations.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/translations.ts new file mode 100644 index 00000000000000..e0d36bcb575871 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/translations.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const KQL_PLACE_HOLDER = i18n.translate('xpack.uptime.kueryBar.searchPlaceholder.kql', { + defaultMessage: + 'Search using kql syntax for monitor IDs, names and type etc (E.g monitor.type: "http" AND tags: "dev")', +}); + +export const SIMPLE_SEARCH_PLACEHOLDER = i18n.translate( + 'xpack.uptime.kueryBar.searchPlaceholder.simple', + { + defaultMessage: 'Search by monitor ID, name, or url (E.g. http:// )', + } +); + +export const CHANGE_SEARCH_BAR_SYNTAX = i18n.translate( + 'xpack.uptime.kueryBar.options.syntax.changeLabel', + { + defaultMessage: 'Change search bar syntax to use Kibana Query Language', + } +); + +export const CHANGE_SEARCH_BAR_SYNTAX_SIMPLE = i18n.translate( + 'xpack.uptime.kueryBar.options.syntax.simple', + { + defaultMessage: 'Change search bar syntax to not use Kibana Query Language', + } +); + +export const SYNTAX_OPTIONS_LABEL = i18n.translate('xpack.uptime.kueryBar.options.syntax', { + defaultMessage: 'SYNTAX OPTIONS', +}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx new file mode 100644 index 00000000000000..ed75747aa3416e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent } from '@testing-library/react'; +import { Typeahead } from './typehead'; +import { render } from '../../../../lib/helper/rtl_helpers'; + +describe('Type head', () => { + jest.useFakeTimers(); + + it('it sets initial value', () => { + const { getByTestId, getByDisplayValue, history } = render( + {}} + suggestions={[]} + loadMore={() => {}} + queryExample="" + /> + ); + + const input = getByTestId('uptimeKueryBarInput'); + + expect(input).toBeInTheDocument(); + expect(getByDisplayValue('elastic')).toBeInTheDocument(); + + fireEvent.change(input, { target: { value: 'kibana' } }); + + // to check if it updateds the query params, needed for debounce wait + jest.advanceTimersByTime(250); + + expect(history.location.search).toBe('?query=kibana'); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx index 99ce6e97a76320..e4dd175b2fe1bd 100644 --- a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/typehead.tsx @@ -5,30 +5,15 @@ * 2.0. */ -import React, { KeyboardEvent, ChangeEvent, MouseEvent, useState, useRef, useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { ChangeEvent, MouseEvent, useState, useRef, useEffect } from 'react'; import { EuiFieldSearch, EuiProgress, EuiOutsideClickDetector } from '@elastic/eui'; import { Suggestions } from './suggestions'; import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; - -const KEY_CODES = { - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - ENTER: 13, - ESC: 27, - TAB: 9, -}; - -interface TypeaheadState { - isSuggestionsVisible: boolean; - index: number | null; - value: string; - inputIsPristine: boolean; - lastSubmitted: string; - selected: QuerySuggestion | null; -} +import { SearchType } from './search_type/search_type'; +import { useKqlSyntax } from './use_kql_syntax'; +import { useKeyEvents } from './use_key_events'; +import { KQL_PLACE_HOLDER, SIMPLE_SEARCH_PLACEHOLDER } from './translations'; +import { useSimpleQuery } from './use_simple_kuery'; interface TypeaheadProps { onChange: (inputValue: string, selectionStart: number | null) => void; @@ -54,181 +39,92 @@ export const Typeahead: React.FC = ({ isLoading, loadMore, }) => { - const [state, setState] = useState({ - isSuggestionsVisible: false, - index: null, - value: '', - inputIsPristine: true, - lastSubmitted: '', - selected: null, - }); + const [value, setValue] = useState(''); + const [index, setIndex] = useState(null); + const [isSuggestionsVisible, setIsSuggestionsVisible] = useState(false); + + const [selected, setSelected] = useState(null); + const [inputIsPristine, setInputIsPristine] = useState(true); + const [lastSubmitted, setLastSubmitted] = useState(''); + + const { kqlSyntax, setKqlSyntax } = useKqlSyntax({ setValue }); const inputRef = useRef(); + const { setQuery } = useSimpleQuery(); + useEffect(() => { - if (state.inputIsPristine && initialValue) { - setState((prevState) => ({ - ...prevState, - value: initialValue, - })); + if (inputIsPristine && initialValue) { + setValue(initialValue); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialValue]); - const incrementIndex = (currentIndex: number) => { - let nextIndex = currentIndex + 1; - if (currentIndex === null || nextIndex >= suggestions.length) { - nextIndex = 0; - } - - setState((prevState) => ({ - ...prevState, - index: nextIndex, - })); - }; - - const decrementIndex = (currentIndex: number) => { - let previousIndex: number | null = currentIndex - 1; - if (previousIndex < 0) { - previousIndex = null; - } - - setState((prevState) => ({ - ...prevState, - index: previousIndex, - })); - }; - - const onKeyUp = (event: KeyboardEvent & ChangeEvent) => { - const { selectionStart } = event.target; - const { value } = state; - switch (event.keyCode) { - case KEY_CODES.LEFT: - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: true, - })); - onChange(value, selectionStart); - break; - case KEY_CODES.RIGHT: - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: true, - })); - onChange(value, selectionStart); - break; - } - }; - - const onKeyDown = (event: KeyboardEvent) => { - const { isSuggestionsVisible, index, value } = state; - switch (event.keyCode) { - case KEY_CODES.DOWN: - event.preventDefault(); - if (isSuggestionsVisible) { - incrementIndex(index!); - } else { - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: true, - index: 0, - })); - } - break; - case KEY_CODES.UP: - event.preventDefault(); - if (isSuggestionsVisible) { - decrementIndex(index!); - } - break; - case KEY_CODES.ENTER: - event.preventDefault(); - if (isSuggestionsVisible && suggestions[index!]) { - selectSuggestion(suggestions[index!]); - } else { - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: false, - })); - - onSubmit(value); - } - break; - case KEY_CODES.ESC: - event.preventDefault(); - - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: false, - })); - - break; - case KEY_CODES.TAB: - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: false, - })); - break; - } - }; - const selectSuggestion = (suggestion: QuerySuggestion) => { const nextInputValue = - state.value.substr(0, suggestion.start) + - suggestion.text + - state.value.substr(suggestion.end); + value.substr(0, suggestion.start) + suggestion.text + value.substr(suggestion.end); - setState((prevState) => ({ - ...prevState, - value: nextInputValue, - index: null, - selected: suggestion, - })); + setValue(nextInputValue); + setSelected(suggestion); + setIndex(null); onChange(nextInputValue, nextInputValue.length); }; - const onClickOutside = () => { - if (state.isSuggestionsVisible) { - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: false, - })); + const { onKeyDown, onKeyUp } = useKeyEvents({ + index, + value, + isSuggestionsVisible, + setIndex, + setIsSuggestionsVisible, + suggestions, + selectSuggestion, + onChange, + onSubmit, + }); + const onClickOutside = () => { + if (isSuggestionsVisible) { + setIsSuggestionsVisible(false); onSuggestionSubmit(); } }; const onChangeInputValue = (event: ChangeEvent) => { - const { value, selectionStart } = event.target; - const hasValue = Boolean(value.trim()); + const { value: valueN, selectionStart } = event.target; + const hasValue = Boolean(valueN.trim()); - setState((prevState) => ({ - ...prevState, - value, - inputIsPristine: false, - isSuggestionsVisible: hasValue, - index: null, - })); + setValue(valueN); + + setInputIsPristine(false); + setIndex(null); + + if (!kqlSyntax) { + setQuery(valueN); + return; + } + + setIsSuggestionsVisible(hasValue); if (!hasValue) { - onSubmit(value); + onSubmit(valueN); } - onChange(value, selectionStart!); + onChange(valueN, selectionStart!); }; const onClickInput = (event: MouseEvent & ChangeEvent) => { - event.stopPropagation(); - const { selectionStart } = event.target; - onChange(state.value, selectionStart!); + if (kqlSyntax) { + event.stopPropagation(); + const { selectionStart } = event.target; + onChange(value, selectionStart!); + } }; const onFocus = () => { - setState((prevState) => ({ - ...prevState, - isSuggestionsVisible: true, - })); + if (kqlSyntax) { + setIsSuggestionsVisible(true); + } }; const onClickSuggestion = (suggestion: QuerySuggestion) => { @@ -236,16 +132,11 @@ export const Typeahead: React.FC = ({ if (inputRef.current) inputRef.current.focus(); }; - const onMouseEnterSuggestion = (index: number) => { - setState((prevState) => ({ - ...prevState, - index, - })); + const onMouseEnterSuggestion = (indexN: number) => { + setIndex(indexN); }; const onSuggestionSubmit = () => { - const { value, lastSubmitted, selected } = state; - if ( lastSubmitted !== value && selected && @@ -253,11 +144,8 @@ export const Typeahead: React.FC = ({ ) { onSubmit(value); - setState((prevState) => ({ - ...prevState, - lastSubmitted: value, - selected: null, - })); + setLastSubmitted(value); + setSelected(null); } }; @@ -268,26 +156,30 @@ export const Typeahead: React.FC = ({ { if (node) { inputRef.current = node; } }} disabled={disabled} - value={state.value} - onKeyDown={onKeyDown} - onKeyUp={onKeyUp} + value={value} + onKeyDown={kqlSyntax ? onKeyDown : undefined} + onKeyUp={kqlSyntax ? onKeyUp : undefined} onFocus={onFocus} onChange={onChangeInputValue} onClick={onClickInput} autoComplete="off" spellCheck={false} + data-test-subj={'uptimeKueryBarInput'} + append={} /> {isLoading && ( @@ -302,15 +194,16 @@ export const Typeahead: React.FC = ({ /> )} - - + {kqlSyntax && ( + + )} ); diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts new file mode 100644 index 00000000000000..ac702cc95dd646 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_key_events.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChangeEvent, KeyboardEvent } from 'react'; +import * as React from 'react'; +import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; + +const KEY_CODES = { + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + ENTER: 13, + ESC: 27, + TAB: 9, +}; + +interface Props { + value: string; + index: number | null; + isSuggestionsVisible: boolean; + setIndex: React.Dispatch>; + setIsSuggestionsVisible: React.Dispatch>; + suggestions: QuerySuggestion[]; + selectSuggestion: (suggestion: QuerySuggestion) => void; + onChange: (inputValue: string, selectionStart: number | null) => void; + onSubmit: (inputValue: string) => void; +} + +export const useKeyEvents = ({ + value, + index, + isSuggestionsVisible, + setIndex, + setIsSuggestionsVisible, + suggestions, + selectSuggestion, + onChange, + onSubmit, +}: Props) => { + const incrementIndex = (currentIndex: number) => { + let nextIndex = currentIndex + 1; + if (currentIndex === null || nextIndex >= suggestions.length) { + nextIndex = 0; + } + + setIndex(nextIndex); + }; + + const decrementIndex = (currentIndex: number) => { + let previousIndex: number | null = currentIndex - 1; + if (previousIndex < 0) { + previousIndex = null; + } + setIndex(previousIndex); + }; + + const onKeyUp = (event: KeyboardEvent & ChangeEvent) => { + const { selectionStart } = event.target; + switch (event.keyCode) { + case KEY_CODES.LEFT: + setIsSuggestionsVisible(true); + onChange(value, selectionStart); + break; + case KEY_CODES.RIGHT: + setIsSuggestionsVisible(true); + onChange(value, selectionStart); + break; + } + }; + + const onKeyDown = (event: KeyboardEvent) => { + switch (event.keyCode) { + case KEY_CODES.DOWN: + event.preventDefault(); + if (isSuggestionsVisible) { + incrementIndex(index!); + } else { + setIndex(0); + setIsSuggestionsVisible(true); + } + break; + case KEY_CODES.UP: + event.preventDefault(); + if (isSuggestionsVisible) { + decrementIndex(index!); + } + break; + case KEY_CODES.ENTER: + event.preventDefault(); + if (isSuggestionsVisible && suggestions[index!]) { + selectSuggestion(suggestions[index!]); + } else { + setIsSuggestionsVisible(false); + onSubmit(value); + } + break; + case KEY_CODES.ESC: + event.preventDefault(); + setIsSuggestionsVisible(false); + break; + case KEY_CODES.TAB: + setIsSuggestionsVisible(false); + break; + } + }; + + return { onKeyUp, onKeyDown }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts new file mode 100644 index 00000000000000..2c945c33b9dc7b --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_kql_syntax.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useState } from 'react'; +import { KQL_SYNTAX_LOCAL_STORAGE } from '../../../../../common/constants'; +import { useUrlParams } from '../../../../hooks'; + +interface Props { + setValue: React.Dispatch>; +} + +export const useKqlSyntax = ({ setValue }: Props) => { + const [kqlSyntax, setKqlSyntax] = useState( + localStorage.getItem(KQL_SYNTAX_LOCAL_STORAGE) === 'true' + ); + + const [getUrlParams] = useUrlParams(); + + const { query, search } = getUrlParams(); + + useEffect(() => { + setValue(query || ''); + }, [query, setValue]); + + useEffect(() => { + setValue(search || ''); + }, [search, setValue]); + + useEffect(() => { + if (query || search) { + // if url has query or params we will give them preference on load + // for selecting syntax type + if (query) { + setKqlSyntax(false); + } + if (search) { + setKqlSyntax(true); + } + } else { + setKqlSyntax(localStorage.getItem(KQL_SYNTAX_LOCAL_STORAGE) === 'true'); + } + // This part is meant to run only when component loads + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + localStorage.setItem(KQL_SYNTAX_LOCAL_STORAGE, String(kqlSyntax)); + setValue(''); + }, [kqlSyntax, setValue]); + + return { kqlSyntax, setKqlSyntax }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts new file mode 100644 index 00000000000000..55df62a7e14d67 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/overview/kuery_bar/typeahead/use_simple_kuery.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import { useUrlParams } from '../../../../hooks'; + +export const useSimpleQuery = () => { + const [getUrlParams, updateUrlParams] = useUrlParams(); + + const { query } = getUrlParams(); + + const [debouncedValue, setDebouncedValue] = useState(query ?? ''); + + useEffect(() => { + setDebouncedValue(query ?? ''); + }, [query]); + + useDebounce( + () => { + updateUrlParams({ query: debouncedValue }); + }, + 250, + [debouncedValue] + ); + + return { query, setQuery: setDebouncedValue }; +}; diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx index f9d87aa40d9a69..5ac6351ba5dec9 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_container.tsx @@ -35,7 +35,7 @@ export const MonitorList: React.FC = (props) => { const dispatch = useDispatch(); const [getUrlValues] = useUrlParams(); - const { dateRangeStart, dateRangeEnd, pagination, statusFilter } = getUrlValues(); + const { dateRangeStart, dateRangeEnd, pagination, statusFilter, query } = getUrlValues(); const { lastRefresh } = useContext(UptimeRefreshContext); @@ -50,6 +50,7 @@ export const MonitorList: React.FC = (props) => { pageSize, pagination, statusFilter, + query, }) ); }, [ @@ -61,6 +62,7 @@ export const MonitorList: React.FC = (props) => { pageSize, pagination, statusFilter, + query, ]); return ( diff --git a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx index 68e04dd7a3c0dd..09c832c603d107 100644 --- a/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx +++ b/x-pack/plugins/uptime/public/components/overview/snapshot/snapshot_container.tsx @@ -21,7 +21,7 @@ interface Props { } export const Snapshot: React.FC = ({ height }: Props) => { - const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); + const { dateRangeStart, dateRangeEnd, query } = useGetUrlParams(); const { lastRefresh } = useContext(UptimeRefreshContext); @@ -31,7 +31,7 @@ export const Snapshot: React.FC = ({ height }: Props) => { const dispatch = useDispatch(); useEffect(() => { - dispatch(getSnapshotCountAction({ dateRangeStart, dateRangeEnd, filters: esKuery })); - }, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, dispatch]); + dispatch(getSnapshotCountAction.get({ query, dateRangeStart, dateRangeEnd, filters: esKuery })); + }, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, dispatch, query]); return ; }; diff --git a/x-pack/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts index dc58a77c09ad08..0178a4a0b65da0 100644 --- a/x-pack/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/plugins/uptime/public/hooks/use_url_params.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; @@ -62,39 +62,42 @@ export const useUrlParams: UptimeUrlParamsHook = () => { } }, [dispatch, filters, selectedFilters]); - const updateUrlParams: UpdateUrlParams = (updatedParams) => { - if (!history || !location) return; - const { pathname, search } = location; - const currentParams = getParsedParams(search); - const mergedParams = { - ...currentParams, - ...updatedParams, - }; + const updateUrlParams: UpdateUrlParams = useCallback( + (updatedParams) => { + if (!history || !location) return; + const { pathname, search } = location; + const currentParams = getParsedParams(search); + const mergedParams = { + ...currentParams, + ...updatedParams, + }; - history.push({ - pathname, - search: stringify( - // drop any parameters that have no value - Object.keys(mergedParams).reduce((params, key) => { - const value = mergedParams[key]; - if (value === undefined || value === '') { - return params; - } - return { - ...params, - [key]: value, - }; - }, {}), - { sort: false } - ), - }); - const filterMap = getMapFromFilters(mergedParams.filters); - if (!filterMap) { - dispatch(setSelectedFilters(null)); - } else { - dispatch(setSelectedFilters(mapMapToObject(filterMap))); - } - }; + history.push({ + pathname, + search: stringify( + // drop any parameters that have no value + Object.keys(mergedParams).reduce((params, key) => { + const value = mergedParams[key]; + if (value === undefined || value === '') { + return params; + } + return { + ...params, + [key]: value, + }; + }, {}), + { sort: false } + ), + }); + const filterMap = getMapFromFilters(mergedParams.filters); + if (!filterMap) { + dispatch(setSelectedFilters(null)); + } else { + dispatch(setSelectedFilters(mapMapToObject(filterMap))); + } + }, + [dispatch, history, location] + ); return [useGetUrlParams, updateUrlParams]; }; diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index e02a2c6f9832f6..a2a67fe7347f52 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -22,6 +22,7 @@ import { } from '../../../../../../src/plugins/kibana_react/public'; import { MountWithReduxProvider } from './helper_with_redux'; import { AppState } from '../../state'; +import { stringifyUrlParams } from './stringify_url_params'; interface KibanaProps { services?: KibanaServices; @@ -40,10 +41,18 @@ interface MockRouterProps extends MockKibanaProviderProps history?: History; } +type Url = + | string + | { + path: string; + queryParams: Record; + }; + interface RenderRouterOptions extends KibanaProviderOptions { history?: History; renderOptions?: Omit; state?: Partial; + url?: Url; } /* default mock core */ @@ -87,10 +96,9 @@ export function MockKibanaProvider({ export function MockRouter({ children, core, - history: customHistory, + history = createMemoryHistory(), kibanaProps, }: MockRouterProps) { - const history = customHistory || createMemoryHistory(); return ( @@ -104,18 +112,45 @@ configure({ testIdAttribute: 'data-test-subj' }); /* Custom react testing library render */ export function render( ui: ReactElement, - { history, core, kibanaProps, renderOptions, state }: RenderRouterOptions = {} + { + history = createMemoryHistory(), + core, + kibanaProps, + renderOptions, + state, + url, + }: RenderRouterOptions = {} ) { const testState: AppState = { ...mockState, ...state, }; - return reactTestLibRender( - - - {ui} - - , - renderOptions - ); + + if (url) { + history = getHistoryFromUrl(url); + } + + return { + ...reactTestLibRender( + + + {ui} + + , + renderOptions + ), + history, + }; } + +const getHistoryFromUrl = (url: Url) => { + if (typeof url === 'string') { + return createMemoryHistory({ + initialEntries: [url], + }); + } + + return createMemoryHistory({ + initialEntries: [url.path + stringifyUrlParams(url.queryParams)], + }); +}; diff --git a/x-pack/plugins/uptime/public/lib/helper/url_params/__snapshots__/get_supported_url_params.test.ts.snap b/x-pack/plugins/uptime/public/lib/helper/url_params/__snapshots__/get_supported_url_params.test.ts.snap index c9e0167d9c2172..a5b8c923159674 100644 --- a/x-pack/plugins/uptime/public/lib/helper/url_params/__snapshots__/get_supported_url_params.test.ts.snap +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/__snapshots__/get_supported_url_params.test.ts.snap @@ -11,6 +11,7 @@ Object { "filters": "", "focusConnectorField": false, "pagination": undefined, + "query": undefined, "search": "", "statusFilter": "", } @@ -27,6 +28,7 @@ Object { "filters": "", "focusConnectorField": false, "pagination": undefined, + "query": undefined, "search": "", "statusFilter": "", } @@ -43,6 +45,7 @@ Object { "filters": "", "focusConnectorField": false, "pagination": undefined, + "query": undefined, "search": "monitor.status: down", "statusFilter": "", } @@ -59,6 +62,7 @@ Object { "filters": "", "focusConnectorField": false, "pagination": undefined, + "query": undefined, "search": "", "statusFilter": "", } @@ -75,6 +79,7 @@ Object { "filters": "", "focusConnectorField": false, "pagination": undefined, + "query": undefined, "search": "", "statusFilter": "", } diff --git a/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts b/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts index 6c4400663f17a7..d7363ff9db32e6 100644 --- a/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts +++ b/x-pack/plugins/uptime/public/lib/helper/url_params/get_supported_url_params.ts @@ -22,6 +22,7 @@ export interface UptimeUrlParams { search: string; statusFilter: string; focusConnectorField?: boolean; + query?: string; } const { @@ -75,9 +76,11 @@ export const getSupportedUrlParams = (params: { statusFilter, pagination, focusConnectorField, + query, } = filteredParams; return { + query, pagination, absoluteDateRangeStart: parseAbsoluteDate( dateRangeStart || DATE_RANGE_START, diff --git a/x-pack/plugins/uptime/public/state/actions/ping.ts b/x-pack/plugins/uptime/public/state/actions/ping.ts index 03539b9f9d8a0a..6b997ba184b0b0 100644 --- a/x-pack/plugins/uptime/public/state/actions/ping.ts +++ b/x-pack/plugins/uptime/public/state/actions/ping.ts @@ -12,12 +12,13 @@ import { PingsResponse, GetPingsParams, } from '../../../common/runtime_types'; +import { createAsyncAction } from './utils'; export const clearPings = createAction('CLEAR PINGS'); -export const getPingHistogram = createAction('GET_PING_HISTOGRAM'); -export const getPingHistogramSuccess = createAction('GET_PING_HISTOGRAM_SUCCESS'); -export const getPingHistogramFail = createAction('GET_PING_HISTOGRAM_FAIL'); +export const getPingHistogram = createAsyncAction( + 'GET_PING_HISTOGRAM' +); export const getPings = createAction('GET PINGS'); export const getPingsSuccess = createAction('GET PINGS SUCCESS'); diff --git a/x-pack/plugins/uptime/public/state/actions/snapshot.ts b/x-pack/plugins/uptime/public/state/actions/snapshot.ts index b921c5b1885ed5..b1ff2996009437 100644 --- a/x-pack/plugins/uptime/public/state/actions/snapshot.ts +++ b/x-pack/plugins/uptime/public/state/actions/snapshot.ts @@ -5,15 +5,10 @@ * 2.0. */ -import { createAction } from 'redux-actions'; import { Snapshot } from '../../../common/runtime_types'; +import { createAsyncAction } from './utils'; +import { SnapShotQueryParams } from '../api'; -export interface GetSnapshotPayload { - dateRangeStart: string; - dateRangeEnd: string; - filters?: string; -} - -export const getSnapshotCountAction = createAction('GET_SNAPSHOT_COUNT'); -export const getSnapshotCountActionSuccess = createAction('GET_SNAPSHOT_COUNT_SUCCESS'); -export const getSnapshotCountActionFail = createAction('GET_SNAPSHOT_COUNT_FAIL'); +export const getSnapshotCountAction = createAsyncAction( + 'GET_SNAPSHOT_COUNT' +); diff --git a/x-pack/plugins/uptime/public/state/api/ping.ts b/x-pack/plugins/uptime/public/state/api/ping.ts index 7d1063c6b2fead..e4fc5cc620b55a 100644 --- a/x-pack/plugins/uptime/public/state/api/ping.ts +++ b/x-pack/plugins/uptime/public/state/api/ping.ts @@ -27,6 +27,7 @@ export const fetchPingHistogram: APIFn dateEnd, filters, bucketSize, + query, }) => { const queryParams = { dateStart, @@ -34,6 +35,7 @@ export const fetchPingHistogram: APIFn monitorId, filters, bucketSize, + query, }; return await apiService.get(API_URLS.PING_HISTOGRAM, queryParams); diff --git a/x-pack/plugins/uptime/public/state/api/snapshot.ts b/x-pack/plugins/uptime/public/state/api/snapshot.ts index 79c74f27840253..d8f38128e3202b 100644 --- a/x-pack/plugins/uptime/public/state/api/snapshot.ts +++ b/x-pack/plugins/uptime/public/state/api/snapshot.ts @@ -13,20 +13,20 @@ export interface SnapShotQueryParams { dateRangeStart: string; dateRangeEnd: string; filters?: string; - statusFilter?: string; + query?: string; } export const fetchSnapshotCount = async ({ dateRangeStart, dateRangeEnd, filters, - statusFilter, + query, }: SnapShotQueryParams): Promise => { const queryParams = { dateRangeStart, dateRangeEnd, ...(filters && { filters }), - ...(statusFilter && { statusFilter }), + ...(query && { query }), }; return await apiService.get(API_URLS.SNAPSHOT_COUNT, queryParams, SnapshotType); diff --git a/x-pack/plugins/uptime/public/state/effects/ping.ts b/x-pack/plugins/uptime/public/state/effects/ping.ts index b28d75f6a3bacf..a9e2023d3b7779 100644 --- a/x-pack/plugins/uptime/public/state/effects/ping.ts +++ b/x-pack/plugins/uptime/public/state/effects/ping.ts @@ -6,14 +6,7 @@ */ import { takeLatest } from 'redux-saga/effects'; -import { - getPingHistogram, - getPingHistogramSuccess, - getPingHistogramFail, - getPings, - getPingsSuccess, - getPingsFail, -} from '../actions'; +import { getPingHistogram, getPings, getPingsSuccess, getPingsFail } from '../actions'; import { fetchPingHistogram, fetchPings } from '../api'; import { fetchEffectFactory } from './fetch_effect'; @@ -23,7 +16,7 @@ export function* fetchPingsEffect() { export function* fetchPingHistogramEffect() { yield takeLatest( - String(getPingHistogram), - fetchEffectFactory(fetchPingHistogram, getPingHistogramSuccess, getPingHistogramFail) + String(getPingHistogram.get), + fetchEffectFactory(fetchPingHistogram, getPingHistogram.success, getPingHistogram.fail) ); } diff --git a/x-pack/plugins/uptime/public/state/effects/snapshot.ts b/x-pack/plugins/uptime/public/state/effects/snapshot.ts index b353aad0e3ac18..978f8eb8964ce1 100644 --- a/x-pack/plugins/uptime/public/state/effects/snapshot.ts +++ b/x-pack/plugins/uptime/public/state/effects/snapshot.ts @@ -6,21 +6,17 @@ */ import { takeLatest } from 'redux-saga/effects'; -import { - getSnapshotCountAction, - getSnapshotCountActionFail, - getSnapshotCountActionSuccess, -} from '../actions'; +import { getSnapshotCountAction } from '../actions'; import { fetchSnapshotCount } from '../api'; import { fetchEffectFactory } from './fetch_effect'; export function* fetchSnapshotCountEffect() { yield takeLatest( - getSnapshotCountAction, + getSnapshotCountAction.get, fetchEffectFactory( fetchSnapshotCount, - getSnapshotCountActionSuccess, - getSnapshotCountActionFail + getSnapshotCountAction.success, + getSnapshotCountAction.fail ) ); } diff --git a/x-pack/plugins/uptime/public/state/reducers/ping.ts b/x-pack/plugins/uptime/public/state/reducers/ping.ts index d96c1f64b499c6..a91734d77b4ab8 100644 --- a/x-pack/plugins/uptime/public/state/reducers/ping.ts +++ b/x-pack/plugins/uptime/public/state/reducers/ping.ts @@ -6,7 +6,7 @@ */ import { handleActions, Action } from 'redux-actions'; -import { getPingHistogram, getPingHistogramSuccess, getPingHistogramFail } from '../actions'; +import { getPingHistogram } from '../actions'; import { HistogramResult } from '../../../common/runtime_types'; export interface PingState { @@ -25,18 +25,18 @@ type MonitorStatusPayload = HistogramResult & Error; export const pingReducer = handleActions( { - [String(getPingHistogram)]: (state) => ({ + [String(getPingHistogram.get)]: (state) => ({ ...state, loading: true, }), - [String(getPingHistogramSuccess)]: (state: PingState, action: Action) => ({ + [String(getPingHistogram.success)]: (state: PingState, action: Action) => ({ ...state, loading: false, pingHistogram: { ...action.payload }, }), - [String(getPingHistogramFail)]: (state, action: Action) => ({ + [String(getPingHistogram.fail)]: (state, action: Action) => ({ ...state, errors: [...state.errors, action.payload], loading: false, diff --git a/x-pack/plugins/uptime/public/state/reducers/snapshot.test.ts b/x-pack/plugins/uptime/public/state/reducers/snapshot.test.ts index c2c2f65a4af4ed..0ab6cf309922ba 100644 --- a/x-pack/plugins/uptime/public/state/reducers/snapshot.test.ts +++ b/x-pack/plugins/uptime/public/state/reducers/snapshot.test.ts @@ -6,15 +6,12 @@ */ import { snapshotReducer } from './snapshot'; -import { - getSnapshotCountAction, - getSnapshotCountActionSuccess, - getSnapshotCountActionFail, -} from '../actions'; +import { getSnapshotCountAction } from '../actions'; +import { IHttpFetchError } from '../../../../../../src/core/public'; describe('snapshot reducer', () => { it('updates existing state', () => { - const action = getSnapshotCountAction({ + const action = getSnapshotCountAction.get({ dateRangeStart: 'now-15m', dateRangeEnd: 'now', filters: 'foo: bar', @@ -32,7 +29,7 @@ describe('snapshot reducer', () => { }); it(`sets the state's status to loading during a fetch`, () => { - const action = getSnapshotCountAction({ + const action = getSnapshotCountAction.get({ dateRangeStart: 'now-15m', dateRangeEnd: 'now', }); @@ -40,7 +37,7 @@ describe('snapshot reducer', () => { }); it('changes the count when a snapshot fetch succeeds', () => { - const action = getSnapshotCountActionSuccess({ + const action = getSnapshotCountAction.success({ up: 10, down: 15, total: 25, @@ -50,8 +47,8 @@ describe('snapshot reducer', () => { }); it('appends a current error to existing errors list', () => { - const action = getSnapshotCountActionFail( - new Error(`I couldn't get your data because the server denied the request`) + const action = getSnapshotCountAction.fail( + new Error(`I couldn't get your data because the server denied the request`) as IHttpFetchError ); expect(snapshotReducer(undefined, action)).toMatchSnapshot(); diff --git a/x-pack/plugins/uptime/public/state/reducers/snapshot.ts b/x-pack/plugins/uptime/public/state/reducers/snapshot.ts index fd7f406dd9a46f..82599416ab2b83 100644 --- a/x-pack/plugins/uptime/public/state/reducers/snapshot.ts +++ b/x-pack/plugins/uptime/public/state/reducers/snapshot.ts @@ -7,11 +7,7 @@ import { Action } from 'redux-actions'; import { Snapshot } from '../../../common/runtime_types'; -import { - getSnapshotCountAction, - getSnapshotCountActionSuccess, - getSnapshotCountActionFail, -} from '../actions'; +import { getSnapshotCountAction } from '../actions'; export interface SnapshotState { count: Snapshot; @@ -31,18 +27,18 @@ const initialState: SnapshotState = { export function snapshotReducer(state = initialState, action: Action): SnapshotState { switch (action.type) { - case String(getSnapshotCountAction): + case String(getSnapshotCountAction.get): return { ...state, loading: true, }; - case String(getSnapshotCountActionSuccess): + case String(getSnapshotCountAction.success): return { ...state, count: action.payload, loading: false, }; - case String(getSnapshotCountActionFail): + case String(getSnapshotCountAction.fail): return { ...state, errors: [...state.errors, action.payload], diff --git a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts index 0bda9e4e8d32aa..ef90794e634b77 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_monitor_states.ts @@ -25,6 +25,7 @@ export interface GetMonitorStatesParams { pageSize: number; filters?: string | null; statusFilter?: string; + query?: string; } // To simplify the handling of the group of pagination vars they're passed back to the client as a string @@ -48,6 +49,7 @@ export const getMonitorStates: UMElasticsearchQueryFn< pageSize, filters, statusFilter, + query, }) => { pagination = pagination || CONTEXT_DEFAULTS.CURSOR_PAGINATION; statusFilter = statusFilter === null ? undefined : statusFilter; @@ -59,7 +61,8 @@ export const getMonitorStates: UMElasticsearchQueryFn< pagination, filters && filters !== '' ? JSON.parse(filters) : null, pageSize, - statusFilter + statusFilter, + query ); const size = Math.min(queryContext.size, QUERY.DEFAULT_AGGS_CAP); diff --git a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts index 5c247cb8738e6a..d59da8029f1b95 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_ping_histogram.ts @@ -22,12 +22,14 @@ export interface GetPingHistogramParams { monitorId?: string; bucketSize?: string; + + query?: string; } export const getPingHistogram: UMElasticsearchQueryFn< GetPingHistogramParams, HistogramResult -> = async ({ uptimeEsClient, from, to, filters, monitorId, bucketSize }) => { +> = async ({ uptimeEsClient, from, to, filters, monitorId, bucketSize, query }) => { const boolFilters = filters ? JSON.parse(filters) : null; const additionalFilters = []; if (monitorId) { @@ -44,6 +46,20 @@ export const getPingHistogram: UMElasticsearchQueryFn< query: { bool: { filter, + ...(query + ? { + minimum_should_match: 1, + should: [ + { + multi_match: { + query: escape(query), + type: 'phrase_prefix', + fields: ['monitor.id.text', 'monitor.name.text', 'url.full.text'], + }, + }, + ], + } + : {}), }, }, size: 0, diff --git a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts index 880f4051509d2c..2999f9ebca0658 100644 --- a/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts +++ b/x-pack/plugins/uptime/server/lib/requests/get_snapshot_counts.ts @@ -15,6 +15,7 @@ export interface GetSnapshotCountParams { dateRangeStart: string; dateRangeEnd: string; filters?: string | null; + query?: string; } export const getSnapshotCount: UMElasticsearchQueryFn = async ({ @@ -22,6 +23,7 @@ export const getSnapshotCount: UMElasticsearchQueryFn => { const context = new QueryContext( uptimeEsClient, @@ -29,7 +31,9 @@ export const getSnapshotCount: UMElasticsearchQueryFn => { const { body: res } = await context.search({ - body: statusCountBody(await context.dateAndCustomFilters()), + body: statusCountBody(await context.dateAndCustomFilters(), context), }); return ( @@ -52,17 +56,32 @@ const statusCount = async (context: QueryContext): Promise => { ); }; -const statusCountBody = (filters: ESFilter[]) => { +const statusCountBody = (filters: ESFilter[], context: QueryContext) => { return { size: 0, query: { bool: { + ...(context.query + ? { + minimum_should_match: 1, + should: [ + { + multi_match: { + query: escape(context.query), + type: 'phrase_prefix', + fields: ['monitor.id.text', 'monitor.name.text', 'url.full.text'], + }, + }, + ], + } + : {}), filter: [ { exists: { field: 'summary', }, }, + ...filters, ], }, diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 50fcd94b57a19a..639a24a2bdffa4 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -52,7 +52,25 @@ const queryBody = async (queryContext: QueryContext, searchAfter: any, size: num const body = { size: 0, - query: { bool: { filter: filters } }, + query: { + bool: { + filter: filters, + ...(queryContext.query + ? { + minimum_should_match: 1, + should: [ + { + multi_match: { + query: escape(queryContext.query), + type: 'phrase_prefix', + fields: ['monitor.id.text', 'monitor.name.text', 'url.full.text'], + }, + }, + ], + } + : {}), + }, + }, aggs: { monitors: { composite: { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts index 524778cff28de9..f377ba74dc8af5 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/query_context.ts @@ -21,6 +21,7 @@ export class QueryContext { size: number; statusFilter?: string; hasTimespanCache?: boolean; + query?: string; constructor( database: UptimeESClient, @@ -29,7 +30,8 @@ export class QueryContext { pagination: CursorPagination, filterClause: any | null, size: number, - statusFilter?: string + statusFilter?: string, + query?: string ) { this.callES = database; this.dateRangeStart = dateRangeStart; @@ -38,6 +40,7 @@ export class QueryContext { this.filterClause = filterClause; this.size = size; this.statusFilter = statusFilter; + this.query = query; } async search(params: TParams) { diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index daa7dc509ef833..8e984684969523 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -19,6 +19,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ filters: schema.maybe(schema.string()), pagination: schema.maybe(schema.string()), statusFilter: schema.maybe(schema.string()), + query: schema.maybe(schema.string()), pageSize: schema.number(), _debug: schema.maybe(schema.boolean()), }), @@ -34,6 +35,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ pagination, statusFilter, pageSize, + query, } = request.query; const decodedPagination = pagination @@ -47,6 +49,7 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ pagination: decodedPagination, pageSize, filters, + query, // this is added to make typescript happy, // this sort of reassignment used to be further downstream but I've moved it here // because this code is going to be decomissioned soon diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index 98d8fe99a43f7e..8c0810d946a1bf 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -20,11 +20,12 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe monitorId: schema.maybe(schema.string()), filters: schema.maybe(schema.string()), bucketSize: schema.maybe(schema.string()), + query: schema.maybe(schema.string()), _debug: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { - const { dateStart, dateEnd, monitorId, filters, bucketSize } = request.query; + const { dateStart, dateEnd, monitorId, filters, bucketSize, query } = request.query; return await libs.requests.getPingHistogram({ uptimeEsClient, @@ -33,6 +34,7 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe monitorId, filters, bucketSize, + query, }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 675384c573bc8c..8c80c4d512b569 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -18,17 +18,19 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs dateRangeStart: schema.string(), dateRangeEnd: schema.string(), filters: schema.maybe(schema.string()), + query: schema.maybe(schema.string()), _debug: schema.maybe(schema.boolean()), }), }, handler: async ({ uptimeEsClient, request }): Promise => { - const { dateRangeStart, dateRangeEnd, filters } = request.query; + const { dateRangeStart, dateRangeEnd, filters, query } = request.query; return await libs.requests.getSnapshotCount({ uptimeEsClient, dateRangeStart, dateRangeEnd, filters, + query, }); }, }); diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index 9f2c55f68fe9be..6c9eb24070d8fb 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -13,6 +13,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + describe('overview page', function () { const DEFAULT_DATE_START = 'Sep 10, 2019 @ 12:40:08.078'; const DEFAULT_DATE_END = 'Sep 11, 2019 @ 19:40:08.078'; @@ -182,6 +184,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); + it('can change query syntax to kql', async () => { + await testSubjects.click('syntaxChangeToKql'); + await testSubjects.click('toggleKqlSyntax'); + await testSubjects.exists('syntaxChangeToSimple'); + }); + it('runs filter query without issues', async () => { await uptime.inputFilterQuery('monitor.status:up and monitor.id:"0000-intermittent"'); await uptime.pageHasExpectedIds(['0000-intermittent']);