mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Implement saved queries and filters (#39140)
Introduces "saved queries". Saved queries are a new saved object type similar to saved searches but more limited in scope. They allow users to store the the query string in the query bar and optionally the set of filters and timefilter in order to reload them anywhere a query is expected: Discover, Visualize, Dashboard, anywhere that uses our full SearchBar component.
This commit is contained in:
parent
cb8133aee5
commit
e233e419cf
62 changed files with 2612 additions and 258 deletions
|
@ -20,7 +20,7 @@
|
|||
import Boom from 'boom';
|
||||
import { getProperty, IndexMapping } from '../../../mappings';
|
||||
|
||||
const TOP_LEVEL_FIELDS = ['_id'];
|
||||
const TOP_LEVEL_FIELDS = ['_id', '_score'];
|
||||
|
||||
export function getSortingParams(
|
||||
mappings: IndexMapping,
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
|
||||
import { resolve } from 'path';
|
||||
import { Legacy } from '../../../../kibana';
|
||||
import { mappings } from './mappings';
|
||||
import { SavedQuery } from './public';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function DataPlugin(kibana: any) {
|
||||
|
@ -35,6 +37,23 @@ export default function DataPlugin(kibana: any) {
|
|||
uiExports: {
|
||||
injectDefaultVars: () => ({}),
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
mappings,
|
||||
savedObjectsManagement: {
|
||||
query: {
|
||||
icon: 'search',
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj: SavedQuery) {
|
||||
return obj.attributes.title;
|
||||
},
|
||||
getInAppUrl(obj: SavedQuery) {
|
||||
return {
|
||||
path: `/app/kibana#/discover?_a=(savedQuery:'${encodeURIComponent(obj.id)}')`,
|
||||
uiCapabilitiesPath: 'discover.show',
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
50
src/legacy/core_plugins/data/mappings.ts
Normal file
50
src/legacy/core_plugins/data/mappings.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export const mappings = {
|
||||
query: {
|
||||
properties: {
|
||||
title: {
|
||||
type: 'text',
|
||||
},
|
||||
description: {
|
||||
type: 'text',
|
||||
},
|
||||
query: {
|
||||
properties: {
|
||||
language: {
|
||||
type: 'keyword',
|
||||
},
|
||||
query: {
|
||||
type: 'keyword',
|
||||
index: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
type: 'object',
|
||||
enabled: false,
|
||||
},
|
||||
timefilter: {
|
||||
type: 'object',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -4,3 +4,4 @@
|
|||
|
||||
@import './filter/filter_bar/index';
|
||||
|
||||
@import './search/search_bar/index';
|
||||
|
|
|
@ -38,7 +38,7 @@ export {
|
|||
StaticIndexPattern,
|
||||
} from './index_patterns';
|
||||
export { Query, QueryBar, QueryBarInput } from './query';
|
||||
export { SearchBar, SearchBarProps } from './search';
|
||||
export { SearchBar, SearchBarProps, SavedQueryAttributes, SavedQuery } from './search';
|
||||
|
||||
/** @public static code */
|
||||
export * from '../common';
|
||||
|
|
|
@ -85,7 +85,7 @@ export class DataPlugin implements Plugin<DataSetup, void, DataPluginSetupDepend
|
|||
indexPatterns: indexPatternsService.indexPatterns,
|
||||
}),
|
||||
query: this.query.setup(),
|
||||
search: this.search.setup(),
|
||||
search: this.search.setup(savedObjectsClient),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -106,6 +106,8 @@ describe('QueryBar', () => {
|
|||
indexPatterns={[mockIndexPattern]}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -125,6 +127,8 @@ describe('QueryBar', () => {
|
|||
store={createMockStorage()}
|
||||
disableAutoFocus={true}
|
||||
intl={null as any}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -136,6 +140,8 @@ describe('QueryBar', () => {
|
|||
<QueryBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
onSubmit={noop}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
appName={'discover'}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
|
@ -151,6 +157,8 @@ describe('QueryBar', () => {
|
|||
<QueryBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
onSubmit={noop}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
appName={'discover'}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
|
@ -167,6 +175,8 @@ describe('QueryBar', () => {
|
|||
<QueryBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
onSubmit={noop}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
store={createMockStorage()}
|
||||
|
@ -187,6 +197,8 @@ describe('QueryBar', () => {
|
|||
uiSettings={setupMock.uiSettings}
|
||||
query={kqlQuery}
|
||||
onSubmit={noop}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
|
@ -206,6 +218,8 @@ describe('QueryBar', () => {
|
|||
uiSettings={setupMock.uiSettings}
|
||||
query={kqlQuery}
|
||||
onSubmit={noop}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
|
@ -225,6 +239,8 @@ describe('QueryBar', () => {
|
|||
<QueryBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
onSubmit={noop}
|
||||
onChange={noop}
|
||||
isDirty={false}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
store={createMockStorage()}
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
import { get, isEqual } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { timeHistory } from 'ui/timefilter/time_history';
|
||||
|
@ -50,13 +48,14 @@ interface DateRange {
|
|||
interface Props {
|
||||
query?: Query;
|
||||
onSubmit: (payload: { dateRange: DateRange; query?: Query }) => void;
|
||||
onChange: (payload: { dateRange: DateRange; query?: Query }) => void;
|
||||
disableAutoFocus?: boolean;
|
||||
appName: string;
|
||||
screenTitle?: string;
|
||||
indexPatterns?: Array<IndexPattern | string>;
|
||||
store: Storage;
|
||||
store?: Storage;
|
||||
intl: InjectedIntl;
|
||||
prepend?: any;
|
||||
prepend?: React.ReactNode;
|
||||
showQueryInput?: boolean;
|
||||
showDatePicker?: boolean;
|
||||
dateRangeFrom?: string;
|
||||
|
@ -66,15 +65,11 @@ interface Props {
|
|||
showAutoRefreshOnly?: boolean;
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
customSubmitButton?: any;
|
||||
isDirty: boolean;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
}
|
||||
|
||||
interface State {
|
||||
query?: Query;
|
||||
inputIsPristine: boolean;
|
||||
currentProps?: Props;
|
||||
dateRangeFrom: string;
|
||||
dateRangeTo: string;
|
||||
isDateRangeInvalid: boolean;
|
||||
}
|
||||
|
||||
|
@ -85,71 +80,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
showAutoRefreshOnly: false,
|
||||
};
|
||||
|
||||
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
if (isEqual(prevState.currentProps, nextProps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextQuery = null;
|
||||
if (nextProps.query && prevState.query) {
|
||||
if (nextProps.query.query !== prevState.query.query) {
|
||||
nextQuery = {
|
||||
query: nextProps.query.query,
|
||||
language: nextProps.query.language,
|
||||
};
|
||||
} else if (nextProps.query.language !== prevState.query.language) {
|
||||
nextQuery = {
|
||||
query: '',
|
||||
language: nextProps.query.language,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let nextDateRange = null;
|
||||
if (
|
||||
nextProps.dateRangeFrom !== get(prevState, 'currentProps.dateRangeFrom') ||
|
||||
nextProps.dateRangeTo !== get(prevState, 'currentProps.dateRangeTo')
|
||||
) {
|
||||
nextDateRange = {
|
||||
dateRangeFrom: nextProps.dateRangeFrom,
|
||||
dateRangeTo: nextProps.dateRangeTo,
|
||||
};
|
||||
}
|
||||
|
||||
const nextState: any = {
|
||||
currentProps: nextProps,
|
||||
};
|
||||
if (nextQuery) {
|
||||
nextState.query = nextQuery;
|
||||
}
|
||||
if (nextDateRange) {
|
||||
nextState.dateRangeFrom = nextDateRange.dateRangeFrom;
|
||||
nextState.dateRangeTo = nextDateRange.dateRangeTo;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/*
|
||||
Keep the "draft" value in local state until the user actually submits the query. There are a couple advantages:
|
||||
|
||||
1. Each app doesn't have to maintain its own "draft" value if it wants to put off updating the query in app state
|
||||
until the user manually submits their changes. Most apps have watches on the query value in app state so we don't
|
||||
want to trigger those on every keypress. Also, some apps (e.g. dashboard) already juggle multiple query values,
|
||||
each with slightly different semantics and I'd rather not add yet another variable to the mix.
|
||||
|
||||
2. Changes to the local component state won't trigger an Angular digest cycle. Triggering digest cycles on every
|
||||
keypress has been a major source of performance issues for us in previous implementations of the query bar.
|
||||
See https://github.com/elastic/kibana/issues/14086
|
||||
*/
|
||||
public state = {
|
||||
query: this.props.query && {
|
||||
query: this.props.query.query,
|
||||
language: this.props.query.language,
|
||||
},
|
||||
inputIsPristine: true,
|
||||
currentProps: this.props,
|
||||
dateRangeFrom: _.get(this.props, 'dateRangeFrom', 'now-15m'),
|
||||
dateRangeTo: _.get(this.props, 'dateRangeTo', 'now'),
|
||||
isDateRangeInvalid: false,
|
||||
};
|
||||
|
||||
|
@ -157,35 +88,26 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
|
||||
private persistedLog: PersistedLog | undefined;
|
||||
|
||||
private isQueryDirty = () => {
|
||||
return (
|
||||
!!this.props.query && !!this.state.query && this.state.query.query !== this.props.query.query
|
||||
);
|
||||
};
|
||||
|
||||
public isDirty = () => {
|
||||
if (!this.props.showDatePicker) {
|
||||
return this.isQueryDirty();
|
||||
}
|
||||
|
||||
return (
|
||||
this.isQueryDirty() ||
|
||||
this.state.dateRangeFrom !== this.props.dateRangeFrom ||
|
||||
this.state.dateRangeTo !== this.props.dateRangeTo
|
||||
);
|
||||
};
|
||||
|
||||
public onClickSubmitButton = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (this.persistedLog && this.state.query) {
|
||||
this.persistedLog.add(this.state.query.query);
|
||||
if (this.persistedLog && this.props.query) {
|
||||
this.persistedLog.add(this.props.query.query);
|
||||
}
|
||||
this.onSubmit(() => event.preventDefault());
|
||||
event.preventDefault();
|
||||
this.onSubmit({ query: this.props.query, dateRange: this.getDateRange() });
|
||||
};
|
||||
|
||||
public onChange = (query: Query) => {
|
||||
this.setState({
|
||||
public getDateRange() {
|
||||
const defaultTimeSetting = this.props.uiSettings.get('timepicker:timeDefaults');
|
||||
return {
|
||||
from: this.props.dateRangeFrom || defaultTimeSetting.from,
|
||||
to: this.props.dateRangeTo || defaultTimeSetting.to,
|
||||
};
|
||||
}
|
||||
|
||||
public onQueryChange = (query: Query) => {
|
||||
this.props.onChange({
|
||||
query,
|
||||
inputIsPristine: false,
|
||||
dateRange: this.getDateRange(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -202,41 +124,37 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
}) => {
|
||||
this.setState(
|
||||
{
|
||||
dateRangeFrom: start,
|
||||
dateRangeTo: end,
|
||||
isDateRangeInvalid: isInvalid,
|
||||
},
|
||||
() => isQuickSelection && this.onSubmit()
|
||||
() => {
|
||||
const retVal = {
|
||||
query: this.props.query,
|
||||
dateRange: {
|
||||
from: start,
|
||||
to: end,
|
||||
},
|
||||
};
|
||||
|
||||
if (isQuickSelection) {
|
||||
this.props.onSubmit(retVal);
|
||||
} else {
|
||||
this.props.onChange(retVal);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
public onSubmit = (preventDefault?: () => void) => {
|
||||
if (preventDefault) {
|
||||
preventDefault();
|
||||
}
|
||||
|
||||
public onSubmit = ({ query, dateRange }: { query?: Query; dateRange: DateRange }) => {
|
||||
this.handleLuceneSyntaxWarning();
|
||||
timeHistory.add(dateRange);
|
||||
|
||||
timeHistory.add({
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
});
|
||||
|
||||
this.props.onSubmit({
|
||||
query: this.state.query && {
|
||||
query: this.state.query.query,
|
||||
language: this.state.query.language,
|
||||
},
|
||||
dateRange: {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
},
|
||||
});
|
||||
this.props.onSubmit({ query, dateRange });
|
||||
};
|
||||
|
||||
private onInputSubmit = (query: Query) => {
|
||||
this.setState({ query }, () => {
|
||||
this.onSubmit();
|
||||
this.onSubmit({
|
||||
query,
|
||||
dateRange: this.getDateRange(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -287,10 +205,10 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
disableAutoFocus={this.props.disableAutoFocus}
|
||||
indexPatterns={this.props.indexPatterns!}
|
||||
prepend={this.props.prepend}
|
||||
query={this.state.query!}
|
||||
query={this.props.query!}
|
||||
screenTitle={this.props.screenTitle}
|
||||
store={this.props.store}
|
||||
onChange={this.onChange}
|
||||
store={this.props.store!}
|
||||
onChange={this.onQueryChange}
|
||||
onSubmit={this.onInputSubmit}
|
||||
persistedLog={this.persistedLog}
|
||||
uiSettings={this.props.uiSettings}
|
||||
|
@ -304,7 +222,9 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private shouldRenderQueryInput() {
|
||||
return this.props.showQueryInput && this.props.indexPatterns && this.props.query;
|
||||
return (
|
||||
this.props.showQueryInput && this.props.indexPatterns && this.props.query && this.props.store
|
||||
);
|
||||
}
|
||||
|
||||
private renderUpdateButton() {
|
||||
|
@ -312,7 +232,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
React.cloneElement(this.props.customSubmitButton, { onClick: this.onClickSubmitButton })
|
||||
) : (
|
||||
<EuiSuperUpdateButton
|
||||
needsUpdate={this.isDirty()}
|
||||
needsUpdate={this.props.isDirty}
|
||||
isDisabled={this.state.isDateRangeInvalid}
|
||||
onClick={this.onClickSubmitButton}
|
||||
data-test-subj="querySubmitButton"
|
||||
|
@ -358,8 +278,8 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
return (
|
||||
<EuiFlexItem className="kbnQueryBar__datePickerWrapper">
|
||||
<EuiSuperDatePicker
|
||||
start={this.state.dateRangeFrom}
|
||||
end={this.state.dateRangeTo}
|
||||
start={this.props.dateRangeFrom}
|
||||
end={this.props.dateRangeTo}
|
||||
isPaused={this.props.isRefreshPaused}
|
||||
refreshInterval={this.props.refreshInterval}
|
||||
onTimeChange={this.onTimeChange}
|
||||
|
@ -375,13 +295,13 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private handleLuceneSyntaxWarning() {
|
||||
if (!this.state.query) return;
|
||||
if (!this.props.query) return;
|
||||
const { intl, store } = this.props;
|
||||
const { query, language } = this.state.query;
|
||||
const { query, language } = this.props.query;
|
||||
if (
|
||||
language === 'kuery' &&
|
||||
typeof query === 'string' &&
|
||||
!store.get('kibana.luceneSyntaxWarningOptOut') &&
|
||||
(!store || !store.get('kibana.luceneSyntaxWarningOptOut')) &&
|
||||
doesKueryExpressionHaveLuceneSyntaxError(query)
|
||||
) {
|
||||
const toast = toastNotifications.addWarning({
|
||||
|
@ -411,7 +331,10 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton size="s" onClick={() => this.onLuceneSyntaxWarningOptOut(toast)}>
|
||||
Don't show again
|
||||
<FormattedMessage
|
||||
id="data.query.queryBar.luceneSyntaxWarningOptOutText"
|
||||
defaultMessage="Don't show again"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -422,6 +345,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private onLuceneSyntaxWarningOptOut(toast: Toast) {
|
||||
if (!this.props.store) return;
|
||||
this.props.store.set('kibana.luceneSyntaxWarningOptOut', true);
|
||||
toastNotifications.remove(toast);
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ interface Props {
|
|||
appName: string;
|
||||
disableAutoFocus?: boolean;
|
||||
screenTitle?: string;
|
||||
prepend?: any;
|
||||
prepend?: React.ReactNode;
|
||||
persistedLog?: PersistedLog;
|
||||
bubbleSubmitEvent?: boolean;
|
||||
languageSwitcherPopoverAnchorPosition?: PopoverAnchorPosition;
|
||||
|
@ -202,10 +202,8 @@ export class QueryBarInputUI extends Component<Props, State> {
|
|||
};
|
||||
|
||||
private onQueryStringChange = (value: string) => {
|
||||
const hasValue = Boolean(value.trim());
|
||||
|
||||
this.setState({
|
||||
isSuggestionsVisible: hasValue,
|
||||
isSuggestionsVisible: true,
|
||||
index: null,
|
||||
suggestionLimit: 50,
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ exports[`SuggestionComponent Should display the suggestion and use the provided
|
|||
<div
|
||||
aria-selected={false}
|
||||
className="kbnTypeahead__item"
|
||||
data-test-subj="autocompleteSuggestion-value-as-promised,-not-helpful"
|
||||
id="suggestion-1"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
|
@ -37,6 +38,7 @@ exports[`SuggestionComponent Should make the element active if the selected prop
|
|||
<div
|
||||
aria-selected={true}
|
||||
className="kbnTypeahead__item active"
|
||||
data-test-subj="autocompleteSuggestion-value-as-promised,-not-helpful"
|
||||
id="suggestion-1"
|
||||
onClick={[Function]}
|
||||
onMouseEnter={[Function]}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import { EuiIcon } from '@elastic/eui';
|
||||
import classNames from 'classnames';
|
||||
import React, { SFC } from 'react';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
|
||||
function getEuiIconType(type: string) {
|
||||
|
@ -48,7 +48,7 @@ interface Props {
|
|||
ariaId: string;
|
||||
}
|
||||
|
||||
export const SuggestionComponent: SFC<Props> = props => {
|
||||
export const SuggestionComponent: FunctionComponent<Props> = props => {
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
|
||||
<div
|
||||
|
@ -62,6 +62,9 @@ export const SuggestionComponent: SFC<Props> = props => {
|
|||
ref={props.innerRef}
|
||||
id={props.ariaId}
|
||||
aria-selected={props.selected}
|
||||
data-test-subj={`autocompleteSuggestion-${
|
||||
props.suggestion.type
|
||||
}-${props.suggestion.text.replace(/\s/g, '-')}`}
|
||||
>
|
||||
<div className={'kbnSuggestionItem kbnSuggestionItem--' + props.suggestion.type}>
|
||||
<div className="kbnSuggestionItem__type">
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'components/saved_query_management/saved_query_management_component';
|
|
@ -0,0 +1,14 @@
|
|||
.saved-query-management-popover {
|
||||
width: 400px;
|
||||
}
|
||||
.saved-query-list {
|
||||
@include euiYScrollWithShadows;
|
||||
}
|
||||
.saved-query-list-wrapper {
|
||||
height: 20vh;
|
||||
overflow-y:hidden;
|
||||
}
|
||||
.saved-query-list li:first-child .saved-query-list-item-text {
|
||||
font-weight: $euiFontWeightBold;
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiOverlayMask,
|
||||
EuiModal,
|
||||
EuiButton,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiModalFooter,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { sortBy } from 'lodash';
|
||||
import { SavedQuery, SavedQueryAttributes } from '../../index';
|
||||
import { SavedQueryService } from '../../lib/saved_query_service';
|
||||
|
||||
interface Props {
|
||||
savedQuery?: SavedQueryAttributes;
|
||||
savedQueryService: SavedQueryService;
|
||||
onSave: (savedQueryMeta: SavedQueryMeta) => void;
|
||||
onClose: () => void;
|
||||
showFilterOption: boolean | undefined;
|
||||
showTimeFilterOption: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface SavedQueryMeta {
|
||||
title: string;
|
||||
description: string;
|
||||
shouldIncludeFilters: boolean;
|
||||
shouldIncludeTimefilter: boolean;
|
||||
}
|
||||
|
||||
export const SaveQueryForm: FunctionComponent<Props> = ({
|
||||
savedQuery,
|
||||
savedQueryService,
|
||||
onSave,
|
||||
onClose,
|
||||
showFilterOption = true,
|
||||
showTimeFilterOption = true,
|
||||
}) => {
|
||||
const [title, setTitle] = useState(savedQuery ? savedQuery.title : '');
|
||||
const [description, setDescription] = useState(savedQuery ? savedQuery.description : '');
|
||||
const [savedQueries, setSavedQueries] = useState<SavedQuery[]>([]);
|
||||
const [shouldIncludeFilters, setShouldIncludeFilters] = useState(
|
||||
savedQuery ? !!savedQuery.filters : true
|
||||
);
|
||||
// Defaults to false because saved queries are meant to be as portable as possible and loading
|
||||
// a saved query with a time filter will override whatever the current value of the global timepicker
|
||||
// is. We expect this option to be used rarely and only when the user knows they want this behavior.
|
||||
const [shouldIncludeTimefilter, setIncludeTimefilter] = useState(
|
||||
savedQuery ? !!savedQuery.timefilter : false
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchQueries = async () => {
|
||||
const allSavedQueries = await savedQueryService.getAllSavedQueries();
|
||||
const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title') as SavedQuery[];
|
||||
setSavedQueries(sortedAllSavedQueries);
|
||||
};
|
||||
fetchQueries();
|
||||
}, []);
|
||||
|
||||
const savedQueryDescriptionText = i18n.translate(
|
||||
'data.search.searchBar.savedQueryDescriptionText',
|
||||
{
|
||||
defaultMessage: 'Save query text and filters that you want to use again.',
|
||||
}
|
||||
);
|
||||
|
||||
const hasTitleConflict = !!savedQueries.find(
|
||||
existingSavedQuery => !savedQuery && existingSavedQuery.attributes.title === title
|
||||
);
|
||||
|
||||
const hasWhitespaceError = title.length > title.trim().length;
|
||||
|
||||
const titleConflictErrorText = i18n.translate(
|
||||
'data.search.searchBar.savedQueryForm.titleConflictText',
|
||||
{
|
||||
defaultMessage: 'Title conflicts with an existing saved query',
|
||||
}
|
||||
);
|
||||
const whitespaceErrorText = i18n.translate(
|
||||
'data.search.searchBar.savedQueryForm.whitespaceErrorText',
|
||||
{
|
||||
defaultMessage: 'Title cannot contain leading or trailing white space',
|
||||
}
|
||||
);
|
||||
const hasErrors = hasWhitespaceError || hasTitleConflict;
|
||||
|
||||
const errors = () => {
|
||||
if (hasWhitespaceError) return [whitespaceErrorText];
|
||||
if (hasTitleConflict) return [titleConflictErrorText];
|
||||
return [];
|
||||
};
|
||||
const saveQueryForm = (
|
||||
<EuiForm isInvalid={hasErrors} error={errors()}>
|
||||
<EuiFormRow>
|
||||
<EuiText color="subdued">{savedQueryDescriptionText}</EuiText>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow
|
||||
label={i18n.translate('data.search.searchBar.savedQueryNameLabelText', {
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
helpText={i18n.translate('data.search.searchBar.savedQueryNameHelpText', {
|
||||
defaultMessage:
|
||||
'Name cannot contain leading or trailing whitespace. Name must be unique.',
|
||||
})}
|
||||
isInvalid={hasErrors}
|
||||
>
|
||||
<EuiFieldText
|
||||
disabled={!!savedQuery}
|
||||
value={title}
|
||||
name="title"
|
||||
onChange={event => {
|
||||
setTitle(event.target.value);
|
||||
}}
|
||||
data-test-subj="saveQueryFormTitle"
|
||||
isInvalid={hasErrors}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={i18n.translate('data.search.searchBar.savedQueryDescriptionLabelText', {
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
>
|
||||
<EuiFieldText
|
||||
value={description}
|
||||
name="description"
|
||||
onChange={event => {
|
||||
setDescription(event.target.value);
|
||||
}}
|
||||
data-test-subj="saveQueryFormDescription"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{showFilterOption && (
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
name="shouldIncludeFilters"
|
||||
label={i18n.translate('data.search.searchBar.savedQueryIncludeFiltersLabelText', {
|
||||
defaultMessage: 'Include filters',
|
||||
})}
|
||||
checked={shouldIncludeFilters}
|
||||
onChange={() => {
|
||||
setShouldIncludeFilters(!shouldIncludeFilters);
|
||||
}}
|
||||
data-test-subj="saveQueryFormIncludeFiltersOption"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
|
||||
{showTimeFilterOption && (
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
name="shouldIncludeTimefilter"
|
||||
label={i18n.translate('data.search.searchBar.savedQueryIncludeTimeFilterLabelText', {
|
||||
defaultMessage: 'Include time filter',
|
||||
})}
|
||||
checked={shouldIncludeTimefilter}
|
||||
onChange={() => {
|
||||
setIncludeTimefilter(!shouldIncludeTimefilter);
|
||||
}}
|
||||
data-test-subj="saveQueryFormIncludeTimeFilterOption"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
</EuiForm>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={onClose} initialFocus="[name=title]">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
{i18n.translate('data.search.searchBar.savedQueryFormTitle', {
|
||||
defaultMessage: 'Save query',
|
||||
})}
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>{saveQueryForm}</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty onClick={onClose} data-test-subj="savedQueryFormCancelButton">
|
||||
{i18n.translate('data.search.searchBar.savedQueryFormCancelButtonText', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton
|
||||
onClick={() =>
|
||||
onSave({
|
||||
title,
|
||||
description,
|
||||
shouldIncludeFilters,
|
||||
shouldIncludeTimefilter,
|
||||
})
|
||||
}
|
||||
fill
|
||||
data-test-subj="savedQueryFormSaveButton"
|
||||
disabled={hasErrors}
|
||||
>
|
||||
{i18n.translate('data.search.searchBar.savedQueryFormSaveButtonText', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiConfirmModal,
|
||||
EuiOverlayMask,
|
||||
EuiIconTip,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SavedQuery } from '../../index';
|
||||
|
||||
interface Props {
|
||||
savedQuery: SavedQuery;
|
||||
isSelected: boolean;
|
||||
showWriteOperations: boolean;
|
||||
onSelect: (savedQuery: SavedQuery) => void;
|
||||
onDelete: (savedQuery: SavedQuery) => void;
|
||||
}
|
||||
|
||||
export const SavedQueryListItem = ({
|
||||
savedQuery,
|
||||
isSelected,
|
||||
onSelect,
|
||||
onDelete,
|
||||
showWriteOperations,
|
||||
}: Props) => {
|
||||
const [showDeletionConfirmationModal, setShowDeletionConfirmationModal] = useState(false);
|
||||
|
||||
const selectButtonAriaLabelText = isSelected
|
||||
? i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSavedQueryListItemSelectedButtonAriaLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'Saved query button selected {savedQueryName}. Press to clear any changes.',
|
||||
values: { savedQueryName: savedQuery.attributes.title },
|
||||
}
|
||||
)
|
||||
: i18n.translate('data.search.searchBar.savedQueryPopoverSavedQueryListItemButtonAriaLabel', {
|
||||
defaultMessage: 'Saved query button {savedQueryName}',
|
||||
values: { savedQueryName: savedQuery.attributes.title },
|
||||
});
|
||||
|
||||
const selectButtonDataTestSubj = isSelected
|
||||
? `load-saved-query-${savedQuery.attributes.title}-button saved-query-list-item-selected`
|
||||
: `load-saved-query-${savedQuery.attributes.title}-button`;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<li
|
||||
key={savedQuery.id}
|
||||
data-test-subj={`saved-query-list-item ${
|
||||
isSelected ? 'saved-query-list-item-selected' : ''
|
||||
}`}
|
||||
>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
onSelect(savedQuery);
|
||||
}}
|
||||
flush="left"
|
||||
data-test-subj={selectButtonDataTestSubj}
|
||||
textProps={isSelected ? { className: 'saved-query-list-item-text' } : undefined}
|
||||
aria-label={selectButtonAriaLabelText}
|
||||
>
|
||||
{savedQuery.attributes.title}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="none" justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
{savedQuery.attributes.description && (
|
||||
<EuiIconTip
|
||||
type="iInCircle"
|
||||
content={savedQuery.attributes.description}
|
||||
aria-label={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSavedQueryListItemDescriptionAriaLabel',
|
||||
{
|
||||
defaultMessage: '{savedQueryName} description',
|
||||
values: { savedQueryName: savedQuery.attributes.title },
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
{showWriteOperations && (
|
||||
<Fragment>
|
||||
<EuiToolTip
|
||||
position="top"
|
||||
content={
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverDeleteButtonTooltip',
|
||||
{
|
||||
defaultMessage: 'Delete saved query',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
setShowDeletionConfirmationModal(true);
|
||||
}}
|
||||
iconType="trash"
|
||||
color="danger"
|
||||
aria-label={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverDeleteButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Delete saved query {savedQueryName}',
|
||||
values: { savedQueryName: savedQuery.attributes.title },
|
||||
}
|
||||
)}
|
||||
data-test-subj={`delete-saved-query-${savedQuery.attributes.title}-button`}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</Fragment>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</li>
|
||||
|
||||
{showDeletionConfirmationModal && (
|
||||
<EuiOverlayMask>
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('data.search.searchBar.savedQueryPopoverConfirmDeletionTitle', {
|
||||
defaultMessage: 'Delete {savedQueryName}?',
|
||||
values: {
|
||||
savedQueryName: savedQuery.attributes.title,
|
||||
},
|
||||
})}
|
||||
confirmButtonText={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverConfirmDeletionConfirmButtonText',
|
||||
{
|
||||
defaultMessage: 'Delete',
|
||||
}
|
||||
)}
|
||||
cancelButtonText={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverConfirmDeletionCancelButtonText',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
onConfirm={() => {
|
||||
onDelete(savedQuery);
|
||||
setShowDeletionConfirmationModal(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowDeletionConfirmationModal(false);
|
||||
}}
|
||||
/>
|
||||
</EuiOverlayMask>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,293 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
EuiPopover,
|
||||
EuiPopoverTitle,
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPagination,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FunctionComponent, useEffect, useState, Fragment } from 'react';
|
||||
import { sortBy } from 'lodash';
|
||||
import { SavedQuery } from '../../index';
|
||||
import { SavedQueryService } from '../../lib/saved_query_service';
|
||||
import { SavedQueryListItem } from './saved_query_list_item';
|
||||
|
||||
const pageCount = 50;
|
||||
|
||||
interface Props {
|
||||
showSaveQuery?: boolean;
|
||||
loadedSavedQuery?: SavedQuery;
|
||||
savedQueryService: SavedQueryService;
|
||||
onSave: () => void;
|
||||
onSaveAsNew: () => void;
|
||||
onLoad: (savedQuery: SavedQuery) => void;
|
||||
onClearSavedQuery: () => void;
|
||||
}
|
||||
|
||||
export const SavedQueryManagementComponent: FunctionComponent<Props> = ({
|
||||
showSaveQuery,
|
||||
loadedSavedQuery,
|
||||
onSave,
|
||||
onSaveAsNew,
|
||||
onLoad,
|
||||
onClearSavedQuery,
|
||||
savedQueryService,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [savedQueries, setSavedQueries] = useState([] as SavedQuery[]);
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchQueries = async () => {
|
||||
const allSavedQueries = await savedQueryService.getAllSavedQueries();
|
||||
const sortedAllSavedQueries = sortBy(allSavedQueries, 'attributes.title');
|
||||
setSavedQueries(sortedAllSavedQueries);
|
||||
};
|
||||
if (isOpen) {
|
||||
fetchQueries();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const goToPage = (pageNumber: number) => {
|
||||
setActivePage(pageNumber);
|
||||
};
|
||||
|
||||
const savedQueryDescriptionText = i18n.translate(
|
||||
'data.search.searchBar.savedQueryDescriptionText',
|
||||
{
|
||||
defaultMessage: 'Save query text and filters that you want to use again.',
|
||||
}
|
||||
);
|
||||
|
||||
const noSavedQueriesDescriptionText =
|
||||
i18n.translate('data.search.searchBar.savedQueryNoSavedQueriesText', {
|
||||
defaultMessage: 'There are no saved queries.',
|
||||
}) +
|
||||
' ' +
|
||||
savedQueryDescriptionText;
|
||||
|
||||
const savedQueryPopoverTitleText = i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverTitleText',
|
||||
{
|
||||
defaultMessage: 'Saved Queries',
|
||||
}
|
||||
);
|
||||
|
||||
const onDeleteSavedQuery = async (savedQuery: SavedQuery) => {
|
||||
setSavedQueries(
|
||||
savedQueries.filter(currentSavedQuery => currentSavedQuery.id !== savedQuery.id)
|
||||
);
|
||||
|
||||
if (loadedSavedQuery && loadedSavedQuery.id === savedQuery.id) {
|
||||
onClearSavedQuery();
|
||||
}
|
||||
|
||||
await savedQueryService.deleteSavedQuery(savedQuery.id);
|
||||
};
|
||||
|
||||
const savedQueryPopoverButton = (
|
||||
<EuiButtonEmpty
|
||||
className="euiFormControlLayout__prepend"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
aria-label={i18n.translate('data.search.searchBar.savedQueryPopoverButtonText', {
|
||||
defaultMessage: 'See saved queries',
|
||||
})}
|
||||
data-test-subj="saved-query-management-popover-button"
|
||||
>
|
||||
#
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const savedQueryRows = () => {
|
||||
// we should be recalculating the savedQueryRows after a delete action
|
||||
const savedQueriesWithoutCurrent = savedQueries.filter(savedQuery => {
|
||||
if (!loadedSavedQuery) return true;
|
||||
return savedQuery.id !== loadedSavedQuery.id;
|
||||
});
|
||||
const savedQueriesReordered =
|
||||
loadedSavedQuery && savedQueriesWithoutCurrent.length !== savedQueries.length
|
||||
? [loadedSavedQuery, ...savedQueriesWithoutCurrent]
|
||||
: [...savedQueriesWithoutCurrent];
|
||||
const savedQueriesDisplayRows = savedQueriesReordered.slice(
|
||||
activePage * pageCount,
|
||||
activePage * pageCount + pageCount
|
||||
);
|
||||
return savedQueriesDisplayRows.map(savedQuery => (
|
||||
<SavedQueryListItem
|
||||
key={savedQuery.id}
|
||||
savedQuery={savedQuery}
|
||||
isSelected={!!loadedSavedQuery && loadedSavedQuery.id === savedQuery.id}
|
||||
onSelect={savedQueryToSelect => {
|
||||
onLoad(savedQueryToSelect);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
onDelete={savedQueryToDelete => onDeleteSavedQuery(savedQueryToDelete)}
|
||||
showWriteOperations={!!showSaveQuery}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiPopover
|
||||
id="savedQueryPopover"
|
||||
button={savedQueryPopoverButton}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
anchorPosition="downLeft"
|
||||
ownFocus
|
||||
>
|
||||
<div
|
||||
className="saved-query-management-popover"
|
||||
data-test-subj="saved-query-management-popover"
|
||||
>
|
||||
<EuiPopoverTitle id={'savedQueryManagementPopoverTitle'}>
|
||||
{savedQueryPopoverTitleText}
|
||||
</EuiPopoverTitle>
|
||||
{savedQueries.length > 0 ? (
|
||||
<Fragment>
|
||||
<EuiFlexGroup wrap>
|
||||
<EuiFlexItem>
|
||||
<EuiText>{savedQueryDescriptionText}</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem className="saved-query-list-wrapper">
|
||||
<ul
|
||||
className="saved-query-list"
|
||||
aria-labelledby={'savedQueryManagementPopoverTitle'}
|
||||
>
|
||||
{savedQueryRows()}
|
||||
</ul>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup justifyContent="spaceAround">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPagination
|
||||
pageCount={Math.ceil(savedQueries.length / pageCount)}
|
||||
activePage={activePage}
|
||||
onPageClick={goToPage}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Fragment>
|
||||
) : (
|
||||
<EuiText grow={false}>{noSavedQueriesDescriptionText}</EuiText>
|
||||
)}
|
||||
<EuiFlexGroup direction="rowReverse" alignItems="center" justifyContent="flexEnd">
|
||||
{showSaveQuery && loadedSavedQuery && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => onSaveAsNew()}
|
||||
aria-label={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSaveAsNewButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Save as a new saved query',
|
||||
}
|
||||
)}
|
||||
data-test-subj="saved-query-management-save-as-new-button"
|
||||
>
|
||||
{i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSaveAsNewButtonText',
|
||||
{
|
||||
defaultMessage: 'Save as new',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={() => onSave()}
|
||||
aria-label={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSaveChangesButtonAriaLabel',
|
||||
{
|
||||
defaultMessage: 'Save changes to {title}',
|
||||
values: { title: loadedSavedQuery.attributes.title },
|
||||
}
|
||||
)}
|
||||
data-test-subj="saved-query-management-save-changes-button"
|
||||
>
|
||||
{i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSaveChangesButtonText',
|
||||
{
|
||||
defaultMessage: 'Save changes',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{showSaveQuery && !loadedSavedQuery && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
onClick={() => onSave()}
|
||||
aria-label={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverSaveButtonAriaLabel',
|
||||
{ defaultMessage: 'Save a new saved query' }
|
||||
)}
|
||||
data-test-subj="saved-query-management-save-button"
|
||||
>
|
||||
{i18n.translate('data.search.searchBar.savedQueryPopoverSaveButtonText', {
|
||||
defaultMessage: 'Save',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem />
|
||||
<EuiFlexItem grow={false}>
|
||||
{loadedSavedQuery && (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => onClearSavedQuery()}
|
||||
aria-label={i18n.translate(
|
||||
'data.search.searchBar.savedQueryPopoverClearButtonAriaLabel',
|
||||
{ defaultMessage: 'Clear current saved query' }
|
||||
)}
|
||||
data-test-subj="saved-query-management-clear-button"
|
||||
>
|
||||
{i18n.translate('data.search.searchBar.savedQueryPopoverClearButtonText', {
|
||||
defaultMessage: 'Clear',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
|
@ -32,6 +32,13 @@ jest.mock('../../../../../data/public', () => {
|
|||
};
|
||||
});
|
||||
|
||||
jest.mock('ui/notify', () => ({
|
||||
toastNotifications: {
|
||||
addSuccess: () => {},
|
||||
addDanger: () => {},
|
||||
},
|
||||
}));
|
||||
|
||||
const noop = jest.fn();
|
||||
|
||||
const createMockWebStorage = () => ({
|
||||
|
@ -66,6 +73,14 @@ const mockIndexPattern = {
|
|||
],
|
||||
} as IndexPattern;
|
||||
|
||||
const mockSavedQueryService = {
|
||||
saveQuery: jest.fn(),
|
||||
getAllSavedQueries: jest.fn(),
|
||||
findSavedQueries: jest.fn(),
|
||||
getSavedQuery: jest.fn(),
|
||||
deleteSavedQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const kqlQuery = {
|
||||
query: 'response:200',
|
||||
language: 'kuery',
|
||||
|
@ -84,6 +99,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
@ -99,6 +115,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
@ -115,6 +132,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
@ -133,6 +151,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
@ -152,6 +171,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
@ -171,6 +191,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
@ -191,6 +212,7 @@ describe('SearchBar', () => {
|
|||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedQueryService={mockSavedQueryService}
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
|
|
|
@ -17,18 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import { EuiFilterButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import classNames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { Storage } from 'ui/storage';
|
||||
import { get, isEqual } from 'lodash';
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { UiSettingsClientContract } from 'src/core/public';
|
||||
import { IndexPattern, Query, QueryBar, FilterBar } from '../../../../../data/public';
|
||||
import { SavedQuery, SavedQueryAttributes } from '../index';
|
||||
import { SavedQueryMeta, SaveQueryForm } from './saved_query_management/save_query_form';
|
||||
import { SavedQueryManagementComponent } from './saved_query_management/saved_query_management_component';
|
||||
import { SavedQueryService } from '../lib/saved_query_service';
|
||||
|
||||
interface DateRange {
|
||||
from: string;
|
||||
|
@ -43,6 +46,7 @@ export interface SearchBarProps {
|
|||
appName: string;
|
||||
intl: InjectedIntl;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
savedQueryService: SavedQueryService;
|
||||
indexPatterns?: IndexPattern[];
|
||||
// Query bar
|
||||
showQueryBar?: boolean;
|
||||
|
@ -50,6 +54,7 @@ export interface SearchBarProps {
|
|||
screenTitle?: string;
|
||||
store?: Storage;
|
||||
query?: Query;
|
||||
savedQuery?: SavedQuery;
|
||||
onQuerySubmit?: (payload: { dateRange: DateRange; query?: Query }) => void;
|
||||
// Filter bar
|
||||
showFilterBar?: boolean;
|
||||
|
@ -63,11 +68,23 @@ export interface SearchBarProps {
|
|||
isRefreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
showAutoRefreshOnly?: boolean;
|
||||
showSaveQuery?: boolean;
|
||||
onRefreshChange?: (options: { isPaused: boolean; refreshInterval: number }) => void;
|
||||
onSaved?: (savedQuery: SavedQuery) => void;
|
||||
onSavedQueryUpdated?: (savedQuery: SavedQuery) => void;
|
||||
onClearSavedQuery?: () => void;
|
||||
customSubmitButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
isFiltersVisible: boolean;
|
||||
showSaveQueryModal: boolean;
|
||||
showSaveNewQueryModal: boolean;
|
||||
showSavedQueryPopover: boolean;
|
||||
currentProps?: SearchBarProps;
|
||||
query?: Query;
|
||||
dateRangeFrom: string;
|
||||
dateRangeTo: string;
|
||||
}
|
||||
|
||||
class SearchBarUI extends Component<SearchBarProps, State> {
|
||||
|
@ -81,15 +98,86 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
public filterBarRef: Element | null = null;
|
||||
public filterBarWrapperRef: Element | null = null;
|
||||
|
||||
public static getDerivedStateFromProps(nextProps: SearchBarProps, prevState: State) {
|
||||
if (isEqual(prevState.currentProps, nextProps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextQuery = null;
|
||||
if (nextProps.query && nextProps.query.query !== get(prevState, 'currentProps.query.query')) {
|
||||
nextQuery = {
|
||||
query: nextProps.query.query,
|
||||
language: nextProps.query.language,
|
||||
};
|
||||
} else if (
|
||||
nextProps.query &&
|
||||
prevState.query &&
|
||||
nextProps.query.language !== prevState.query.language
|
||||
) {
|
||||
nextQuery = {
|
||||
query: '',
|
||||
language: nextProps.query.language,
|
||||
};
|
||||
}
|
||||
|
||||
let nextDateRange = null;
|
||||
if (
|
||||
nextProps.dateRangeFrom !== get(prevState, 'currentProps.dateRangeFrom') ||
|
||||
nextProps.dateRangeTo !== get(prevState, 'currentProps.dateRangeTo')
|
||||
) {
|
||||
nextDateRange = {
|
||||
dateRangeFrom: nextProps.dateRangeFrom,
|
||||
dateRangeTo: nextProps.dateRangeTo,
|
||||
};
|
||||
}
|
||||
|
||||
const nextState: any = {
|
||||
currentProps: nextProps,
|
||||
};
|
||||
if (nextQuery) {
|
||||
nextState.query = nextQuery;
|
||||
}
|
||||
if (nextDateRange) {
|
||||
nextState.dateRangeFrom = nextDateRange.dateRangeFrom;
|
||||
nextState.dateRangeTo = nextDateRange.dateRangeTo;
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
/*
|
||||
Keep the "draft" value in local state until the user actually submits the query. There are a couple advantages:
|
||||
|
||||
1. Each app doesn't have to maintain its own "draft" value if it wants to put off updating the query in app state
|
||||
until the user manually submits their changes. Most apps have watches on the query value in app state so we don't
|
||||
want to trigger those on every keypress. Also, some apps (e.g. dashboard) already juggle multiple query values,
|
||||
each with slightly different semantics and I'd rather not add yet another variable to the mix.
|
||||
|
||||
2. Changes to the local component state won't trigger an Angular digest cycle. Triggering digest cycles on every
|
||||
keypress has been a major source of performance issues for us in previous implementations of the query bar.
|
||||
See https://github.com/elastic/kibana/issues/14086
|
||||
*/
|
||||
public state = {
|
||||
isFiltersVisible: true,
|
||||
showSaveQueryModal: false,
|
||||
showSaveNewQueryModal: false,
|
||||
showSavedQueryPopover: false,
|
||||
currentProps: this.props,
|
||||
query: this.props.query ? { ...this.props.query } : undefined,
|
||||
dateRangeFrom: get(this.props, 'dateRangeFrom', 'now-15m'),
|
||||
dateRangeTo: get(this.props, 'dateRangeTo', 'now'),
|
||||
};
|
||||
|
||||
private getFilterLength() {
|
||||
if (this.props.showFilterBar && this.props.filters) {
|
||||
return this.props.filters.length;
|
||||
public isDirty = () => {
|
||||
if (!this.props.showDatePicker && this.state.query && this.props.query) {
|
||||
return this.state.query.query !== this.props.query.query;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
(this.state.query && this.props.query && this.state.query.query !== this.props.query.query) ||
|
||||
this.state.dateRangeFrom !== this.props.dateRangeFrom ||
|
||||
this.state.dateRangeTo !== this.props.dateRangeTo
|
||||
);
|
||||
};
|
||||
|
||||
private getFilterUpdateFunction() {
|
||||
if (this.props.showFilterBar && this.props.onFiltersUpdated) {
|
||||
|
@ -101,7 +189,7 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
private shouldRenderQueryBar() {
|
||||
const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly;
|
||||
const showQueryInput =
|
||||
this.props.showQueryInput && this.props.indexPatterns && this.props.query;
|
||||
this.props.showQueryInput && this.props.indexPatterns && this.state.query;
|
||||
return this.props.showQueryBar && (showDatePicker || showQueryInput);
|
||||
}
|
||||
|
||||
|
@ -109,46 +197,6 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
return this.props.showFilterBar && this.props.filters && this.props.indexPatterns;
|
||||
}
|
||||
|
||||
private getFilterTriggerButton() {
|
||||
const filterCount = this.getFilterLength();
|
||||
const filtersAppliedText = this.props.intl.formatMessage(
|
||||
{
|
||||
id: 'data.search.searchBar.searchBar.filtersButtonFiltersAppliedTitle',
|
||||
defaultMessage:
|
||||
'{filterCount} {filterCount, plural, one {filter} other {filters}} applied.',
|
||||
},
|
||||
{
|
||||
filterCount,
|
||||
}
|
||||
);
|
||||
const clickToShowOrHideText = this.state.isFiltersVisible
|
||||
? this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.searchBar.filtersButtonClickToShowTitle',
|
||||
defaultMessage: 'Select to hide',
|
||||
})
|
||||
: this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.searchBar.filtersButtonClickToHideTitle',
|
||||
defaultMessage: 'Select to show',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFilterButton
|
||||
onClick={this.toggleFiltersVisible}
|
||||
isSelected={this.state.isFiltersVisible}
|
||||
hasActiveFilters={this.state.isFiltersVisible}
|
||||
numFilters={filterCount ? this.getFilterLength() : undefined}
|
||||
aria-controls="GlobalFilterGroup"
|
||||
aria-expanded={!!this.state.isFiltersVisible}
|
||||
title={`${filterCount ? filtersAppliedText : ''} ${clickToShowOrHideText}`}
|
||||
>
|
||||
{i18n.translate('data.search.searchBar.searchBar.filtersButtonLabel', {
|
||||
defaultMessage: 'Filters',
|
||||
description: 'The noun "filter" in plural.',
|
||||
})}
|
||||
</EuiFilterButton>
|
||||
);
|
||||
}
|
||||
|
||||
public setFilterBarHeight = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const height =
|
||||
|
@ -164,12 +212,131 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
public ro = new ResizeObserver(this.setFilterBarHeight);
|
||||
/* eslint-enable */
|
||||
|
||||
public toggleFiltersVisible = () => {
|
||||
public onSave = async (savedQueryMeta: SavedQueryMeta, saveAsNew = false) => {
|
||||
if (!this.state.query) return;
|
||||
|
||||
const savedQueryAttributes: SavedQueryAttributes = {
|
||||
title: savedQueryMeta.title,
|
||||
description: savedQueryMeta.description,
|
||||
query: this.state.query,
|
||||
};
|
||||
|
||||
if (savedQueryMeta.shouldIncludeFilters) {
|
||||
savedQueryAttributes.filters = this.props.filters;
|
||||
}
|
||||
|
||||
if (
|
||||
savedQueryMeta.shouldIncludeTimefilter &&
|
||||
this.state.dateRangeTo !== undefined &&
|
||||
this.state.dateRangeFrom !== undefined &&
|
||||
this.props.refreshInterval !== undefined &&
|
||||
this.props.isRefreshPaused !== undefined
|
||||
) {
|
||||
savedQueryAttributes.timefilter = {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
refreshInterval: {
|
||||
value: this.props.refreshInterval,
|
||||
pause: this.props.isRefreshPaused,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (this.props.savedQuery && !saveAsNew) {
|
||||
response = await this.props.savedQueryService.saveQuery(savedQueryAttributes, {
|
||||
overwrite: true,
|
||||
});
|
||||
} else {
|
||||
response = await this.props.savedQueryService.saveQuery(savedQueryAttributes);
|
||||
}
|
||||
|
||||
toastNotifications.addSuccess(`Your query "${response.attributes.title}" was saved`);
|
||||
|
||||
this.setState({
|
||||
showSaveQueryModal: false,
|
||||
showSaveNewQueryModal: false,
|
||||
});
|
||||
|
||||
if (this.props.onSaved) {
|
||||
this.props.onSaved(response);
|
||||
}
|
||||
|
||||
if (this.props.onQuerySubmit) {
|
||||
this.props.onQuerySubmit({
|
||||
query: this.state.query,
|
||||
dateRange: {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toastNotifications.addDanger(`An error occured while saving your query: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
public onInitiateSave = () => {
|
||||
this.setState({
|
||||
isFiltersVisible: !this.state.isFiltersVisible,
|
||||
showSaveQueryModal: true,
|
||||
});
|
||||
};
|
||||
|
||||
public onInitiateSaveNew = () => {
|
||||
this.setState({
|
||||
showSaveNewQueryModal: true,
|
||||
});
|
||||
};
|
||||
|
||||
public onQueryBarChange = (queryAndDateRange: { dateRange: DateRange; query?: Query }) => {
|
||||
this.setState({
|
||||
query: queryAndDateRange.query,
|
||||
dateRangeFrom: queryAndDateRange.dateRange.from,
|
||||
dateRangeTo: queryAndDateRange.dateRange.to,
|
||||
});
|
||||
};
|
||||
|
||||
public onQueryBarSubmit = (queryAndDateRange: { dateRange?: DateRange; query?: Query }) => {
|
||||
this.setState(
|
||||
{
|
||||
query: queryAndDateRange.query,
|
||||
dateRangeFrom:
|
||||
(queryAndDateRange.dateRange && queryAndDateRange.dateRange.from) ||
|
||||
this.state.dateRangeFrom,
|
||||
dateRangeTo:
|
||||
(queryAndDateRange.dateRange && queryAndDateRange.dateRange.to) || this.state.dateRangeTo,
|
||||
},
|
||||
() => {
|
||||
if (this.props.onQuerySubmit) {
|
||||
this.props.onQuerySubmit({
|
||||
query: this.state.query,
|
||||
dateRange: {
|
||||
from: this.state.dateRangeFrom,
|
||||
to: this.state.dateRangeTo,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
public onLoadSavedQuery = (savedQuery: SavedQuery) => {
|
||||
const dateRangeFrom = get(savedQuery, 'attributes.timefilter.from', this.state.dateRangeFrom);
|
||||
const dateRangeTo = get(savedQuery, 'attributes.timefilter.to', this.state.dateRangeTo);
|
||||
|
||||
this.setState({
|
||||
query: savedQuery.attributes.query,
|
||||
dateRangeFrom,
|
||||
dateRangeTo,
|
||||
});
|
||||
|
||||
if (this.props.onSavedQueryUpdated) {
|
||||
this.props.onSavedQueryUpdated(savedQuery);
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.filterBarRef) {
|
||||
this.setFilterBarHeight();
|
||||
|
@ -191,26 +358,43 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const savedQueryManagement = this.state.query && this.props.onClearSavedQuery && (
|
||||
<SavedQueryManagementComponent
|
||||
showSaveQuery={this.props.showSaveQuery}
|
||||
loadedSavedQuery={this.props.savedQuery}
|
||||
onSave={this.onInitiateSave}
|
||||
onSaveAsNew={this.onInitiateSaveNew}
|
||||
onLoad={this.onLoadSavedQuery}
|
||||
savedQueryService={this.props.savedQueryService}
|
||||
onClearSavedQuery={this.props.onClearSavedQuery}
|
||||
></SavedQueryManagementComponent>
|
||||
);
|
||||
|
||||
let queryBar;
|
||||
if (this.shouldRenderQueryBar()) {
|
||||
queryBar = (
|
||||
<QueryBar
|
||||
uiSettings={this.props.uiSettings}
|
||||
query={this.props.query}
|
||||
query={this.state.query}
|
||||
screenTitle={this.props.screenTitle}
|
||||
onSubmit={this.props.onQuerySubmit!}
|
||||
onSubmit={this.onQueryBarSubmit}
|
||||
appName={this.props.appName}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
store={this.props.store!}
|
||||
prepend={this.props.showFilterBar ? this.getFilterTriggerButton() : undefined}
|
||||
store={this.props.store}
|
||||
prepend={this.props.showFilterBar ? savedQueryManagement : undefined}
|
||||
showDatePicker={this.props.showDatePicker}
|
||||
showQueryInput={this.props.showQueryInput}
|
||||
dateRangeFrom={this.props.dateRangeFrom}
|
||||
dateRangeTo={this.props.dateRangeTo}
|
||||
dateRangeFrom={this.state.dateRangeFrom}
|
||||
dateRangeTo={this.state.dateRangeTo}
|
||||
isRefreshPaused={this.props.isRefreshPaused}
|
||||
refreshInterval={this.props.refreshInterval}
|
||||
showAutoRefreshOnly={this.props.showAutoRefreshOnly}
|
||||
showQueryInput={this.props.showQueryInput}
|
||||
onRefreshChange={this.props.onRefreshChange}
|
||||
onChange={this.onQueryBarChange}
|
||||
isDirty={this.isDirty()}
|
||||
customSubmitButton={
|
||||
this.props.customSubmitButton ? this.props.customSubmitButton : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -249,6 +433,26 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
<div className="globalQueryBar">
|
||||
{queryBar}
|
||||
{filterBar}
|
||||
|
||||
{this.state.showSaveQueryModal ? (
|
||||
<SaveQueryForm
|
||||
savedQuery={this.props.savedQuery ? this.props.savedQuery.attributes : undefined}
|
||||
savedQueryService={this.props.savedQueryService}
|
||||
onSave={this.onSave}
|
||||
onClose={() => this.setState({ showSaveQueryModal: false })}
|
||||
showFilterOption={this.props.showFilterBar}
|
||||
showTimeFilterOption={this.props.showDatePicker}
|
||||
/>
|
||||
) : null}
|
||||
{this.state.showSaveNewQueryModal ? (
|
||||
<SaveQueryForm
|
||||
savedQueryService={this.props.savedQueryService}
|
||||
onSave={savedQueryMeta => this.onSave(savedQueryMeta, true)}
|
||||
onClose={() => this.setState({ showSaveNewQueryModal: false })}
|
||||
showFilterOption={this.props.showFilterBar}
|
||||
showTimeFilterOption={this.props.showDatePicker}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,4 +17,25 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { RefreshInterval, TimeRange } from 'ui/timefilter/timefilter';
|
||||
import { Query } from '../../query/query_bar';
|
||||
|
||||
export * from './components';
|
||||
|
||||
type SavedQueryTimeFilter = TimeRange & {
|
||||
refreshInterval: RefreshInterval;
|
||||
};
|
||||
|
||||
export interface SavedQuery {
|
||||
id: string;
|
||||
attributes: SavedQueryAttributes;
|
||||
}
|
||||
|
||||
export interface SavedQueryAttributes {
|
||||
title: string;
|
||||
description: string;
|
||||
query: Query;
|
||||
filters?: Filter[];
|
||||
timefilter?: SavedQueryTimeFilter;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedQueryAttributes } from '../index';
|
||||
import { createSavedQueryService } from './saved_query_service';
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
|
||||
const savedQueryAttributes: SavedQueryAttributes = {
|
||||
title: 'foo',
|
||||
description: 'bar',
|
||||
query: {
|
||||
language: 'kuery',
|
||||
query: 'response:200',
|
||||
},
|
||||
};
|
||||
|
||||
const savedQueryAttributesWithFilters: SavedQueryAttributes = {
|
||||
...savedQueryAttributes,
|
||||
filters: [
|
||||
{
|
||||
query: { match_all: {} },
|
||||
$state: { store: FilterStateStore.APP_STATE },
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
timefilter: {
|
||||
to: 'now',
|
||||
from: 'now-15m',
|
||||
refreshInterval: {
|
||||
pause: false,
|
||||
value: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockSavedObjectsClient = {
|
||||
create: jest.fn(),
|
||||
error: jest.fn(),
|
||||
find: jest.fn(),
|
||||
get: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const { deleteSavedQuery, getSavedQuery, findSavedQueries, saveQuery } = createSavedQueryService(
|
||||
// @ts-ignore
|
||||
mockSavedObjectsClient
|
||||
);
|
||||
|
||||
describe('saved query service', () => {
|
||||
afterEach(() => {
|
||||
mockSavedObjectsClient.create.mockReset();
|
||||
mockSavedObjectsClient.find.mockReset();
|
||||
mockSavedObjectsClient.get.mockReset();
|
||||
mockSavedObjectsClient.delete.mockReset();
|
||||
});
|
||||
|
||||
describe('saveQuery', function() {
|
||||
it('should create a saved object for the given attributes', async () => {
|
||||
mockSavedObjectsClient.create.mockReturnValue({
|
||||
id: 'foo',
|
||||
attributes: savedQueryAttributes,
|
||||
});
|
||||
|
||||
const response = await saveQuery(savedQueryAttributes);
|
||||
expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, {
|
||||
id: 'foo',
|
||||
});
|
||||
expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
|
||||
});
|
||||
|
||||
it('should allow overwriting an existing saved query', async () => {
|
||||
mockSavedObjectsClient.create.mockReturnValue({
|
||||
id: 'foo',
|
||||
attributes: savedQueryAttributes,
|
||||
});
|
||||
|
||||
const response = await saveQuery(savedQueryAttributes, { overwrite: true });
|
||||
expect(mockSavedObjectsClient.create).toHaveBeenCalledWith('query', savedQueryAttributes, {
|
||||
id: 'foo',
|
||||
overwrite: true,
|
||||
});
|
||||
expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
|
||||
});
|
||||
|
||||
it('should optionally accept filters and timefilters in object format', async () => {
|
||||
const serializedSavedQueryAttributesWithFilters = {
|
||||
...savedQueryAttributesWithFilters,
|
||||
filters: savedQueryAttributesWithFilters.filters,
|
||||
timefilter: savedQueryAttributesWithFilters.timefilter,
|
||||
};
|
||||
|
||||
mockSavedObjectsClient.create.mockReturnValue({
|
||||
id: 'foo',
|
||||
attributes: serializedSavedQueryAttributesWithFilters,
|
||||
});
|
||||
|
||||
const response = await saveQuery(savedQueryAttributesWithFilters);
|
||||
|
||||
expect(mockSavedObjectsClient.create).toHaveBeenCalledWith(
|
||||
'query',
|
||||
serializedSavedQueryAttributesWithFilters,
|
||||
{ id: 'foo' }
|
||||
);
|
||||
expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributesWithFilters });
|
||||
});
|
||||
|
||||
it('should throw an error when saved objects client returns error', async () => {
|
||||
mockSavedObjectsClient.create.mockReturnValue({
|
||||
error: {
|
||||
error: '123',
|
||||
message: 'An Error',
|
||||
},
|
||||
});
|
||||
|
||||
let error = null;
|
||||
try {
|
||||
await saveQuery(savedQueryAttributes);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).not.toBe(null);
|
||||
});
|
||||
});
|
||||
describe('findSavedQueries', function() {
|
||||
it('should find and return saved queries without search text', async () => {
|
||||
mockSavedObjectsClient.find.mockReturnValue({
|
||||
savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
|
||||
});
|
||||
|
||||
const response = await findSavedQueries();
|
||||
expect(response).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]);
|
||||
});
|
||||
|
||||
it('should find and return saved queries with search text matching the title field', async () => {
|
||||
mockSavedObjectsClient.find.mockReturnValue({
|
||||
savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
|
||||
});
|
||||
const response = await findSavedQueries('foo');
|
||||
expect(mockSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
search: 'foo',
|
||||
searchFields: ['title^5', 'description'],
|
||||
sortField: '_score',
|
||||
type: 'query',
|
||||
});
|
||||
expect(response).toEqual([{ id: 'foo', attributes: savedQueryAttributes }]);
|
||||
});
|
||||
it('should find and return parsed filters and timefilters items', async () => {
|
||||
const serializedSavedQueryAttributesWithFilters = {
|
||||
...savedQueryAttributesWithFilters,
|
||||
filters: savedQueryAttributesWithFilters.filters,
|
||||
timefilter: savedQueryAttributesWithFilters.timefilter,
|
||||
};
|
||||
mockSavedObjectsClient.find.mockReturnValue({
|
||||
savedObjects: [{ id: 'foo', attributes: serializedSavedQueryAttributesWithFilters }],
|
||||
});
|
||||
const response = await findSavedQueries('bar');
|
||||
expect(response).toEqual([{ id: 'foo', attributes: savedQueryAttributesWithFilters }]);
|
||||
});
|
||||
it('should return an array of saved queries', async () => {
|
||||
mockSavedObjectsClient.find.mockReturnValue({
|
||||
savedObjects: [{ id: 'foo', attributes: savedQueryAttributes }],
|
||||
});
|
||||
const response = await findSavedQueries();
|
||||
expect(response).toEqual(
|
||||
expect.objectContaining([
|
||||
{
|
||||
attributes: {
|
||||
description: 'bar',
|
||||
query: { language: 'kuery', query: 'response:200' },
|
||||
title: 'foo',
|
||||
},
|
||||
id: 'foo',
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSavedQuery', function() {
|
||||
it('should retrieve a saved query by id', async () => {
|
||||
mockSavedObjectsClient.get.mockReturnValue({ id: 'foo', attributes: savedQueryAttributes });
|
||||
|
||||
const response = await getSavedQuery('foo');
|
||||
expect(response).toEqual({ id: 'foo', attributes: savedQueryAttributes });
|
||||
});
|
||||
it('should only return saved queries', async () => {
|
||||
mockSavedObjectsClient.get.mockReturnValue({ id: 'foo', attributes: savedQueryAttributes });
|
||||
|
||||
await getSavedQuery('foo');
|
||||
expect(mockSavedObjectsClient.get).toHaveBeenCalledWith('query', 'foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteSavedQuery', function() {
|
||||
it('should delete the saved query for the given ID', async () => {
|
||||
await deleteSavedQuery('foo');
|
||||
expect(mockSavedObjectsClient.delete).toHaveBeenCalledWith('query', 'foo');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectAttributes } from 'src/core/server';
|
||||
import { SavedObjectsClientContract } from 'src/core/public';
|
||||
import { SavedQueryAttributes, SavedQuery } from '../index';
|
||||
|
||||
type SerializedSavedQueryAttributes = SavedObjectAttributes &
|
||||
SavedQueryAttributes & {
|
||||
query: {
|
||||
query: string;
|
||||
language: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface SavedQueryService {
|
||||
saveQuery: (
|
||||
attributes: SavedQueryAttributes,
|
||||
config?: { overwrite: boolean }
|
||||
) => Promise<SavedQuery>;
|
||||
getAllSavedQueries: () => Promise<SavedQuery[]>;
|
||||
findSavedQueries: (searchText?: string) => Promise<SavedQuery[]>;
|
||||
getSavedQuery: (id: string) => Promise<SavedQuery>;
|
||||
deleteSavedQuery: (id: string) => Promise<{}>;
|
||||
}
|
||||
|
||||
export const createSavedQueryService = (
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): SavedQueryService => {
|
||||
const saveQuery = async (attributes: SavedQueryAttributes, { overwrite = false } = {}) => {
|
||||
const query = {
|
||||
query:
|
||||
typeof attributes.query.query === 'string'
|
||||
? attributes.query.query
|
||||
: JSON.stringify(attributes.query.query),
|
||||
language: attributes.query.language,
|
||||
};
|
||||
|
||||
const queryObject: SerializedSavedQueryAttributes = {
|
||||
title: attributes.title.trim(), // trim whitespace before save as an extra precaution against circumventing the front end
|
||||
description: attributes.description,
|
||||
query,
|
||||
};
|
||||
|
||||
if (attributes.filters) {
|
||||
queryObject.filters = attributes.filters;
|
||||
}
|
||||
|
||||
if (attributes.timefilter) {
|
||||
queryObject.timefilter = attributes.timefilter;
|
||||
}
|
||||
|
||||
let rawQueryResponse;
|
||||
if (!overwrite) {
|
||||
rawQueryResponse = await savedObjectsClient.create('query', queryObject, {
|
||||
id: attributes.title,
|
||||
});
|
||||
} else {
|
||||
rawQueryResponse = await savedObjectsClient.create('query', queryObject, {
|
||||
id: attributes.title,
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (rawQueryResponse.error) {
|
||||
throw new Error(rawQueryResponse.error.message);
|
||||
}
|
||||
|
||||
return parseSavedQueryObject(rawQueryResponse);
|
||||
};
|
||||
|
||||
const getAllSavedQueries = async (): Promise<SavedQuery[]> => {
|
||||
const response = await savedObjectsClient.find<SerializedSavedQueryAttributes>({
|
||||
type: 'query',
|
||||
});
|
||||
|
||||
return response.savedObjects.map(
|
||||
(savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) =>
|
||||
parseSavedQueryObject(savedObject)
|
||||
);
|
||||
};
|
||||
|
||||
const findSavedQueries = async (searchText: string = ''): Promise<SavedQuery[]> => {
|
||||
const response = await savedObjectsClient.find<SerializedSavedQueryAttributes>({
|
||||
type: 'query',
|
||||
search: searchText,
|
||||
searchFields: ['title^5', 'description'],
|
||||
sortField: '_score',
|
||||
});
|
||||
|
||||
return response.savedObjects.map(
|
||||
(savedObject: { id: string; attributes: SerializedSavedQueryAttributes }) =>
|
||||
parseSavedQueryObject(savedObject)
|
||||
);
|
||||
};
|
||||
|
||||
const getSavedQuery = async (id: string): Promise<SavedQuery> => {
|
||||
const response = await savedObjectsClient.get<SerializedSavedQueryAttributes>('query', id);
|
||||
return parseSavedQueryObject(response);
|
||||
};
|
||||
|
||||
const deleteSavedQuery = async (id: string) => {
|
||||
return await savedObjectsClient.delete('query', id);
|
||||
};
|
||||
|
||||
const parseSavedQueryObject = (savedQuery: {
|
||||
id: string;
|
||||
attributes: SerializedSavedQueryAttributes;
|
||||
}) => {
|
||||
let queryString;
|
||||
try {
|
||||
queryString = JSON.parse(savedQuery.attributes.query.query);
|
||||
} catch (error) {
|
||||
queryString = savedQuery.attributes.query.query;
|
||||
}
|
||||
const savedQueryItems: SavedQueryAttributes = {
|
||||
title: savedQuery.attributes.title || '',
|
||||
description: savedQuery.attributes.description || '',
|
||||
query: {
|
||||
query: queryString,
|
||||
language: savedQuery.attributes.query.language,
|
||||
},
|
||||
};
|
||||
if (savedQuery.attributes.filters) {
|
||||
savedQueryItems.filters = savedQuery.attributes.filters;
|
||||
}
|
||||
if (savedQuery.attributes.timefilter) {
|
||||
savedQueryItems.timefilter = savedQuery.attributes.timefilter;
|
||||
}
|
||||
return {
|
||||
id: savedQuery.id,
|
||||
attributes: savedQueryItems,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
saveQuery,
|
||||
getAllSavedQueries,
|
||||
findSavedQueries,
|
||||
getSavedQuery,
|
||||
deleteSavedQuery,
|
||||
};
|
||||
};
|
|
@ -17,14 +17,21 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from 'src/core/public';
|
||||
import { createSavedQueryService } from './search_bar/lib/saved_query_service';
|
||||
|
||||
/**
|
||||
* Search Service
|
||||
* @internal
|
||||
*/
|
||||
|
||||
export class SearchService {
|
||||
public setup() {
|
||||
return {};
|
||||
public setup(savedObjectsClient: SavedObjectsClientContract) {
|
||||
return {
|
||||
services: {
|
||||
savedQueryService: createSavedQueryService(savedObjectsClient),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
|
|
|
@ -173,7 +173,7 @@ export default function (kibana) {
|
|||
},
|
||||
},
|
||||
search: {
|
||||
icon: 'search',
|
||||
icon: 'discoverApp',
|
||||
defaultSearchField: 'title',
|
||||
isImportableAndExportable: true,
|
||||
getTitle(obj) {
|
||||
|
@ -268,17 +268,20 @@ export default function (kibana) {
|
|||
show: true,
|
||||
createShortUrl: true,
|
||||
save: true,
|
||||
saveQuery: true,
|
||||
},
|
||||
visualize: {
|
||||
show: true,
|
||||
createShortUrl: true,
|
||||
delete: true,
|
||||
save: true,
|
||||
saveQuery: true,
|
||||
},
|
||||
dashboard: {
|
||||
createNew: true,
|
||||
show: true,
|
||||
showWriteControls: true,
|
||||
saveQuery: true,
|
||||
},
|
||||
catalogue: {
|
||||
discover: true,
|
||||
|
|
|
@ -3,16 +3,18 @@
|
|||
ng-class="{'dshAppContainer--withMargins': model.useMargins}"
|
||||
>
|
||||
<!-- Local nav. -->
|
||||
<kbn-top-nav
|
||||
<kbn-top-nav
|
||||
ng-show="chrome.getVisible()"
|
||||
app-name="'dashboard'"
|
||||
config="topNavMenu"
|
||||
|
||||
config="topNavMenu"
|
||||
|
||||
show-search-bar="chrome.getVisible()"
|
||||
show-filter-bar="showFilterBar()"
|
||||
show-save-query="showSaveQuery"
|
||||
|
||||
filters="model.filters"
|
||||
query="model.query"
|
||||
saved-query="savedQuery"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="indexPatterns"
|
||||
|
@ -22,6 +24,9 @@
|
|||
date-range-to="model.timeRange.to"
|
||||
is-refresh-paused="model.refreshInterval.pause"
|
||||
refresh-interval="model.refreshInterval.value"
|
||||
on-saved="onQuerySaved"
|
||||
on-saved-query-updated="onSavedQueryUpdated"
|
||||
on-clear-saved-query="onClearSavedQuery"
|
||||
on-refresh-change="onRefreshChange">
|
||||
</kbn-top-nav>
|
||||
|
||||
|
|
|
@ -39,8 +39,8 @@ import { Filter } from '@kbn/es-query';
|
|||
import { TimeRange } from 'ui/timefilter/time_history';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
import { IPrivate } from 'ui/private';
|
||||
import { StaticIndexPattern, Query, SavedQuery } from 'plugins/data';
|
||||
import moment from 'moment';
|
||||
import { StaticIndexPattern, Query } from '../../../data/public';
|
||||
|
||||
import { ViewMode } from '../../../embeddable_api/public/np_ready/public';
|
||||
import { SavedObjectDashboard } from './saved_dashboard/saved_dashboard';
|
||||
|
@ -63,6 +63,7 @@ export interface DashboardAppScope extends ng.IScope {
|
|||
| { to: string | moment.Moment | undefined; from: string | moment.Moment | undefined };
|
||||
refreshInterval: any;
|
||||
};
|
||||
savedQuery?: SavedQuery;
|
||||
refreshInterval: any;
|
||||
panels: SavedDashboardPanel[];
|
||||
indexPatterns: StaticIndexPattern[];
|
||||
|
@ -83,9 +84,13 @@ export interface DashboardAppScope extends ng.IScope {
|
|||
$listenAndDigestAsync: any;
|
||||
onCancelApplyFilters: () => void;
|
||||
onApplyFilters: (filters: Filter[]) => void;
|
||||
onQuerySaved: (savedQuery: SavedQuery) => void;
|
||||
onSavedQueryUpdated: (savedQuery: SavedQuery) => void;
|
||||
onClearSavedQuery: () => void;
|
||||
topNavMenu: any;
|
||||
showFilterBar: () => boolean;
|
||||
showAddPanel: any;
|
||||
showSaveQuery: boolean;
|
||||
kbnTopNav: any;
|
||||
enterEditMode: () => void;
|
||||
$listen: any;
|
||||
|
|
|
@ -51,11 +51,14 @@ import { KbnUrl } from 'ui/url/kbn_url';
|
|||
import { Filter } from '@kbn/es-query';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
import { IPrivate } from 'ui/private';
|
||||
import { Query } from 'src/legacy/core_plugins/data/public';
|
||||
import { Query, SavedQuery } from 'src/legacy/core_plugins/data/public';
|
||||
import { SaveOptions } from 'ui/saved_objects/saved_object';
|
||||
import { capabilities } from 'ui/capabilities';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { SavedObjectFinder } from 'ui/saved_objects/components/saved_object_finder';
|
||||
import { data } from '../../../data/public/setup';
|
||||
|
||||
import {
|
||||
DashboardContainer,
|
||||
DASHBOARD_CONTAINER_TYPE,
|
||||
|
@ -85,6 +88,8 @@ import { DashboardAppScope } from './dashboard_app';
|
|||
import { VISUALIZE_EMBEDDABLE_TYPE } from '../visualize/embeddable';
|
||||
import { convertSavedDashboardPanelToPanelState } from './lib/embeddable_saved_object_converters';
|
||||
|
||||
const { savedQueryService } = data.search.services;
|
||||
|
||||
export class DashboardAppController {
|
||||
// Part of the exposed plugin API - do not remove without careful consideration.
|
||||
appStatus: {
|
||||
|
@ -150,6 +155,7 @@ export class DashboardAppController {
|
|||
if (dashboardStateManager.getIsTimeSavedWithDashboard() && !getAppState.previouslyStored()) {
|
||||
dashboardStateManager.syncTimefilterWithDashboard(timefilter);
|
||||
}
|
||||
$scope.showSaveQuery = capabilities.get().dashboard.saveQuery as boolean;
|
||||
|
||||
const updateIndexPatterns = (container?: DashboardContainer) => {
|
||||
if (!container || isErrorEmbeddable(container)) {
|
||||
|
@ -420,6 +426,75 @@ export class DashboardAppController {
|
|||
$scope.appState.$newFilters = [];
|
||||
};
|
||||
|
||||
$scope.onQuerySaved = savedQuery => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
};
|
||||
|
||||
$scope.onSavedQueryUpdated = savedQuery => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
};
|
||||
|
||||
$scope.onClearSavedQuery = () => {
|
||||
delete $scope.savedQuery;
|
||||
dashboardStateManager.setSavedQueryId(undefined);
|
||||
queryFilter.removeAll();
|
||||
dashboardStateManager.applyFilters(
|
||||
{
|
||||
query: '',
|
||||
language:
|
||||
localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'),
|
||||
},
|
||||
[]
|
||||
);
|
||||
courier.fetch();
|
||||
};
|
||||
|
||||
const updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
|
||||
queryFilter.setFilters(savedQuery.attributes.filters || []);
|
||||
dashboardStateManager.applyFilters(
|
||||
savedQuery.attributes.query,
|
||||
savedQuery.attributes.filters || []
|
||||
);
|
||||
if (savedQuery.attributes.timefilter) {
|
||||
timefilter.setTime({
|
||||
from: savedQuery.attributes.timefilter.from,
|
||||
to: savedQuery.attributes.timefilter.to,
|
||||
});
|
||||
if (savedQuery.attributes.timefilter.refreshInterval) {
|
||||
timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval);
|
||||
}
|
||||
}
|
||||
courier.fetch();
|
||||
};
|
||||
|
||||
$scope.$watch('savedQuery', (newSavedQuery: SavedQuery, oldSavedQuery: SavedQuery) => {
|
||||
if (!newSavedQuery) return;
|
||||
dashboardStateManager.setSavedQueryId(newSavedQuery.id);
|
||||
|
||||
if (newSavedQuery.id === (oldSavedQuery && oldSavedQuery.id)) {
|
||||
updateStateFromSavedQuery(newSavedQuery);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch(
|
||||
() => {
|
||||
return dashboardStateManager.getSavedQueryId();
|
||||
},
|
||||
newSavedQueryId => {
|
||||
if (!newSavedQueryId) {
|
||||
$scope.savedQuery = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery: SavedQuery) => {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
updateStateFromSavedQuery(savedQuery);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
$scope.$watch('appState.$newFilters', (filters: Filter[] = []) => {
|
||||
if (filters.length === 1) {
|
||||
$scope.onApplyFilters(filters);
|
||||
|
@ -433,6 +508,13 @@ export class DashboardAppController {
|
|||
$scope.updateQueryAndFetch({ query });
|
||||
});
|
||||
|
||||
$scope.$watch(
|
||||
() => capabilities.get().dashboard.saveQuery,
|
||||
newCapability => {
|
||||
$scope.showSaveQuery = newCapability as boolean;
|
||||
}
|
||||
);
|
||||
|
||||
$scope.$listenAndDigestAsync(timefilter, 'fetch', () => {
|
||||
// The only reason this is here is so that search embeddables work on a dashboard with
|
||||
// a refresh interval turned on. This kicks off the search poller. It should be
|
||||
|
|
|
@ -252,6 +252,15 @@ export class DashboardStateManager {
|
|||
return migrateLegacyQuery(this.appState.query);
|
||||
}
|
||||
|
||||
public getSavedQueryId() {
|
||||
return this.appState.savedQuery;
|
||||
}
|
||||
|
||||
public setSavedQueryId(id?: string) {
|
||||
this.appState.savedQuery = id;
|
||||
this.saveState();
|
||||
}
|
||||
|
||||
public getUseMargins() {
|
||||
// Existing dashboards that don't define this should default to false.
|
||||
return this.appState.options.useMargins === undefined
|
||||
|
|
|
@ -112,6 +112,7 @@ export interface DashboardAppStateParameters {
|
|||
query: Query | string;
|
||||
filters: Filter[];
|
||||
viewMode: ViewMode;
|
||||
savedQuery?: string;
|
||||
}
|
||||
|
||||
// This could probably be improved if we flesh out AppState more... though AppState will be going away
|
||||
|
|
|
@ -72,6 +72,10 @@ import { buildVislibDimensions } from 'ui/visualize/loader/pipeline_helpers/buil
|
|||
import 'ui/capabilities/route_setup';
|
||||
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
|
||||
|
||||
import { setup as data } from '../../../../../core_plugins/data/public/legacy';
|
||||
|
||||
const { savedQueryService } = data.search.services;
|
||||
|
||||
const fetchStatuses = {
|
||||
UNINITIALIZED: 'uninitialized',
|
||||
LOADING: 'loading',
|
||||
|
@ -217,6 +221,12 @@ function discoverController(
|
|||
$scope.minimumVisibleRows = 50;
|
||||
$scope.fetchStatus = fetchStatuses.UNINITIALIZED;
|
||||
$scope.refreshInterval = timefilter.getRefreshInterval();
|
||||
$scope.savedQuery = $route.current.locals.savedQuery;
|
||||
$scope.showSaveQuery = uiCapabilities.discover.saveQuery;
|
||||
|
||||
$scope.$watch(() => uiCapabilities.discover.saveQuery, (newCapability) => {
|
||||
$scope.showSaveQuery = newCapability;
|
||||
});
|
||||
|
||||
$scope.intervalEnabled = function (interval) {
|
||||
return interval.val !== 'custom';
|
||||
|
@ -282,6 +292,9 @@ function discoverController(
|
|||
title={savedSearch.title}
|
||||
showCopyOnSave={savedSearch.id ? true : false}
|
||||
objectType="search"
|
||||
description={i18n.translate('kbn.discover.localMenu.saveSaveSearchDescription', {
|
||||
defaultMessage: 'Save your Discover search so you can use it in visualizations and dashboards',
|
||||
})}
|
||||
/>);
|
||||
showSaveModal(saveModal);
|
||||
}
|
||||
|
@ -375,7 +388,7 @@ function discoverController(
|
|||
|
||||
// searchSource which applies time range
|
||||
const timeRangeSearchSource = savedSearch.searchSource.create();
|
||||
if(isDefaultTypeIndexPattern($scope.indexPattern)) {
|
||||
if (isDefaultTypeIndexPattern($scope.indexPattern)) {
|
||||
timeRangeSearchSource.setField('filter', () => {
|
||||
return timefilter.createFilter($scope.indexPattern);
|
||||
});
|
||||
|
@ -392,7 +405,7 @@ function discoverController(
|
|||
if (savedSearch.id && savedSearch.title) {
|
||||
chrome.breadcrumbs.set([{
|
||||
text: discoverBreadcrumbsTitle,
|
||||
href: '#/discover'
|
||||
href: '#/discover',
|
||||
}, { text: savedSearch.title }]);
|
||||
} else {
|
||||
chrome.breadcrumbs.set([{
|
||||
|
@ -487,15 +500,18 @@ function discoverController(
|
|||
|
||||
function getStateDefaults() {
|
||||
return {
|
||||
query: $scope.searchSource.getField('query') || {
|
||||
query: '',
|
||||
language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage')
|
||||
},
|
||||
query: ($scope.savedQuery && $scope.savedQuery.attributes.query)
|
||||
|| $scope.searchSource.getField('query')
|
||||
|| {
|
||||
query: '',
|
||||
language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage')
|
||||
},
|
||||
sort: getSort.array(savedSearch.sort, $scope.indexPattern, config.get('discover:sort:defaultOrder')),
|
||||
columns: savedSearch.columns.length > 0 ? savedSearch.columns : config.get('defaultColumns').slice(),
|
||||
index: $scope.indexPattern.id,
|
||||
interval: 'auto',
|
||||
filters: _.cloneDeep($scope.searchSource.getOwnField('filter'))
|
||||
filters: ($scope.savedQuery && $scope.savedQuery.attributes.filters)
|
||||
|| _.cloneDeep($scope.searchSource.getOwnField('filter'))
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -903,6 +919,68 @@ function discoverController(
|
|||
$scope.minimumVisibleRows = $scope.hits;
|
||||
};
|
||||
|
||||
$scope.onQuerySaved = savedQuery => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
};
|
||||
|
||||
$scope.onSavedQueryUpdated = savedQuery => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
};
|
||||
|
||||
$scope.onClearSavedQuery = () => {
|
||||
delete $scope.savedQuery;
|
||||
delete $state.savedQuery;
|
||||
$state.query = {
|
||||
query: '',
|
||||
language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage'),
|
||||
};
|
||||
queryFilter.removeAll();
|
||||
$state.save();
|
||||
$scope.fetch();
|
||||
};
|
||||
|
||||
const updateStateFromSavedQuery = (savedQuery) => {
|
||||
$state.query = savedQuery.attributes.query;
|
||||
queryFilter.setFilters(savedQuery.attributes.filters || []);
|
||||
|
||||
if (savedQuery.attributes.timefilter) {
|
||||
timefilter.setTime({
|
||||
from: savedQuery.attributes.timefilter.from,
|
||||
to: savedQuery.attributes.timefilter.to,
|
||||
});
|
||||
if (savedQuery.attributes.timefilter.refreshInterval) {
|
||||
timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.fetch();
|
||||
};
|
||||
|
||||
$scope.$watch('savedQuery', (newSavedQuery, oldSavedQuery) => {
|
||||
if (!newSavedQuery) return;
|
||||
|
||||
$state.savedQuery = newSavedQuery.id;
|
||||
$state.save();
|
||||
|
||||
if (newSavedQuery.id === (oldSavedQuery && oldSavedQuery.id)) {
|
||||
updateStateFromSavedQuery(newSavedQuery);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('state.savedQuery', newSavedQueryId => {
|
||||
if (!newSavedQueryId) {
|
||||
$scope.savedQuery = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
updateStateFromSavedQuery(savedQuery);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function setupVisualization() {
|
||||
// If no timefield has been specified we don't create a histogram of messages
|
||||
if (!$scope.opts.timefield) return;
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
config="topNavMenu"
|
||||
show-search-bar="true"
|
||||
show-date-picker="enableTimeRangeSelector"
|
||||
show-save-query="showSaveQuery"
|
||||
query="state.query"
|
||||
saved-query="savedQuery"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="[indexPattern]"
|
||||
|
@ -17,6 +19,9 @@
|
|||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
on-saved="onQuerySaved"
|
||||
on-saved-query-updated="onSavedQueryUpdated"
|
||||
on-clear-saved-query="onClearSavedQuery"
|
||||
>
|
||||
</kbn-top-nav>
|
||||
|
||||
|
|
|
@ -23,25 +23,26 @@
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Local nav.
|
||||
Most visualizations have all search bar components enabled
|
||||
Some visualizations have fewer options but all visualizations have a search bar
|
||||
|
||||
<!--
|
||||
Local nav.
|
||||
Most visualizations have all search bar components enabled
|
||||
Some visualizations have fewer options but all visualizations have a search bar
|
||||
which is why show-search-baris set to "true".
|
||||
All visualizaions also have least a timepicker \ autorefresh component, which is why
|
||||
show-query-bar is set to "true".
|
||||
-->
|
||||
<kbn-top-nav
|
||||
<kbn-top-nav
|
||||
app-name="'visualize'"
|
||||
config="topNavMenu"
|
||||
show-search-bar="true"
|
||||
show-query-bar="true"
|
||||
show-query-bar="true"
|
||||
show-query-input="showQueryInput()"
|
||||
show-filter-bar="showFilterBar() && chrome.getVisible()"
|
||||
show-date-picker="showQueryBarTimePicker()"
|
||||
show-auto-refresh-only="!showQueryBarTimePicker()"
|
||||
query="state.query"
|
||||
saved-query="savedQuery"
|
||||
screen-title="state.vis.title"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="[indexPattern]"
|
||||
|
@ -52,6 +53,10 @@
|
|||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
show-save-query="showSaveQuery"
|
||||
on-saved="onQuerySaved"
|
||||
on-saved-query-updated="onSavedQueryUpdated"
|
||||
on-clear-saved-query="onClearSavedQuery"
|
||||
>
|
||||
</kbn-top-nav>
|
||||
|
||||
|
|
|
@ -54,8 +54,10 @@ import { showSaveModal } from 'ui/saved_objects/show_saved_object_save_modal';
|
|||
import { SavedObjectSaveModal } from 'ui/saved_objects/components/saved_object_save_modal';
|
||||
import { getEditBreadcrumbs, getCreateBreadcrumbs } from '../breadcrumbs';
|
||||
import { npStart } from 'ui/new_platform';
|
||||
import { setup as data } from '../../../../../core_plugins/data/public/legacy';
|
||||
import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util';
|
||||
|
||||
const { savedQueryService } = data.search.services;
|
||||
|
||||
uiRoutes
|
||||
.when(VisualizeConstants.CREATE_PATH, {
|
||||
|
@ -334,6 +336,12 @@ function VisEditor(
|
|||
}
|
||||
});
|
||||
|
||||
$scope.showSaveQuery = capabilities.get().visualize.saveQuery;
|
||||
|
||||
$scope.$watch(() => capabilities.get().visualize.saveQuery, (newCapability) => {
|
||||
$scope.showSaveQuery = newCapability;
|
||||
});
|
||||
|
||||
function init() {
|
||||
// export some objects
|
||||
$scope.savedVis = savedVis;
|
||||
|
@ -464,6 +472,67 @@ function VisEditor(
|
|||
});
|
||||
};
|
||||
|
||||
$scope.onQuerySaved = savedQuery => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
};
|
||||
|
||||
$scope.onSavedQueryUpdated = savedQuery => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
};
|
||||
|
||||
$scope.onClearSavedQuery = () => {
|
||||
delete $scope.savedQuery;
|
||||
delete $state.savedQuery;
|
||||
$state.query = {
|
||||
query: '',
|
||||
language: localStorage.get('kibana.userQueryLanguage') || config.get('search:queryLanguage')
|
||||
};
|
||||
queryFilter.removeAll();
|
||||
$state.save();
|
||||
$scope.fetch();
|
||||
};
|
||||
|
||||
const updateStateFromSavedQuery = (savedQuery) => {
|
||||
$state.query = savedQuery.attributes.query;
|
||||
queryFilter.setFilters(savedQuery.attributes.filters || []);
|
||||
|
||||
if (savedQuery.attributes.timefilter) {
|
||||
timefilter.setTime({
|
||||
from: savedQuery.attributes.timefilter.from,
|
||||
to: savedQuery.attributes.timefilter.to,
|
||||
});
|
||||
if (savedQuery.attributes.timefilter.refreshInterval) {
|
||||
timefilter.setRefreshInterval(savedQuery.attributes.timefilter.refreshInterval);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.fetch();
|
||||
};
|
||||
|
||||
$scope.$watch('savedQuery', (newSavedQuery, oldSavedQuery) => {
|
||||
if (!newSavedQuery) return;
|
||||
$state.savedQuery = newSavedQuery.id;
|
||||
$state.save();
|
||||
|
||||
if (newSavedQuery.id === (oldSavedQuery && oldSavedQuery.id)) {
|
||||
updateStateFromSavedQuery(newSavedQuery);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('state.savedQuery', newSavedQueryId => {
|
||||
if (!newSavedQueryId) {
|
||||
$scope.savedQuery = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
savedQueryService.getSavedQuery(newSavedQueryId).then((savedQuery) => {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.savedQuery = savedQuery;
|
||||
updateStateFromSavedQuery(savedQuery);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Called when the user clicks "Save" button.
|
||||
*/
|
||||
|
|
|
@ -24,6 +24,7 @@ import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
|||
|
||||
import { coreMock } from '../../../../../core/public/mocks';
|
||||
const setupMock = coreMock.createSetup();
|
||||
const startMock = coreMock.createStart();
|
||||
|
||||
jest.mock('../../../../core_plugins/data/public', () => {
|
||||
return {
|
||||
|
@ -54,14 +55,25 @@ describe('TopNavMenu', () => {
|
|||
];
|
||||
|
||||
it('Should render nothing when no config is provided', () => {
|
||||
const component = shallowWithIntl(<TopNavMenu name="test" uiSettings={setupMock.uiSettings} />);
|
||||
const component = shallowWithIntl(
|
||||
<TopNavMenu
|
||||
name="test"
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedObjectsClient={startMock.savedObjects.client}
|
||||
/>
|
||||
);
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
|
||||
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render 1 menu item', () => {
|
||||
const component = shallowWithIntl(
|
||||
<TopNavMenu name="test" uiSettings={setupMock.uiSettings} config={[menuItems[0]]} />
|
||||
<TopNavMenu
|
||||
name="test"
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedObjectsClient={startMock.savedObjects.client}
|
||||
config={[menuItems[0]]}
|
||||
/>
|
||||
);
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(1);
|
||||
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
|
||||
|
@ -69,7 +81,12 @@ describe('TopNavMenu', () => {
|
|||
|
||||
it('Should render multiple menu items', () => {
|
||||
const component = shallowWithIntl(
|
||||
<TopNavMenu name="test" uiSettings={setupMock.uiSettings} config={menuItems} />
|
||||
<TopNavMenu
|
||||
name="test"
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedObjectsClient={startMock.savedObjects.client}
|
||||
config={menuItems}
|
||||
/>
|
||||
);
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(menuItems.length);
|
||||
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
|
||||
|
@ -77,7 +94,12 @@ describe('TopNavMenu', () => {
|
|||
|
||||
it('Should render search bar', () => {
|
||||
const component = shallowWithIntl(
|
||||
<TopNavMenu name="test" uiSettings={setupMock.uiSettings} showSearchBar={true} />
|
||||
<TopNavMenu
|
||||
name="test"
|
||||
uiSettings={setupMock.uiSettings}
|
||||
savedObjectsClient={startMock.savedObjects.client}
|
||||
showSearchBar={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
|
||||
|
|
|
@ -21,14 +21,16 @@ import React from 'react';
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { UiSettingsClientContract } from 'src/core/public';
|
||||
import { UiSettingsClientContract, SavedObjectsClientContract } from 'src/core/public';
|
||||
import { TopNavMenuData } from './top_nav_menu_data';
|
||||
import { TopNavMenuItem } from './top_nav_menu_item';
|
||||
import { SearchBar, SearchBarProps } from '../../../../core_plugins/data/public';
|
||||
import { createSavedQueryService } from '../../../data/public/search/search_bar/lib/saved_query_service';
|
||||
|
||||
type Props = Partial<SearchBarProps> & {
|
||||
name: string;
|
||||
uiSettings: UiSettingsClientContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
config?: TopNavMenuData[];
|
||||
showSearchBar?: boolean;
|
||||
};
|
||||
|
@ -58,11 +60,14 @@ export function TopNavMenu(props: Props) {
|
|||
// Validate presense of all required fields
|
||||
if (!props.showSearchBar) return;
|
||||
|
||||
const savedQueryService = createSavedQueryService(props.savedObjectsClient);
|
||||
|
||||
return (
|
||||
<SearchBar
|
||||
query={props.query}
|
||||
filters={props.filters}
|
||||
uiSettings={props.uiSettings}
|
||||
savedQueryService={savedQueryService}
|
||||
showQueryBar={props.showQueryBar}
|
||||
showQueryInput={props.showQueryInput}
|
||||
showFilterBar={props.showFilterBar}
|
||||
|
@ -79,6 +84,11 @@ export function TopNavMenu(props: Props) {
|
|||
refreshInterval={props.refreshInterval}
|
||||
indexPatterns={props.indexPatterns}
|
||||
store={props.store}
|
||||
savedQuery={props.savedQuery}
|
||||
showSaveQuery={props.showSaveQuery}
|
||||
onClearSavedQuery={props.onClearSavedQuery}
|
||||
onSaved={props.onSaved}
|
||||
onSavedQueryUpdated={props.onSavedQueryUpdated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,11 +18,15 @@
|
|||
*/
|
||||
import { Server } from '../../server/kbn_server';
|
||||
import { Capabilities } from '../../../core/public';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { SavedObjectsManagementDefinition } from '../../../core/server/saved_objects/management';
|
||||
|
||||
export type InitPluginFunction = (server: Server) => void;
|
||||
export interface UiExports {
|
||||
injectDefaultVars?: (server: Server) => { [key: string]: any };
|
||||
styleSheetPaths?: string;
|
||||
savedObjectsManagement?: SavedObjectsManagementDefinition;
|
||||
mappings?: unknown;
|
||||
visTypes?: string[];
|
||||
interpreter?: string[];
|
||||
hacks?: string[];
|
||||
|
|
|
@ -47,6 +47,7 @@ module.directive('kbnTopNav', () => {
|
|||
const localStorage = new Storage(window.localStorage);
|
||||
child.setAttribute('store', 'store');
|
||||
child.setAttribute('ui-settings', 'uiSettings');
|
||||
child.setAttribute('saved-objects-client', 'savedObjectsClient');
|
||||
|
||||
// Append helper directive
|
||||
elem.append(child);
|
||||
|
@ -54,6 +55,7 @@ module.directive('kbnTopNav', () => {
|
|||
const linkFn = ($scope, _, $attr) => {
|
||||
$scope.store = localStorage;
|
||||
$scope.uiSettings = chrome.getUiSettingsClient();
|
||||
$scope.savedObjectsClient = chrome.getSavedObjectsClient();
|
||||
|
||||
// Watch config changes
|
||||
$scope.$watch(() => {
|
||||
|
@ -92,14 +94,19 @@ module.directive('kbnTopNavHelper', (reactDirective) => {
|
|||
['disabledButtons', { watchDepth: 'reference' }],
|
||||
|
||||
['query', { watchDepth: 'reference' }],
|
||||
['savedQuery', { watchDepth: 'reference' }],
|
||||
['store', { watchDepth: 'reference' }],
|
||||
['uiSettings', { watchDepth: 'reference' }],
|
||||
['savedObjectsClient', { watchDepth: 'reference' }],
|
||||
['intl', { watchDepth: 'reference' }],
|
||||
['store', { watchDepth: 'reference' }],
|
||||
|
||||
['onQuerySubmit', { watchDepth: 'reference' }],
|
||||
['onFiltersUpdated', { watchDepth: 'reference' }],
|
||||
['onRefreshChange', { watchDepth: 'reference' }],
|
||||
['onClearSavedQuery', { watchDepth: 'reference' }],
|
||||
['onSaved', { watchDepth: 'reference' }],
|
||||
['onSavedQueryUpdated', { watchDepth: 'reference' }],
|
||||
|
||||
['indexPatterns', { watchDepth: 'collection' }],
|
||||
['filters', { watchDepth: 'collection' }],
|
||||
|
@ -111,6 +118,7 @@ module.directive('kbnTopNavHelper', (reactDirective) => {
|
|||
'showQueryBar',
|
||||
'showQueryInput',
|
||||
'showDatePicker',
|
||||
'showSaveQuery',
|
||||
|
||||
'appName',
|
||||
'screenTitle',
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
|
||||
interface OnSaveProps {
|
||||
newTitle: string;
|
||||
|
@ -50,6 +51,7 @@ interface Props {
|
|||
objectType: string;
|
||||
confirmButtonLabel?: React.ReactNode;
|
||||
options?: React.ReactNode;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -94,6 +96,11 @@ export class SavedObjectSaveModal extends React.Component<Props, State> {
|
|||
{this.renderDuplicateTitleCallout()}
|
||||
|
||||
<EuiForm>
|
||||
{this.props.description && (
|
||||
<EuiFormRow>
|
||||
<EuiText color="subdued">{this.props.description}</EuiText>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{this.renderCopyOnSave()}
|
||||
|
||||
<EuiFormRow
|
||||
|
|
|
@ -23,7 +23,7 @@ import { RefreshInterval } from '../../../../plugins/data/public';
|
|||
|
||||
// NOTE: These types are somewhat guessed, they may be incorrect.
|
||||
|
||||
export { RefreshInterval };
|
||||
export { RefreshInterval, TimeRange };
|
||||
|
||||
export interface Timefilter {
|
||||
time: TimeRange;
|
||||
|
|
|
@ -227,7 +227,7 @@ export default function ({ getService }) {
|
|||
.then(resp => {
|
||||
expect(resp.body.saved_objects).to.have.length(1);
|
||||
expect(resp.body.saved_objects[0].meta).to.eql({
|
||||
icon: 'search',
|
||||
icon: 'discoverApp',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
|
|
|
@ -262,7 +262,7 @@ export default function ({ getService }) {
|
|||
type: 'search',
|
||||
relationship: 'child',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
icon: 'discoverApp',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
|
@ -299,7 +299,7 @@ export default function ({ getService }) {
|
|||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'search',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
icon: 'discoverApp',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
|
@ -342,7 +342,7 @@ export default function ({ getService }) {
|
|||
type: 'search',
|
||||
relationship: 'parent',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
icon: 'discoverApp',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
|
@ -379,7 +379,7 @@ export default function ({ getService }) {
|
|||
id: '960372e0-3224-11e8-a572-ffca06da1357',
|
||||
type: 'search',
|
||||
meta: {
|
||||
icon: 'search',
|
||||
icon: 'discoverApp',
|
||||
title: 'OneRecord',
|
||||
editUrl: '/management/kibana/objects/savedSearches/960372e0-3224-11e8-a572-ffca06da1357',
|
||||
inAppUrl: {
|
||||
|
|
|
@ -139,7 +139,7 @@ export default function ({ getService }) {
|
|||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: 'child "type" fails because ["type" at position 0 fails because ' +
|
||||
'["0" must be one of [config, dashboard, index-pattern, search, url, visualization]]]',
|
||||
'["0" must be one of [config, dashboard, index-pattern, query, search, url, visualization]]]',
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['type.0'],
|
||||
|
|
128
test/functional/apps/discover/_saved_queries.js
Normal file
128
test/functional/apps/discover/_saved_queries.js
Normal file
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export default function ({ getService, getPageObjects }) {
|
||||
const log = getService('log');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
|
||||
|
||||
const defaultSettings = {
|
||||
defaultIndex: 'logstash-*',
|
||||
};
|
||||
const filterBar = getService('filterBar');
|
||||
const queryBar = getService('queryBar');
|
||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('saved queries saved objects', function describeIndexTests() {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
|
||||
before(async function () {
|
||||
log.debug('load kibana index with default index pattern');
|
||||
await esArchiver.load('discover');
|
||||
|
||||
// and load a set of makelogs data
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await kibanaServer.uiSettings.replace(defaultSettings);
|
||||
log.debug('discover');
|
||||
await PageObjects.common.navigateToApp('discover');
|
||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||
});
|
||||
|
||||
describe('saved query management component functionality', function () {
|
||||
before(async function () {
|
||||
// set up a query with filters and a time filter
|
||||
log.debug('set up a query with filters to save');
|
||||
await queryBar.setQuery('response:200');
|
||||
await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
|
||||
const fromTime = '2015-09-20 08:00:00.000';
|
||||
const toTime = '2015-09-21 08:00:00.000';
|
||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||
});
|
||||
|
||||
it('should show the saved query management component when there are no saved queries', async () => {
|
||||
await savedQueryManagementComponent.openSavedQueryManagementComponent();
|
||||
const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover');
|
||||
expect(descriptionText)
|
||||
.to
|
||||
.eql('SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave');
|
||||
});
|
||||
|
||||
it('should allow a query to be saved via the saved objects management component', async () => {
|
||||
await savedQueryManagementComponent.saveNewQuery('OkResponse', '200 responses for .jpg over 24 hours', true, true);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse');
|
||||
});
|
||||
|
||||
it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
const toTime = '2015-09-23 18:31:44.000';
|
||||
await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime);
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
await savedQueryManagementComponent.loadSavedQuery('OkResponse');
|
||||
const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes();
|
||||
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
|
||||
expect(timePickerValues.start).to.not.eql(fromTime);
|
||||
expect(timePickerValues.end).to.not.eql(toTime);
|
||||
});
|
||||
|
||||
it('allows saving changes to a currently loaded query via the saved query management component', async () => {
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQuery(
|
||||
'OkResponse',
|
||||
'404 responses',
|
||||
false,
|
||||
false
|
||||
);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse');
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
expect(await queryBar.getQueryString()).to.eql('');
|
||||
await savedQueryManagementComponent.loadSavedQuery('OkResponse');
|
||||
expect(await queryBar.getQueryString()).to.eql('response:404');
|
||||
});
|
||||
|
||||
it('allows saving the currently loaded query as a new query', async () => {
|
||||
await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery('OkResponseCopy', '200 responses', false, false);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy');
|
||||
});
|
||||
|
||||
it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy');
|
||||
await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy');
|
||||
expect(await queryBar.getQueryString()).to.eql('');
|
||||
});
|
||||
|
||||
it('does not allow saving a query with a non-unique name', async () => {
|
||||
await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse');
|
||||
});
|
||||
|
||||
it('does not allow saving a query with leading or trailing whitespace in the name', async () => {
|
||||
await savedQueryManagementComponent.saveNewQueryWithNameError('OkResponse ');
|
||||
});
|
||||
it('allows clearing the currently loaded saved query', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OkResponse');
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
expect(await queryBar.getQueryString()).to.eql('');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -32,6 +32,7 @@ export default function ({ getService, loadTestFile }) {
|
|||
return esArchiver.unload('logstash_functional');
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./_saved_queries'));
|
||||
loadTestFile(require.resolve('./_discover'));
|
||||
loadTestFile(require.resolve('./_errors'));
|
||||
loadTestFile(require.resolve('./_field_data'));
|
||||
|
|
|
@ -231,6 +231,35 @@
|
|||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"query": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"timefilter": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -241,4 +270,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -240,6 +240,35 @@
|
|||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"query": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"timefilter": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -250,4 +279,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,8 @@ import { ToastsProvider } from './toasts';
|
|||
import { PieChartProvider } from './visualizations';
|
||||
// @ts-ignore not TS yet
|
||||
import { VisualizeListingTableProvider } from './visualize_listing_table';
|
||||
// @ts-ignore not TS yet
|
||||
import { SavedQueryManagementComponentProvider } from './saved_query_management_component';
|
||||
|
||||
export const services = {
|
||||
...commonServiceProviders,
|
||||
|
@ -89,4 +91,5 @@ export const services = {
|
|||
appsMenu: AppsMenuProvider,
|
||||
globalNav: GlobalNavProvider,
|
||||
toasts: ToastsProvider,
|
||||
savedQueryManagementComponent: SavedQueryManagementComponentProvider,
|
||||
};
|
||||
|
|
146
test/functional/services/saved_query_management_component.js
Normal file
146
test/functional/services/saved_query_management_component.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
export function SavedQueryManagementComponentProvider({ getService }) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const queryBar = getService('queryBar');
|
||||
const retry = getService('retry');
|
||||
|
||||
class SavedQueryManagementComponent {
|
||||
|
||||
async saveNewQuery(name, description, includeFilters, includeTimeFilter) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click('saved-query-management-save-button');
|
||||
await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter);
|
||||
}
|
||||
|
||||
async saveNewQueryWithNameError(name) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click('saved-query-management-save-button');
|
||||
if (name) {
|
||||
await testSubjects.setValue('saveQueryFormTitle', name);
|
||||
}
|
||||
const saveQueryFormSaveButtonStatus = await testSubjects.isEnabled('savedQueryFormSaveButton');
|
||||
expect(saveQueryFormSaveButtonStatus).to.not.eql(true);
|
||||
await testSubjects.click('savedQueryFormCancelButton');
|
||||
}
|
||||
|
||||
async saveCurrentlyLoadedAsNewQuery(name, description, includeFilters, includeTimeFilter) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click('saved-query-management-save-as-new-button');
|
||||
await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter);
|
||||
}
|
||||
|
||||
async updateCurrentlyLoadedQuery(description, includeFilters, includeTimeFilter) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click('saved-query-management-save-changes-button');
|
||||
await this.submitSaveQueryForm(null, description, includeFilters, includeTimeFilter);
|
||||
}
|
||||
|
||||
async loadSavedQuery(title) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click(`load-saved-query-${title}-button`);
|
||||
await retry.try(async () => {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
const selectedSavedQueryText = await testSubjects.getVisibleText('saved-query-list-item-selected');
|
||||
expect(selectedSavedQueryText).to.eql(title);
|
||||
});
|
||||
await this.closeSavedQueryManagementComponent();
|
||||
}
|
||||
|
||||
async deleteSavedQuery(title) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click(`delete-saved-query-${title}-button`);
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
}
|
||||
|
||||
async clearCurrentlyLoadedQuery() {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.click('saved-query-management-clear-button');
|
||||
await this.closeSavedQueryManagementComponent();
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.be.empty();
|
||||
}
|
||||
|
||||
async submitSaveQueryForm(title, description, includeFilters, includeTimeFilter) {
|
||||
if (title) {
|
||||
await testSubjects.setValue('saveQueryFormTitle', title);
|
||||
}
|
||||
await testSubjects.setValue('saveQueryFormDescription', description);
|
||||
|
||||
const currentIncludeFiltersValue = (await testSubjects.getAttribute('saveQueryFormIncludeFiltersOption', 'checked')) === 'true';
|
||||
if (currentIncludeFiltersValue !== includeFilters) {
|
||||
await testSubjects.click('saveQueryFormIncludeFiltersOption');
|
||||
}
|
||||
|
||||
const currentIncludeTimeFilterValue = (await testSubjects.getAttribute('saveQueryFormIncludeTimeFilterOption', 'checked')) === 'true';
|
||||
if (currentIncludeTimeFilterValue !== includeTimeFilter) {
|
||||
await testSubjects.click('saveQueryFormIncludeTimeFilterOption');
|
||||
}
|
||||
|
||||
await testSubjects.click('savedQueryFormSaveButton');
|
||||
}
|
||||
|
||||
async savedQueryExistOrFail(title) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.existOrFail(`load-saved-query-${title}-button`);
|
||||
}
|
||||
|
||||
async savedQueryMissingOrFail(title) {
|
||||
await retry.try(async () => {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.missingOrFail(`load-saved-query-${title}-button`);
|
||||
});
|
||||
await this.closeSavedQueryManagementComponent();
|
||||
}
|
||||
|
||||
async openSavedQueryManagementComponent() {
|
||||
const isOpenAlready = await testSubjects.exists('saved-query-management-popover');
|
||||
if (isOpenAlready) return;
|
||||
|
||||
await testSubjects.click('saved-query-management-popover-button');
|
||||
}
|
||||
|
||||
async closeSavedQueryManagementComponent() {
|
||||
const isOpenAlready = await testSubjects.exists('saved-query-management-popover');
|
||||
if (!isOpenAlready) return;
|
||||
|
||||
await testSubjects.click('saved-query-management-popover-button');
|
||||
}
|
||||
|
||||
async saveNewQueryMissingOrFail() {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.missingOrFail('saved-query-management-save-button');
|
||||
}
|
||||
|
||||
async updateCurrentlyLoadedQueryMissingOrFail() {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.missingOrFail('saved-query-management-save-changes-button');
|
||||
}
|
||||
|
||||
async deleteSavedQueryMissingOrFail(title) {
|
||||
await this.openSavedQueryManagementComponent();
|
||||
await testSubjects.missingOrFail(`delete-saved-query-${title}-button`);
|
||||
}
|
||||
}
|
||||
|
||||
return new SavedQueryManagementComponent();
|
||||
}
|
|
@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { indexPatternService } from '../../../kibana_services';
|
||||
import { Storage } from 'ui/storage';
|
||||
|
||||
import { QueryBar } from 'plugins/data';
|
||||
import { SearchBar } from 'plugins/data';
|
||||
|
||||
const settings = chrome.getUiSettingsClient();
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
|
@ -91,12 +91,14 @@ export class FilterEditor extends Component {
|
|||
anchorPosition="leftCenter"
|
||||
>
|
||||
<div className="mapFilterEditor" data-test-subj="mapFilterEditor">
|
||||
<QueryBar
|
||||
<SearchBar
|
||||
uiSettings={settings}
|
||||
query={layerQuery ? layerQuery : { language: settings.get('search:queryLanguage'), query: '' }}
|
||||
onSubmit={this._onQueryChange}
|
||||
appName="maps"
|
||||
showFilterBar={false}
|
||||
showDatePicker={false}
|
||||
showQueryInput={true}
|
||||
query={layerQuery ? layerQuery : { language: settings.get('search:queryLanguage'), query: '' }}
|
||||
onQuerySubmit={this._onQueryChange}
|
||||
appName="maps"
|
||||
indexPatterns={this.state.indexPatterns}
|
||||
store={localStorage}
|
||||
customSubmitButton={
|
||||
|
|
|
@ -14,7 +14,7 @@ import {
|
|||
EuiFormHelpText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { QueryBar } from 'plugins/data';
|
||||
import { SearchBar } from 'plugins/data';
|
||||
import { Storage } from 'ui/storage';
|
||||
|
||||
const settings = chrome.getUiSettingsClient();
|
||||
|
@ -78,12 +78,14 @@ export class WhereExpression extends Component {
|
|||
defaultMessage="Use a query to narrow right source."
|
||||
/>
|
||||
</EuiFormHelpText>
|
||||
<QueryBar
|
||||
<SearchBar
|
||||
uiSettings={settings}
|
||||
query={whereQuery ? whereQuery : { language: settings.get('search:queryLanguage'), query: '' }}
|
||||
onSubmit={this._onQueryChange}
|
||||
appName="maps"
|
||||
showFilterBar={false}
|
||||
showDatePicker={false}
|
||||
showQueryInput={true}
|
||||
query={whereQuery ? whereQuery : { language: settings.get('search:queryLanguage'), query: '' }}
|
||||
onQuerySubmit={this._onQueryChange}
|
||||
appName="maps"
|
||||
indexPatterns={[indexPattern]}
|
||||
store={localStorage}
|
||||
customSubmitButton={
|
||||
|
|
|
@ -20,15 +20,15 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['search', 'url'],
|
||||
all: ['search', 'url', 'query'],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
ui: ['show', 'createShortUrl', 'save'],
|
||||
ui: ['show', 'createShortUrl', 'save', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'search'],
|
||||
read: ['index-pattern', 'search', 'query'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
|
@ -46,15 +46,15 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['visualization', 'url'],
|
||||
all: ['visualization', 'url', 'query'],
|
||||
read: ['index-pattern', 'search'],
|
||||
},
|
||||
ui: ['show', 'createShortUrl', 'delete', 'save'],
|
||||
ui: ['show', 'createShortUrl', 'delete', 'save', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['index-pattern', 'search', 'visualization'],
|
||||
read: ['index-pattern', 'search', 'visualization', 'query'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
|
@ -72,7 +72,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
all: ['dashboard', 'url'],
|
||||
all: ['dashboard', 'url', 'query'],
|
||||
read: [
|
||||
'index-pattern',
|
||||
'search',
|
||||
|
@ -82,7 +82,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
'map',
|
||||
],
|
||||
},
|
||||
ui: ['createNew', 'show', 'showWriteControls'],
|
||||
ui: ['createNew', 'show', 'showWriteControls', 'saveQuery'],
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
|
@ -95,6 +95,7 @@ const buildKibanaFeatures = (savedObjectTypes: string[]) => {
|
|||
'canvas-workpad',
|
||||
'map',
|
||||
'dashboard',
|
||||
'query',
|
||||
],
|
||||
},
|
||||
ui: ['show'],
|
||||
|
|
|
@ -796,8 +796,6 @@
|
|||
"data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) は、シンプルなクエリ構文とスクリプトフィールドのサポートを提供します。また、KQL はベーシックライセンス以上をご利用の場合、自動入力も提供します。KQL をオフにすると、Kibana は Lucene を使用します。",
|
||||
"data.query.queryBar.syntaxOptionsDescription.docsLinkText": "こちら",
|
||||
"data.query.queryBar.syntaxOptionsTitle": "構文オプション",
|
||||
"data.search.searchBar.searchBar.filtersButtonClickToHideTitle": "選択して表示",
|
||||
"data.search.searchBar.searchBar.filtersButtonClickToShowTitle": "選択して非表示",
|
||||
"embeddableApi.actionPanel.title": "オプション",
|
||||
"embeddableApi.actions.applyFilterActionTitle": "現在のビューにフィルターを適用",
|
||||
"embeddableApi.addPanel.createNew": "新規 {factoryName} を作成",
|
||||
|
|
|
@ -796,8 +796,6 @@
|
|||
"data.query.queryBar.syntaxOptionsDescription": "{docsLink} (KQL) 提供简化查询语法并支持脚本字段。如果您具有基本许可或更高级别的许可,KQL 还提供自动填充功能。如果关闭 KQL,Kibana 将使用 Lucene。",
|
||||
"data.query.queryBar.syntaxOptionsDescription.docsLinkText": "此处",
|
||||
"data.query.queryBar.syntaxOptionsTitle": "语法选项",
|
||||
"data.search.searchBar.searchBar.filtersButtonClickToHideTitle": "选择以显示",
|
||||
"data.search.searchBar.searchBar.filtersButtonClickToShowTitle": "选择以隐藏",
|
||||
"embeddableApi.actionPanel.title": "选项",
|
||||
"embeddableApi.actions.applyFilterActionTitle": "将筛选应用于当前视图",
|
||||
"embeddableApi.addPanel.createNew": "创建新的{factoryName}",
|
||||
|
|
|
@ -20,6 +20,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const panelActions = getService('dashboardPanelActions');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const globalNav = getService('globalNav');
|
||||
const queryBar = getService('queryBar');
|
||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||
|
||||
describe('dashboard security', () => {
|
||||
before(async () => {
|
||||
|
@ -187,6 +189,35 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await panelActions.openContextMenu();
|
||||
await panelActions.expectExistsEditPanelAction();
|
||||
});
|
||||
|
||||
it('allow saving via the saved query management component popover with no query loaded', async () => {
|
||||
await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo');
|
||||
});
|
||||
|
||||
it('allow saving a currently loaded saved query as a new query via the saved query management component ', async () => {
|
||||
await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery(
|
||||
'foo2',
|
||||
'bar2',
|
||||
true,
|
||||
false
|
||||
);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo2');
|
||||
});
|
||||
|
||||
it('allow saving changes to a currently loaded query via the saved query management component', async () => {
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false);
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
await savedQueryManagementComponent.loadSavedQuery('foo2');
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.eql('response:404');
|
||||
});
|
||||
|
||||
it('allows deleting saved queries in the saved query management component ', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQuery('foo2');
|
||||
await savedQueryManagementComponent.savedQueryMissingOrFail('foo2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('global dashboard read-only privileges', () => {
|
||||
|
@ -272,6 +303,33 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it(`Permalinks doesn't show create short-url button`, async () => {
|
||||
await PageObjects.share.openShareMenuItem('Permalinks');
|
||||
await PageObjects.share.createShortUrlMissingOrFail();
|
||||
// close the menu
|
||||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
it('allows loading a saved query via the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.eql('response:200');
|
||||
});
|
||||
|
||||
it('does not allow saving via the saved query management component popover with no query loaded', async () => {
|
||||
await savedQueryManagementComponent.saveNewQueryMissingOrFail();
|
||||
});
|
||||
|
||||
it('does not allow saving changes to saved query from the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQueryMissingOrFail();
|
||||
});
|
||||
|
||||
it('does not allow deleting a saved query from the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQueryMissingOrFail('OKJpgs');
|
||||
});
|
||||
|
||||
it('allows clearing the currently loaded saved query', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -21,6 +21,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
]);
|
||||
const testSubjects = getService('testSubjects');
|
||||
const appsMenu = getService('appsMenu');
|
||||
const queryBar = getService('queryBar');
|
||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||
|
||||
async function setDiscoverTimeRange() {
|
||||
const fromTime = '2015-09-19 06:31:44.000';
|
||||
|
@ -100,6 +102,37 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('Permalinks shows create short-url button', async () => {
|
||||
await PageObjects.share.openShareMenuItem('Permalinks');
|
||||
await PageObjects.share.createShortUrlExistOrFail();
|
||||
// close the menu
|
||||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
it('allow saving via the saved query management component popover with no query loaded', async () => {
|
||||
await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo');
|
||||
});
|
||||
|
||||
it('allow saving a currently loaded saved query as a new query via the saved query management component ', async () => {
|
||||
await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery(
|
||||
'foo2',
|
||||
'bar2',
|
||||
true,
|
||||
false
|
||||
);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo2');
|
||||
});
|
||||
|
||||
it('allow saving changes to a currently loaded query via the saved query management component', async () => {
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false);
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
await savedQueryManagementComponent.loadSavedQuery('foo2');
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.eql('response:404');
|
||||
});
|
||||
|
||||
it('allows deleting saved queries in the saved query management component ', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQuery('foo2');
|
||||
await savedQueryManagementComponent.savedQueryMissingOrFail('foo2');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -167,6 +200,31 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.share.openShareMenuItem('Permalinks');
|
||||
await PageObjects.share.createShortUrlMissingOrFail();
|
||||
});
|
||||
|
||||
it('allows loading a saved query via the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.eql('response:200');
|
||||
});
|
||||
|
||||
it('does not allow saving via the saved query management component popover with no query loaded', async () => {
|
||||
await savedQueryManagementComponent.saveNewQueryMissingOrFail();
|
||||
});
|
||||
|
||||
it('does not allow saving changes to saved query from the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQueryMissingOrFail();
|
||||
});
|
||||
|
||||
it('does not allow deleting a saved query from the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQueryMissingOrFail('OKJpgs');
|
||||
});
|
||||
|
||||
it('allows clearing the currently loaded saved query', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
});
|
||||
});
|
||||
|
||||
describe('discover and visualize privileges', () => {
|
||||
|
|
|
@ -21,6 +21,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const testSubjects = getService('testSubjects');
|
||||
const appsMenu = getService('appsMenu');
|
||||
const globalNav = getService('globalNav');
|
||||
const queryBar = getService('queryBar');
|
||||
const savedQueryManagementComponent = getService('savedQueryManagementComponent');
|
||||
|
||||
describe('feature controls security', () => {
|
||||
before(async () => {
|
||||
|
@ -112,6 +114,41 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it('Permalinks shows create short-url button', async () => {
|
||||
await PageObjects.share.openShareMenuItem('Permalinks');
|
||||
await PageObjects.share.createShortUrlExistOrFail();
|
||||
// close menu
|
||||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
it('allow saving via the saved query management component popover with no saved query loaded', async () => {
|
||||
await queryBar.setQuery('response:200');
|
||||
await savedQueryManagementComponent.saveNewQuery('foo', 'bar', true, false);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo');
|
||||
await savedQueryManagementComponent.closeSavedQueryManagementComponent();
|
||||
});
|
||||
|
||||
it('allow saving a currently loaded saved query as a new query via the saved query management component ', async () => {
|
||||
await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery(
|
||||
'foo2',
|
||||
'bar2',
|
||||
true,
|
||||
false
|
||||
);
|
||||
await savedQueryManagementComponent.savedQueryExistOrFail('foo2');
|
||||
await savedQueryManagementComponent.closeSavedQueryManagementComponent();
|
||||
});
|
||||
|
||||
it('allow saving changes to a currently loaded query via the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('foo2');
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQuery('bar2', false, false);
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
await savedQueryManagementComponent.loadSavedQuery('foo2');
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.eql('response:404');
|
||||
});
|
||||
|
||||
it('allows deleting saved queries in the saved query management component ', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQuery('foo2');
|
||||
await savedQueryManagementComponent.savedQueryMissingOrFail('foo2');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -194,6 +231,33 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
it(`Permalinks doesn't show create short-url button`, async () => {
|
||||
await PageObjects.share.openShareMenuItem('Permalinks');
|
||||
await PageObjects.share.createShortUrlMissingOrFail();
|
||||
// close the menu
|
||||
await PageObjects.share.clickShareTopNavButton();
|
||||
});
|
||||
|
||||
it('allows loading a saved query via the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
const queryString = await queryBar.getQueryString();
|
||||
expect(queryString).to.eql('response:200');
|
||||
});
|
||||
|
||||
it('does not allow saving via the saved query management component popover with no query loaded', async () => {
|
||||
await savedQueryManagementComponent.saveNewQueryMissingOrFail();
|
||||
});
|
||||
|
||||
it('does not allow saving changes to saved query from the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
await queryBar.setQuery('response:404');
|
||||
await savedQueryManagementComponent.updateCurrentlyLoadedQueryMissingOrFail();
|
||||
});
|
||||
|
||||
it('does not allow deleting a saved query from the saved query management component', async () => {
|
||||
await savedQueryManagementComponent.deleteSavedQueryMissingOrFail('OKJpgs');
|
||||
});
|
||||
|
||||
it('allows clearing the currently loaded saved query', async () => {
|
||||
await savedQueryManagementComponent.loadSavedQuery('OKJpgs');
|
||||
await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -169,3 +169,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "query:okjpgs",
|
||||
"source": {
|
||||
"query": {
|
||||
"title": "OKJpgs",
|
||||
"description": "Ok responses for jpg files",
|
||||
"query": {
|
||||
"query": "response:200",
|
||||
"language": "kuery"
|
||||
},
|
||||
"filters": [{"meta":{"index":"b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b","alias":null,"negate":false,"type":"phrase","key":"extension.raw","value":"jpg","params":{"query":"jpg"},"disabled":false},"query":{"match":{"extension.raw":{"query":"jpg","type":"phrase"}}},"$state":{"store":"appState"}}]
|
||||
},
|
||||
"type": "query",
|
||||
"updated_at": "2019-07-17T17:54:26.378Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -481,6 +481,35 @@
|
|||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"query": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"timefilter": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,3 +35,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "query:okjpgs",
|
||||
"source": {
|
||||
"query": {
|
||||
"title": "OKJpgs",
|
||||
"description": "Ok responses for jpg files",
|
||||
"query": {
|
||||
"query": "response:200",
|
||||
"language": "kuery"
|
||||
},
|
||||
"filters": [{"meta":{"index":"b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b","alias":null,"negate":false,"type":"phrase","key":"extension.raw","value":"jpg","params":{"query":"jpg"},"disabled":false},"query":{"match":{"extension.raw":{"query":"jpg","type":"phrase"}}},"$state":{"store":"appState"}}]
|
||||
},
|
||||
"type": "query",
|
||||
"updated_at": "2019-07-17T17:54:26.378Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -453,6 +453,35 @@
|
|||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"query": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"timefilter": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,3 +108,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".kibana",
|
||||
"type": "doc",
|
||||
"id": "query:okjpgs",
|
||||
"source": {
|
||||
"query": {
|
||||
"title": "OKJpgs",
|
||||
"description": "Ok responses for jpg files",
|
||||
"query": {
|
||||
"query": "response:200",
|
||||
"language": "kuery"
|
||||
},
|
||||
"filters": [{"meta":{"index":"b15b1d40-a8bb-11e9-98cf-2bb06ef63e0b","alias":null,"negate":false,"type":"phrase","key":"extension.raw","value":"jpg","params":{"query":"jpg"},"disabled":false},"query":{"match":{"extension.raw":{"query":"jpg","type":"phrase"}}},"$state":{"store":"appState"}}]
|
||||
},
|
||||
"type": "query",
|
||||
"updated_at": "2019-07-17T17:54:26.378Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -453,6 +453,35 @@
|
|||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"query": {
|
||||
"properties": {
|
||||
"language": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"query": {
|
||||
"type": "keyword",
|
||||
"index": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
},
|
||||
"timefilter": {
|
||||
"type": "object",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest<any
|
|||
expect(resp.body).to.eql({
|
||||
statusCode: 400,
|
||||
error: 'Bad Request',
|
||||
message: `child \"objects\" fails because [\"objects\" at position 0 fails because [child \"type\" fails because [\"type\" must be one of [canvas-element, canvas-workpad, config, dashboard, globaltype, index-pattern, map, search, url, visualization]]]]`,
|
||||
message: `child \"objects\" fails because [\"objects\" at position 0 fails because [child \"type\" fails because [\"type\" must be one of [canvas-element, canvas-workpad, config, dashboard, globaltype, index-pattern, map, query, search, url, visualization]]]]`,
|
||||
validation: {
|
||||
source: 'payload',
|
||||
keys: ['objects.0.type'],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue