mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* kbn top nav in discover * New top nav in dashboard and vis editor * Stop using template feature of kbn top nav * Changed console menu to new directive * Use search bar in top nav in discover and maps Support search bar with no filter bar (TS) * Moved storage instantiation to angular directive * Make index patterns optional (for timepicker only setup) * Moved discover result count away from top nav * Removed unused name attribute in top nav. Use app-name instead.
This commit is contained in:
parent
6be4a1e8dd
commit
c3217640d2
46 changed files with 1489 additions and 415 deletions
|
@ -1,4 +1,7 @@
|
|||
<kbn-top-nav name="console" config="topNavMenu"></kbn-top-nav>
|
||||
<kbn-top-nav-v2
|
||||
app-name="'console'"
|
||||
config="topNavMenu"
|
||||
></kbn-top-nav-v2>
|
||||
<kbn-dev-tools-app data-test-subj="console">
|
||||
<sense-history ng-show="showHistory" is-shown="showHistory" close="closeHistory()" history-dirty="lastRequestTimestamp"></sense-history>
|
||||
<div class="conApp">
|
||||
|
|
|
@ -28,7 +28,7 @@ import { showHelpPanel } from './help_show_panel';
|
|||
export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
|
||||
return [
|
||||
{
|
||||
key: 'history',
|
||||
id: 'history',
|
||||
label: i18n.translate('console.topNav.historyTabLabel', {
|
||||
defaultMessage: 'History',
|
||||
}),
|
||||
|
@ -36,12 +36,12 @@ export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
|
|||
defaultMessage: 'History',
|
||||
}),
|
||||
run: () => {
|
||||
toggleHistory();
|
||||
$scope.$evalAsync(toggleHistory);
|
||||
},
|
||||
testId: 'consoleHistoryButton',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
id: 'settings',
|
||||
label: i18n.translate('console.topNav.settingsTabLabel', {
|
||||
defaultMessage: 'Settings',
|
||||
}),
|
||||
|
@ -54,7 +54,7 @@ export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
|
|||
testId: 'consoleSettingsButton',
|
||||
},
|
||||
{
|
||||
key: 'help',
|
||||
id: 'help',
|
||||
label: i18n.translate('console.topNav.helpTabLabel', {
|
||||
defaultMessage: 'Help',
|
||||
}),
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
// SASSTODO: Probably not the right file for this selector, but temporary until the files get re-organized
|
||||
.globalQueryBar {
|
||||
padding: 0px $euiSizeS $euiSizeS $euiSizeS;
|
||||
}
|
||||
|
||||
.globalQueryBar:not(:empty) {
|
||||
padding-bottom: $euiSizeS;
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@ export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from '.
|
|||
/** @public types */
|
||||
export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns';
|
||||
export { Query } from './query';
|
||||
export { SearchBar, SearchBarProps } from './search';
|
||||
export { FilterManager, FilterStateManager, uniqFilters } from './filter/filter_manager';
|
||||
|
||||
/** @public static code */
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
exports[`QueryBar Should render the given query 1`] = `
|
||||
<EuiFlexGroup
|
||||
className="kbnQueryBar"
|
||||
className="kbnQueryBar kbnQueryBar--withDatePicker"
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
responsive={true}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<InjectIntl(QueryBarInputUI)
|
||||
|
@ -59,13 +59,53 @@ exports[`QueryBar Should render the given query 1`] = `
|
|||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiSuperUpdateButton
|
||||
data-test-subj="querySubmitButton"
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
needsUpdate={false}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
className="kbnQueryBar__datePickerWrapper"
|
||||
>
|
||||
<EuiSuperDatePicker
|
||||
commonlyUsedRanges={
|
||||
Array [
|
||||
Object {
|
||||
"end": "now/d",
|
||||
"label": "Today",
|
||||
"start": "now/d",
|
||||
},
|
||||
]
|
||||
}
|
||||
dateFormat="YY"
|
||||
end="now"
|
||||
isAutoRefreshOnly={false}
|
||||
isPaused={true}
|
||||
onTimeChange={[Function]}
|
||||
recentlyUsedRanges={
|
||||
Array [
|
||||
Object {
|
||||
"end": undefined,
|
||||
"start": undefined,
|
||||
},
|
||||
]
|
||||
}
|
||||
refreshInterval={0}
|
||||
showUpdateButton={false}
|
||||
start="now-15m"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiSuperUpdateButton
|
||||
data-test-subj="querySubmitButton"
|
||||
isDisabled={false}
|
||||
isLoading={false}
|
||||
needsUpdate={true}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
`;
|
||||
|
|
|
@ -66,6 +66,8 @@ const mockIndexPattern = {
|
|||
};
|
||||
|
||||
describe('QueryBar', () => {
|
||||
const QUERY_INPUT_SELECTOR = 'InjectIntl(QueryBarInputUI)';
|
||||
const TIMEPICKER_SELECTOR = 'EuiSuperDatePicker';
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
@ -102,4 +104,104 @@ describe('QueryBar', () => {
|
|||
|
||||
expect(mockPersistedLogFactory.mock.calls[0][0]).toBe('typeahead:discover-kuery');
|
||||
});
|
||||
|
||||
it('Should render only timepicker when no options provided', () => {
|
||||
const component = shallowWithIntl(
|
||||
<QueryBar.WrappedComponent
|
||||
onSubmit={noop}
|
||||
appName={'discover'}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
|
||||
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(1);
|
||||
});
|
||||
|
||||
it('Should not show timepicker when asked', () => {
|
||||
const component = shallowWithIntl(
|
||||
<QueryBar.WrappedComponent
|
||||
onSubmit={noop}
|
||||
appName={'discover'}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
|
||||
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render timepicker with options', () => {
|
||||
const component = shallowWithIntl(
|
||||
<QueryBar.WrappedComponent
|
||||
onSubmit={noop}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
showDatePicker={true}
|
||||
dateRangeFrom={'now-7d'}
|
||||
dateRangeTo={'now'}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
|
||||
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(1);
|
||||
});
|
||||
|
||||
it('Should render only query input bar', () => {
|
||||
const component = shallowWithIntl(
|
||||
<QueryBar.WrappedComponent
|
||||
query={kqlQuery}
|
||||
onSubmit={noop}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(1);
|
||||
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should NOT render query input bar if disabled', () => {
|
||||
const component = shallowWithIntl(
|
||||
<QueryBar.WrappedComponent
|
||||
query={kqlQuery}
|
||||
onSubmit={noop}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
showQueryInput={false}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
|
||||
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should NOT render query input bar if missing options', () => {
|
||||
const component = shallowWithIntl(
|
||||
<QueryBar.WrappedComponent
|
||||
onSubmit={noop}
|
||||
appName={'discover'}
|
||||
screenTitle={'Another Screen'}
|
||||
store={createMockStorage()}
|
||||
intl={null as any}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(QUERY_INPUT_SELECTOR).length).toBe(0);
|
||||
expect(component.find(TIMEPICKER_SELECTOR).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import { doesKueryExpressionHaveLuceneSyntaxError } from '@kbn/es-query';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
@ -37,6 +36,7 @@ import { documentationLinks } from 'ui/documentation_links';
|
|||
import { Toast, toastNotifications } from 'ui/notify';
|
||||
import chrome from 'ui/chrome';
|
||||
import { PersistedLog } from 'ui/persisted_log';
|
||||
import { IndexPattern } from '../../../index_patterns';
|
||||
import { QueryBarInput } from './query_bar_input';
|
||||
|
||||
import { getQueryLog } from '../lib/get_query_log';
|
||||
|
@ -50,15 +50,16 @@ interface DateRange {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
query: Query;
|
||||
onSubmit: (payload: { dateRange: DateRange; query: Query }) => void;
|
||||
query?: Query;
|
||||
onSubmit: (payload: { dateRange: DateRange; query?: Query }) => void;
|
||||
disableAutoFocus?: boolean;
|
||||
appName: string;
|
||||
screenTitle: string;
|
||||
indexPatterns: Array<IndexPattern | string>;
|
||||
screenTitle?: string;
|
||||
indexPatterns?: Array<IndexPattern | string>;
|
||||
store: Storage;
|
||||
intl: InjectedIntl;
|
||||
prepend?: any;
|
||||
showQueryInput?: boolean;
|
||||
showDatePicker?: boolean;
|
||||
dateRangeFrom?: string;
|
||||
dateRangeTo?: string;
|
||||
|
@ -70,7 +71,7 @@ interface Props {
|
|||
}
|
||||
|
||||
interface State {
|
||||
query: Query;
|
||||
query?: Query;
|
||||
inputIsPristine: boolean;
|
||||
currentProps?: Props;
|
||||
dateRangeFrom: string;
|
||||
|
@ -79,22 +80,30 @@ interface State {
|
|||
}
|
||||
|
||||
export class QueryBarUI extends Component<Props, State> {
|
||||
public static defaultProps = {
|
||||
showQueryInput: true,
|
||||
showDatePicker: true,
|
||||
showAutoRefreshOnly: false,
|
||||
};
|
||||
|
||||
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
|
||||
if (isEqual(prevState.currentProps, nextProps)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let nextQuery = null;
|
||||
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,
|
||||
};
|
||||
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;
|
||||
|
@ -134,7 +143,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
See https://github.com/elastic/kibana/issues/14086
|
||||
*/
|
||||
public state = {
|
||||
query: {
|
||||
query: this.props.query && {
|
||||
query: this.props.query.query,
|
||||
language: this.props.query.language,
|
||||
},
|
||||
|
@ -149,20 +158,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.state.query.query !== this.props.query.query;
|
||||
return this.isQueryDirty();
|
||||
}
|
||||
|
||||
return (
|
||||
this.state.query.query !== this.props.query.query ||
|
||||
this.isQueryDirty() ||
|
||||
this.state.dateRangeFrom !== this.props.dateRangeFrom ||
|
||||
this.state.dateRangeTo !== this.props.dateRangeTo
|
||||
);
|
||||
};
|
||||
|
||||
public onClickSubmitButton = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (this.persistedLog) {
|
||||
if (this.persistedLog && this.state.query) {
|
||||
this.persistedLog.add(this.state.query.query);
|
||||
}
|
||||
this.onSubmit(() => event.preventDefault());
|
||||
|
@ -209,7 +224,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
});
|
||||
|
||||
this.props.onSubmit({
|
||||
query: {
|
||||
query: this.state.query && {
|
||||
query: this.state.query.query,
|
||||
language: this.state.query.language,
|
||||
},
|
||||
|
@ -227,10 +242,12 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
};
|
||||
|
||||
public componentDidMount() {
|
||||
if (!this.props.query) return;
|
||||
this.persistedLog = getQueryLog(this.props.appName, this.props.query.language);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (!this.props.query || !prevProps.query) return;
|
||||
if (prevProps.query.language !== this.props.query.language) {
|
||||
this.persistedLog = getQueryLog(this.props.appName, this.props.query.language);
|
||||
}
|
||||
|
@ -243,25 +260,40 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
|
||||
return (
|
||||
<EuiFlexGroup className={classes} responsive={!!this.props.showDatePicker} gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<QueryBarInput
|
||||
appName={this.props.appName}
|
||||
disableAutoFocus={this.props.disableAutoFocus}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
prepend={this.props.prepend}
|
||||
query={this.state.query}
|
||||
screenTitle={this.props.screenTitle}
|
||||
store={this.props.store}
|
||||
onChange={this.onChange}
|
||||
onSubmit={this.onInputSubmit}
|
||||
persistedLog={this.persistedLog}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{this.renderQueryInput()}
|
||||
<EuiFlexItem grow={false}>{this.renderUpdateButton()}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
private renderQueryInput() {
|
||||
if (!this.shouldRenderQueryInput()) return;
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<QueryBarInput
|
||||
appName={this.props.appName}
|
||||
disableAutoFocus={this.props.disableAutoFocus}
|
||||
indexPatterns={this.props.indexPatterns!}
|
||||
prepend={this.props.prepend}
|
||||
query={this.state.query!}
|
||||
screenTitle={this.props.screenTitle}
|
||||
store={this.props.store}
|
||||
onChange={this.onChange}
|
||||
onSubmit={this.onInputSubmit}
|
||||
persistedLog={this.persistedLog}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
private shouldRenderDatePicker() {
|
||||
return this.props.showDatePicker || this.props.showAutoRefreshOnly;
|
||||
}
|
||||
|
||||
private shouldRenderQueryInput() {
|
||||
return this.props.showQueryInput && this.props.indexPatterns && this.props.query;
|
||||
}
|
||||
|
||||
private renderUpdateButton() {
|
||||
const button = this.props.customSubmitButton ? (
|
||||
React.cloneElement(this.props.customSubmitButton, { onClick: this.onClickSubmitButton })
|
||||
|
@ -274,7 +306,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
/>
|
||||
);
|
||||
|
||||
if (!this.props.showDatePicker) {
|
||||
if (!this.shouldRenderDatePicker()) {
|
||||
return button;
|
||||
}
|
||||
|
||||
|
@ -287,7 +319,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private renderDatePicker() {
|
||||
if (!this.props.showDatePicker) {
|
||||
if (!this.shouldRenderDatePicker()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -330,6 +362,7 @@ export class QueryBarUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
private handleLuceneSyntaxWarning() {
|
||||
if (!this.state.query) return;
|
||||
const { intl, store } = this.props;
|
||||
const { query, language } = this.state.query;
|
||||
if (
|
||||
|
|
|
@ -42,6 +42,16 @@ const mockChromeFactory = jest.fn(() => {
|
|||
return {
|
||||
get: (key: string) => {
|
||||
switch (key) {
|
||||
case 'timepicker:quickRanges':
|
||||
return [
|
||||
{
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
display: 'Today',
|
||||
},
|
||||
];
|
||||
case 'dateFormat':
|
||||
return 'YY';
|
||||
case 'history:limit':
|
||||
return 10;
|
||||
default:
|
||||
|
|
|
@ -18,3 +18,5 @@
|
|||
*/
|
||||
|
||||
export { SearchService, SearchSetup } from './search_service';
|
||||
|
||||
export * from './search_bar';
|
||||
|
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { SearchBar } from './search_bar';
|
||||
export * from './search_bar';
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
import { SearchBar } from './search_bar';
|
||||
|
||||
jest.mock('../../../filter/filter_bar', () => {
|
||||
return {
|
||||
FilterBar: () => <div className="filterBar"></div>,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../../query/query_bar', () => {
|
||||
return {
|
||||
QueryBar: () => <div className="queryBar"></div>,
|
||||
};
|
||||
});
|
||||
|
||||
const noop = jest.fn();
|
||||
|
||||
const createMockWebStorage = () => ({
|
||||
clear: jest.fn(),
|
||||
getItem: jest.fn(),
|
||||
key: jest.fn(),
|
||||
removeItem: jest.fn(),
|
||||
setItem: jest.fn(),
|
||||
length: 0,
|
||||
});
|
||||
|
||||
const createMockStorage = () => ({
|
||||
store: createMockWebStorage(),
|
||||
get: jest.fn(),
|
||||
set: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
clear: jest.fn(),
|
||||
});
|
||||
|
||||
const mockIndexPattern = {
|
||||
id: '1234',
|
||||
title: 'logstash-*',
|
||||
fields: [
|
||||
{
|
||||
name: 'response',
|
||||
type: 'number',
|
||||
esTypes: ['integer'],
|
||||
aggregatable: true,
|
||||
filterable: true,
|
||||
searchable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const kqlQuery = {
|
||||
query: 'response:200',
|
||||
language: 'kuery',
|
||||
};
|
||||
|
||||
describe('SearchBar', () => {
|
||||
const SEARCH_BAR_ROOT = '.globalQueryBar';
|
||||
const FILTER_BAR = '.filterBar';
|
||||
const QUERY_BAR = '.queryBar';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Should render query bar when no options provided (in reality - timepicker)', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(0);
|
||||
expect(component.find(QUERY_BAR).length).toBe(1);
|
||||
});
|
||||
|
||||
it('Should render empty when timepicker is off and no options provided', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(0);
|
||||
expect(component.find(QUERY_BAR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render filter bar, when required fields are provided', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
filters={[]}
|
||||
onFiltersUpdated={noop}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(1);
|
||||
expect(component.find(QUERY_BAR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should NOT render filter bar, if disabled', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
showFilterBar={false}
|
||||
filters={[]}
|
||||
onFiltersUpdated={noop}
|
||||
showDatePicker={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(0);
|
||||
expect(component.find(QUERY_BAR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render query bar, when required fields are provided', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
screenTitle={'test screen'}
|
||||
store={createMockStorage()}
|
||||
onQuerySubmit={noop}
|
||||
query={kqlQuery}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(0);
|
||||
expect(component.find(QUERY_BAR).length).toBe(1);
|
||||
});
|
||||
|
||||
it('Should NOT render query bar, if disabled', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
screenTitle={'test screen'}
|
||||
store={createMockStorage()}
|
||||
onQuerySubmit={noop}
|
||||
query={kqlQuery}
|
||||
showQueryBar={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(0);
|
||||
expect(component.find(QUERY_BAR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render query bar and filter bar', () => {
|
||||
const component = mountWithIntl(
|
||||
<SearchBar.WrappedComponent
|
||||
appName={'test'}
|
||||
indexPatterns={[mockIndexPattern]}
|
||||
intl={null as any}
|
||||
screenTitle={'test screen'}
|
||||
store={createMockStorage()}
|
||||
onQuerySubmit={noop}
|
||||
query={kqlQuery}
|
||||
filters={[]}
|
||||
onFiltersUpdated={noop}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(SEARCH_BAR_ROOT).length).toBe(1);
|
||||
expect(component.find(FILTER_BAR).length).toBe(1);
|
||||
expect(component.find(QUERY_BAR).length).toBe(1);
|
||||
});
|
||||
});
|
|
@ -24,9 +24,9 @@ import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
|||
import classNames from 'classnames';
|
||||
import React, { Component } from 'react';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { IndexPattern } from 'ui/index_patterns';
|
||||
import { Storage } from 'ui/storage';
|
||||
|
||||
import { IndexPattern } from '../../../index_patterns';
|
||||
import { Query, QueryBar } from '../../../query/query_bar';
|
||||
import { FilterBar } from '../../../filter/filter_bar';
|
||||
|
||||
|
@ -39,22 +39,26 @@ interface DateRange {
|
|||
* NgReact lib requires that changes to the props need to be made in the directive config as well
|
||||
* See [search_bar\directive\index.js] file
|
||||
*/
|
||||
interface Props {
|
||||
query: Query;
|
||||
onQuerySubmit: (payload: { dateRange: DateRange; query: Query }) => void;
|
||||
disableAutoFocus?: boolean;
|
||||
export interface SearchBarProps {
|
||||
appName: string;
|
||||
screenTitle: string;
|
||||
indexPatterns: IndexPattern[];
|
||||
store: Storage;
|
||||
filters: Filter[];
|
||||
onFiltersUpdated: (filters: Filter[]) => void;
|
||||
showQueryBar: boolean;
|
||||
showFilterBar: boolean;
|
||||
intl: InjectedIntl;
|
||||
indexPatterns?: IndexPattern[];
|
||||
// Query bar
|
||||
showQueryBar?: boolean;
|
||||
showQueryInput?: boolean;
|
||||
screenTitle?: string;
|
||||
store?: Storage;
|
||||
query?: Query;
|
||||
onQuerySubmit?: (payload: { dateRange: DateRange; query?: Query }) => void;
|
||||
// Filter bar
|
||||
showFilterBar?: boolean;
|
||||
filters?: Filter[];
|
||||
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||
// Date picker
|
||||
showDatePicker?: boolean;
|
||||
dateRangeFrom?: string;
|
||||
dateRangeTo?: string;
|
||||
// Autorefresh
|
||||
isRefreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
showAutoRefreshOnly?: boolean;
|
||||
|
@ -65,10 +69,12 @@ interface State {
|
|||
isFiltersVisible: boolean;
|
||||
}
|
||||
|
||||
class SearchBarUI extends Component<Props, State> {
|
||||
class SearchBarUI extends Component<SearchBarProps, State> {
|
||||
public static defaultProps = {
|
||||
showQueryBar: true,
|
||||
showFilterBar: true,
|
||||
showDatePicker: true,
|
||||
showAutoRefreshOnly: false,
|
||||
};
|
||||
|
||||
public filterBarRef: Element | null = null;
|
||||
|
@ -78,6 +84,61 @@ class SearchBarUI extends Component<Props, State> {
|
|||
isFiltersVisible: true,
|
||||
};
|
||||
|
||||
private getFilterLength() {
|
||||
if (this.props.showFilterBar && this.props.filters) {
|
||||
return this.props.filters.length;
|
||||
}
|
||||
}
|
||||
|
||||
private getFilterUpdateFunction() {
|
||||
if (this.props.showFilterBar && this.props.onFiltersUpdated) {
|
||||
return this.props.onFiltersUpdated;
|
||||
}
|
||||
return (filters: Filter[]) => {};
|
||||
}
|
||||
|
||||
private shouldRenderQueryBar() {
|
||||
const showDatePicker = this.props.showDatePicker || this.props.showAutoRefreshOnly;
|
||||
const showQueryInput =
|
||||
this.props.showQueryInput && this.props.indexPatterns && this.props.query;
|
||||
return this.props.showQueryBar && (showDatePicker || showQueryInput);
|
||||
}
|
||||
|
||||
private shouldRenderFilterBar() {
|
||||
return this.props.showFilterBar && this.props.filters && this.props.indexPatterns;
|
||||
}
|
||||
|
||||
private getFilterTriggerButton() {
|
||||
const filtersAppliedText = this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.filtersButtonFiltersAppliedTitle',
|
||||
defaultMessage: 'filters applied.',
|
||||
});
|
||||
const clickToShowOrHideText = this.state.isFiltersVisible
|
||||
? this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.filtersButtonClickToShowTitle',
|
||||
defaultMessage: 'Select to hide',
|
||||
})
|
||||
: this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.filtersButtonClickToHideTitle',
|
||||
defaultMessage: 'Select to show',
|
||||
});
|
||||
|
||||
const filterCount = this.getFilterLength();
|
||||
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}`}
|
||||
>
|
||||
Filters
|
||||
</EuiFilterButton>
|
||||
);
|
||||
}
|
||||
|
||||
public setFilterBarHeight = () => {
|
||||
requestAnimationFrame(() => {
|
||||
const height =
|
||||
|
@ -114,85 +175,62 @@ class SearchBarUI extends Component<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const filtersAppliedText = this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.filtersButtonFiltersAppliedTitle',
|
||||
defaultMessage: 'filters applied.',
|
||||
});
|
||||
const clickToShowOrHideText = this.state.isFiltersVisible
|
||||
? this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.filtersButtonClickToShowTitle',
|
||||
defaultMessage: 'Select to hide',
|
||||
})
|
||||
: this.props.intl.formatMessage({
|
||||
id: 'data.search.searchBar.filtersButtonClickToHideTitle',
|
||||
defaultMessage: 'Select to show',
|
||||
});
|
||||
let queryBar;
|
||||
if (this.shouldRenderQueryBar()) {
|
||||
queryBar = (
|
||||
<QueryBar
|
||||
query={this.props.query}
|
||||
screenTitle={this.props.screenTitle}
|
||||
onSubmit={this.props.onQuerySubmit!}
|
||||
appName={this.props.appName}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
store={this.props.store!}
|
||||
prepend={this.props.showFilterBar ? this.getFilterTriggerButton() : undefined}
|
||||
showDatePicker={this.props.showDatePicker}
|
||||
showQueryInput={this.props.showQueryInput}
|
||||
dateRangeFrom={this.props.dateRangeFrom}
|
||||
dateRangeTo={this.props.dateRangeTo}
|
||||
isRefreshPaused={this.props.isRefreshPaused}
|
||||
refreshInterval={this.props.refreshInterval}
|
||||
showAutoRefreshOnly={this.props.showAutoRefreshOnly}
|
||||
onRefreshChange={this.props.onRefreshChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const filterTriggerButton = (
|
||||
<EuiFilterButton
|
||||
onClick={this.toggleFiltersVisible}
|
||||
isSelected={this.state.isFiltersVisible}
|
||||
hasActiveFilters={this.state.isFiltersVisible}
|
||||
numFilters={this.props.filters.length > 0 ? this.props.filters.length : undefined}
|
||||
aria-controls="GlobalFilterGroup"
|
||||
aria-expanded={!!this.state.isFiltersVisible}
|
||||
title={`${this.props.filters.length} ${filtersAppliedText} ${clickToShowOrHideText}`}
|
||||
>
|
||||
Filters
|
||||
</EuiFilterButton>
|
||||
);
|
||||
|
||||
const classes = classNames('globalFilterGroup__wrapper', {
|
||||
'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible,
|
||||
});
|
||||
let filterBar;
|
||||
if (this.shouldRenderFilterBar()) {
|
||||
const filterGroupClasses = classNames('globalFilterGroup__wrapper', {
|
||||
'globalFilterGroup__wrapper-isVisible': this.state.isFiltersVisible,
|
||||
});
|
||||
filterBar = (
|
||||
<div
|
||||
id="GlobalFilterGroup"
|
||||
ref={node => {
|
||||
this.filterBarWrapperRef = node;
|
||||
}}
|
||||
className={filterGroupClasses}
|
||||
>
|
||||
<div
|
||||
ref={node => {
|
||||
this.filterBarRef = node;
|
||||
}}
|
||||
>
|
||||
<FilterBar
|
||||
className="globalFilterGroup__filterBar"
|
||||
filters={this.props.filters!}
|
||||
onFiltersUpdated={this.getFilterUpdateFunction()}
|
||||
indexPatterns={this.props.indexPatterns!}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="globalQueryBar">
|
||||
{this.props.showQueryBar ? (
|
||||
<QueryBar
|
||||
query={this.props.query}
|
||||
screenTitle={this.props.screenTitle}
|
||||
onSubmit={this.props.onQuerySubmit}
|
||||
appName={this.props.appName}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
store={this.props.store}
|
||||
prepend={this.props.showFilterBar ? filterTriggerButton : ''}
|
||||
showDatePicker={this.props.showDatePicker}
|
||||
dateRangeFrom={this.props.dateRangeFrom}
|
||||
dateRangeTo={this.props.dateRangeTo}
|
||||
isRefreshPaused={this.props.isRefreshPaused}
|
||||
refreshInterval={this.props.refreshInterval}
|
||||
showAutoRefreshOnly={this.props.showAutoRefreshOnly}
|
||||
onRefreshChange={this.props.onRefreshChange}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
{this.props.showFilterBar ? (
|
||||
<div
|
||||
id="GlobalFilterGroup"
|
||||
ref={node => {
|
||||
this.filterBarWrapperRef = node;
|
||||
}}
|
||||
className={classes}
|
||||
>
|
||||
<div
|
||||
ref={node => {
|
||||
this.filterBarRef = node;
|
||||
}}
|
||||
>
|
||||
<FilterBar
|
||||
className="globalFilterGroup__filterBar"
|
||||
filters={this.props.filters}
|
||||
onFiltersUpdated={this.props.onFiltersUpdated}
|
||||
indexPatterns={this.props.indexPatterns}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{queryBar}
|
||||
{filterBar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { SearchBar } from './components';
|
||||
export * from './components';
|
||||
|
||||
// @ts-ignore
|
||||
export { setupDirective } from './directive';
|
||||
|
|
|
@ -3,30 +3,26 @@
|
|||
ng-class="{'dshAppContainer--withMargins': model.useMargins}"
|
||||
>
|
||||
<!-- Local nav. -->
|
||||
<kbn-top-nav name="dashboard" config="topNavMenu">
|
||||
<!-- Transcluded elements. -->
|
||||
<div data-transclude-slots>
|
||||
<!-- Search. -->
|
||||
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
||||
<search-bar
|
||||
query="model.query"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
app-name="'dashboard'"
|
||||
index-patterns="indexPatterns"
|
||||
filters="model.filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
show-filter-bar="showFilterBar()"
|
||||
show-date-picker="true"
|
||||
date-range-from="model.timeRange.from"
|
||||
date-range-to="model.timeRange.to"
|
||||
is-refresh-paused="model.refreshInterval.pause"
|
||||
refresh-interval="model.refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
></search-bar>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
<kbn-top-nav-v2
|
||||
app-name="'dashboard'"
|
||||
config="topNavMenu"
|
||||
|
||||
show-search-bar="chrome.getVisible()"
|
||||
show-filter-bar="showFilterBar()"
|
||||
|
||||
filters="model.filters"
|
||||
query="model.query"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="indexPatterns"
|
||||
filters="model.filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
date-range-from="model.timeRange.from"
|
||||
date-range-to="model.timeRange.to"
|
||||
is-refresh-paused="model.refreshInterval.pause"
|
||||
refresh-interval="model.refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange">
|
||||
</kbn-top-nav-v2>
|
||||
|
||||
<!--
|
||||
The top nav is hidden in embed mode but the filter bar must still be present so
|
||||
|
|
|
@ -547,11 +547,21 @@ export class DashboardAppController {
|
|||
|
||||
$scope.showAddPanel = () => {
|
||||
dashboardStateManager.setFullScreenMode(false);
|
||||
$scope.kbnTopNav.click(TopNavIds.ADD);
|
||||
/*
|
||||
* Temp solution for triggering menu click.
|
||||
* When de-angularizing this code, please call the underlaying action function
|
||||
* directly and not via the top nav object.
|
||||
**/
|
||||
navActions[TopNavIds.ADD]();
|
||||
};
|
||||
$scope.enterEditMode = () => {
|
||||
dashboardStateManager.setFullScreenMode(false);
|
||||
$scope.kbnTopNav.click('edit');
|
||||
/*
|
||||
* Temp solution for triggering menu click.
|
||||
* When de-angularizing this code, please call the underlaying action function
|
||||
* directly and not via the top nav object.
|
||||
**/
|
||||
navActions[TopNavIds.ENTER_EDIT_MODE]();
|
||||
};
|
||||
const navActions: {
|
||||
[key: string]: NavAction;
|
||||
|
@ -641,7 +651,7 @@ export class DashboardAppController {
|
|||
}
|
||||
};
|
||||
|
||||
navActions[TopNavIds.OPTIONS] = (menuItem, navController, anchorElement) => {
|
||||
navActions[TopNavIds.OPTIONS] = anchorElement => {
|
||||
showOptionsPopover({
|
||||
anchorElement,
|
||||
useMargins: dashboardStateManager.getUseMargins(),
|
||||
|
@ -654,7 +664,7 @@ export class DashboardAppController {
|
|||
},
|
||||
});
|
||||
};
|
||||
navActions[TopNavIds.SHARE] = (menuItem, navController, anchorElement) => {
|
||||
navActions[TopNavIds.SHARE] = anchorElement => {
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
allowEmbed: true,
|
||||
|
|
|
@ -61,7 +61,8 @@ export function getTopNavConfig(
|
|||
|
||||
function getFullScreenConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', {
|
||||
id: 'full-screen',
|
||||
label: i18n.translate('kbn.dashboard.topNave.fullScreenButtonAriaLabel', {
|
||||
defaultMessage: 'full screen',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.fullScreenConfigDescription', {
|
||||
|
@ -77,7 +78,8 @@ function getFullScreenConfig(action: NavAction) {
|
|||
*/
|
||||
function getEditConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', {
|
||||
id: 'edit',
|
||||
label: i18n.translate('kbn.dashboard.topNave.editButtonAriaLabel', {
|
||||
defaultMessage: 'edit',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.editConfigDescription', {
|
||||
|
@ -96,7 +98,8 @@ function getEditConfig(action: NavAction) {
|
|||
*/
|
||||
function getSaveConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', {
|
||||
id: 'save',
|
||||
label: i18n.translate('kbn.dashboard.topNave.saveButtonAriaLabel', {
|
||||
defaultMessage: 'save',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.saveConfigDescription', {
|
||||
|
@ -112,7 +115,8 @@ function getSaveConfig(action: NavAction) {
|
|||
*/
|
||||
function getViewConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', {
|
||||
id: 'cancel',
|
||||
label: i18n.translate('kbn.dashboard.topNave.cancelButtonAriaLabel', {
|
||||
defaultMessage: 'cancel',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.viewConfigDescription', {
|
||||
|
@ -128,7 +132,8 @@ function getViewConfig(action: NavAction) {
|
|||
*/
|
||||
function getCloneConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', {
|
||||
id: 'clone',
|
||||
label: i18n.translate('kbn.dashboard.topNave.cloneButtonAriaLabel', {
|
||||
defaultMessage: 'clone',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.cloneConfigDescription', {
|
||||
|
@ -144,7 +149,8 @@ function getCloneConfig(action: NavAction) {
|
|||
*/
|
||||
function getAddConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', {
|
||||
id: 'add',
|
||||
label: i18n.translate('kbn.dashboard.topNave.addButtonAriaLabel', {
|
||||
defaultMessage: 'add',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.addConfigDescription', {
|
||||
|
@ -160,7 +166,8 @@ function getAddConfig(action: NavAction) {
|
|||
*/
|
||||
function getShareConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', {
|
||||
id: 'share',
|
||||
label: i18n.translate('kbn.dashboard.topNave.shareButtonAriaLabel', {
|
||||
defaultMessage: 'share',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.shareConfigDescription', {
|
||||
|
@ -176,7 +183,8 @@ function getShareConfig(action: NavAction) {
|
|||
*/
|
||||
function getOptionsConfig(action: NavAction) {
|
||||
return {
|
||||
key: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', {
|
||||
id: 'options',
|
||||
label: i18n.translate('kbn.dashboard.topNave.optionsButtonAriaLabel', {
|
||||
defaultMessage: 'options',
|
||||
}),
|
||||
description: i18n.translate('kbn.dashboard.topNave.optionsConfigDescription', {
|
||||
|
|
|
@ -32,7 +32,7 @@ import {
|
|||
|
||||
import { ViewMode } from '../../../embeddable_api/public';
|
||||
|
||||
export type NavAction = (menuItem: any, navController: any, anchorElement: any) => void;
|
||||
export type NavAction = (anchorElement?: any) => void;
|
||||
|
||||
export interface GridData {
|
||||
w: number;
|
||||
|
|
|
@ -35,6 +35,20 @@ discover-app {
|
|||
}
|
||||
}
|
||||
|
||||
.dscResultCount {
|
||||
text-align: center;
|
||||
padding-top: $euiSizeXS;
|
||||
padding-left: $euiSizeM;
|
||||
|
||||
.dscResultHits {
|
||||
padding-left: $euiSizeXS;
|
||||
}
|
||||
|
||||
> .kuiLink {
|
||||
padding-left: $euiSizeM;
|
||||
}
|
||||
}
|
||||
|
||||
.dscTimechart__header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
@ -239,7 +239,7 @@ function discoverController(
|
|||
|
||||
const getTopNavLinks = () => {
|
||||
const newSearch = {
|
||||
key: 'new',
|
||||
id: 'new',
|
||||
label: i18n.translate('kbn.discover.localMenu.localMenu.newSearchTitle', {
|
||||
defaultMessage: 'New',
|
||||
}),
|
||||
|
@ -251,7 +251,7 @@ function discoverController(
|
|||
};
|
||||
|
||||
const saveSearch = {
|
||||
key: 'save',
|
||||
id: 'save',
|
||||
label: i18n.translate('kbn.discover.localMenu.saveTitle', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
|
@ -291,7 +291,7 @@ function discoverController(
|
|||
};
|
||||
|
||||
const openSearch = {
|
||||
key: 'open',
|
||||
id: 'open',
|
||||
label: i18n.translate('kbn.discover.localMenu.openTitle', {
|
||||
defaultMessage: 'Open',
|
||||
}),
|
||||
|
@ -309,7 +309,7 @@ function discoverController(
|
|||
};
|
||||
|
||||
const shareSearch = {
|
||||
key: 'share',
|
||||
id: 'share',
|
||||
label: i18n.translate('kbn.discover.localMenu.shareTitle', {
|
||||
defaultMessage: 'Share',
|
||||
}),
|
||||
|
@ -317,7 +317,7 @@ function discoverController(
|
|||
defaultMessage: 'Share Search',
|
||||
}),
|
||||
testId: 'shareTopNavButton',
|
||||
run: async (menuItem, navController, anchorElement) => { // eslint-disable-line no-unused-vars
|
||||
run: async (anchorElement) => {
|
||||
const sharingData = await this.getSharingData();
|
||||
showShareContextMenu({
|
||||
anchorElement,
|
||||
|
@ -337,7 +337,7 @@ function discoverController(
|
|||
};
|
||||
|
||||
const inspectSearch = {
|
||||
key: 'inspect',
|
||||
id: 'inspect',
|
||||
label: i18n.translate('kbn.discover.localMenu.inspectTitle', {
|
||||
defaultMessage: 'Inspect',
|
||||
}),
|
||||
|
|
|
@ -1,58 +1,25 @@
|
|||
<discover-app class="app-container">
|
||||
<!-- Local nav. -->
|
||||
<kbn-top-nav name="discover" config="topNavMenu">
|
||||
<!-- Transcluded elements. -->
|
||||
<div data-transclude-slots>
|
||||
<!-- Breadcrumbs. -->
|
||||
<div data-transclude-slot="topLeftCorner" class="kuiLocalBreadcrumbs">
|
||||
<h1 id="kui_local_breadcrumb" class="kuiLocalBreadcrumb" ng-if="opts.savedSearch.id">
|
||||
<span class="kuiLocalBreadcrumb__emphasis">
|
||||
<button
|
||||
class="kuiLink"
|
||||
type="button"
|
||||
id="reload_saved_search"
|
||||
aria-label="{{::'kbn.discover.reloadSavedSearchAriaLabel' | i18n: {defaultMessage: 'Reload saved search'} }}"
|
||||
tooltip="{{::'kbn.discover.reloadSavedSearchTooltip' | i18n: {defaultMessage: 'Reload saved search'} }}"
|
||||
tooltip-placement="right"
|
||||
tooltip-append-to-body="1"
|
||||
ng-click="resetQuery()"
|
||||
>
|
||||
<span class="kuiIcon fa-undo small"></span>
|
||||
{{::'kbn.discover.reloadSavedSearchButton' | i18n: {defaultMessage: 'Reload'} }}
|
||||
</button>
|
||||
</span>
|
||||
</h1>
|
||||
<div class="kuiLocalBreadcrumb">
|
||||
<span data-test-subj="discoverQueryHits" class="kuiLocalBreadcrumb__emphasis">{{(hits || 0) | number:0}}</span>
|
||||
<span
|
||||
i18n-id="kbn.discover.hitsPluralTitle"
|
||||
i18n-default-message="{hits, plural, one {hit} other {hits}}"
|
||||
i18n-values="{ hits }"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search. -->
|
||||
<div data-transclude-slot="bottomRow" class="fullWidth">
|
||||
<search-bar
|
||||
query="state.query"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
app-name="'discover'"
|
||||
index-patterns="[indexPattern]"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
show-date-picker="enableTimeRangeSelector"
|
||||
date-range-from="time.from"
|
||||
date-range-to="time.to"
|
||||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
></search-bar>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
|
||||
<kbn-top-nav-v2
|
||||
app-name="'discover'"
|
||||
config="topNavMenu"
|
||||
show-search-bar="true"
|
||||
show-date-picker="enableTimeRangeSelector"
|
||||
query="state.query"
|
||||
screen-title="screenTitle"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="[indexPattern]"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
date-range-from="time.from"
|
||||
date-range-to="time.to"
|
||||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
>
|
||||
</kbn-top-nav-v2>
|
||||
|
||||
<main class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-2 sidebar-container collapsible-sidebar" id="discover-sidebar">
|
||||
|
@ -113,6 +80,7 @@
|
|||
class="kuiButton kuiButton--basic kuiButton--iconText dscSkipButton"
|
||||
ng-click="showAllRows(); scrollToBottom()"
|
||||
>
|
||||
|
||||
<span class="kuiButton__inner">
|
||||
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-chevron-down"></span>
|
||||
<span
|
||||
|
@ -122,6 +90,25 @@
|
|||
</span>
|
||||
</button>
|
||||
|
||||
<div class="dscResultCount">
|
||||
<strong data-test-subj="discoverQueryHits">{{(hits || 0) | number:0}}</strong>
|
||||
<span
|
||||
class="dscResultHits"
|
||||
i18n-id="kbn.discover.hitsPluralTitle"
|
||||
i18n-default-message="{hits, plural, one {hit} other {hits}}"
|
||||
i18n-values="{ hits }"
|
||||
></span>
|
||||
<button
|
||||
ng-if="opts.savedSearch.id"
|
||||
class="kuiLink"
|
||||
type="button"
|
||||
id="reload_saved_search"
|
||||
ng-click="resetQuery()"
|
||||
>
|
||||
{{::'kbn.discover.reloadSavedSearchButton' | i18n: {defaultMessage: 'Reset search'} }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
aria-label="{{::'kbn.discover.histogramOfFoundDocumentsAriaLabel' | i18n: {defaultMessage: 'Histogram of found documents'} }}"
|
||||
class="dscTimechart"
|
||||
|
|
|
@ -26,6 +26,9 @@ a tilemap in an iframe: https://github.com/elastic/kibana/issues/16457 */
|
|||
}
|
||||
}
|
||||
|
||||
.visEditor__linkedMessage {
|
||||
padding: $euiSizeS;
|
||||
}
|
||||
|
||||
.visEditor__content {
|
||||
@include flex-parent();
|
||||
|
|
|
@ -1,55 +1,59 @@
|
|||
<visualize-app class="app-container visEditor visEditor--{{ vis.type.name }}">
|
||||
<!-- Local nav. -->
|
||||
<kbn-top-nav name="visualize" config="topNavMenu">
|
||||
<!-- Transcluded elements. -->
|
||||
<div data-transclude-slots>
|
||||
<!-- Search. -->
|
||||
<div
|
||||
data-transclude-slot="bottomRow"
|
||||
ng-show="chrome.getVisible()"
|
||||
class="fullWidth"
|
||||
<!-- Linked search. -->
|
||||
<div
|
||||
ng-show="chrome.getVisible()"
|
||||
ng-if="vis.type.requiresSearch && state.linked"
|
||||
class="fullWidth visEditor__linkedMessage"
|
||||
>
|
||||
<div class="kuiVerticalRhythmSmall">
|
||||
{{ ::'kbn.visualize.linkedToSearchInfoText' | i18n: { defaultMessage: 'Linked to Saved Search' } }}
|
||||
<a
|
||||
href="#/discover/{{savedVis.savedSearch.id}}"
|
||||
>
|
||||
<div ng-if="vis.type.requiresSearch && state.linked" class="kuiVerticalRhythmSmall">
|
||||
{{ ::'kbn.visualize.linkedToSearchInfoText' | i18n: { defaultMessage: 'Linked to Saved Search' } }}
|
||||
<a
|
||||
href="#/discover/{{savedVis.savedSearch.id}}"
|
||||
>
|
||||
{{ savedVis.savedSearch.title }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
data-test-subj="unlinkSavedSearch"
|
||||
href=""
|
||||
ng-dblclick="unlink()"
|
||||
tooltip="{{ ::'kbn.visualize.linkedToSearch.unlinkButtonTooltip' | i18n: { defaultMessage: 'Double click to unlink from Saved Search' } }}"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-chain-broken"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="fullWidth kuiVerticalRhythmSmall">
|
||||
<search-bar
|
||||
query="state.query"
|
||||
screen-title="state.vis.title"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
app-name="'visualize'"
|
||||
index-patterns="[indexPattern]"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
show-query-bar="showQueryBar()"
|
||||
show-filter-bar="vis.type.options.showFilterBar && chrome.getVisible()"
|
||||
show-date-picker="enableQueryBarTimeRangeSelector"
|
||||
date-range-from="timeRange.from"
|
||||
date-range-to="timeRange.to"
|
||||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
show-auto-refresh-only="showAutoRefreshOnlyInQueryBar"
|
||||
on-refresh-change="onRefreshChange"
|
||||
></search-bar>
|
||||
</div>
|
||||
</div>
|
||||
{{ savedVis.savedSearch.title }}
|
||||
</a>
|
||||
|
||||
<a
|
||||
data-test-subj="unlinkSavedSearch"
|
||||
href=""
|
||||
ng-dblclick="unlink()"
|
||||
tooltip="{{ ::'kbn.visualize.linkedToSearch.unlinkButtonTooltip' | i18n: { defaultMessage: 'Double click to unlink from Saved Search' } }}"
|
||||
>
|
||||
<span aria-hidden="true" class="kuiIcon fa-chain-broken"></span>
|
||||
</a>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
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-v2
|
||||
app-name="'visualize'"
|
||||
config="topNavMenu"
|
||||
show-search-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"
|
||||
screen-title="state.vis.title"
|
||||
on-query-submit="updateQueryAndFetch"
|
||||
index-patterns="[indexPattern]"
|
||||
filters="filters"
|
||||
on-filters-updated="onFiltersUpdated"
|
||||
date-range-from="timeRange.from"
|
||||
date-range-to="timeRange.to"
|
||||
is-refresh-paused="refreshInterval.pause"
|
||||
refresh-interval="refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
>
|
||||
</kbn-top-nav-v2>
|
||||
|
||||
<!--
|
||||
The top nav is hidden in embed mode but the filter bar must still be present so
|
||||
|
|
|
@ -150,7 +150,8 @@ function VisEditor(
|
|||
};
|
||||
|
||||
$scope.topNavMenu = [...(capabilities.get().visualize.save ? [{
|
||||
key: i18n.translate('kbn.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save' }),
|
||||
id: 'save',
|
||||
label: i18n.translate('kbn.topNavMenu.saveVisualizationButtonLabel', { defaultMessage: 'save' }),
|
||||
description: i18n.translate('kbn.visualize.topNavMenu.saveVisualizationButtonAriaLabel', {
|
||||
defaultMessage: 'Save Visualization',
|
||||
}),
|
||||
|
@ -203,12 +204,13 @@ function VisEditor(
|
|||
showSaveModal(saveModal);
|
||||
}
|
||||
}] : []), {
|
||||
key: i18n.translate('kbn.topNavMenu.shareVisualizationButtonLabel', { defaultMessage: 'share' }),
|
||||
id: 'share',
|
||||
label: i18n.translate('kbn.topNavMenu.shareVisualizationButtonLabel', { defaultMessage: 'share' }),
|
||||
description: i18n.translate('kbn.visualize.topNavMenu.shareVisualizationButtonAriaLabel', {
|
||||
defaultMessage: 'Share Visualization',
|
||||
}),
|
||||
testId: 'shareTopNavButton',
|
||||
run: (menuItem, navController, anchorElement) => {
|
||||
run: (anchorElement) => {
|
||||
const hasUnappliedChanges = vis.dirty;
|
||||
const hasUnsavedChanges = $appStatus.dirty;
|
||||
showShareContextMenu({
|
||||
|
@ -226,7 +228,8 @@ function VisEditor(
|
|||
});
|
||||
}
|
||||
}, {
|
||||
key: i18n.translate('kbn.topNavMenu.openInspectorButtonLabel', { defaultMessage: 'inspect' }),
|
||||
id: 'inspector',
|
||||
label: i18n.translate('kbn.topNavMenu.openInspectorButtonLabel', { defaultMessage: 'inspect' }),
|
||||
description: i18n.translate('kbn.visualize.topNavMenu.openInspectorButtonAriaLabel', {
|
||||
defaultMessage: 'Open Inspector for visualization',
|
||||
}),
|
||||
|
@ -249,7 +252,8 @@ function VisEditor(
|
|||
}
|
||||
}
|
||||
}, {
|
||||
key: i18n.translate('kbn.topNavMenu.refreshButtonLabel', { defaultMessage: 'refresh' }),
|
||||
id: 'refresh',
|
||||
label: i18n.translate('kbn.topNavMenu.refreshButtonLabel', { defaultMessage: 'refresh' }),
|
||||
description: i18n.translate('kbn.visualize.topNavMenu.refreshButtonAriaLabel', {
|
||||
defaultMessage: 'Refresh',
|
||||
}),
|
||||
|
@ -351,10 +355,18 @@ function VisEditor(
|
|||
|
||||
$scope.isAddToDashMode = () => addToDashMode;
|
||||
|
||||
$scope.showQueryBar = () => {
|
||||
$scope.showFilterBar = () => {
|
||||
return vis.type.options.showFilterBar;
|
||||
};
|
||||
|
||||
$scope.showQueryInput = () => {
|
||||
return vis.type.requiresSearch && vis.type.options.showQueryBar;
|
||||
};
|
||||
|
||||
$scope.showQueryBarTimePicker = () => {
|
||||
return vis.type.options.showTimePicker;
|
||||
};
|
||||
|
||||
$scope.timeRange = timefilter.getTime();
|
||||
$scope.opts = _.pick($scope, 'savedVis', 'isAddToDashMode');
|
||||
|
||||
|
@ -370,33 +382,6 @@ function VisEditor(
|
|||
|
||||
$state.replace();
|
||||
|
||||
$scope.$watchMulti([
|
||||
'searchSource.getField("index")',
|
||||
'vis.type.options.showTimePicker',
|
||||
$scope.showQueryBar,
|
||||
], function ([index, requiresTimePicker, showQueryBar]) {
|
||||
const showTimeFilter = Boolean((!index || index.timeFieldName) && requiresTimePicker);
|
||||
|
||||
if (showQueryBar) {
|
||||
timefilter.disableTimeRangeSelector();
|
||||
timefilter.disableAutoRefreshSelector();
|
||||
$scope.enableQueryBarTimeRangeSelector = true;
|
||||
$scope.showAutoRefreshOnlyInQueryBar = !showTimeFilter;
|
||||
}
|
||||
else if (showTimeFilter) {
|
||||
timefilter.enableTimeRangeSelector();
|
||||
timefilter.enableAutoRefreshSelector();
|
||||
$scope.enableQueryBarTimeRangeSelector = false;
|
||||
$scope.showAutoRefreshOnlyInQueryBar = false;
|
||||
}
|
||||
else {
|
||||
timefilter.disableTimeRangeSelector();
|
||||
timefilter.enableAutoRefreshSelector();
|
||||
$scope.enableQueryBarTimeRangeSelector = false;
|
||||
$scope.showAutoRefreshOnlyInQueryBar = false;
|
||||
}
|
||||
});
|
||||
|
||||
const updateTimeRange = () => {
|
||||
$scope.timeRange = timefilter.getTime();
|
||||
// In case we are running in embedded mode (i.e. we used the visualize loader to embed)
|
||||
|
|
42
src/legacy/core_plugins/kibana_react/index.ts
Normal file
42
src/legacy/core_plugins/kibana_react/index.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { resolve } from 'path';
|
||||
import { Legacy } from '../../../../kibana';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function DataPlugin(kibana: any) {
|
||||
const config: Legacy.PluginSpecOptions = {
|
||||
id: 'kibana_react',
|
||||
require: [],
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
config: (Joi: any) => {
|
||||
return Joi.object({
|
||||
enabled: Joi.boolean().default(true),
|
||||
}).default();
|
||||
},
|
||||
init: (server: Legacy.Server) => ({}),
|
||||
uiExports: {
|
||||
injectDefaultVars: () => ({}),
|
||||
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
|
||||
},
|
||||
};
|
||||
|
||||
return new kibana.Plugin(config);
|
||||
}
|
4
src/legacy/core_plugins/kibana_react/package.json
Normal file
4
src/legacy/core_plugins/kibana_react/package.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"name": "kibana_react",
|
||||
"version": "kibana"
|
||||
}
|
3
src/legacy/core_plugins/kibana_react/public/index.scss
Normal file
3
src/legacy/core_plugins/kibana_react/public/index.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@import 'src/legacy/ui/public/styles/styling_constants';
|
||||
|
||||
@import './top_nav_menu/index';
|
26
src/legacy/core_plugins/kibana_react/public/index.ts
Normal file
26
src/legacy/core_plugins/kibana_react/public/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO these are imports from the old plugin world.
|
||||
// Once the new platform is ready, they can get removed
|
||||
// and handled by the platform itself in the setup method
|
||||
// of the ExpressionExectorService
|
||||
|
||||
/** @public types */
|
||||
export { TopNavMenu, TopNavMenuData } from './top_nav_menu';
|
|
@ -0,0 +1,7 @@
|
|||
.kbnTopNavMenu__wrapper {
|
||||
z-index: 5;
|
||||
|
||||
.kbnTopNavMenu {
|
||||
padding: $euiSizeS 0px $euiSizeXS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { TopNavMenu } from './top_nav_menu';
|
||||
export { TopNavMenuData } from './top_nav_menu_data';
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { TopNavMenu } from './top_nav_menu';
|
||||
import { TopNavMenuData } from './top_nav_menu_data';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
jest.mock('../../../../core_plugins/data/public', () => {
|
||||
return {
|
||||
SearchBar: () => <div className="searchBar"></div>,
|
||||
SearchBarProps: {},
|
||||
};
|
||||
});
|
||||
|
||||
describe('TopNavMenu', () => {
|
||||
const TOP_NAV_ITEM_SELECTOR = 'TopNavMenuItem';
|
||||
const SEARCH_BAR_SELECTOR = 'SearchBar';
|
||||
const menuItems: TopNavMenuData[] = [
|
||||
{
|
||||
id: 'test',
|
||||
label: 'test',
|
||||
run: jest.fn(),
|
||||
},
|
||||
{
|
||||
id: 'test2',
|
||||
label: 'test2',
|
||||
run: jest.fn(),
|
||||
},
|
||||
{
|
||||
id: 'test3',
|
||||
label: 'test3',
|
||||
run: jest.fn(),
|
||||
},
|
||||
];
|
||||
|
||||
it('Should render nothing when no config is provided', () => {
|
||||
const component = shallowWithIntl(<TopNavMenu name="test" />);
|
||||
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" config={[menuItems[0]]} />);
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(1);
|
||||
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render multiple menu items', () => {
|
||||
const component = shallowWithIntl(<TopNavMenu name="test" config={menuItems} />);
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(menuItems.length);
|
||||
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(0);
|
||||
});
|
||||
|
||||
it('Should render search bar', () => {
|
||||
const component = shallowWithIntl(<TopNavMenu name="test" showSearchBar={true} />);
|
||||
|
||||
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
|
||||
expect(component.find(`span > ${SEARCH_BAR_SELECTOR}`).length).toBe(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { TopNavMenuData } from './top_nav_menu_data';
|
||||
import { TopNavMenuItem } from './top_nav_menu_item';
|
||||
import { SearchBar, SearchBarProps } from '../../../../core_plugins/data/public';
|
||||
|
||||
type Props = Partial<SearchBarProps> & {
|
||||
name: string;
|
||||
config?: TopNavMenuData[];
|
||||
showSearchBar?: boolean;
|
||||
};
|
||||
|
||||
/*
|
||||
* Top Nav Menu is a convenience wrapper component for:
|
||||
* - Top navigation menu - configured by an array of `TopNavMenuData` objects
|
||||
* - Search Bar - which includes Filter Bar \ Query Input \ Timepicker.
|
||||
*
|
||||
* See SearchBar documentation to learn more about its properties.
|
||||
*
|
||||
**/
|
||||
|
||||
export function TopNavMenu(props: Props) {
|
||||
function renderItems() {
|
||||
if (!props.config) return;
|
||||
return props.config.map((menuItem: TopNavMenuData, i: number) => {
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={`nav-menu-${i}`}>
|
||||
<TopNavMenuItem {...menuItem} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function renderSearchBar() {
|
||||
// Validate presense of all required fields
|
||||
if (!props.showSearchBar) return;
|
||||
|
||||
return (
|
||||
<SearchBar
|
||||
query={props.query}
|
||||
filters={props.filters}
|
||||
showQueryBar={props.showQueryBar}
|
||||
showQueryInput={props.showQueryInput}
|
||||
showFilterBar={props.showFilterBar}
|
||||
showDatePicker={props.showDatePicker}
|
||||
appName={props.appName!}
|
||||
screenTitle={props.screenTitle!}
|
||||
onQuerySubmit={props.onQuerySubmit}
|
||||
onFiltersUpdated={props.onFiltersUpdated}
|
||||
dateRangeFrom={props.dateRangeFrom}
|
||||
dateRangeTo={props.dateRangeTo}
|
||||
isRefreshPaused={props.isRefreshPaused}
|
||||
showAutoRefreshOnly={props.showAutoRefreshOnly}
|
||||
onRefreshChange={props.onRefreshChange}
|
||||
refreshInterval={props.refreshInterval}
|
||||
indexPatterns={props.indexPatterns}
|
||||
store={props.store}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderLayout() {
|
||||
return (
|
||||
<span className="kbnTopNavMenu__wrapper">
|
||||
<EuiFlexGroup
|
||||
data-test-subj="top-nav"
|
||||
justifyContent="flexStart"
|
||||
gutterSize="none"
|
||||
className="kbnTopNavMenu"
|
||||
responsive={false}
|
||||
>
|
||||
{renderItems()}
|
||||
</EuiFlexGroup>
|
||||
{renderSearchBar()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <I18nProvider>{renderLayout()}</I18nProvider>;
|
||||
}
|
||||
|
||||
TopNavMenu.defaultProps = {
|
||||
showSearchBar: false,
|
||||
showQueryBar: true,
|
||||
showQueryInput: true,
|
||||
showDatePicker: true,
|
||||
showFilterBar: true,
|
||||
screenTitle: '',
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 type TopNavMenuAction = (anchorElement: EventTarget) => void;
|
||||
|
||||
export interface TopNavMenuData {
|
||||
id?: string;
|
||||
label: string;
|
||||
run: TopNavMenuAction;
|
||||
description?: string;
|
||||
testId?: string;
|
||||
className?: string;
|
||||
disableButton?: boolean | (() => boolean);
|
||||
tooltip?: string | (() => string);
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { TopNavMenuItem } from './top_nav_menu_item';
|
||||
import { TopNavMenuData } from './top_nav_menu_data';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
describe('TopNavMenu', () => {
|
||||
it('Should render and click an item', () => {
|
||||
const data: TopNavMenuData = {
|
||||
id: 'test',
|
||||
label: 'test',
|
||||
run: jest.fn(),
|
||||
};
|
||||
|
||||
const component = shallowWithIntl(<TopNavMenuItem {...data} />);
|
||||
expect(component.prop('isDisabled')).toEqual(false);
|
||||
|
||||
const event = { currentTarget: { value: 'a' } };
|
||||
component.simulate('click', event);
|
||||
expect(data.run).toBeCalledTimes(1);
|
||||
expect(data.run).toHaveBeenCalledWith(event.currentTarget);
|
||||
|
||||
component.simulate('click', event);
|
||||
expect(data.run).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('Should render item with all attributes', () => {
|
||||
const data: TopNavMenuData = {
|
||||
id: 'test',
|
||||
label: 'test',
|
||||
description: 'description',
|
||||
testId: 'test-class-name',
|
||||
disableButton: false,
|
||||
run: jest.fn(),
|
||||
};
|
||||
|
||||
const component = shallowWithIntl(<TopNavMenuItem {...data} />);
|
||||
expect(component.prop('isDisabled')).toEqual(false);
|
||||
|
||||
const event = { currentTarget: { value: 'a' } };
|
||||
component.simulate('click', event);
|
||||
expect(data.run).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Should render disabled item and it shouldnt be clickable', () => {
|
||||
const data: TopNavMenuData = {
|
||||
id: 'test',
|
||||
label: 'test',
|
||||
disableButton: true,
|
||||
run: jest.fn(),
|
||||
};
|
||||
|
||||
const component = shallowWithIntl(<TopNavMenuItem {...data} />);
|
||||
expect(component.prop('isDisabled')).toEqual(true);
|
||||
|
||||
const event = { currentTarget: { value: 'a' } };
|
||||
component.simulate('click', event);
|
||||
expect(data.run).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should render item with disable function and it shouldnt be clickable', () => {
|
||||
const data: TopNavMenuData = {
|
||||
id: 'test',
|
||||
label: 'test',
|
||||
disableButton: () => true,
|
||||
run: jest.fn(),
|
||||
};
|
||||
|
||||
const component = shallowWithIntl(<TopNavMenuItem {...data} />);
|
||||
expect(component.prop('isDisabled')).toEqual(true);
|
||||
|
||||
const event = { currentTarget: { value: 'a' } };
|
||||
component.simulate('click', event);
|
||||
expect(data.run).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 { capitalize, isFunction } from 'lodash';
|
||||
import React, { MouseEvent } from 'react';
|
||||
import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui';
|
||||
|
||||
import { TopNavMenuData } from './top_nav_menu_data';
|
||||
|
||||
export function TopNavMenuItem(props: TopNavMenuData) {
|
||||
function isDisabled(): boolean {
|
||||
const val = isFunction(props.disableButton) ? props.disableButton() : props.disableButton;
|
||||
return val!;
|
||||
}
|
||||
|
||||
function getTooltip(): string {
|
||||
const val = isFunction(props.tooltip) ? props.tooltip() : props.tooltip;
|
||||
return val!;
|
||||
}
|
||||
|
||||
function handleClick(e: MouseEvent<HTMLButtonElement>) {
|
||||
if (isDisabled()) return;
|
||||
props.run(e.currentTarget);
|
||||
}
|
||||
|
||||
const btn = (
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
isDisabled={isDisabled()}
|
||||
onClick={handleClick}
|
||||
data-test-subj={props.testId}
|
||||
>
|
||||
{capitalize(props.label || props.id!)}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const tooltip = getTooltip();
|
||||
if (tooltip) {
|
||||
return <EuiToolTip content={tooltip}>{btn}</EuiToolTip>;
|
||||
} else {
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
|
||||
TopNavMenuItem.defaultProps = {
|
||||
disableButton: false,
|
||||
tooltip: '',
|
||||
};
|
|
@ -144,7 +144,7 @@ app.controller('timelion', function (
|
|||
|
||||
$timeout(function () {
|
||||
if (config.get('timelion:showTutorial', true)) {
|
||||
$scope.kbnTopNav.open('help');
|
||||
$scope.toggleMenu('showHelp');
|
||||
}
|
||||
}, 0);
|
||||
|
||||
|
@ -163,7 +163,7 @@ app.controller('timelion', function (
|
|||
function getTopNavMenu() {
|
||||
|
||||
const newSheetAction = {
|
||||
key: 'new',
|
||||
id: 'new',
|
||||
label: i18n.translate('timelion.topNavMenu.newSheetButtonLabel', {
|
||||
defaultMessage: 'New',
|
||||
}),
|
||||
|
@ -175,19 +175,21 @@ app.controller('timelion', function (
|
|||
};
|
||||
|
||||
const addSheetAction = {
|
||||
key: 'add',
|
||||
id: 'add',
|
||||
label: i18n.translate('timelion.topNavMenu.addChartButtonLabel', {
|
||||
defaultMessage: 'Add',
|
||||
}),
|
||||
description: i18n.translate('timelion.topNavMenu.addChartButtonAriaLabel', {
|
||||
defaultMessage: 'Add a chart',
|
||||
}),
|
||||
run: function () { $scope.newCell(); },
|
||||
run: function () {
|
||||
$scope.$evalAsync(() => $scope.newCell());
|
||||
},
|
||||
testId: 'timelionAddChartButton',
|
||||
};
|
||||
|
||||
const saveSheetAction = {
|
||||
key: 'save',
|
||||
id: 'save',
|
||||
label: i18n.translate('timelion.topNavMenu.saveSheetButtonLabel', {
|
||||
defaultMessage: 'Save',
|
||||
}),
|
||||
|
@ -195,15 +197,13 @@ app.controller('timelion', function (
|
|||
defaultMessage: 'Save Sheet',
|
||||
}),
|
||||
run: () => {
|
||||
const curState = $scope.menus.showSave;
|
||||
$scope.closeMenus();
|
||||
$scope.menus.showSave = !curState;
|
||||
$scope.$evalAsync(() => $scope.toggleMenu('showSave'));
|
||||
},
|
||||
testId: 'timelionSaveButton',
|
||||
};
|
||||
|
||||
const deleteSheetAction = {
|
||||
key: 'delete',
|
||||
id: 'delete',
|
||||
label: i18n.translate('timelion.topNavMenu.deleteSheetButtonLabel', {
|
||||
defaultMessage: 'Delete',
|
||||
}),
|
||||
|
@ -239,18 +239,21 @@ app.controller('timelion', function (
|
|||
}),
|
||||
};
|
||||
|
||||
confirmModal(
|
||||
i18n.translate('timelion.topNavMenu.delete.modal.warningText', {
|
||||
defaultMessage: `You can't recover deleted sheets.`,
|
||||
}),
|
||||
confirmModalOptions
|
||||
);
|
||||
$scope.$evalAsync(() => {
|
||||
confirmModal(
|
||||
i18n.translate('timelion.topNavMenu.delete.modal.warningText', {
|
||||
defaultMessage: `You can't recover deleted sheets.`,
|
||||
}),
|
||||
confirmModalOptions
|
||||
);
|
||||
});
|
||||
|
||||
},
|
||||
testId: 'timelionDeleteButton',
|
||||
};
|
||||
|
||||
const openSheetAction = {
|
||||
key: 'open',
|
||||
id: 'open',
|
||||
label: i18n.translate('timelion.topNavMenu.openSheetButtonLabel', {
|
||||
defaultMessage: 'Open',
|
||||
}),
|
||||
|
@ -258,15 +261,13 @@ app.controller('timelion', function (
|
|||
defaultMessage: 'Open Sheet',
|
||||
}),
|
||||
run: () => {
|
||||
const curState = $scope.menus.showLoad;
|
||||
$scope.closeMenus();
|
||||
$scope.menus.showLoad = !curState;
|
||||
$scope.$evalAsync(() => $scope.toggleMenu('showLoad'));
|
||||
},
|
||||
testId: 'timelionOpenButton',
|
||||
};
|
||||
|
||||
const optionsAction = {
|
||||
key: 'options',
|
||||
id: 'options',
|
||||
label: i18n.translate('timelion.topNavMenu.optionsButtonLabel', {
|
||||
defaultMessage: 'Options',
|
||||
}),
|
||||
|
@ -274,15 +275,13 @@ app.controller('timelion', function (
|
|||
defaultMessage: 'Options',
|
||||
}),
|
||||
run: () => {
|
||||
const curState = $scope.menus.showOptions;
|
||||
$scope.closeMenus();
|
||||
$scope.menus.showOptions = !curState;
|
||||
$scope.$evalAsync(() => $scope.toggleMenu('showOptions'));
|
||||
},
|
||||
testId: 'timelionOptionsButton',
|
||||
};
|
||||
|
||||
const helpAction = {
|
||||
key: 'help',
|
||||
id: 'help',
|
||||
label: i18n.translate('timelion.topNavMenu.helpButtonLabel', {
|
||||
defaultMessage: 'Help',
|
||||
}),
|
||||
|
@ -290,9 +289,7 @@ app.controller('timelion', function (
|
|||
defaultMessage: 'Help',
|
||||
}),
|
||||
run: () => {
|
||||
const curState = $scope.menus.showHelp;
|
||||
$scope.closeMenus();
|
||||
$scope.menus.showHelp = !curState;
|
||||
$scope.$evalAsync(() => $scope.toggleMenu('showHelp'));
|
||||
},
|
||||
testId: 'timelionDocsButton',
|
||||
};
|
||||
|
@ -303,9 +300,30 @@ app.controller('timelion', function (
|
|||
return [newSheetAction, addSheetAction, openSheetAction, optionsAction, helpAction];
|
||||
}
|
||||
|
||||
let refresher;
|
||||
const setRefreshData = function () {
|
||||
if (refresher) $timeout.cancel(refresher);
|
||||
const interval = timefilter.getRefreshInterval();
|
||||
if (interval.value > 0 && !interval.pause) {
|
||||
function startRefresh() {
|
||||
refresher = $timeout(function () {
|
||||
if (!$scope.running) $scope.search();
|
||||
startRefresh();
|
||||
}, interval.value);
|
||||
}
|
||||
startRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
const init = function () {
|
||||
$scope.running = false;
|
||||
$scope.search();
|
||||
setRefreshData();
|
||||
|
||||
$scope.model = {
|
||||
timeRange: timefilter.getTime(),
|
||||
refreshInterval: timefilter.getRefreshInterval(),
|
||||
};
|
||||
|
||||
$scope.$listen($scope.state, 'fetch_with_changes', $scope.search);
|
||||
$scope.$listen(timefilter, 'fetch', $scope.search);
|
||||
|
@ -319,7 +337,7 @@ app.controller('timelion', function (
|
|||
dontShowHelp: function () {
|
||||
config.set('timelion:showTutorial', false);
|
||||
$scope.setPage(0);
|
||||
$scope.kbnTopNav.close('help');
|
||||
$scope.closeMenus();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -330,6 +348,12 @@ app.controller('timelion', function (
|
|||
showOptions: false,
|
||||
};
|
||||
|
||||
$scope.toggleMenu = (menuName) => {
|
||||
const curState = $scope.menus[menuName];
|
||||
$scope.closeMenus();
|
||||
$scope.menus[menuName] = !curState;
|
||||
};
|
||||
|
||||
$scope.closeMenus = () => {
|
||||
_.forOwn($scope.menus, function (value, key) {
|
||||
$scope.menus[key] = false;
|
||||
|
@ -337,20 +361,25 @@ app.controller('timelion', function (
|
|||
};
|
||||
};
|
||||
|
||||
let refresher;
|
||||
$scope.$listen(timefilter, 'refreshIntervalUpdate', function () {
|
||||
if (refresher) $timeout.cancel(refresher);
|
||||
const interval = timefilter.getRefreshInterval();
|
||||
if (interval.value > 0 && !interval.pause) {
|
||||
function startRefresh() {
|
||||
refresher = $timeout(function () {
|
||||
if (!$scope.running) $scope.search();
|
||||
startRefresh();
|
||||
}, interval.value);
|
||||
}
|
||||
startRefresh();
|
||||
}
|
||||
});
|
||||
$scope.onTimeUpdate = function ({ dateRange }) {
|
||||
$scope.model.timeRange = {
|
||||
...dateRange
|
||||
};
|
||||
timefilter.setTime(dateRange);
|
||||
};
|
||||
|
||||
$scope.onRefreshChange = function ({ isPaused, refreshInterval }) {
|
||||
$scope.model.refreshInterval = {
|
||||
pause: isPaused,
|
||||
value: refreshInterval,
|
||||
};
|
||||
timefilter.setRefreshInterval({
|
||||
pause: isPaused,
|
||||
value: refreshInterval ? refreshInterval : $scope.refreshInterval.value
|
||||
});
|
||||
|
||||
setRefreshData();
|
||||
};
|
||||
|
||||
$scope.$watch(function () { return savedSheet.lastSavedTitle; }, function (newTitle) {
|
||||
docTitle.change(savedSheet.id ? newTitle : undefined);
|
||||
|
|
|
@ -1,23 +1,32 @@
|
|||
<div class="timApp app-container" ng-controller="timelion">
|
||||
<span class="kuiLocalTitle">
|
||||
<span class="timApp__stats" ng-show="stats">
|
||||
<span
|
||||
i18n-id="timelion.topNavMenu.statsDescription"
|
||||
i18n-default-message="Query Time {queryTime}ms / Processing Time {processingTime}ms"
|
||||
i18n-values="{
|
||||
queryTime: stats.queryTime - stats.invokeTime,
|
||||
processingTime: stats.sheetTime - stats.queryTime,
|
||||
}"></span>
|
||||
</span>
|
||||
</span>
|
||||
<!-- Local nav. -->
|
||||
<kbn-top-nav name="timelion" config="topNavMenu">
|
||||
<!-- Transcluded elements. -->
|
||||
<div data-transclude-slots>
|
||||
<div data-transclude-slot="topLeftCorner">
|
||||
<span class="kuiLocalTitle">
|
||||
<span class="timApp__stats" ng-show="stats">
|
||||
<span
|
||||
i18n-id="timelion.topNavMenu.statsDescription"
|
||||
i18n-default-message="Query Time {queryTime}ms / Processing Time {processingTime}ms"
|
||||
i18n-values="{
|
||||
queryTime: stats.queryTime - stats.invokeTime,
|
||||
processingTime: stats.sheetTime - stats.queryTime,
|
||||
}"></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
<kbn-top-nav-v2
|
||||
app-name="'timelion'"
|
||||
config="topNavMenu"
|
||||
show-search-bar="true"
|
||||
show-search-bar-inline="true"
|
||||
show-filter-bar="false"
|
||||
show-query-input="false"
|
||||
date-range-from="model.timeRange.from"
|
||||
date-range-to="model.timeRange.to"
|
||||
is-refresh-paused="model.refreshInterval.pause"
|
||||
refresh-interval="model.refreshInterval.value"
|
||||
on-refresh-change="onRefreshChange"
|
||||
on-query-submit="onTimeUpdate">
|
||||
</kbn-top-nav-v2>
|
||||
|
||||
|
||||
<div class="timApp__menus">
|
||||
<timelion-help ng-show="menus.showHelp"></timelion-help>
|
||||
<timelion-save ng-show="menus.showSave"></timelion-save>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
* 1. Make sure the timepicker is always one right, even if the main menu doesn't exist
|
||||
*/
|
||||
|
||||
kbn-top-nav {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.kbnTopNav {
|
||||
background-color: $euiPageBackgroundColor;
|
||||
border-bottom: $euiBorderThin;
|
||||
|
|
|
@ -18,3 +18,4 @@
|
|||
*/
|
||||
|
||||
import './kbn_top_nav';
|
||||
import './kbn_top_nav2';
|
||||
|
|
120
src/legacy/ui/public/kbn_top_nav/kbn_top_nav2.js
Normal file
120
src/legacy/ui/public/kbn_top_nav/kbn_top_nav2.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 'ngreact';
|
||||
import { wrapInI18nContext } from 'ui/i18n';
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { TopNavMenu } from '../../../core_plugins/kibana_react/public';
|
||||
import { Storage } from 'ui/storage';
|
||||
|
||||
const module = uiModules.get('kibana');
|
||||
|
||||
module.directive('kbnTopNavV2', () => {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '',
|
||||
compile: (elem) => {
|
||||
const child = document.createElement('kbn-top-nav-v2-helper');
|
||||
|
||||
// Copy attributes to the child directive
|
||||
for (const attr of elem[0].attributes) {
|
||||
child.setAttribute(attr.name, attr.value);
|
||||
}
|
||||
|
||||
// Add a special attribute that will change every time that one
|
||||
// of the config array's disableButton function return value changes.
|
||||
child.setAttribute('disabled-buttons', 'disabledButtons');
|
||||
|
||||
// Pass in storage
|
||||
const localStorage = new Storage(window.localStorage);
|
||||
child.setAttribute('store', 'store');
|
||||
|
||||
// Append helper directive
|
||||
elem.append(child);
|
||||
|
||||
const linkFn = ($scope, _, $attr) => {
|
||||
$scope.store = localStorage;
|
||||
|
||||
// Watch config changes
|
||||
$scope.$watch(() => {
|
||||
const config = $scope.$eval($attr.config);
|
||||
return config.map((item) => {
|
||||
// Copy key into id, as it's a reserved react propery.
|
||||
// This is done for Angular directive backward compatibility.
|
||||
// In React only id is recognized.
|
||||
if (item.key && !item.id) {
|
||||
item.id = item.key;
|
||||
}
|
||||
|
||||
// Watch the disableButton functions
|
||||
if (typeof item.disableButton === 'function') {
|
||||
return item.disableButton();
|
||||
}
|
||||
return item.disableButton;
|
||||
});
|
||||
}, (newVal) => {
|
||||
$scope.disabledButtons = newVal;
|
||||
},
|
||||
true);
|
||||
};
|
||||
|
||||
return linkFn;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('kbnTopNavV2Helper', (reactDirective) => {
|
||||
return reactDirective(
|
||||
wrapInI18nContext(TopNavMenu),
|
||||
[
|
||||
['name', { watchDepth: 'reference' }],
|
||||
['config', { watchDepth: 'value' }],
|
||||
['disabledButtons', { watchDepth: 'reference' }],
|
||||
|
||||
['query', { watchDepth: 'reference' }],
|
||||
['store', { watchDepth: 'reference' }],
|
||||
['intl', { watchDepth: 'reference' }],
|
||||
['store', { watchDepth: 'reference' }],
|
||||
|
||||
['onQuerySubmit', { watchDepth: 'reference' }],
|
||||
['onFiltersUpdated', { watchDepth: 'reference' }],
|
||||
['onRefreshChange', { watchDepth: 'reference' }],
|
||||
|
||||
['indexPatterns', { watchDepth: 'collection' }],
|
||||
['filters', { watchDepth: 'collection' }],
|
||||
|
||||
// All modifiers default to true.
|
||||
// Set to false to hide subcomponents.
|
||||
'showSearchBar',
|
||||
'showFilterBar',
|
||||
'showQueryBar',
|
||||
'showQueryInput',
|
||||
'showDatePicker',
|
||||
|
||||
'appName',
|
||||
'screenTitle',
|
||||
'dateRangeFrom',
|
||||
'dateRangeTo',
|
||||
'isRefreshPaused',
|
||||
'refreshInterval',
|
||||
'disableAutoFocus',
|
||||
'showAutoRefreshOnly',
|
||||
],
|
||||
);
|
||||
});
|
|
@ -60,8 +60,8 @@ input[type='checkbox'],
|
|||
padding-bottom: $euiSizeS;
|
||||
}
|
||||
|
||||
> kbn-top-nav {
|
||||
z-index: 5;
|
||||
.globalQueryBar {
|
||||
padding: 0px $euiSizeS $euiSizeS $euiSizeS;
|
||||
}
|
||||
|
||||
> nav,
|
||||
|
|
|
@ -30,7 +30,7 @@ export function InspectorProvider({ getService }) {
|
|||
|
||||
return new class Inspector {
|
||||
async getIsEnabled() {
|
||||
const ariaDisabled = await testSubjects.getAttribute('openInspectorButton', 'aria-disabled');
|
||||
const ariaDisabled = await testSubjects.getAttribute('openInspectorButton', 'disabled');
|
||||
return ariaDisabled !== 'true';
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +1,22 @@
|
|||
<div id="maps-plugin" ng-class="{mapFullScreen: isFullScreen}">
|
||||
<div id="maps-top-nav">
|
||||
<div>
|
||||
<kbn-top-nav name="map" config="topNavMenu">
|
||||
<div data-transclude-slots>
|
||||
<!-- Search. -->
|
||||
<div ng-show="chrome.getVisible()" class="fullWidth" data-transclude-slot="bottomRow">
|
||||
<query-bar
|
||||
query="query"
|
||||
app-name="'maps'"
|
||||
on-submit="updateQueryAndDispatch"
|
||||
index-patterns="indexPatterns"
|
||||
show-date-picker="showDatePicker"
|
||||
date-range-from="time.from"
|
||||
date-range-to="time.to"
|
||||
is-refresh-paused="refreshConfig.isPaused"
|
||||
refresh-interval="refreshConfig.interval"
|
||||
on-refresh-change="onRefreshChange"
|
||||
></query-bar>
|
||||
|
||||
<div class="eui-hideFor--xs eui-hideFor--s">
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</kbn-top-nav>
|
||||
<kbn-top-nav-v2
|
||||
app-name="'maps'"
|
||||
config="topNavMenu"
|
||||
show-search-bar="chrome.getVisible()"
|
||||
show-filter-bar="false"
|
||||
show-date-picker="showDatePicker"
|
||||
query="query"
|
||||
on-query-submit="updateQueryAndDispatch"
|
||||
index-patterns="indexPatterns"
|
||||
date-range-from="time.from"
|
||||
date-range-to="time.to"
|
||||
is-refresh-paused="refreshConfig.isPaused"
|
||||
refresh-interval="refreshConfig.interval"
|
||||
on-refresh-change="onRefreshChange"
|
||||
>
|
||||
</kbn-top-nav-v2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -332,7 +332,8 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta
|
|||
timefilter.disableAutoRefreshSelector();
|
||||
$scope.showDatePicker = true; // used by query-bar directive to enable timepikcer in query bar
|
||||
$scope.topNavMenu = [{
|
||||
key: i18n.translate('xpack.maps.mapController.fullScreenButtonLabel', {
|
||||
id: 'full-screen',
|
||||
label: i18n.translate('xpack.maps.mapController.fullScreenButtonLabel', {
|
||||
defaultMessage: `full screen`
|
||||
}),
|
||||
description: i18n.translate('xpack.maps.mapController.fullScreenDescription', {
|
||||
|
@ -343,7 +344,8 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta
|
|||
store.dispatch(enableFullScreen());
|
||||
}
|
||||
}, {
|
||||
key: i18n.translate('xpack.maps.mapController.openInspectorButtonLabel', {
|
||||
id: 'inspect',
|
||||
label: i18n.translate('xpack.maps.mapController.openInspectorButtonLabel', {
|
||||
defaultMessage: `inspect`
|
||||
}),
|
||||
description: i18n.translate('xpack.maps.mapController.openInspectorDescription', {
|
||||
|
@ -355,7 +357,8 @@ app.controller('GisMapController', ($scope, $route, kbnUrl, localStorage, AppSta
|
|||
Inspector.open(inspectorAdapters, {});
|
||||
}
|
||||
}, ...(capabilities.get().maps.save ? [{
|
||||
key: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', {
|
||||
id: 'save',
|
||||
label: i18n.translate('xpack.maps.mapController.saveMapButtonLabel', {
|
||||
defaultMessage: `save`
|
||||
}),
|
||||
description: i18n.translate('xpack.maps.mapController.saveMapDescription', {
|
||||
|
|
|
@ -22,7 +22,6 @@ import { capabilities } from 'ui/capabilities';
|
|||
import chrome from 'ui/chrome';
|
||||
import routes from 'ui/routes';
|
||||
import 'ui/kbn_top_nav';
|
||||
import 'ui/angular-bootstrap'; // required for kbn-top-nav button tooltips
|
||||
import { uiModules } from 'ui/modules';
|
||||
import { docTitle } from 'ui/doc_title';
|
||||
import 'ui/autoload/styles';
|
||||
|
|
|
@ -1581,9 +1581,6 @@
|
|||
"kbn.discover.notifications.notSavedSearchTitle": "検索「{savedSearchTitle}」は保存されませんでした。",
|
||||
"kbn.discover.notifications.savedSearchTitle": "検索「{savedSearchTitle}」が保存されました。",
|
||||
"kbn.discover.painlessError.painlessScriptedFieldErrorMessage": "Painless スクリプトのフィールド「{script}」のエラー.",
|
||||
"kbn.discover.reloadSavedSearchAriaLabel": "保存された検索を再読み込みします",
|
||||
"kbn.discover.reloadSavedSearchButton": "再読み込み",
|
||||
"kbn.discover.reloadSavedSearchTooltip": "保存された検索を再読み込みします",
|
||||
"kbn.discover.rootBreadcrumb": "ディスカバリ",
|
||||
"kbn.discover.savedSearch.newSavedSearchTitle": "新しく保存された検索",
|
||||
"kbn.discover.savedSearch.savedObjectName": "保存された検索",
|
||||
|
|
|
@ -1582,9 +1582,6 @@
|
|||
"kbn.discover.notifications.notSavedSearchTitle": "搜索 “{savedSearchTitle}” 未保存。",
|
||||
"kbn.discover.notifications.savedSearchTitle": "搜索 “{savedSearchTitle}” 已保存",
|
||||
"kbn.discover.painlessError.painlessScriptedFieldErrorMessage": "Painless 脚本字段 “{script}” 有错误。",
|
||||
"kbn.discover.reloadSavedSearchAriaLabel": "重新加载已保存搜索",
|
||||
"kbn.discover.reloadSavedSearchButton": "重新加载",
|
||||
"kbn.discover.reloadSavedSearchTooltip": "重新加载已保存搜索",
|
||||
"kbn.discover.rootBreadcrumb": "Discover",
|
||||
"kbn.discover.savedSearch.newSavedSearchTitle": "新保存的搜索",
|
||||
"kbn.discover.savedSearch.savedObjectName": "已保存搜索",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue