Reactify Top Nav Menu (kbn_top_nav) (#40262) (#41637)

* 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:
Liza Katz 2019-07-21 16:26:50 +03:00 committed by GitHub
parent 6be4a1e8dd
commit c3217640d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1489 additions and 415 deletions

View file

@ -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">

View file

@ -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',
}),

View file

@ -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;
}

View file

@ -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 */

View file

@ -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>
`;

View file

@ -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);
});
});

View file

@ -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 (

View file

@ -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:

View file

@ -18,3 +18,5 @@
*/
export { SearchService, SearchSetup } from './search_service';
export * from './search_bar';

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { SearchBar } from './search_bar';
export * from './search_bar';

View file

@ -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);
});
});

View file

@ -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>
);
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
export { SearchBar } from './components';
export * from './components';
// @ts-ignore
export { setupDirective } from './directive';

View file

@ -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

View file

@ -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,

View file

@ -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', {

View file

@ -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;

View file

@ -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;

View file

@ -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',
}),

View file

@ -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"

View file

@ -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();

View file

@ -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>
&nbsp;
<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>
&nbsp;
<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

View file

@ -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)

View 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);
}

View file

@ -0,0 +1,4 @@
{
"name": "kibana_react",
"version": "kibana"
}

View file

@ -0,0 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
@import './top_nav_menu/index';

View 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';

View file

@ -0,0 +1,7 @@
.kbnTopNavMenu__wrapper {
z-index: 5;
.kbnTopNavMenu {
padding: $euiSizeS 0px $euiSizeXS;
}
}

View file

@ -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';

View file

@ -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);
});
});

View file

@ -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: '',
};

View file

@ -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);
}

View file

@ -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);
});
});

View file

@ -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: '',
};

View file

@ -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);

View file

@ -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>

View file

@ -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;

View file

@ -18,3 +18,4 @@
*/
import './kbn_top_nav';
import './kbn_top_nav2';

View 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',
],
);
});

View file

@ -60,8 +60,8 @@ input[type='checkbox'],
padding-bottom: $euiSizeS;
}
> kbn-top-nav {
z-index: 5;
.globalQueryBar {
padding: 0px $euiSizeS $euiSizeS $euiSizeS;
}
> nav,

View file

@ -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';
}

View file

@ -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>

View file

@ -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', {

View file

@ -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';

View file

@ -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": "保存された検索",

View file

@ -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": "已保存搜索",