diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts
index 3134a5bfe2c67..a1696298117b0 100644
--- a/src/plugins/dashboard/public/application/application.ts
+++ b/src/plugins/dashboard/public/application/application.ts
@@ -38,12 +38,7 @@ import { EmbeddableStart } from '../../../embeddable/public';
import { NavigationPublicPluginStart as NavigationStart } from '../../../navigation/public';
import { DataPublicPluginStart } from '../../../data/public';
import { SharePluginStart } from '../../../share/public';
-import {
- KibanaLegacyStart,
- configureAppAngularModule,
- createTopNavDirective,
- createTopNavHelper,
-} from '../../../kibana_legacy/public';
+import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public';
import { SavedObjectLoader } from '../../../saved_objects/public';
export interface RenderDeps {
@@ -114,13 +109,11 @@ function mountDashboardApp(appBasePath: string, element: HTMLElement) {
function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) {
createLocalI18nModule();
- createLocalTopNavModule(navigation);
createLocalIconModule();
const dashboardAngularModule = angular.module(moduleName, [
...thirdPartyAngularDependencies,
'app/dashboard/I18n',
- 'app/dashboard/TopNav',
'app/dashboard/icon',
]);
return dashboardAngularModule;
@@ -132,13 +125,6 @@ function createLocalIconModule() {
.directive('icon', reactDirective => reactDirective(EuiIcon));
}
-function createLocalTopNavModule(navigation: NavigationStart) {
- angular
- .module('app/dashboard/TopNav', ['react'])
- .directive('kbnTopNav', createTopNavDirective)
- .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui));
-}
-
function createLocalI18nModule() {
angular
.module('app/dashboard/I18n', [])
diff --git a/src/plugins/dashboard/public/application/dashboard_app.html b/src/plugins/dashboard/public/application/dashboard_app.html
index 3cf8932958b6d..87a5728ac2059 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.html
+++ b/src/plugins/dashboard/public/application/dashboard_app.html
@@ -2,52 +2,7 @@
class="app-container dshAppContainer"
ng-class="{'dshAppContainer--withMargins': model.useMargins}"
>
-
-
-
-
-
-
-
-
+
{{screenTitle}}
diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx
index 150cd8f8fcbb5..f101935b9288d 100644
--- a/src/plugins/dashboard/public/application/dashboard_app.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app.tsx
@@ -33,7 +33,6 @@ import { SavedObjectDashboard } from '../saved_dashboards';
export interface DashboardAppScope extends ng.IScope {
dash: SavedObjectDashboard;
appState: DashboardAppState;
- screenTitle: string;
model: {
query: Query;
filters: Filter[];
@@ -54,21 +53,7 @@ export interface DashboardAppScope extends ng.IScope {
getShouldShowEditHelp: () => boolean;
getShouldShowViewHelp: () => boolean;
updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void;
- onRefreshChange: ({
- isPaused,
- refreshInterval,
- }: {
- isPaused: boolean;
- refreshInterval: any;
- }) => void;
- onFiltersUpdated: (filters: Filter[]) => void;
- onCancelApplyFilters: () => void;
- onApplyFilters: (filters: Filter[]) => void;
- onQuerySaved: (savedQuery: SavedQuery) => void;
- onSavedQueryUpdated: (savedQuery: SavedQuery) => void;
- onClearSavedQuery: () => void;
topNavMenu: any;
- showFilterBar: () => boolean;
showAddPanel: any;
showSaveQuery: boolean;
kbnTopNav: any;
diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
index 283fe9f0a83a4..b4a53234bffac 100644
--- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
@@ -21,12 +21,15 @@ import _, { uniq } from 'lodash';
import { i18n } from '@kbn/i18n';
import { EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui';
import React from 'react';
+import ReactDOM from 'react-dom';
import angular from 'angular';
import { Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { History } from 'history';
import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public';
+import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public';
+import { TimeRange } from 'src/plugins/data/public';
import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen';
import {
@@ -87,6 +90,7 @@ export interface DashboardAppControllerDependencies extends RenderDeps {
dashboardConfig: KibanaLegacyStart['dashboardConfig'];
history: History;
kbnUrlStateStorage: IKbnUrlStateStorage;
+ navigation: NavigationStart;
}
export class DashboardAppController {
@@ -123,10 +127,13 @@ export class DashboardAppController {
history,
kbnUrlStateStorage,
usageCollection,
+ navigation,
}: DashboardAppControllerDependencies) {
const filterManager = queryService.filterManager;
const queryFilter = filterManager;
const timefilter = queryService.timefilter.timefilter;
+ let showSearchBar = true;
+ let showQueryBar = true;
let lastReloadRequestTime = 0;
const dash = ($scope.dash = $route.current.locals.dash);
@@ -243,6 +250,9 @@ export class DashboardAppController {
}
};
+ const showFilterBar = () =>
+ $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode();
+
const getEmptyScreenProps = (
shouldShowEditHelp: boolean,
isEmptyInReadOnlyMode: boolean
@@ -310,7 +320,6 @@ export class DashboardAppController {
refreshInterval: timefilter.getRefreshInterval(),
};
$scope.panels = dashboardStateManager.getPanels();
- $scope.screenTitle = dashboardStateManager.getTitle();
};
updateState();
@@ -515,49 +524,8 @@ export class DashboardAppController {
}
};
- $scope.onRefreshChange = function({ isPaused, refreshInterval }) {
- timefilter.setRefreshInterval({
- pause: isPaused,
- value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value,
- });
- };
-
- $scope.onFiltersUpdated = filters => {
- // The filters will automatically be set when the queryFilter emits an update event (see below)
- queryFilter.setFilters(filters);
- };
-
- $scope.onQuerySaved = savedQuery => {
- $scope.savedQuery = savedQuery;
- };
-
- $scope.onSavedQueryUpdated = savedQuery => {
- $scope.savedQuery = { ...savedQuery };
- };
-
- $scope.onClearSavedQuery = () => {
- delete $scope.savedQuery;
- dashboardStateManager.setSavedQueryId(undefined);
- dashboardStateManager.applyFilters(
- {
- query: '',
- language:
- localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'),
- },
- queryFilter.getGlobalFilters()
- );
- // Making this method sync broke the updates.
- // Temporary fix, until we fix the complex state in this file.
- setTimeout(() => {
- queryFilter.setFilters(queryFilter.getGlobalFilters());
- }, 0);
- };
-
const updateStateFromSavedQuery = (savedQuery: SavedQuery) => {
- const savedQueryFilters = savedQuery.attributes.filters || [];
- const globalFilters = queryFilter.getGlobalFilters();
- const allFilters = [...globalFilters, ...savedQueryFilters];
-
+ const allFilters = filterManager.getFilters();
dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters);
if (savedQuery.attributes.timefilter) {
timefilter.setTime({
@@ -616,6 +584,42 @@ export class DashboardAppController {
}
);
+ const onSavedQueryIdChange = (savedQueryId?: string) => {
+ dashboardStateManager.setSavedQueryId(savedQueryId);
+ };
+
+ const getNavBarProps = () => {
+ const isFullScreenMode = dashboardStateManager.getFullScreenMode();
+ const screenTitle = dashboardStateManager.getTitle();
+ return {
+ appName: 'dashboard',
+ config: $scope.isVisible ? $scope.topNavMenu : undefined,
+ className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined,
+ screenTitle,
+ showSearchBar,
+ showQueryBar,
+ showFilterBar: showFilterBar(),
+ indexPatterns: $scope.indexPatterns,
+ showSaveQuery: $scope.showSaveQuery,
+ query: $scope.model.query,
+ savedQuery: $scope.savedQuery,
+ onSavedQueryIdChange,
+ savedQueryId: dashboardStateManager.getSavedQueryId(),
+ useDefaultBehaviors: true,
+ onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }): void => {
+ if (!payload.query) {
+ $scope.updateQueryAndFetch({ query: $scope.model.query, dateRange: payload.dateRange });
+ } else {
+ $scope.updateQueryAndFetch({ query: payload.query, dateRange: payload.dateRange });
+ }
+ },
+ };
+ };
+ const dashboardNavBar = document.getElementById('dashboardChrome');
+ const updateNavBar = () => {
+ ReactDOM.render(, dashboardNavBar);
+ };
+
$scope.timefilterSubscriptions$ = new Subscription();
$scope.timefilterSubscriptions$.add(
@@ -707,6 +711,8 @@ export class DashboardAppController {
revertChangesAndExitEditMode();
}
});
+
+ updateNavBar();
};
/**
@@ -761,9 +767,6 @@ export class DashboardAppController {
});
}
- $scope.showFilterBar = () =>
- $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode();
-
$scope.showAddPanel = () => {
dashboardStateManager.setFullScreenMode(false);
/*
@@ -785,7 +788,11 @@ export class DashboardAppController {
const navActions: {
[key: string]: NavAction;
} = {};
- navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true);
+ navActions[TopNavIds.FULL_SCREEN] = () => {
+ dashboardStateManager.setFullScreenMode(true);
+ showQueryBar = false;
+ updateNavBar();
+ };
navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW);
navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT);
navActions[TopNavIds.SAVE] = () => {
@@ -858,6 +865,7 @@ export class DashboardAppController {
if ((response as { error: Error }).error) {
dashboardStateManager.setTitle(currentTitle);
}
+ updateNavBar();
return response;
});
};
@@ -939,6 +947,9 @@ export class DashboardAppController {
const visibleSubscription = chrome.getIsVisible$().subscribe(isVisible => {
$scope.$evalAsync(() => {
$scope.isVisible = isVisible;
+ showSearchBar = isVisible || showFilterBar();
+ showQueryBar = !dashboardStateManager.getFullScreenMode() && isVisible;
+ updateNavBar();
});
});
@@ -949,6 +960,11 @@ export class DashboardAppController {
navActions,
dashboardConfig.getHideWriteControls()
);
+ updateNavBar();
+ });
+
+ $scope.$watch('indexPatterns', () => {
+ updateNavBar();
});
$scope.$on('$destroy', () => {
@@ -965,9 +981,6 @@ export class DashboardAppController {
if (outputSubscription) {
outputSubscription.unsubscribe();
}
- if (dashboardContainer) {
- dashboardContainer.destroy();
- }
});
}
}
diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss
index 5befe4789dd6c..a6ddf7a8b4264 100644
--- a/src/plugins/navigation/public/top_nav_menu/_index.scss
+++ b/src/plugins/navigation/public/top_nav_menu/_index.scss
@@ -8,4 +8,8 @@
padding: 0 $euiSizeS;
}
}
+
+ .kbnTopNavMenu-isFullScreen {
+ padding: 0;
+ }
}
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
index 8e0e8b3031132..74cfd125c2e3a 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx
@@ -75,4 +75,17 @@ describe('TopNavMenu', () => {
expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0);
expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1);
});
+
+ it('Should render with a class name', () => {
+ const component = shallowWithIntl(
+
+ );
+ expect(component.find('.kbnTopNavMenu').length).toBe(1);
+ expect(component.find('.myCoolClass').length).toBeTruthy();
+ });
});
diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
index 14ad40f13e388..d492c7feb61a7 100644
--- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
+++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx
@@ -21,6 +21,7 @@ import React from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import classNames from 'classnames';
import { TopNavMenuData } from './top_nav_menu_data';
import { TopNavMenuItem } from './top_nav_menu_item';
import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public';
@@ -29,6 +30,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & {
config?: TopNavMenuData[];
showSearchBar?: boolean;
data?: DataPublicPluginStart;
+ className?: string;
};
/*
@@ -65,6 +67,7 @@ export function TopNavMenu(props: TopNavMenuProps) {
}
function renderLayout() {
+ const className = classNames('kbnTopNavMenu', props.className);
return (
{renderItems()}
diff --git a/test/functional/apps/dashboard/dashboard_saved_query.js b/test/functional/apps/dashboard/dashboard_saved_query.js
new file mode 100644
index 0000000000000..99d0aed082e70
--- /dev/null
+++ b/test/functional/apps/dashboard/dashboard_saved_query.js
@@ -0,0 +1,128 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import expect from '@kbn/expect';
+
+export default function({ getService, getPageObjects }) {
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']);
+ const browser = getService('browser');
+ const queryBar = getService('queryBar');
+ const savedQueryManagementComponent = getService('savedQueryManagementComponent');
+ const testSubjects = getService('testSubjects');
+
+ describe('dashboard saved queries', function describeIndexTests() {
+ before(async function() {
+ await esArchiver.load('dashboard/current/kibana');
+ await kibanaServer.uiSettings.replace({
+ defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
+ });
+ await PageObjects.common.navigateToApp('dashboard');
+ });
+
+ describe('saved query management component functionality', function() {
+ before(async () => {
+ await PageObjects.dashboard.gotoDashboardLandingPage();
+ await PageObjects.dashboard.clickNewDashboard();
+ });
+
+ it('should show the saved query management component when there are no saved queries', async () => {
+ await savedQueryManagementComponent.openSavedQueryManagementComponent();
+ const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover');
+ expect(descriptionText).to.eql(
+ 'SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query'
+ );
+ });
+
+ it('should allow a query to be saved via the saved objects management component', async () => {
+ await queryBar.setQuery('response:200');
+ await savedQueryManagementComponent.saveNewQuery(
+ 'OkResponse',
+ '200 responses for .jpg over 24 hours',
+ true,
+ true
+ );
+ await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse');
+ await savedQueryManagementComponent.savedQueryTextExist('response:200');
+ });
+
+ it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => {
+ await PageObjects.timePicker.setDefaultAbsoluteRange();
+ await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
+ await savedQueryManagementComponent.loadSavedQuery('OkResponse');
+ const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes();
+ expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime);
+ expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime);
+ });
+
+ it('preserves the currently loaded query when the page is reloaded', async () => {
+ await browser.refresh();
+ const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes();
+ expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime);
+ expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime);
+ expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse');
+ });
+
+ it('allows saving changes to a currently loaded query via the saved query management component', async () => {
+ await queryBar.setQuery('response:404');
+ await savedQueryManagementComponent.updateCurrentlyLoadedQuery(
+ 'OkResponse',
+ '404 responses',
+ false,
+ false
+ );
+ await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse');
+ await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
+ expect(await queryBar.getQueryString()).to.eql('');
+ await savedQueryManagementComponent.loadSavedQuery('OkResponse');
+ expect(await queryBar.getQueryString()).to.eql('response:404');
+ });
+
+ it('allows saving the currently loaded query as a new query', async () => {
+ await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery(
+ 'OkResponseCopy',
+ '200 responses',
+ false,
+ false
+ );
+ await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy');
+ });
+
+ it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => {
+ await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy');
+ await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy');
+ expect(await queryBar.getQueryString()).to.eql('');
+ });
+
+ it('resets any changes to a loaded query on reloading the same saved query', async () => {
+ await savedQueryManagementComponent.loadSavedQuery('OkResponse');
+ await queryBar.setQuery('response:503');
+ await savedQueryManagementComponent.loadSavedQuery('OkResponse');
+ expect(await queryBar.getQueryString()).to.eql('response:404');
+ });
+
+ it('allows clearing the currently loaded saved query', async () => {
+ await savedQueryManagementComponent.loadSavedQuery('OkResponse');
+ await savedQueryManagementComponent.clearCurrentlyLoadedQuery();
+ expect(await queryBar.getQueryString()).to.eql('');
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/dashboard/full_screen_mode.js b/test/functional/apps/dashboard/full_screen_mode.js
index df00f64530ca0..17eb6d8f08a9c 100644
--- a/test/functional/apps/dashboard/full_screen_mode.js
+++ b/test/functional/apps/dashboard/full_screen_mode.js
@@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const dashboardPanelActions = getService('dashboardPanelActions');
const PageObjects = getPageObjects(['dashboard', 'common']);
+ const filterBar = getService('filterBar');
describe('full screen mode', () => {
before(async () => {
@@ -81,5 +82,22 @@ export default function({ getService, getPageObjects }) {
expect(isChromeVisible).to.be(true);
});
});
+
+ it('shows filter bar in fullscreen mode', async () => {
+ await filterBar.addFilter('bytes', 'is', '12345678');
+ await PageObjects.dashboard.waitForRenderComplete();
+ await PageObjects.dashboard.clickFullScreenMode();
+ await retry.try(async () => {
+ const isChromeHidden = await PageObjects.common.isChromeHidden();
+ expect(isChromeHidden).to.be(true);
+ });
+ expect(await filterBar.getFilterCount()).to.be(1);
+ await PageObjects.dashboard.clickExitFullScreenLogoButton();
+ await retry.try(async () => {
+ const isChromeVisible = await PageObjects.common.isChromeVisible();
+ expect(isChromeVisible).to.be(true);
+ });
+ await filterBar.removeFilter('bytes');
+ });
});
}
diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js
index 6666ccc57d584..bd8e6812147e1 100644
--- a/test/functional/apps/dashboard/index.js
+++ b/test/functional/apps/dashboard/index.js
@@ -74,6 +74,7 @@ export default function({ getService, loadTestFile }) {
loadTestFile(require.resolve('./panel_expand_toggle'));
loadTestFile(require.resolve('./dashboard_grid'));
loadTestFile(require.resolve('./view_edit'));
+ loadTestFile(require.resolve('./dashboard_saved_query'));
// Order of test suites *shouldn't* be important but there's a bug for the view_edit test above
// https://github.com/elastic/kibana/issues/46752
// The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test.
diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js
index 76f3a3aea365f..9b50eeda20073 100644
--- a/test/functional/apps/discover/_saved_queries.js
+++ b/test/functional/apps/discover/_saved_queries.js
@@ -74,6 +74,7 @@ export default function({ getService, getPageObjects }) {
true
);
await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse');
+ await savedQueryManagementComponent.savedQueryTextExist('response:200');
});
it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => {
diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts
index 244c1cd214de5..66bf15f3da53c 100644
--- a/test/functional/services/saved_query_management_component.ts
+++ b/test/functional/services/saved_query_management_component.ts
@@ -151,6 +151,12 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide
await testSubjects.existOrFail(`~load-saved-query-${title}-button`);
}
+ async savedQueryTextExist(text: string) {
+ await this.openSavedQueryManagementComponent();
+ const queryString = await queryBar.getQueryString();
+ expect(queryString).to.eql(text);
+ }
+
async savedQueryMissingOrFail(title: string) {
await retry.try(async () => {
await this.openSavedQueryManagementComponent();