diff --git a/src/core_plugins/kibana/public/dashboard/index.js b/src/core_plugins/kibana/public/dashboard/index.js index cc8036c96046e..c0ee5424b2552 100644 --- a/src/core_plugins/kibana/public/dashboard/index.js +++ b/src/core_plugins/kibana/public/dashboard/index.js @@ -12,6 +12,7 @@ import { DashboardListingController } from './listing/dashboard_listing'; import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; import { SavedObjectNotFound } from 'ui/errors'; import { FeatureCatalogueRegistryProvider, FeatureCatalogueCategory } from 'ui/registry/feature_catalogue'; +import { SavedObjectsClientProvider } from 'ui/saved_objects'; uiRoutes .defaults(/dashboard/, { @@ -20,7 +21,32 @@ uiRoutes .when(DashboardConstants.LANDING_PAGE_PATH, { template: dashboardListingTemplate, controller: DashboardListingController, - controllerAs: 'listingController' + controllerAs: 'listingController', + resolve: { + dash: function ($route, Private, courier, kbnUrl) { + const savedObjectsClient = Private(SavedObjectsClientProvider); + const title = $route.current.params.title; + if (title) { + return savedObjectsClient.find({ + search: `"${title}"`, + search_fields: 'title', + type: 'dashboard', + }).then(results => { + // The search isn't an exact match, lets see if we can find a single exact match to use + const matchingDashboards = results.savedObjects.filter( + dashboard => dashboard.attributes.title.toLowerCase() === title.toLowerCase()); + if (matchingDashboards.length === 1) { + kbnUrl.redirect(createDashboardEditUrl(matchingDashboards[0].id)); + } else { + kbnUrl.redirect(`${DashboardConstants.LANDING_PAGE_PATH}?filter="${title}"`); + } + throw uiRoutes.WAIT_FOR_URL_CHANGE_TOKEN; + }).catch(courier.redirectWhenMissing({ + 'dashboard': DashboardConstants.LANDING_PAGE_PATH + })); + } + } + } }) .when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, { template: dashboardTemplate, diff --git a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js index 876a55c46bfcb..0981f5d4aa3b6 100644 --- a/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js +++ b/src/core_plugins/kibana/public/dashboard/listing/dashboard_listing.js @@ -5,7 +5,7 @@ import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constan import { SortableProperties } from 'ui_framework/services'; import { ConfirmationButtonTypes } from 'ui/modals'; -export function DashboardListingController($injector, $scope) { +export function DashboardListingController($injector, $scope, $location) { const $filter = $injector.get('$filter'); const confirmModal = $injector.get('confirmModal'); const Notifier = $injector.get('Notifier'); @@ -69,7 +69,7 @@ export function DashboardListingController($injector, $scope) { this.isFetchingItems = false; this.items = []; this.pageOfItems = []; - this.filter = ''; + this.filter = ($location.search()).filter || ''; this.pager = pagerFactory.create(this.items.length, 20, 1); @@ -78,6 +78,7 @@ export function DashboardListingController($injector, $scope) { $scope.$watch(() => this.filter, () => { deselectAll(); fetchItems(); + $location.search('filter', this.filter); }); this.isAscending = (name) => sortableProperties.isAscendingByName(name); this.getSortedProperty = () => sortableProperties.getSortedProperty(); diff --git a/test/functional/apps/dashboard/_dashboard_listing.js b/test/functional/apps/dashboard/_dashboard_listing.js index 6c45081496363..1e5ced4733fdf 100644 --- a/test/functional/apps/dashboard/_dashboard_listing.js +++ b/test/functional/apps/dashboard/_dashboard_listing.js @@ -1,7 +1,8 @@ import expect from 'expect.js'; -export default function ({ getPageObjects }) { +export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['dashboard', 'header', 'common']); + const remote = getService('remote'); describe('dashboard listing page', function describeIndexTests() { const dashboardName = 'Dashboard Listing Test'; @@ -104,5 +105,84 @@ export default function ({ getPageObjects }) { expect(countOfDashboards).to.equal(1); }); }); + + describe('search by title', function () { + it('loads a dashboard if title matches', async function () { + const currentUrl = await remote.getCurrentUrl(); + const newUrl = currentUrl + '&title=Two%20Words'; + // Only works on a hard refresh. + const useTimeStamp = true; + await remote.get(newUrl.toString(), useTimeStamp); + + const onDashboardLandingPage = await PageObjects.dashboard.onDashboardLandingPage(); + expect(onDashboardLandingPage).to.equal(false); + }); + + it('title match is case insensitive', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + const currentUrl = await remote.getCurrentUrl(); + const newUrl = currentUrl + '&title=two%20words'; + // Only works on a hard refresh. + const useTimeStamp = true; + await remote.get(newUrl.toString(), useTimeStamp); + + const onDashboardLandingPage = await PageObjects.dashboard.onDashboardLandingPage(); + expect(onDashboardLandingPage).to.equal(false); + }); + + it('stays on listing page if title matches no dashboards', async function () { + await PageObjects.dashboard.gotoDashboardLandingPage(); + const currentUrl = await remote.getCurrentUrl(); + const newUrl = currentUrl + '&title=nodashboardsnamedme'; + // Only works on a hard refresh. + const useTimeStamp = true; + await remote.get(newUrl.toString(), useTimeStamp); + + await PageObjects.header.waitUntilLoadingHasFinished(); + const onDashboardLandingPage = await PageObjects.dashboard.onDashboardLandingPage(); + expect(onDashboardLandingPage).to.equal(true); + }); + + it('preloads search filter bar when there is no match', async function () { + const searchFilter = await PageObjects.dashboard.getSearchFilterValue(); + expect(searchFilter).to.equal('"nodashboardsnamedme"'); + }); + + it('stays on listing page if title matches two dashboards', async function () { + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.saveDashboard('two words', { needsConfirm: true }); + await PageObjects.dashboard.gotoDashboardLandingPage(); + const currentUrl = await remote.getCurrentUrl(); + const newUrl = currentUrl + '&title=two%20words'; + // Only works on a hard refresh. + const useTimeStamp = true; + await remote.get(newUrl.toString(), useTimeStamp); + + await PageObjects.header.waitUntilLoadingHasFinished(); + const onDashboardLandingPage = await PageObjects.dashboard.onDashboardLandingPage(); + expect(onDashboardLandingPage).to.equal(true); + }); + + it('preloads search filter bar when there is more than one match', async function () { + const searchFilter = await PageObjects.dashboard.getSearchFilterValue(); + expect(searchFilter).to.equal('"two words"'); + }); + + it('matches a title with many special characters', async function () { + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.dashboard.saveDashboard('i am !@#$%^&*()_+~`,.<>{}[]; so special'); + await PageObjects.dashboard.gotoDashboardLandingPage(); + const currentUrl = await remote.getCurrentUrl(); + // Need to encode that one. + const newUrl = currentUrl + '&title=i%20am%20%21%40%23%24%25%5E%26%2A%28%29_%2B~%60%2C.%3C%3E%7B%7D%5B%5D%3B%20so%20special'; + // Only works on a hard refresh. + const useTimeStamp = true; + await remote.get(newUrl.toString(), useTimeStamp); + + await PageObjects.header.waitUntilLoadingHasFinished(); + const onDashboardLandingPage = await PageObjects.dashboard.onDashboardLandingPage(); + expect(onDashboardLandingPage).to.equal(false); + }); + }); }); } diff --git a/test/functional/page_objects/dashboard_page.js b/test/functional/page_objects/dashboard_page.js index ea36c05efe1e2..de1be75b174cf 100644 --- a/test/functional/page_objects/dashboard_page.js +++ b/test/functional/page_objects/dashboard_page.js @@ -292,11 +292,15 @@ export function DashboardPageProvider({ getService, getPageObjects }) { /** * * @param dashName {String} - * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean}} + * @param saveOptions {{storeTimeWithDashboard: boolean, saveAsNew: boolean, needsConfirm: false}} */ async saveDashboard(dashName, saveOptions = {}) { await this.enterDashboardTitleAndClickSave(dashName, saveOptions); + if (saveOptions.needsConfirm) { + await PageObjects.common.clickConfirmOnModal(); + } + await PageObjects.header.waitUntilLoadingHasFinished(); // verify that green message at the top of the page. @@ -346,6 +350,11 @@ export function DashboardPageProvider({ getService, getPageObjects }) { }); } + async getSearchFilterValue() { + const searchFilter = await testSubjects.find('searchFilter'); + return await searchFilter.getProperty('value'); + } + async searchForDashboardWithName(dashName) { log.debug(`searchForDashboardWithName: ${dashName}`);