[Uptime] Migrate uptime query bar to global kuery bar (#93889) (#96389)

This commit is contained in:
Shahzad 2021-04-07 15:16:26 +02:00 committed by GitHub
parent ac4e3fecfe
commit 5d54c30d78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 566 additions and 1996 deletions

View file

@ -415,6 +415,34 @@
"signature": [
"string | undefined"
]
},
{
"tags": [],
"id": "def-public.QueryStringInputProps.autoSubmit",
"type": "CompoundType",
"label": "autoSubmit",
"description": [],
"source": {
"path": "src/plugins/data/public/ui/query_string_input/query_string_input.tsx",
"lineNumber": 72
},
"signature": [
"boolean | undefined"
]
},
{
"tags": [],
"id": "def-public.QueryStringInputProps.storageKey",
"type": "string",
"label": "storageKey",
"description": [],
"source": {
"path": "src/plugins/data/public/ui/query_string_input/query_string_input.tsx",
"lineNumber": 76
},
"signature": [
"string | undefined"
]
}
],
"source": {
@ -460,7 +488,7 @@
"section": "def-public.SearchBarProps",
"text": "SearchBarProps"
},
", \"filters\" | \"query\" | \"intl\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, \"filters\" | \"query\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, any> & { WrappedComponent: React.ComponentType<Pick<",
", \"filters\" | \"query\" | \"intl\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"storageKey\" | \"disableLanguageSwitcher\" | \"isInvalid\" | \"autoSubmit\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, \"filters\" | \"query\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"storageKey\" | \"disableLanguageSwitcher\" | \"isInvalid\" | \"autoSubmit\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\">, any> & { WrappedComponent: React.ComponentType<Pick<",
{
"pluginId": "data",
"scope": "public",
@ -468,7 +496,7 @@
"section": "def-public.SearchBarProps",
"text": "SearchBarProps"
},
", \"filters\" | \"query\" | \"intl\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\"> & ReactIntl.InjectedIntlProps>; }"
", \"filters\" | \"query\" | \"intl\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"storageKey\" | \"disableLanguageSwitcher\" | \"isInvalid\" | \"autoSubmit\" | \"timeHistory\" | \"onFiltersUpdated\" | \"onRefreshChange\"> & ReactIntl.InjectedIntlProps>; }"
],
"initialIsOpen": false
},
@ -480,7 +508,7 @@
"description": [],
"source": {
"path": "src/plugins/data/public/ui/search_bar/search_bar.tsx",
"lineNumber": 80
"lineNumber": 84
},
"signature": [
"SearchBarOwnProps & SearchBarInjectedDeps"
@ -521,4 +549,4 @@
"misc": [],
"objects": []
}
}
}

View file

@ -482,7 +482,7 @@
},
"signature": [
"SearchBarOwnProps",
" & { appName: string; useDefaultBehaviors?: boolean | undefined; savedQueryId?: string | undefined; onSavedQueryIdChange?: ((savedQueryId?: string | undefined) => void) | undefined; } & Pick<SearchBarProps, \"filters\" | \"query\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"onFiltersUpdated\" | \"onRefreshChange\"> & { config?: TopNavMenuData[] | undefined; badges?: (({ iconType?: string | React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined; iconSide?: \"left\" | \"right\" | undefined; color?: string | undefined; isDisabled?: boolean | undefined; closeButtonProps?: Partial<",
" & { appName: string; useDefaultBehaviors?: boolean | undefined; savedQueryId?: string | undefined; onSavedQueryIdChange?: ((savedQueryId?: string | undefined) => void) | undefined; } & Pick<SearchBarProps, \"filters\" | \"query\" | \"indexPatterns\" | \"isLoading\" | \"customSubmitButton\" | \"screenTitle\" | \"dataTestSubj\" | \"showQueryBar\" | \"showQueryInput\" | \"showFilterBar\" | \"showDatePicker\" | \"showAutoRefreshOnly\" | \"isRefreshPaused\" | \"refreshInterval\" | \"dateRangeFrom\" | \"dateRangeTo\" | \"showSaveQuery\" | \"savedQuery\" | \"onQueryChange\" | \"onQuerySubmit\" | \"onSaved\" | \"onSavedQueryUpdated\" | \"onClearSavedQuery\" | \"onRefresh\" | \"indicateNoData\" | \"placeholder\" | \"isClearable\" | \"iconType\" | \"nonKqlMode\" | \"nonKqlModeHelpText\" | \"storageKey\" | \"disableLanguageSwitcher\" | \"isInvalid\" | \"autoSubmit\" | \"onFiltersUpdated\" | \"onRefreshChange\"> & { config?: TopNavMenuData[] | undefined; badges?: (({ iconType?: string | React.ComponentClass<{}, any> | React.FunctionComponent<{}> | undefined; iconSide?: \"left\" | \"right\" | undefined; color?: string | undefined; isDisabled?: boolean | undefined; closeButtonProps?: Partial<",
"EuiIconProps",
"> | undefined; } & ",
"CommonProps",

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) &gt; [autoSubmit](./kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md)
## QueryStringInputProps.autoSubmit property
<b>Signature:</b>
```typescript
autoSubmit?: boolean;
```

View file

@ -14,6 +14,7 @@ export interface QueryStringInputProps
| Property | Type | Description |
| --- | --- | --- |
| [autoSubmit](./kibana-plugin-plugins-data-public.querystringinputprops.autosubmit.md) | <code>boolean</code> | |
| [bubbleSubmitEvent](./kibana-plugin-plugins-data-public.querystringinputprops.bubblesubmitevent.md) | <code>boolean</code> | |
| [className](./kibana-plugin-plugins-data-public.querystringinputprops.classname.md) | <code>string</code> | |
| [dataTestSubj](./kibana-plugin-plugins-data-public.querystringinputprops.datatestsubj.md) | <code>string</code> | |
@ -36,5 +37,6 @@ export interface QueryStringInputProps
| [query](./kibana-plugin-plugins-data-public.querystringinputprops.query.md) | <code>Query</code> | |
| [screenTitle](./kibana-plugin-plugins-data-public.querystringinputprops.screentitle.md) | <code>string</code> | |
| [size](./kibana-plugin-plugins-data-public.querystringinputprops.size.md) | <code>SuggestionsListSize</code> | |
| [storageKey](./kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md) | <code>string</code> | |
| [submitOnBlur](./kibana-plugin-plugins-data-public.querystringinputprops.submitonblur.md) | <code>boolean</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [QueryStringInputProps](./kibana-plugin-plugins-data-public.querystringinputprops.md) &gt; [storageKey](./kibana-plugin-plugins-data-public.querystringinputprops.storagekey.md)
## QueryStringInputProps.storageKey property
<b>Signature:</b>
```typescript
storageKey?: string;
```

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "intl" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "intl" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "isInvalid" | "storageKey" | "intl" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "disableLanguageSwitcher" | "autoSubmit" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "isInvalid" | "storageKey" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "disableLanguageSwitcher" | "autoSubmit" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "placeholder" | "isLoading" | "iconType" | "indexPatterns" | "filters" | "dataTestSubj" | "isClearable" | "isInvalid" | "storageKey" | "intl" | "refreshInterval" | "nonKqlMode" | "nonKqlModeHelpText" | "screenTitle" | "disableLanguageSwitcher" | "autoSubmit" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
}
```

View file

@ -7,6 +7,7 @@
*/
export const DEFAULT_QUERY_LANGUAGE = 'kuery';
export const KIBANA_USER_QUERY_LANGUAGE_KEY = 'kibana.userQueryLanguage';
export const UI_SETTINGS = {
META_FIELDS: 'metaFields',

View file

@ -2030,6 +2030,8 @@ export const QueryStringInput: (props: QueryStringInputProps) => JSX.Element;
//
// @public (undocumented)
export interface QueryStringInputProps {
// (undocumented)
autoSubmit?: boolean;
// (undocumented)
bubbleSubmitEvent?: boolean;
// (undocumented)
@ -2079,6 +2081,8 @@ export interface QueryStringInputProps {
// (undocumented)
size?: SuggestionsListSize;
// (undocumented)
storageKey?: string;
// (undocumented)
submitOnBlur?: boolean;
}

View file

@ -11,7 +11,7 @@ import { skip } from 'rxjs/operators';
import { PublicMethodsOf } from '@kbn/utility-types';
import { CoreStart } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { Query, UI_SETTINGS } from '../../../common';
import { KIBANA_USER_QUERY_LANGUAGE_KEY, Query, UI_SETTINGS } from '../../../common';
export class QueryStringManager {
private query$: BehaviorSubject<Query>;
@ -25,7 +25,7 @@ export class QueryStringManager {
private getDefaultLanguage() {
return (
this.storage.get('kibana.userQueryLanguage') ||
this.storage.get(KIBANA_USER_QUERY_LANGUAGE_KEY) ||
this.uiSettings.get(UI_SETTINGS.SEARCH_QUERY_LANGUAGE)
);
}

View file

@ -37,6 +37,7 @@ import { QueryLanguageSwitcher } from './language_switcher';
import { PersistedLog, getQueryLog, matchPairs, toUser, fromUser } from '../../query';
import { SuggestionsListSize } from '../typeahead/suggestions_component';
import { SuggestionsComponent } from '..';
import { KIBANA_USER_QUERY_LANGUAGE_KEY } from '../../../common';
export interface QueryStringInputProps {
indexPatterns: Array<IIndexPattern | string>;
@ -67,6 +68,14 @@ export interface QueryStringInputProps {
*/
nonKqlMode?: 'lucene' | 'text';
nonKqlModeHelpText?: string;
/**
* @param autoSubmit if user selects a value, in that case kuery will be auto submitted
*/
autoSubmit?: boolean;
/**
* @param storageKey this key is used to use user preference between kql and non-kql mode
*/
storageKey?: string;
}
interface Props extends QueryStringInputProps {
@ -99,6 +108,10 @@ const KEY_CODES = {
// Needed for React.lazy
// eslint-disable-next-line import/no-default-export
export default class QueryStringInputUI extends Component<Props, State> {
static defaultProps = {
storageKey: KIBANA_USER_QUERY_LANGUAGE_KEY,
};
public state: State = {
isSuggestionsVisible: false,
index: null,
@ -218,7 +231,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
const recentSearches = this.persistedLog.get();
const matchingRecentSearches = recentSearches.filter((recentQuery) => {
const recentQueryString = typeof recentQuery === 'object' ? toUser(recentQuery) : recentQuery;
return recentQueryString.includes(query);
return recentQueryString !== '' && recentQueryString.includes(query);
});
return matchingRecentSearches.map((recentSearch) => {
const text = toUser(recentSearch);
@ -393,8 +406,13 @@ export default class QueryStringInputUI extends Component<Props, State> {
selectionStart: start + (cursorIndex ? cursorIndex : text.length),
selectionEnd: start + (cursorIndex ? cursorIndex : text.length),
});
const isTypeRecentSearch = type === QuerySuggestionTypes.RecentSearch;
if (type === QuerySuggestionTypes.RecentSearch) {
const isAutoSubmitAndValid =
this.props.autoSubmit &&
(type === QuerySuggestionTypes.Value || [':*', ': *'].includes(value.trim()));
if (isTypeRecentSearch || isAutoSubmitAndValid) {
this.setState({ isSuggestionsVisible: false, index: null });
this.onSubmit({ query: newQueryString, language: this.props.query.language });
}
@ -488,12 +506,16 @@ export default class QueryStringInputUI extends Component<Props, State> {
body: JSON.stringify({ opt_in: language === 'kuery' }),
});
this.services.storage.set('kibana.userQueryLanguage', language);
const storageKey = this.props.storageKey;
this.services.storage.set(storageKey!, language);
const newQuery = { query: '', language };
this.onChange(newQuery);
this.onSubmit(newQuery);
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`);
this.reportUiCounter?.(
METRIC_TYPE.LOADED,
storageKey ? `${storageKey}:language:${language}` : `query_string:language:${language}`
);
};
private onOutsideClick = () => {
@ -756,6 +778,9 @@ export default class QueryStringInputUI extends Component<Props, State> {
})}
onClick={() => {
this.onQueryStringChange('');
if (this.props.autoSubmit) {
this.onSubmit({ query: '', language: this.props.query.language });
}
}}
>
<EuiIcon className="euiFormControlLayoutClearButton__icon" type="cross" />

View file

@ -22943,7 +22943,6 @@
"xpack.uptime.filterPopout.searchMessage.ariaLabel": "{title} を検索",
"xpack.uptime.filterPopover.filterItem.label": "{title} {item}でフィルタリングします。",
"xpack.uptime.integrationLink.missingDataMessage": "この統合に必要なデータが見つかりませんでした。",
"xpack.uptime.kueryBar.indexPatternMissingWarningMessage": "インデックスパターンの取得中にエラーが発生しました。",
"xpack.uptime.locationAvailabilityViewToggleLegend": "トグルを表示",
"xpack.uptime.locationMap.locations.missing.message": "重要な位置情報構成がありません。{codeBlock}フィールドを使用して、アップタイムチェック用に一意の地域を作成できます。",
"xpack.uptime.locationMap.locations.missing.message1": "詳細については、ドキュメンテーションを参照してください。",
@ -23083,9 +23082,6 @@
"xpack.uptime.overviewPageLink.disabled.ariaLabel": "無効になったページ付けボタンです。モニターリストがこれ以上ナビゲーションできないことを示しています。",
"xpack.uptime.overviewPageLink.next.ariaLabel": "次の結果ページ",
"xpack.uptime.overviewPageLink.prev.ariaLabel": "前の結果ページ",
"xpack.uptime.overviewPageParsingErrorCallout.content": "フィルタークエリの解析中にエラーが発生しました。{content}",
"xpack.uptime.overviewPageParsingErrorCallout.noMessage": "エラーメッセージはありませんでした",
"xpack.uptime.overviewPageParsingErrorCallout.title": "エラーを解析中",
"xpack.uptime.page_header.settingsLink": "設定",
"xpack.uptime.pingist.durationSecondsColumnFormatting": "{seconds}秒",
"xpack.uptime.pingist.durationSecondsColumnFormatting.singular": "{seconds}秒",

View file

@ -23301,7 +23301,6 @@
"xpack.uptime.filterPopout.searchMessage.ariaLabel": "搜索 {title}",
"xpack.uptime.filterPopover.filterItem.label": "按 {title} {item} 筛选。",
"xpack.uptime.integrationLink.missingDataMessage": "未找到此集成的所需数据。",
"xpack.uptime.kueryBar.indexPatternMissingWarningMessage": "检索索引模式时出错。",
"xpack.uptime.locationAvailabilityViewToggleLegend": "视图切换",
"xpack.uptime.locationMap.locations.missing.message": "重要的地理位置配置缺失。您可以使用 {codeBlock} 字段为您的运行时间检查创建独特的地理区域。",
"xpack.uptime.locationMap.locations.missing.message1": "在我们的文档中获取更多的信息。",
@ -23441,9 +23440,6 @@
"xpack.uptime.overviewPageLink.disabled.ariaLabel": "禁用的分页按钮表示在监测列表中无法进行进一步导航。",
"xpack.uptime.overviewPageLink.next.ariaLabel": "下页结果",
"xpack.uptime.overviewPageLink.prev.ariaLabel": "上页结果",
"xpack.uptime.overviewPageParsingErrorCallout.content": "解析筛选查询时出错。{content}",
"xpack.uptime.overviewPageParsingErrorCallout.noMessage": "没有错误消息",
"xpack.uptime.overviewPageParsingErrorCallout.title": "解析错误",
"xpack.uptime.page_header.settingsLink": "设置",
"xpack.uptime.pingist.durationSecondsColumnFormatting": "{seconds} 秒",
"xpack.uptime.pingist.durationSecondsColumnFormatting.singular": "{seconds} 秒",

View file

@ -14,12 +14,5 @@
"server": true,
"ui": true,
"version": "8.0.0",
"requiredBundles": [
"observability",
"kibanaReact",
"home",
"data",
"ml",
"maps"
]
"requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "maps"]
}

View file

@ -29,6 +29,7 @@ import {
import { alertTypeInitializers } from '../lib/alert_types';
import { FetchDataParams, ObservabilityPublicSetup } from '../../../observability/public';
import { PLUGIN } from '../../common/constants/plugin';
import { IStorageWrapper } from '../../../../../src/plugins/kibana_utils/public';
export interface ClientPluginsSetup {
data: DataPublicPluginSetup;
@ -43,6 +44,13 @@ export interface ClientPluginsStart {
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
}
export interface UptimePluginServices extends Partial<CoreStart> {
embeddable: EmbeddableStart;
data: DataPublicPluginStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
storage: IStorageWrapper;
}
export type ClientSetup = void;
export type ClientStart = void;

View file

@ -31,6 +31,7 @@ import { store } from '../state';
import { kibanaService } from '../state/kibana_service';
import { ActionMenu } from '../components/common/header/action_menu';
import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common';
import { Storage } from '../../../../../src/plugins/kibana_utils/public';
export interface UptimeAppColors {
danger: string;
@ -96,12 +97,20 @@ const Application = (props: UptimeAppProps) => {
store.dispatch(setBasePath(basePath));
const storage = new Storage(window.localStorage);
return (
<EuiErrorBoundary>
<i18nCore.Context>
<ReduxProvider store={store}>
<KibanaContextProvider
services={{ ...core, ...plugins, triggersActionsUi: startPlugins.triggersActionsUi }}
services={{
...core,
...plugins,
storage,
data: startPlugins.data,
triggersActionsUi: startPlugins.triggersActionsUi,
}}
>
<Router history={appMountParameters.history}>
<EuiThemeProvider darkMode={darkMode}>

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OverviewPageParsingErrorCallout renders without errors when a valid error is provided 1`] = `
<EuiCallOut
color="danger"
iconType="alert"
style={
Object {
"width": "100%",
}
}
title="Parsing error"
>
<p>
<FormattedMessage
defaultMessage="There was an error parsing the filter query. {content}"
id="xpack.uptime.overviewPageParsingErrorCallout.content"
values={
Object {
"content": <EuiCodeBlock>
Unable to convert to Elasticsearch query, invalid syntax.
</EuiCodeBlock>,
}
}
/>
</p>
</EuiCallOut>
`;
exports[`OverviewPageParsingErrorCallout renders without errors when an error with no message is provided 1`] = `
<EuiCallOut
color="danger"
iconType="alert"
style={
Object {
"width": "100%",
}
}
title="Parsing error"
>
<p>
<FormattedMessage
defaultMessage="There was an error parsing the filter query. {content}"
id="xpack.uptime.overviewPageParsingErrorCallout.content"
values={
Object {
"content": <EuiCodeBlock>
There was no error message
</EuiCodeBlock>,
}
}
/>
</p>
</EuiCallOut>
`;

View file

@ -0,0 +1,62 @@
/*
* 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 { EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { QueryStringInput } from '../../../../../../../../src/plugins/data/public';
import { useIndexPattern } from '../../query_bar/use_index_pattern';
import { isValidKuery } from '../../query_bar/query_bar';
import * as labels from '../translations';
import { useGetUrlParams } from '../../../../hooks';
interface Props {
query: string;
onChange: (query: string) => void;
}
export const AlertQueryBar = ({ query, onChange }: Props) => {
const { index_pattern: indexPattern } = useIndexPattern();
const { search } = useGetUrlParams();
const [inputVal, setInputVal] = useState<string>(search ?? '');
useEffect(() => {
onChange(search);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<EuiFlexItem grow={1} style={{ flexBasis: 485 }}>
<QueryStringInput
indexPatterns={indexPattern ? [indexPattern] : []}
iconType="search"
isClearable={true}
onChange={(queryN) => {
setInputVal(queryN?.query as string);
if (isValidKuery(queryN?.query as string)) {
// we want to submit when user clears or paste a complete kuery
onChange(queryN.query as string);
}
}}
onSubmit={(queryN) => {
if (queryN) onChange(queryN.query as string);
}}
query={{ query: inputVal, language: 'kuery' }}
aria-label={labels.ALERT_KUERY_BAR_ARIA}
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
autoSubmit={true}
disableLanguageSwitcher={true}
isInvalid={!!(inputVal && !query)}
placeholder={i18n.translate('xpack.uptime.alerts.searchPlaceholder.kql', {
defaultMessage: 'Filter using kql syntax',
})}
/>
</EuiFlexItem>
);
};

View file

@ -5,30 +5,18 @@
* 2.0.
*/
import React, { useMemo, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { isRight } from 'fp-ts/lib/Either';
import {
selectMonitorStatusAlert,
overviewFiltersSelector,
snapshotDataSelector,
esKuerySelector,
selectedFiltersSelector,
} from '../../../../state/selectors';
import { AlertMonitorStatusComponent } from '../index';
import {
fetchOverviewFilters,
setSearchTextAction,
setEsKueryString,
getSnapshotCountAction,
} from '../../../../state/actions';
import { overviewFiltersSelector, selectedFiltersSelector } from '../../../../state/selectors';
import { AlertMonitorStatusComponent } from '../monitor_status_alert/alert_monitor_status';
import { fetchOverviewFilters, setSearchTextAction } from '../../../../state/actions';
import {
AtomicStatusCheckParamsType,
GetMonitorAvailabilityParamsType,
} from '../../../../../common/runtime_types';
import { useIndexPattern } from '../../kuery_bar/use_index_pattern';
import { useUpdateKueryString } from '../../../../hooks';
import { useSnapShotCount } from './use_snap_shot';
interface Props {
alertParams: { [key: string]: any };
@ -63,27 +51,17 @@ export const AlertMonitorStatus: React.FC<Props> = ({
}, [alertParams, dispatch]);
const overviewFilters = useSelector(overviewFiltersSelector);
const { locations } = useSelector(selectMonitorStatusAlert);
useEffect(() => {
if (alertParams.search) {
dispatch(setSearchTextAction(alertParams.search));
}
}, [alertParams, dispatch]);
const { index_pattern: indexPattern } = useIndexPattern();
const { count, loading } = useSelector(snapshotDataSelector);
const esKuery = useSelector(esKuerySelector);
const [esFilters] = useUpdateKueryString(
indexPattern,
alertParams.search,
alertParams.filters === undefined || typeof alertParams.filters === 'string'
? ''
: JSON.stringify(Array.from(Object.entries(alertParams.filters)))
);
useEffect(() => {
dispatch(setEsKueryString(esFilters ?? ''));
}, [dispatch, esFilters]);
const { count, loading } = useSnapShotCount({
query: alertParams.search,
filters: alertParams.filters,
});
const isOldAlert = React.useMemo(
() =>
@ -92,15 +70,6 @@ export const AlertMonitorStatus: React.FC<Props> = ({
!isRight(GetMonitorAvailabilityParamsType.decode(alertParams)),
[alertParams]
);
useEffect(() => {
dispatch(
getSnapshotCountAction.get({
dateRangeStart: 'now-24h',
dateRangeEnd: 'now',
filters: esKuery,
})
);
}, [dispatch, esKuery]);
const selectedFilters = useSelector(selectedFiltersSelector);
useEffect(() => {
@ -118,19 +87,14 @@ export const AlertMonitorStatus: React.FC<Props> = ({
}
}, [alertParams, setAlertParams, selectedFilters]);
const { pathname } = useLocation();
const shouldUpdateUrl = useMemo(() => pathname.indexOf('app/uptime') !== -1, [pathname]);
return (
<AlertMonitorStatusComponent
alertParams={alertParams}
enabled={enabled}
hasFilters={!!overviewFilters?.filters}
isOldAlert={isOldAlert}
locations={locations || []}
numTimes={numTimes}
setAlertParams={setAlertParams}
shouldUpdateUrl={shouldUpdateUrl}
snapshotCount={count.total}
snapshotLoading={loading}
timerange={timerange}

View file

@ -5,7 +5,6 @@
* 2.0.
*/
export { AlertMonitorStatus } from './alert_monitor_status';
export {
ToggleAlertFlyoutButton,
ToggleAlertFlyoutButtonProps,

View file

@ -0,0 +1,34 @@
/*
* 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 { useUpdateKueryString } from '../../../../hooks';
import { useIndexPattern } from '../../query_bar/use_index_pattern';
import { useFetcher } from '../../../../../../observability/public';
import { fetchSnapshotCount } from '../../../../state/api';
export const useSnapShotCount = ({ query, filters }: { query: string; filters: [] | string }) => {
const parsedFilters =
filters === undefined || typeof filters === 'string'
? ''
: JSON.stringify(Array.from(Object.entries(filters)));
const { index_pattern: indexPattern } = useIndexPattern();
const [esKuery, error] = useUpdateKueryString(indexPattern, query, parsedFilters);
const { data, loading } = useFetcher(
() =>
fetchSnapshotCount({
dateRangeStart: 'now-24h',
dateRangeEnd: 'now',
filters: error ? undefined : esKuery,
}),
[esKuery, query]
);
return { count: data || { total: 0, up: 0, down: 0 }, loading };
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { AlertMonitorStatusComponent } from './alert_monitor_status';
export { AlertMonitorStatusComponent } from './monitor_status_alert/alert_monitor_status';
export { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button';
export { UptimeAlertsFlyoutWrapperComponent } from './uptime_alerts_flyout_wrapper';
export * from './alerts_containers';

View file

@ -32,17 +32,10 @@ describe('FiltersExpressionSelect', () => {
tags: [],
}}
setAlertParams={jest.fn()}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);
expect(component).toMatchInlineSnapshot(`
<Fragment>
<EuiSpacer
size="xs"
/>
</Fragment>
`);
expect(component).toMatchInlineSnapshot(`<Fragment />`);
});
it.each([
@ -71,7 +64,6 @@ describe('FiltersExpressionSelect', () => {
locations: [],
}}
setAlertParams={jest.fn()}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);
@ -99,7 +91,6 @@ describe('FiltersExpressionSelect', () => {
locations: ['nyc'],
}}
setAlertParams={setAlertParamsMock}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);
@ -194,7 +185,6 @@ describe('FiltersExpressionSelect', () => {
onRemoveFilter={jest.fn()}
filters={filters}
setAlertParams={jest.fn()}
setUpdatedFieldValues={jest.fn()}
shouldUpdateUrl={false}
/>
);

View file

@ -13,13 +13,7 @@ import { alertFilterLabels, filterAriaLabels } from './translations';
import { FilterExpressionsSelectProps } from './filters_expression_select_container';
import { OverviewFiltersState } from '../../../../state/reducers/overview_filters';
type FilterFieldUpdate = (updateTarget: { fieldName: string; values: string[] }) => void;
interface OwnProps {
setUpdatedFieldValues: FilterFieldUpdate;
}
type Props = FilterExpressionsSelectProps & Pick<OverviewFiltersState, 'filters'> & OwnProps;
type Props = FilterExpressionsSelectProps & Pick<OverviewFiltersState, 'filters'>;
export const FiltersExpressionsSelect: React.FC<Props> = ({
alertParams,
@ -27,13 +21,15 @@ export const FiltersExpressionsSelect: React.FC<Props> = ({
newFilters,
onRemoveFilter,
setAlertParams,
setUpdatedFieldValues,
}) => {
const { tags, ports, schemes, locations } = overviewFilters;
const selectedPorts = alertParams?.filters?.['url.port'] ?? [];
const selectedLocations = alertParams?.filters?.['observer.geo.name'] ?? [];
const selectedSchemes = alertParams?.filters?.['monitor.type'] ?? [];
const selectedTags = alertParams?.filters?.tags ?? [];
const alertFilters = alertParams?.filters;
const selectedPorts = alertFilters?.['url.port'] ?? [];
const selectedLocations = alertFilters?.['observer.geo.name'] ?? [];
const selectedSchemes = alertFilters?.['monitor.type'] ?? [];
const selectedTags = alertFilters?.tags ?? [];
const onFilterFieldChange = (fieldName: string, values: string[]) => {
// the `filters` field is no longer a string
@ -54,7 +50,6 @@ export const FiltersExpressionsSelect: React.FC<Props> = ({
)
);
}
setUpdatedFieldValues({ fieldName, values });
};
const monitorFilters = [
@ -162,12 +157,9 @@ export const FiltersExpressionsSelect: React.FC<Props> = ({
}}
/>
</EuiFlexItem>
<EuiSpacer size="xs" />
</EuiFlexGroup>
))}
<EuiSpacer size="xs" />
</>
);
};

View file

@ -5,11 +5,10 @@
* 2.0.
*/
import React, { useState } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import { FiltersExpressionsSelect } from './filters_expression_select';
import { overviewFiltersSelector } from '../../../../state/selectors';
import { useFilterUpdate } from '../../../../hooks/use_filter_update';
export interface FilterExpressionsSelectProps {
alertParams: { [key: string]: any };
@ -20,20 +19,7 @@ export interface FilterExpressionsSelectProps {
}
export const FiltersExpressionSelectContainer: React.FC<FilterExpressionsSelectProps> = (props) => {
const [updatedFieldValues, setUpdatedFieldValues] = useState<{
fieldName: string;
values: string[];
}>({ fieldName: '', values: [] });
useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values, props.shouldUpdateUrl);
const overviewFilters = useSelector(overviewFiltersSelector);
return (
<FiltersExpressionsSelect
{...overviewFilters}
{...props}
setUpdatedFieldValues={setUpdatedFieldValues}
/>
);
return <FiltersExpressionsSelect {...overviewFilters} {...props} />;
};

View file

@ -22,8 +22,10 @@ describe('AddFilterButton component', () => {
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={false}
flush="left"
iconType="plusInCircleFilled"
onClick={[Function]}
size="s"
>
Add filter
</EuiButtonEmpty>
@ -86,8 +88,10 @@ describe('AddFilterButton component', () => {
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={false}
flush="left"
iconType="plusInCircleFilled"
onClick={[Function]}
size="s"
>
Add filter
</EuiButtonEmpty>
@ -137,8 +141,10 @@ describe('AddFilterButton component', () => {
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={true}
flush="left"
iconType="plusInCircleFilled"
onClick={[Function]}
size="s"
>
Add filter
</EuiButtonEmpty>

View file

@ -7,7 +7,7 @@
import React, { useState } from 'react';
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import * as labels from './translations';
import * as labels from '../translations';
interface Props {
newFilters: string[];
@ -60,6 +60,8 @@ export const AddFilterButton: React.FC<Props> = ({ newFilters, onNewFilter, aler
disabled={items.length === 0}
iconType="plusInCircleFilled"
onClick={onButtonClick}
size="s"
flush="left"
>
{labels.ADD_FILTER}
</EuiButtonEmpty>

View file

@ -21,8 +21,6 @@ describe('alert monitor status component', () => {
enabled: true,
hasFilters: false,
isOldAlert: true,
locations: [],
shouldUpdateUrl: false,
snapshotCount: 0,
snapshotLoading: false,
numTimes: 14,
@ -37,15 +35,30 @@ describe('alert monitor status component', () => {
<OldAlertCallOut
isOldAlert={true}
/>
<EuiSpacer
size="m"
<EuiCallOut
iconType="iInCircle"
size="s"
title={
<span>
<FormattedMessage
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
id="xpack.uptime.alerts.monitorStatus.monitorCallOut.title"
values={
Object {
"snapshotCount": 0,
}
}
/>
</span>
}
/>
<KueryBar
aria-label="Input that allows filtering criteria for the monitor status alert"
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
defaultKuery="monitor.id: foo"
shouldUpdateUrl={false}
updateDefaultKuery={[Function]}
<EuiSpacer
size="s"
/>
<AlertQueryBar
onChange={[Function]}
query="monitor.id: foo"
/>
<EuiSpacer
size="s"
@ -94,24 +107,6 @@ describe('alert monitor status component', () => {
isOldAlert={true}
setAlertParams={[MockFunction]}
/>
<EuiSpacer
size="l"
/>
<EuiCallOut
iconType="iInCircle"
size="s"
title={
<FormattedMessage
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
id="xpack.uptime.alerts.monitorStatus.monitorCallOut.title"
values={
Object {
"snapshotCount": 0,
}
}
/>
}
/>
<EuiSpacer
size="m"
/>

View file

@ -6,26 +6,23 @@
*/
import React, { useState } from 'react';
import { EuiCallOut, EuiSpacer, EuiHorizontalRule } from '@elastic/eui';
import { EuiCallOut, EuiSpacer, EuiHorizontalRule, EuiLoadingSpinner } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import * as labels from './translations';
import { FiltersExpressionSelectContainer, StatusExpressionSelect } from './monitor_expressions';
import { FiltersExpressionSelectContainer, StatusExpressionSelect } from '../monitor_expressions';
import { AddFilterButton } from './add_filter_btn';
import { OldAlertCallOut } from './old_alert_call_out';
import { AvailabilityExpressionSelect } from './monitor_expressions/availability_expression_select';
import { KueryBar } from '..';
import { AvailabilityExpressionSelect } from '../monitor_expressions/availability_expression_select';
import { AlertQueryBar } from '../alert_query_bar/query_bar';
export interface AlertMonitorStatusProps {
alertParams: { [key: string]: any };
enabled: boolean;
hasFilters: boolean;
isOldAlert: boolean;
locations: string[];
snapshotCount: number;
snapshotLoading: boolean;
snapshotLoading?: boolean;
numTimes: number;
setAlertParams: (key: string, value: any) => void;
shouldUpdateUrl: boolean;
timerange: {
from: string;
to: string;
@ -38,7 +35,6 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
hasFilters,
isOldAlert,
setAlertParams,
shouldUpdateUrl,
snapshotCount,
snapshotLoading,
} = props;
@ -52,14 +48,26 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
<>
<OldAlertCallOut isOldAlert={isOldAlert} />
<EuiSpacer size="m" />
<EuiCallOut
size="s"
title={
<span>
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.monitorCallOut.title"
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
values={{ snapshotCount: snapshotLoading ? '...' : snapshotCount }}
/>{' '}
{snapshotLoading && <EuiLoadingSpinner />}
</span>
}
iconType="iInCircle"
/>
<KueryBar
aria-label={labels.ALERT_KUERY_BAR_ARIA}
defaultKuery={alertParams.search}
shouldUpdateUrl={shouldUpdateUrl}
updateDefaultKuery={(value: string) => setAlertParams('search', value)}
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
<EuiSpacer size="s" />
<AlertQueryBar
query={alertParams.search || ''}
onChange={(value: string) => setAlertParams('search', value)}
/>
<EuiSpacer size="s" />
@ -81,7 +89,7 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
}
}}
setAlertParams={setAlertParams}
shouldUpdateUrl={shouldUpdateUrl}
shouldUpdateUrl={false}
/>
<EuiHorizontalRule />
@ -100,20 +108,6 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = (p
setAlertParams={setAlertParams}
/>
<EuiSpacer size="l" />
<EuiCallOut
size="s"
title={
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.monitorCallOut.title"
defaultMessage="This alert will apply to approximately {snapshotCount} monitors."
values={{ snapshotCount: snapshotLoading ? '...' : snapshotCount }}
/>
}
iconType="iInCircle"
/>
<EuiSpacer size="m" />
</>
);

View file

@ -10,6 +10,3 @@ export * from './empty_state';
export * from './filter_group';
export * from './alerts';
export * from './snapshot';
export * from './kuery_bar';
export { ParsingErrorCallout } from './parsing_error_callout';

View file

@ -1,8 +0,0 @@
/*
* 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.
*/
export { KueryBar } from './kuery_bar';

View file

@ -1,185 +0,0 @@
/*
* 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, { useState, useEffect } from 'react';
import { EuiCallOut, htmlIdGenerator } from '@elastic/eui';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n/react';
import { Typeahead } from './typeahead';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { useSearchText, useUrlParams } from '../../../hooks';
import {
esKuery,
IIndexPattern,
QuerySuggestion,
DataPublicPluginStart,
} from '../../../../../../../src/plugins/data/public';
import { useIndexPattern } from './use_index_pattern';
const Container = styled.div`
margin-bottom: 4px;
position: relative;
`;
interface State {
suggestions: QuerySuggestion[];
isLoadingIndexPattern: boolean;
}
function convertKueryToEsQuery(kuery: string, indexPattern: IIndexPattern) {
const ast = esKuery.fromKueryExpression(kuery);
return esKuery.toElasticsearchQuery(ast, indexPattern);
}
interface Props {
'aria-label': string;
defaultKuery?: string;
'data-test-subj': string;
shouldUpdateUrl?: boolean;
updateDefaultKuery?: (value: string) => void;
}
export function KueryBar({
'aria-label': ariaLabel,
defaultKuery,
'data-test-subj': dataTestSubj,
shouldUpdateUrl,
updateDefaultKuery,
}: Props) {
const { loading, index_pattern: indexPattern } = useIndexPattern();
const { updateSearchText } = useSearchText();
const {
services: {
data: { autocomplete },
},
} = useKibana<{ data: DataPublicPluginStart }>();
const [state, setState] = useState<State>({
suggestions: [],
isLoadingIndexPattern: true,
});
const [suggestionLimit, setSuggestionLimit] = useState(15);
const [isLoadingSuggestions, setIsLoadingSuggestions] = useState<boolean>(false);
let currentRequestCheck: string;
const [getUrlParams, updateUrlParams] = useUrlParams();
const { search: kuery, query } = getUrlParams();
useEffect(() => {
updateSearchText(kuery);
}, [kuery, updateSearchText]);
useEffect(() => {
if (updateDefaultKuery && kuery) {
updateDefaultKuery(kuery);
} else if (defaultKuery && updateDefaultKuery) {
updateDefaultKuery(defaultKuery);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const indexPatternMissing = loading && !indexPattern;
async function onChange(inputValue: string, selectionStart: number | null) {
if (!indexPattern) {
return;
}
setIsLoadingSuggestions(true);
setState({ ...state, suggestions: [] });
setSuggestionLimit(15);
const currentRequest = htmlIdGenerator()();
currentRequestCheck = currentRequest;
try {
const suggestions = (
(await autocomplete.getQuerySuggestions({
language: 'kuery',
indexPatterns: [indexPattern],
query: inputValue,
selectionStart: selectionStart || 0,
selectionEnd: selectionStart || 0,
useTimeRange: true,
})) || []
).filter((suggestion: QuerySuggestion) => !suggestion.text.startsWith('span.'));
if (currentRequest !== currentRequestCheck) {
return;
}
setIsLoadingSuggestions(false);
setState({ ...state, suggestions });
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error while fetching suggestions', e);
}
}
function onSubmit(inputValue: string) {
if (indexPattern === null) {
return;
}
try {
const res = convertKueryToEsQuery(inputValue, indexPattern);
if (!res) {
return;
}
if (shouldUpdateUrl !== false) {
updateUrlParams({ search: inputValue.trim() });
}
updateSearchText(inputValue);
if (updateDefaultKuery) {
updateDefaultKuery(inputValue);
}
} catch (e) {
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
}
}
const increaseLimit = () => {
setSuggestionLimit(suggestionLimit + 15);
};
return (
<Container>
<Typeahead
ariaLabel={ariaLabel}
dataTestSubj={dataTestSubj}
disabled={indexPatternMissing}
isLoading={isLoadingSuggestions || loading}
initialValue={defaultKuery || kuery || query}
onChange={onChange}
onSubmit={onSubmit}
suggestions={state.suggestions.slice(0, suggestionLimit)}
loadMore={increaseLimit}
queryExample=""
/>
{indexPatternMissing && !loading && (
<EuiCallOut
style={{ display: 'inline-block', marginTop: '10px' }}
title={
<div>
<FormattedMessage
id="xpack.uptime.kueryBar.indexPatternMissingWarningMessage"
// TODO: we need to determine the best instruction to provide if the index pattern is missing
defaultMessage="There was an error retrieving the index pattern."
/>
</div>
}
color="warning"
iconType="alert"
size="s"
/>
)}
</Container>
);
}

View file

@ -1,8 +0,0 @@
/*
* 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.
*/
export { Typeahead } from './typehead';

View file

@ -1,71 +0,0 @@
/*
* 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(
<SearchType kqlSyntax={kqlSyntax} setKqlSyntax={setKqlSyntax} />
);
// 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(
<SearchType kqlSyntax={kqlSyntax} setKqlSyntax={setKqlSyntax} />
);
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(<SearchType kqlSyntax={true} setKqlSyntax={setKqlSyntax} />, {
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(<SearchType kqlSyntax={false} setKqlSyntax={setKqlSyntax} />, {
url: '/app/uptime?search=test',
});
expect(history?.location.search).toBe('');
});
});

View file

@ -1,144 +0,0 @@
/*
* 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 ? (
<EuiButtonEmpty
data-test-subj="syntaxChangeToSimple"
onClick={onButtonClick}
aria-label={CHANGE_SEARCH_BAR_SYNTAX_SIMPLE}
title={CHANGE_SEARCH_BAR_SYNTAX_SIMPLE}
>
KQL
</EuiButtonEmpty>
) : (
<BoxesVerticalIcon
color="text"
iconType="boxesVertical"
onClick={onButtonClick}
data-test-subj="syntaxChangeToKql"
aria-label={CHANGE_SEARCH_BAR_SYNTAX}
title={CHANGE_SEARCH_BAR_SYNTAX}
/>
);
return (
<EuiPopover
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
ownFocus={true}
anchorPosition="downRight"
>
<div style={{ width: '360px' }}>
<EuiPopoverTitle>{SYNTAX_OPTIONS_LABEL}</EuiPopoverTitle>
<EuiText>
<p>
<KqlDescription href={docLinks!.links.query.kueryQuerySyntax} />
</p>
</EuiText>
<EuiSpacer />
<EuiFormRow label={KIBANA_QUERY_LANGUAGE} hasChildLabel={false}>
<EuiSwitch
name="switch"
label={kqlSyntax ? 'On' : 'Off'}
checked={kqlSyntax}
onChange={() => setKqlSyntax(!kqlSyntax)}
data-test-subj="toggleKqlSyntax"
/>
</EuiFormRow>
</div>
</EuiPopover>
);
};
const KqlDescription = ({ href }: { href: string }) => {
return (
<FormattedMessage
id="xpack.uptime.queryBar.syntaxOptionsDescription"
defaultMessage="The {docsLink} (KQL) offers a simplified query
syntax and support for scripted fields. KQL also provides autocomplete if you have
a Basic license or above. If you turn off KQL, Uptime
uses simple wildcard search against {searchField} fields."
values={{
docsLink: (
<EuiLink href={href} target="_blank" external>
{KIBANA_QUERY_LANGUAGE}
</EuiLink>
),
searchField: <strong>Monitor Name, ID, Url</strong>,
}}
/>
);
};
const KIBANA_QUERY_LANGUAGE = i18n.translate('xpack.uptime.query.queryBar.kqlFullLanguageName', {
defaultMessage: 'Kibana Query Language',
});

View file

@ -1,89 +0,0 @@
/*
* 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, { useRef, useEffect, RefObject } from 'react';
import { EuiSuggestItem } from '@elastic/eui';
import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
const SuggestionItem = euiStyled.div<{ selected: boolean }>`
background: ${(props) => (props.selected ? props.theme.eui.euiColorLightestShade : 'initial')};
`;
function getIconColor(type: string) {
switch (type) {
case 'field':
return 'tint5';
case 'value':
return 'tint0';
case 'operator':
return 'tint1';
case 'conjunction':
return 'tint3';
case 'recentSearch':
return 'tint10';
default:
return 'tint5';
}
}
function getEuiIconType(type: string) {
switch (type) {
case 'field':
return 'kqlField';
case 'value':
return 'kqlValue';
case 'recentSearch':
return 'search';
case 'conjunction':
return 'kqlSelector';
case 'operator':
return 'kqlOperand';
default:
throw new Error(`Unknown type ${type}`);
}
}
interface SuggestionProps {
onClick: (sug: QuerySuggestion) => void;
onMouseEnter: () => void;
selected: boolean;
suggestion: QuerySuggestion;
innerRef: (node: any) => void;
}
export const Suggestion: React.FC<SuggestionProps> = ({
innerRef,
selected,
suggestion,
onClick,
onMouseEnter,
}) => {
const childNode: RefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);
useEffect(() => {
if (childNode.current) {
innerRef(childNode.current);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [childNode]);
return (
<SuggestionItem ref={childNode} selected={selected}>
<EuiSuggestItem
type={{ iconType: getEuiIconType(suggestion.type), color: getIconColor(suggestion.type) }}
label={suggestion.text}
onClick={() => onClick(suggestion)}
onMouseEnter={onMouseEnter}
// @ts-ignore
description={suggestion.description}
/>
</SuggestionItem>
);
};

View file

@ -1,146 +0,0 @@
/*
* 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, { useRef, useState, useEffect } from 'react';
import { isEmpty } from 'lodash';
import { rgba } from 'polished';
import { Suggestion } from './suggestion';
import { QuerySuggestion } from '../../../../../../../../src/plugins/data/public';
import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common';
export const unit = 16;
export const units = {
unit,
eighth: unit / 8,
quarter: unit / 4,
half: unit / 2,
minus: unit * 0.75,
plus: unit * 1.5,
double: unit * 2,
triple: unit * 3,
quadruple: unit * 4,
};
export function px(value: number): string {
return `${value}px`;
}
const List = euiStyled.ul`
width: 100%;
border: 1px solid ${(props) => props.theme.eui.euiColorLightShade};
border-radius: ${px(units.quarter)};
background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
z-index: 10;
max-height: ${px(unit * 20)};
overflow: auto;
position: absolute;
&::-webkit-scrollbar {
height: ${({ theme }) => theme.eui.euiScrollBar};
width: ${({ theme }) => theme.eui.euiScrollBar};
}
&::-webkit-scrollbar-thumb {
background-clip: content-box;
background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)};
border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent;
}
&::-webkit-scrollbar-corner,
&::-webkit-scrollbar-track {
background-color: transparent;
}
`;
interface SuggestionsProps {
index: number;
onClick: (sug: QuerySuggestion) => void;
onMouseEnter: (index: number) => void;
show?: boolean;
suggestions: QuerySuggestion[];
loadMore: () => void;
}
export const Suggestions: React.FC<SuggestionsProps> = ({
show,
index,
onClick,
suggestions,
onMouseEnter,
loadMore,
}) => {
const [childNodes, setChildNodes] = useState<HTMLDivElement[]>([]);
const parentNode = useRef<HTMLUListElement>(null);
useEffect(() => {
const scrollIntoView = () => {
const parent = parentNode.current;
const child = childNodes[index];
if (index == null || !parent || !child) {
return;
}
const scrollTop = Math.max(
Math.min(parent.scrollTop, child.offsetTop),
child.offsetTop + child.offsetHeight - parent.offsetHeight
);
parent.scrollTop = scrollTop;
};
scrollIntoView();
}, [index, childNodes]);
if (!show || isEmpty(suggestions)) {
return null;
}
const handleScroll = () => {
const parent = parentNode.current;
if (!loadMore || !parent) {
return;
}
const position = parent.scrollTop + parent.offsetHeight;
const height = parent.scrollHeight;
const remaining = height - position;
const margin = 50;
if (!height || !position) {
return;
}
if (remaining <= margin) {
loadMore();
}
};
const suggestionsNodes = suggestions.map((suggestion, currIndex) => {
const key = suggestion + '_' + currIndex;
return (
<Suggestion
innerRef={(node) => {
const nodes = childNodes;
nodes[currIndex] = node;
setChildNodes([...nodes]);
}}
selected={currIndex === index}
suggestion={suggestion}
onClick={onClick}
onMouseEnter={() => onMouseEnter(currIndex)}
key={key}
/>
);
});
return (
<List ref={parentNode} onScroll={handleScroll}>
{suggestionsNodes}
</List>
);
};

View file

@ -1,44 +0,0 @@
/*
* 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(
<Typeahead
ariaLabel={'Search for data'}
dataTestSubj={'kueryBar'}
disabled={false}
isLoading={false}
initialValue={'elastic'}
onChange={jest.fn()}
onSubmit={() => {}}
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');
});
});

View file

@ -1,210 +0,0 @@
/*
* 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, { 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';
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;
onSubmit: (inputValue: string) => void;
suggestions: QuerySuggestion[];
queryExample: string;
initialValue?: string;
isLoading?: boolean;
disabled?: boolean;
dataTestSubj: string;
ariaLabel: string;
loadMore: () => void;
}
export const Typeahead: React.FC<TypeaheadProps> = ({
initialValue,
suggestions,
onChange,
onSubmit,
dataTestSubj,
ariaLabel,
disabled,
isLoading,
loadMore,
}) => {
const [value, setValue] = useState('');
const [index, setIndex] = useState<number | null>(null);
const [isSuggestionsVisible, setIsSuggestionsVisible] = useState(false);
const [selected, setSelected] = useState<QuerySuggestion | null>(null);
const [inputIsPristine, setInputIsPristine] = useState(true);
const [lastSubmitted, setLastSubmitted] = useState('');
const { kqlSyntax, setKqlSyntax } = useKqlSyntax({ setValue });
const inputRef = useRef<HTMLInputElement>();
const { setQuery } = useSimpleQuery();
useEffect(() => {
if (inputIsPristine && initialValue) {
setValue(initialValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [initialValue]);
const selectSuggestion = (suggestion: QuerySuggestion) => {
const nextInputValue =
value.substr(0, suggestion.start) + suggestion.text + value.substr(suggestion.end);
setValue(nextInputValue);
setSelected(suggestion);
setIndex(null);
onChange(nextInputValue, nextInputValue.length);
};
const { onKeyDown, onKeyUp } = useKeyEvents({
index,
value,
isSuggestionsVisible,
setIndex,
setIsSuggestionsVisible,
suggestions,
selectSuggestion,
onChange,
onSubmit,
});
const onClickOutside = () => {
if (isSuggestionsVisible) {
setIsSuggestionsVisible(false);
onSuggestionSubmit();
}
};
const onChangeInputValue = (event: ChangeEvent<HTMLInputElement>) => {
const { value: valueN, selectionStart } = event.target;
const hasValue = Boolean(valueN.trim());
setValue(valueN);
setInputIsPristine(false);
setIndex(null);
if (!kqlSyntax) {
setQuery(valueN);
return;
}
setIsSuggestionsVisible(hasValue);
if (!hasValue) {
onSubmit(valueN);
}
onChange(valueN, selectionStart!);
};
const onClickInput = (event: MouseEvent<HTMLInputElement> & ChangeEvent<HTMLInputElement>) => {
if (kqlSyntax) {
event.stopPropagation();
const { selectionStart } = event.target;
onChange(value, selectionStart!);
}
};
const onFocus = () => {
if (kqlSyntax) {
setIsSuggestionsVisible(true);
}
};
const onClickSuggestion = (suggestion: QuerySuggestion) => {
selectSuggestion(suggestion);
if (inputRef.current) inputRef.current.focus();
};
const onMouseEnterSuggestion = (indexN: number) => {
setIndex(indexN);
};
const onSuggestionSubmit = () => {
if (
lastSubmitted !== value &&
selected &&
(selected.type === 'value' || selected.text.trim() === ': *')
) {
onSubmit(value);
setLastSubmitted(value);
setSelected(null);
}
};
return (
<EuiOutsideClickDetector onOutsideClick={onClickOutside}>
<span>
<div data-test-subj={dataTestSubj} style={{ position: 'relative' }}>
<EuiFieldSearch
aria-label={ariaLabel}
fullWidth
style={
kqlSyntax
? {
backgroundImage: 'none',
}
: {}
}
placeholder={kqlSyntax ? KQL_PLACE_HOLDER : SIMPLE_SEARCH_PLACEHOLDER}
inputRef={(node) => {
if (node) {
inputRef.current = node;
}
}}
disabled={disabled}
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={<SearchType kqlSyntax={kqlSyntax} setKqlSyntax={setKqlSyntax} />}
/>
{isLoading && (
<EuiProgress
size="xs"
color="accent"
position="absolute"
style={{
bottom: 0,
top: 'initial',
}}
/>
)}
</div>
{kqlSyntax && (
<Suggestions
show={isSuggestionsVisible}
suggestions={suggestions}
index={index!}
onClick={onClickSuggestion}
onMouseEnter={onMouseEnterSuggestion}
loadMore={loadMore}
/>
)}
</span>
</EuiOutsideClickDetector>
);
};

View file

@ -1,113 +0,0 @@
/*
* 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<React.SetStateAction<number | null>>;
setIsSuggestionsVisible: React.Dispatch<React.SetStateAction<boolean>>;
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<HTMLInputElement> & ChangeEvent<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 };
};

View file

@ -1,56 +0,0 @@
/*
* 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<React.SetStateAction<string>>;
}
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 };
};

View file

@ -1,32 +0,0 @@
/*
* 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 };
};

View file

@ -8,7 +8,7 @@
import React, { useContext, useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getMonitorList } from '../../../state/actions';
import { monitorListSelector } from '../../../state/selectors';
import { esKuerySelector, monitorListSelector } from '../../../state/selectors';
import { MonitorListComponent } from './monitor_list';
import { useUrlParams } from '../../../hooks';
import { UptimeRefreshContext } from '../../../contexts';
@ -28,7 +28,7 @@ const getPageSizeValue = () => {
};
export const MonitorList: React.FC<MonitorListProps> = (props) => {
const { filters } = props;
const filters = useSelector(esKuerySelector);
const [pageSize, setPageSize] = useState<number>(getPageSizeValue);

View file

@ -1,31 +0,0 @@
/*
* 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 { useDispatch, useSelector } from 'react-redux';
import React, { useCallback } from 'react';
import { OverviewPageComponent } from '../../pages/overview';
import { selectIndexPattern } from '../../state/selectors';
import { setEsKueryString } from '../../state/actions';
export const OverviewPage: React.FC = (props) => {
const dispatch = useDispatch();
const setEsKueryFilters = useCallback(
(esFilters: string) => dispatch(setEsKueryString(esFilters)),
[dispatch]
);
const { index_pattern: indexPattern, loading } = useSelector(selectIndexPattern);
return (
<OverviewPageComponent
setEsKueryFilters={setEsKueryFilters}
indexPattern={indexPattern}
loading={loading}
{...props}
/>
);
};

View file

@ -1,27 +0,0 @@
/*
* 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 { shallowWithIntl } from '@kbn/test/jest';
import { ParsingErrorCallout } from './parsing_error_callout';
describe('OverviewPageParsingErrorCallout', () => {
it('renders without errors when a valid error is provided', () => {
expect(
shallowWithIntl(
<ParsingErrorCallout
error={{ message: 'Unable to convert to Elasticsearch query, invalid syntax.' }}
/>
)
).toMatchSnapshot();
});
it('renders without errors when an error with no message is provided', () => {
const error: any = {};
expect(shallowWithIntl(<ParsingErrorCallout error={error} />)).toMatchSnapshot();
});
});

View file

@ -1,48 +0,0 @@
/*
* 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 { EuiCallOut, EuiCodeBlock } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
interface HasMessage {
message: string;
}
interface ParsingErrorCalloutProps {
error: HasMessage;
}
export const ParsingErrorCallout = ({ error }: ParsingErrorCalloutProps) => (
<EuiCallOut
title={i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.title', {
defaultMessage: 'Parsing error',
})}
color="danger"
iconType="alert"
style={{ width: '100%' }}
>
<p>
<FormattedMessage
id="xpack.uptime.overviewPageParsingErrorCallout.content"
defaultMessage="There was an error parsing the filter query. {content}"
values={{
content: (
<EuiCodeBlock>
{error.message
? error.message
: i18n.translate('xpack.uptime.overviewPageParsingErrorCallout.noMessage', {
defaultMessage: 'There was no error message',
})}
</EuiCodeBlock>
),
}}
/>
</p>
</EuiCallOut>
);

View file

@ -0,0 +1,84 @@
/*
* 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, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFlexItem } from '@elastic/eui';
import { QueryStringInput } from '../../../../../../../src/plugins/data/public/';
import { useIndexPattern } from './use_index_pattern';
import { SyntaxType, useQueryBar } from './use_query_bar';
import { KQL_PLACE_HOLDER, SIMPLE_SEARCH_PLACEHOLDER } from './translations';
import { useGetUrlParams } from '../../../hooks';
const SYNTAX_STORAGE = 'uptime:queryBarSyntax';
export const isValidKuery = (query: string) => {
if (query === '') {
return true;
}
const listOfOperators = [':', '>=', '=>', '>', '<'];
for (let i = 0; i < listOfOperators.length; i++) {
const operator = listOfOperators[i];
const qParts = query.trim().split(operator);
if (query.includes(operator) && qParts.length > 1 && qParts[1]) {
return true;
}
}
return false;
};
export const QueryBar = () => {
const { index_pattern: indexPattern } = useIndexPattern();
const { search: urlValue } = useGetUrlParams();
const { query, setQuery } = useQueryBar();
const [inputVal, setInputVal] = useState<string>(query.query);
const isInValid = () => {
if (query.language === SyntaxType.text) {
return false;
}
return inputVal?.trim() !== urlValue?.trim();
};
return (
<EuiFlexItem grow={1} style={{ flexBasis: 485 }}>
<QueryStringInput
indexPatterns={indexPattern ? [indexPattern] : []}
nonKqlMode="text"
iconType="search"
isClearable={true}
onChange={(queryN) => {
if (queryN?.language === SyntaxType.text) {
setQuery({ query: queryN.query as string, language: queryN.language });
}
if (queryN?.language === SyntaxType.kuery && isValidKuery(queryN?.query as string)) {
// we want to submit when user clears or paste a complete kuery
setQuery({ query: queryN.query as string, language: queryN.language });
}
setInputVal(queryN?.query as string);
}}
onSubmit={(queryN) => {
if (queryN) setQuery({ query: queryN.query as string, language: queryN.language });
}}
query={{ ...query, query: inputVal }}
aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', {
defaultMessage: 'Input filter criteria for the overview page',
})}
data-test-subj="uptimeSearchBarInput"
autoSubmit={true}
storageKey={SYNTAX_STORAGE}
placeholder={
query.language === SyntaxType.kuery ? KQL_PLACE_HOLDER : SIMPLE_SEARCH_PLACEHOLDER
}
isInvalid={isInValid()}
/>
</EuiFlexItem>
);
};

View file

@ -18,21 +18,3 @@ export const SIMPLE_SEARCH_PLACEHOLDER = i18n.translate(
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',
});

View file

@ -0,0 +1,96 @@
/*
* 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 { useCallback, useEffect, useState } from 'react';
import { useDebounce } from 'react-use';
import { useDispatch } from 'react-redux';
import { useGetUrlParams, useUpdateKueryString, useUrlParams } from '../../../hooks';
import { setEsKueryString } from '../../../state/actions';
import { useIndexPattern } from './use_index_pattern';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { UptimePluginServices } from '../../../apps/plugin';
export enum SyntaxType {
text = 'text',
kuery = 'kuery',
}
const SYNTAX_STORAGE = 'uptime:queryBarSyntax';
export const useQueryBar = () => {
const { index_pattern: indexPattern } = useIndexPattern();
const dispatch = useDispatch();
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams();
const { search, query: queryParam, filters: paramFilters } = params;
const {
services: { storage },
} = useKibana<UptimePluginServices>();
const [query, setQuery] = useState(
queryParam
? {
query: queryParam,
language: SyntaxType.text,
}
: search
? { query: search, language: SyntaxType.kuery }
: {
query: '',
language: storage.get(SYNTAX_STORAGE) ?? SyntaxType.text,
}
);
const updateUrlParams = useUrlParams()[1];
const [esFilters, error] = useUpdateKueryString(
indexPattern,
query.language === SyntaxType.kuery ? (query.query as string) : undefined,
paramFilters
);
const setEsKueryFilters = useCallback(
(esFiltersN: string) => dispatch(setEsKueryString(esFiltersN)),
[dispatch]
);
useEffect(() => {
setEsKueryFilters(esFilters ?? '');
}, [esFilters, setEsKueryFilters]);
useDebounce(
() => {
if (query.language === SyntaxType.text && queryParam !== query.query) {
updateUrlParams({ query: query.query as string });
}
if (query.language === SyntaxType.kuery) {
updateUrlParams({ query: '' });
}
},
350,
[query]
);
useDebounce(
() => {
if (query.language === SyntaxType.kuery && !error && esFilters) {
updateUrlParams({ search: query.query as string });
}
if (query.language === SyntaxType.text) {
updateUrlParams({ search: '' });
}
if (query.language === SyntaxType.kuery && query.query === '') {
updateUrlParams({ search: '' });
}
},
250,
[esFilters, error]
);
return { query, setQuery };
};

View file

@ -6,4 +6,3 @@
*/
export { SnapshotComponent } from './snapshot';
export { Snapshot } from './snapshot_container';

View file

@ -7,8 +7,9 @@
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
import { Snapshot } from '../../../common/runtime_types';
import { SnapshotComponent } from './snapshot/snapshot';
import { SnapshotComponent } from './snapshot';
import { Snapshot } from '../../../../common/runtime_types/snapshot';
import * as hook from './use_snap_shot';
describe('Snapshot component', () => {
const snapshot: Snapshot = {
@ -18,7 +19,9 @@ describe('Snapshot component', () => {
};
it('renders without errors', () => {
const wrapper = shallowWithIntl(<SnapshotComponent count={snapshot} loading={false} />);
jest.spyOn(hook, 'useSnapShotCount').mockReturnValue({ count: snapshot, loading: false });
const wrapper = shallowWithIntl(<SnapshotComponent />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -10,13 +10,11 @@ import React from 'react';
import { DonutChart } from '../../common/charts';
import { ChartWrapper } from '../../common/charts/chart_wrapper';
import { SnapshotHeading } from './snapshot_heading';
import { Snapshot as SnapshotType } from '../../../../common/runtime_types';
import { useSnapShotCount } from './use_snap_shot';
const SNAPSHOT_CHART_HEIGHT = 144;
interface SnapshotComponentProps {
count: SnapshotType;
loading: boolean;
height?: string;
}
@ -25,10 +23,14 @@ interface SnapshotComponentProps {
* glean the status of their uptime environment.
* @param props the props required by the component
*/
export const SnapshotComponent: React.FC<SnapshotComponentProps> = ({ count, height, loading }) => (
<ChartWrapper loading={loading} height={height}>
<SnapshotHeading total={count.total} />
<EuiSpacer size="xs" />
<DonutChart up={count.up} down={count.down} height={SNAPSHOT_CHART_HEIGHT} />
</ChartWrapper>
);
export const SnapshotComponent: React.FC<SnapshotComponentProps> = ({ height }) => {
const { count, loading } = useSnapShotCount();
return (
<ChartWrapper loading={loading} height={height}>
<SnapshotHeading total={count.total} />
<EuiSpacer size="xs" />
<DonutChart up={count.up} down={count.down} height={SNAPSHOT_CHART_HEIGHT} />
</ChartWrapper>
);
};

View file

@ -1,37 +0,0 @@
/*
* 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, { useContext, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useGetUrlParams } from '../../../hooks';
import { getSnapshotCountAction } from '../../../state/actions';
import { SnapshotComponent } from './snapshot';
import { esKuerySelector, snapshotDataSelector } from '../../../state/selectors';
import { UptimeRefreshContext } from '../../../contexts';
interface Props {
/**
* Height is needed, since by default charts takes height of 100%
*/
height?: string;
}
export const Snapshot: React.FC<Props> = ({ height }: Props) => {
const { dateRangeStart, dateRangeEnd, query } = useGetUrlParams();
const { lastRefresh } = useContext(UptimeRefreshContext);
const { count, loading } = useSelector(snapshotDataSelector);
const esKuery = useSelector(esKuerySelector);
const dispatch = useDispatch();
useEffect(() => {
dispatch(getSnapshotCountAction.get({ query, dateRangeStart, dateRangeEnd, filters: esKuery }));
}, [dateRangeStart, dateRangeEnd, esKuery, lastRefresh, dispatch, query]);
return <SnapshotComponent count={count} height={height} loading={loading} />;
};

View file

@ -0,0 +1,29 @@
/*
* 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 { useContext } from 'react';
import { useSelector } from 'react-redux';
import { useGetUrlParams } from '../../../hooks';
import { esKuerySelector } from '../../../state/selectors';
import { UptimeRefreshContext } from '../../../contexts';
import { useFetcher } from '../../../../../observability/public';
import { fetchSnapshotCount } from '../../../state/api';
export const useSnapShotCount = () => {
const { dateRangeStart, dateRangeEnd, query } = useGetUrlParams();
const { lastRefresh } = useContext(UptimeRefreshContext);
const esKuery = useSelector(esKuerySelector);
const { data, loading } = useFetcher(
() => fetchSnapshotCount({ query, dateRangeStart, dateRangeEnd, filters: esKuery }),
[dateRangeStart, dateRangeEnd, esKuery, lastRefresh, query]
);
return { count: data || { total: 0, up: 0, down: 0 }, loading };
};

View file

@ -8,7 +8,7 @@
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { PingHistogram } from '../monitor';
import { Snapshot } from './snapshot/snapshot_container';
import { SnapshotComponent } from './snapshot';
const STATUS_CHART_HEIGHT = '160px';
@ -16,7 +16,7 @@ export const StatusPanel = ({}) => (
<EuiPanel>
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={2}>
<Snapshot height={STATUS_CHART_HEIGHT} />
<SnapshotComponent height={STATUS_CHART_HEIGHT} />
</EuiFlexItem>
<EuiFlexItem grow={10}>
<PingHistogram height={STATUS_CHART_HEIGHT} isResponsive={true} />

View file

@ -209,7 +209,7 @@ exports[`useUrlParams deletes keys that do not have truthy values 1`] = `
}
>
<div>
{"pagination":"foo","absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false}
{"pagination":"foo","absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-12","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false,"query":""}
</div>
<button
id="setUrlParams"
@ -433,7 +433,7 @@ exports[`useUrlParams gets the expected values using the context 1`] = `
hook={[Function]}
>
<div>
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false}
{"absoluteDateRangeStart":20,"absoluteDateRangeEnd":20,"autorefreshInterval":60000,"autorefreshIsPaused":false,"dateRangeStart":"now-15m","dateRangeEnd":"now","filters":"","search":"","statusFilter":"","focusConnectorField":false,"query":""}
</div>
<button
id="setUrlParams"

View file

@ -33,15 +33,6 @@ export const mockState: AppState = {
loading: false,
errors: [],
},
snapshot: {
count: {
up: 2,
down: 0,
total: 2,
},
errors: [],
loading: false,
},
ui: {
alertFlyoutVisible: false,
basePath: 'yyz',

View file

@ -11,8 +11,8 @@ import { CoreStart } from 'kibana/public';
import { store } from '../../../state';
import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public';
import { ClientPluginsStart } from '../../../apps/plugin';
import { AlertMonitorStatus } from '../../../components/overview/alerts/alerts_containers';
import { kibanaService } from '../../../state/kibana_service';
import { AlertMonitorStatus } from '../../../components/overview/alerts/alerts_containers/alert_monitor_status';
interface Props {
core: CoreStart;

View file

@ -11,7 +11,7 @@ Object {
"filters": "",
"focusConnectorField": false,
"pagination": undefined,
"query": undefined,
"query": "",
"search": "",
"statusFilter": "",
}
@ -28,7 +28,7 @@ Object {
"filters": "",
"focusConnectorField": false,
"pagination": undefined,
"query": undefined,
"query": "",
"search": "",
"statusFilter": "",
}
@ -45,7 +45,7 @@ Object {
"filters": "",
"focusConnectorField": false,
"pagination": undefined,
"query": undefined,
"query": "",
"search": "monitor.status: down",
"statusFilter": "",
}
@ -62,7 +62,7 @@ Object {
"filters": "",
"focusConnectorField": false,
"pagination": undefined,
"query": undefined,
"query": "",
"search": "",
"statusFilter": "",
}
@ -79,7 +79,7 @@ Object {
"filters": "",
"focusConnectorField": false,
"pagination": undefined,
"query": undefined,
"query": "",
"search": "",
"statusFilter": "",
}

View file

@ -63,6 +63,7 @@ describe('getSupportedUrlParams', () => {
pagination: undefined,
search: SEARCH,
statusFilter: STATUS_FILTER,
query: '',
});
});

View file

@ -80,7 +80,6 @@ export const getSupportedUrlParams = (params: {
} = filteredParams;
return {
query,
pagination,
absoluteDateRangeStart: parseAbsoluteDate(
dateRangeStart || DATE_RANGE_START,
@ -99,5 +98,6 @@ export const getSupportedUrlParams = (params: {
search: search || SEARCH,
statusFilter: statusFilter || STATUS_FILTER,
focusConnectorField: !!focusConnectorField,
query: query || '',
};
};

View file

@ -85,107 +85,7 @@ exports[`MonitorPage shallow renders expected elements for valid props 1`] = `
}
}
>
<Memo()
indexPattern={
Object {
"fields": Array [
Object {
"aggregatable": true,
"esTypes": Array [
"date",
],
"name": "@timestamp",
"readFromDocValues": true,
"searchable": true,
"type": "date",
},
Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "monitor.check_group",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"esTypes": Array [
"long",
],
"name": "monitor.duration.us",
"readFromDocValues": true,
"searchable": true,
"type": "number",
},
Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "monitor.id",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"esTypes": Array [
"ip",
],
"name": "monitor.ip",
"readFromDocValues": true,
"searchable": true,
"type": "ip",
},
Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "monitor.name",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "monitor.status",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
Object {
"aggregatable": true,
"esTypes": Array [
"date_range",
],
"name": "monitor.timespan",
"readFromDocValues": true,
"searchable": true,
"type": "unknown",
},
Object {
"aggregatable": true,
"esTypes": Array [
"keyword",
],
"name": "monitor.type",
"readFromDocValues": true,
"searchable": true,
"type": "string",
},
],
"title": "heartbeat-7*",
}
}
loading={false}
setEsKueryFilters={[MockFunction]}
/>
<OverviewPageComponent />
</ContextProvider>
</ContextProvider>
`;

View file

@ -10,94 +10,7 @@ import { OverviewPageComponent } from './overview';
import { shallowWithRouter } from '../lib';
describe('MonitorPage', () => {
const indexPattern = {
fields: [
{
name: '@timestamp',
type: 'date',
esTypes: ['date'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.check_group',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.duration.us',
type: 'number',
esTypes: ['long'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.id',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.ip',
type: 'ip',
esTypes: ['ip'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.name',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.status',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.timespan',
type: 'unknown',
esTypes: ['date_range'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'monitor.type',
type: 'string',
esTypes: ['keyword'],
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
],
title: 'heartbeat-7*',
};
it('shallow renders expected elements for valid props', () => {
expect(
shallowWithRouter(
<OverviewPageComponent
indexPattern={indexPattern}
setEsKueryFilters={jest.fn()}
loading={false}
/>
)
).toMatchSnapshot();
expect(shallowWithRouter(<OverviewPageComponent />)).toMatchSnapshot();
});
});

View file

@ -8,24 +8,16 @@
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { i18n } from '@kbn/i18n';
import { useDispatch } from 'react-redux';
import { useGetUrlParams } from '../hooks';
import { IIndexPattern } from '../../../../../src/plugins/data/public';
import { useUpdateKueryString } from '../hooks';
import { useBreadcrumbs } from '../hooks/use_breadcrumbs';
import { useTrackPageview } from '../../../observability/public';
import { MonitorList } from '../components/overview/monitor_list/monitor_list_container';
import { EmptyState, FilterGroup, KueryBar, ParsingErrorCallout } from '../components/overview';
import { EmptyState, FilterGroup } from '../components/overview';
import { StatusPanel } from '../components/overview/status_panel';
import { getConnectorsAction, getMonitorAlertsAction } from '../state/alerts/alerts';
import { useInitApp } from '../hooks/use_init_app';
interface Props {
loading: boolean;
indexPattern: IIndexPattern | null;
setEsKueryFilters: (esFilters: string) => void;
}
import { QueryBar } from '../components/overview/query_bar/query_bar';
const EuiFlexItemStyled = styled(EuiFlexItem)`
&& {
@ -39,54 +31,33 @@ const EuiFlexItemStyled = styled(EuiFlexItem)`
}
`;
export const OverviewPageComponent = React.memo(
({ indexPattern, setEsKueryFilters, loading }: Props) => {
const { absoluteDateRangeStart, absoluteDateRangeEnd, ...params } = useGetUrlParams();
const { search, filters: urlFilters } = params;
export const OverviewPageComponent = () => {
useTrackPageview({ app: 'uptime', path: 'overview' });
useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 });
useTrackPageview({ app: 'uptime', path: 'overview' });
useTrackPageview({ app: 'uptime', path: 'overview', delay: 15000 });
useInitApp();
useInitApp();
const dispatch = useDispatch();
const [esFilters, error] = useUpdateKueryString(indexPattern, search, urlFilters);
useEffect(() => {
dispatch(getConnectorsAction.get());
dispatch(getMonitorAlertsAction.get());
}, [dispatch]);
useEffect(() => {
setEsKueryFilters(esFilters ?? '');
}, [esFilters, setEsKueryFilters]);
useBreadcrumbs([]); // No extra breadcrumbs on overview
const dispatch = useDispatch();
useEffect(() => {
dispatch(getConnectorsAction.get());
dispatch(getMonitorAlertsAction.get());
}, [dispatch]);
useBreadcrumbs([]); // No extra breadcrumbs on overview
return (
<>
<EmptyState>
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
<EuiFlexItem grow={1} style={{ flexBasis: 485 }}>
<KueryBar
aria-label={i18n.translate('xpack.uptime.filterBar.ariaLabel', {
defaultMessage: 'Input filter criteria for the overview page',
})}
data-test-subj="xpack.uptime.filterBar"
/>
</EuiFlexItem>
<EuiFlexItemStyled grow={true}>
<FilterGroup esFilters={esFilters} />
</EuiFlexItemStyled>
{error && !loading && <ParsingErrorCallout error={error} />}
</EuiFlexGroup>
<EuiSpacer size="xs" />
<StatusPanel />
<EuiSpacer size="s" />
<MonitorList filters={esFilters} />
</EmptyState>
</>
);
}
);
return (
<EmptyState>
<EuiFlexGroup gutterSize="xs" wrap responsive={false}>
<QueryBar />
<EuiFlexItemStyled grow={true}>
<FilterGroup />
</EuiFlexItemStyled>
</EuiFlexGroup>
<EuiSpacer size="xs" />
<StatusPanel />
<EuiSpacer size="s" />
<MonitorList />
</EmptyState>
);
};

View file

@ -7,7 +7,6 @@
import React, { FC, useEffect } from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import { OverviewPage } from './components/overview/overview_container';
import { Props as PageHeaderProps, PageHeader } from './components/common/header/page_header';
import {
CERTIFICATES_ROUTE,
@ -20,6 +19,7 @@ import {
import { MonitorPage, StepDetailPage, NotFoundPage, SettingsPage } from './pages';
import { CertificatesPage } from './pages/certificates';
import { UptimePage, useUptimeTelemetry } from './hooks';
import { OverviewPageComponent } from './pages/overview';
import { SyntheticsCheckSteps } from './pages/synthetics/synthetics_checks';
interface RouteProps {
@ -83,7 +83,7 @@ const Routes: RouteProps[] = [
{
title: baseTitle,
path: OVERVIEW_ROUTE,
component: OverviewPage,
component: OverviewPageComponent,
dataTestSubj: 'uptimeOverviewPage',
telemetryId: UptimePage.Overview,
headerProps: {

View file

@ -30,5 +30,3 @@ export const fetchOverviewFiltersFail = createAction<Error>('FETCH_OVERVIEW_FILT
export const fetchOverviewFiltersSuccess = createAction<OverviewFilters>(
'FETCH_OVERVIEW_FILTERS_SUCCESS'
);
export const setOverviewFilters = createAction<OverviewFilters>('SET_OVERVIEW_FILTERS');

View file

@ -8,7 +8,6 @@
import { fork } from 'redux-saga/effects';
import { fetchMonitorDetailsEffect } from './monitor';
import { fetchOverviewFiltersEffect } from './overview_filters';
import { fetchSnapshotCountEffect } from './snapshot';
import { fetchMonitorListEffect } from './monitor_list';
import { fetchMonitorStatusEffect } from './monitor_status';
import { fetchDynamicSettingsEffect, setDynamicSettingsEffect } from './dynamic_settings';
@ -24,7 +23,6 @@ import { fetchNetworkEventsEffect } from './network_events';
export function* rootEffect() {
yield fork(fetchMonitorDetailsEffect);
yield fork(fetchSnapshotCountEffect);
yield fork(fetchOverviewFiltersEffect);
yield fork(fetchMonitorListEffect);
yield fork(fetchMonitorStatusEffect);

View file

@ -1,22 +0,0 @@
/*
* 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 { takeLatest } from 'redux-saga/effects';
import { getSnapshotCountAction } from '../actions';
import { fetchSnapshotCount } from '../api';
import { fetchEffectFactory } from './fetch_effect';
export function* fetchSnapshotCountEffect() {
yield takeLatest(
getSnapshotCountAction.get,
fetchEffectFactory(
fetchSnapshotCount,
getSnapshotCountAction.success,
getSnapshotCountAction.fail
)
);
}

View file

@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`snapshot reducer appends a current error to existing errors list 1`] = `
Object {
"count": Object {
"down": 0,
"total": 0,
"up": 0,
},
"errors": Array [
[Error: I couldn't get your data because the server denied the request],
],
"loading": false,
}
`;
exports[`snapshot reducer changes the count when a snapshot fetch succeeds 1`] = `
Object {
"count": Object {
"down": 15,
"total": 25,
"up": 10,
},
"errors": Array [],
"loading": false,
}
`;
exports[`snapshot reducer sets the state's status to loading during a fetch 1`] = `
Object {
"count": Object {
"down": 0,
"total": 0,
"up": 0,
},
"errors": Array [],
"loading": true,
}
`;
exports[`snapshot reducer updates existing state 1`] = `
Object {
"count": Object {
"down": 1,
"total": 4,
"up": 3,
},
"errors": Array [],
"loading": true,
}
`;

View file

@ -8,7 +8,6 @@
import { combineReducers } from 'redux';
import { monitorReducer } from './monitor';
import { overviewFiltersReducer } from './overview_filters';
import { snapshotReducer } from './snapshot';
import { uiReducer } from './ui';
import { monitorStatusReducer } from './monitor_status';
import { monitorListReducer } from './monitor_list';
@ -28,7 +27,6 @@ import { networkEventsReducer } from './network_events';
export const rootReducer = combineReducers({
monitor: monitorReducer,
overviewFilters: overviewFiltersReducer,
snapshot: snapshotReducer,
ui: uiReducer,
monitorList: monitorListReducer,
monitorStatus: monitorStatusReducer,

View file

@ -11,7 +11,6 @@ import {
fetchOverviewFilters,
fetchOverviewFiltersFail,
fetchOverviewFiltersSuccess,
setOverviewFilters,
GetOverviewFiltersPayload,
OverviewFiltersPayload,
} from '../actions';
@ -51,11 +50,6 @@ export const overviewFiltersReducer = handleActions<OverviewFiltersState, Overvi
errors: [...state.errors, action.payload],
loading: false,
}),
[String(setOverviewFilters)]: (state, action: Action<OverviewFilters>) => ({
...state,
filters: action.payload,
}),
},
initialState
);

View file

@ -1,56 +0,0 @@
/*
* 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 { snapshotReducer } from './snapshot';
import { getSnapshotCountAction } from '../actions';
import { IHttpFetchError } from '../../../../../../src/core/public';
describe('snapshot reducer', () => {
it('updates existing state', () => {
const action = getSnapshotCountAction.get({
dateRangeStart: 'now-15m',
dateRangeEnd: 'now',
filters: 'foo: bar',
});
expect(
snapshotReducer(
{
count: { down: 1, total: 4, up: 3 },
errors: [],
loading: false,
},
action
)
).toMatchSnapshot();
});
it(`sets the state's status to loading during a fetch`, () => {
const action = getSnapshotCountAction.get({
dateRangeStart: 'now-15m',
dateRangeEnd: 'now',
});
expect(snapshotReducer(undefined, action)).toMatchSnapshot();
});
it('changes the count when a snapshot fetch succeeds', () => {
const action = getSnapshotCountAction.success({
up: 10,
down: 15,
total: 25,
});
expect(snapshotReducer(undefined, action)).toMatchSnapshot();
});
it('appends a current error to existing errors list', () => {
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();
});
});

View file

@ -1,49 +0,0 @@
/*
* 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 { Action } from 'redux-actions';
import { Snapshot } from '../../../common/runtime_types';
import { getSnapshotCountAction } from '../actions';
export interface SnapshotState {
count: Snapshot;
errors: any[];
loading: boolean;
}
const initialState: SnapshotState = {
count: {
down: 0,
total: 0,
up: 0,
},
errors: [],
loading: false,
};
export function snapshotReducer(state = initialState, action: Action<any>): SnapshotState {
switch (action.type) {
case String(getSnapshotCountAction.get):
return {
...state,
loading: true,
};
case String(getSnapshotCountAction.success):
return {
...state,
count: action.payload,
loading: false,
};
case String(getSnapshotCountAction.fail):
return {
...state,
errors: [...state.errors, action.payload],
};
default:
return state;
}
}

View file

@ -31,15 +31,6 @@ describe('state selectors', () => {
loading: false,
errors: [],
},
snapshot: {
count: {
up: 2,
down: 0,
total: 2,
},
errors: [],
loading: false,
},
ui: {
alertFlyoutVisible: false,
basePath: 'yyz',

View file

@ -35,8 +35,6 @@ export const selectPingHistogram = ({ ping }: AppState) => ping;
export const selectPingList = ({ pingList }: AppState) => pingList;
export const snapshotDataSelector = ({ snapshot }: AppState) => snapshot;
export const mlCapabilitiesSelector = (state: AppState) => state.ml.mlCapabilities;
export const hasMLFeatureSelector = createSelector(

View file

@ -188,9 +188,8 @@ 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');
await testSubjects.click('switchQueryLanguageButton');
await testSubjects.click('languageToggle');
});
it('runs filter query without issues', async () => {

View file

@ -42,7 +42,7 @@ export function UptimeCommonProvider({ getService, getPageObjects }: FtrProvider
await browser.pressKeys(browser.keys.ENTER);
},
async setFilterText(filterQuery: string) {
await this.setKueryBarText('xpack.uptime.filterBar', filterQuery);
await this.setKueryBarText('queryInput', filterQuery);
},
async goToNextPage() {
await testSubjects.click('xpack.uptime.monitorList.nextButton', 5000);