Put share link back in edit mode (#11003) (#11060)

* Put share link back in edit mode

See https://github.com/elastic/kibana/issues/10951 for background and
reasoning

* Preserve backward compatibility with pre 5.3 dashboard links

* Fix paths so create and edit use landing page as a substring

Apparently it has to be this way for the chrome nav to handle app
navigation the right way.

* Replace more strings with constants

* Update tests to point to new default dashboard url landing page

* [utils] move encodeQueryComponent into shared utils

* [uiNavLink] allow specifying subUrl pattern

* [dashboards] use dashboard urls that are more like before 5.3

* update comments

* Remove one comment, add another

* Remove failing toJSON test since the function was removed

Submitting now, but pending feedback that toJSON was removed
intentionally.

* Fix tests that need subUrlBase set manually

* fix bad markup in breadcrumbs

* Preserve 5.3 create dashboard links!

* add a comment
This commit is contained in:
Stacey Gammon 2017-04-05 19:48:16 -04:00 committed by GitHub
parent 194752f63e
commit 4e88a167d1
18 changed files with 116 additions and 110 deletions

View file

@ -84,7 +84,13 @@ module.exports = function (kibana) {
id: 'kibana:dashboard',
title: 'Dashboard',
order: -1001,
url: `${kbnBaseUrl}#/dashboard`,
url: `${kbnBaseUrl}#/dashboards`,
// The subUrlBase is the common substring of all urls for this app. If not given, it defaults to the url
// above. This app has to use a different subUrlBase, in addition to the url above, because "#/dashboard"
// routes to a page that creates a new dashboard. When we introduced a landing page, we needed to change
// the url above in order to preserve the original url for BWC. The subUrlBase helps the Chrome api nav
// to determine what url to use for the app link.
subUrlBase: `${kbnBaseUrl}#/dashboard`,
description: 'compose visualizations for much win',
icon: 'plugins/kibana/assets/dashboard.svg',
}, {

View file

@ -7,12 +7,14 @@
<div
data-transclude-slot="topLeftCorner"
>
<!-- Breadcrumbs. -->
<bread-crumbs
page-title="getDashTitle()"
use-links="true"
omit-current-page="true"
></bread-crumbs>
<div class="kuiLocalBreadcrumbs" data-test-subj="breadcrumbs">
<div class="kuiLocalBreadcrumb">
<a class="kuiLocalBreadcrumb__link" href="{{landingPageUrl()}}">Dashboard</a>
</div>
<div class="kuiLocalBreadcrumb">
{{ getDashTitle() }}
</div>
</div>
</div>
<!-- Search. -->

View file

@ -7,6 +7,7 @@ import chrome from 'ui/chrome';
import 'plugins/kibana/dashboard/grid';
import 'plugins/kibana/dashboard/panel/panel';
import { SavedObjectNotFound } from 'ui/errors';
import { getDashboardTitle, getUnsavedChangesWarningMessage } from './dashboard_strings';
import { DashboardViewMode } from './dashboard_view_mode';
import { TopNavIds } from './top_nav/top_nav_ids';
@ -15,11 +16,12 @@ import dashboardTemplate from 'plugins/kibana/dashboard/dashboard.html';
import FilterBarQueryFilterProvider from 'ui/filter_bar/query_filter';
import DocTitleProvider from 'ui/doc_title';
import { getTopNavConfig } from './top_nav/get_top_nav_config';
import { DashboardConstants } from './dashboard_constants';
import { DashboardConstants, createDashboardEditUrl } from './dashboard_constants';
import { VisualizeConstants } from 'plugins/kibana/visualize/visualize_constants';
import UtilsBrushEventProvider from 'ui/utils/brush_event';
import FilterBarFilterBarClickHandlerProvider from 'ui/filter_bar/filter_bar_click_handler';
import { DashboardState } from './dashboard_state';
import notify from 'ui/notify';
const app = uiModules.get('app/dashboard', [
'elasticsearch',
@ -31,24 +33,37 @@ const app = uiModules.get('app/dashboard', [
]);
uiRoutes
.when('/dashboard/create', {
.when(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {
template: dashboardTemplate,
resolve: {
dash: function (savedDashboards, courier) {
return savedDashboards.get()
.catch(courier.redirectWhenMissing({
'dashboard': '/dashboard'
'dashboard': DashboardConstants.LANDING_PAGE_PATH
}));
}
}
})
.when('/dashboard/:id', {
.when(createDashboardEditUrl(':id'), {
template: dashboardTemplate,
resolve: {
dash: function (savedDashboards, Notifier, $route, $location, courier) {
return savedDashboards.get($route.current.params.id)
dash: function (savedDashboards, Notifier, $route, $location, courier, kbnUrl, AppState) {
const id = $route.current.params.id;
return savedDashboards.get(id)
.catch((error) => {
// Preserve BWC of v5.3.0 links for new, unsaved dashboards.
// See https://github.com/elastic/kibana/issues/10951 for more context.
if (error instanceof SavedObjectNotFound && id === 'create') {
// Note "new AppState" is neccessary so the state in the url is preserved through the redirect.
kbnUrl.redirect(DashboardConstants.CREATE_NEW_DASHBOARD_URL, {}, new AppState());
notify.error(
'The url "dashboard/create" is deprecated and will be removed in 6.0. Please update your bookmarks.');
} else {
throw error;
}
})
.catch(courier.redirectWhenMissing({
'dashboard' : '/dashboard'
'dashboard' : DashboardConstants.LANDING_PAGE_PATH
}));
}
}
@ -113,6 +128,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
$scope.expandedPanel = null;
$scope.dashboardViewMode = dashboardState.getViewMode();
$scope.landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
$scope.getBrushEvent = () => brushEvent(dashboardState.getAppState());
$scope.getFilterBarClickHandler = () => filterBarClickHandler(dashboardState.getAppState());
$scope.hasExpandedPanel = () => $scope.expandedPanel !== null;
@ -192,10 +208,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
function revertChangesAndExitEditMode() {
dashboardState.resetState();
const refreshUrl = dash.id ?
DashboardConstants.EXISTING_DASHBOARD_URL : DashboardConstants.CREATE_NEW_DASHBOARD_URL;
const refreshUrlOptions = dash.id ? { id: dash.id } : {};
kbnUrl.change(refreshUrl, refreshUrlOptions);
kbnUrl.change(dash.id ? createDashboardEditUrl(dash.id) : DashboardConstants.CREATE_NEW_DASHBOARD_URL);
// This is only necessary for new dashboards, which will default to Edit mode.
updateViewMode(DashboardViewMode.VIEW);
@ -231,9 +244,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
if (id) {
notify.info(`Saved Dashboard as "${dash.title}"`);
if (dash.id !== $routeParams.id) {
kbnUrl.change(
`${DashboardConstants.EXISTING_DASHBOARD_URL}`,
{ id: dash.id });
kbnUrl.change(createDashboardEditUrl(dash.id));
} else {
docTitle.change(dash.lastSavedTitle);
updateViewMode(DashboardViewMode.VIEW);

View file

@ -1,8 +1,12 @@
import { encodeQueryComponent } from '../../../../utils';
export const DashboardConstants = {
ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM: 'addToDashboard',
NEW_VISUALIZATION_ID_PARAM: 'addVisualization',
LANDING_PAGE_PATH: '/dashboard',
CREATE_NEW_DASHBOARD_URL: '/dashboard/create',
EXISTING_DASHBOARD_URL: '/dashboard/{{id}}'
LANDING_PAGE_PATH: '/dashboards',
CREATE_NEW_DASHBOARD_URL: '/dashboard',
};
export function createDashboardEditUrl(id) {
return `/dashboard/${encodeQueryComponent(id)}`;
}

View file

@ -47,7 +47,7 @@
<!-- Create dashboard button -->
<a
class="kuiButton kuiButton--primary"
href="#/dashboard/create"
href="{{listingController.getCreateDashboardHref()}}"
aria-label="Create new dashboard"
data-test-subj="newDashboardLink"
ng-if="listingController.getSelectedItemsCount() === 0"
@ -97,7 +97,7 @@
<div class="kuiPromptForItems__actions">
<a
class="kuiButton kuiButton--primary kuiButton--iconText"
href="#/dashboard/create"
href="{{listingController.getCreateDashboardHref()}}"
>
<span class="kuiButton__inner">
<span class="kuiButton__icon kuiIcon fa-plus"></span>

View file

@ -2,6 +2,7 @@ import SavedObjectRegistryProvider from 'ui/saved_objects/saved_object_registry'
import 'ui/pager_control';
import 'ui/pager';
import _ from 'lodash';
import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants';
export function DashboardListingController($injector, $scope) {
const $filter = $injector.get('$filter');
@ -145,6 +146,10 @@ export function DashboardListingController($injector, $scope) {
};
this.getUrlForItem = function getUrlForItem(item) {
return `#/dashboard/${item.id}`;
return `#${createDashboardEditUrl(item.id)}`;
};
this.getCreateDashboardHref = function getCreateDashboardHref() {
return `#${DashboardConstants.CREATE_NEW_DASHBOARD_URL}`;
};
}

View file

@ -17,7 +17,8 @@ export function getTopNavConfig(dashboardMode, actions) {
getSaveConfig(),
getViewConfig(actions[TopNavIds.EXIT_EDIT_MODE]),
getAddConfig(),
getOptionsConfig()];
getOptionsConfig(),
getShareConfig()];
default:
return [];
}

View file

@ -135,25 +135,4 @@ describe('UiNavLink', () => {
expect(link.tooltip).to.be('');
});
});
describe('#toJSON', () => {
it ('returns the expected properties', () => {
const uiExports = {
urlBasePath: 'http://localhost:5601/rnd'
};
const spec = {
id: 'kibana:discover',
title: 'Discover',
order: -1003,
url: '/app/kibana#/discover',
description: 'interactively explore your data',
icon: 'plugins/kibana/assets/discover.svg',
};
const link = new UiNavLink(uiExports, spec);
const json = link.toJSON();
['id', 'title', 'url', 'order', 'description', 'icon', 'linkToLastSubUrl', 'hidden', 'disabled', 'tooltip']
.forEach(expectedProperty => expect(json).to.have.property(expectedProperty));
});
});
});

View file

@ -95,9 +95,18 @@ describe('chrome nav apis', function () {
it('injects the globalState of the current url to all links for the same app', function () {
const appUrlStore = new StubBrowserStorage();
const nav = [
{ url: 'https://localhost:9200/app/kibana#discover' },
{ url: 'https://localhost:9200/app/kibana#visualize' },
{ url: 'https://localhost:9200/app/kibana#dashboard' },
{
url: 'https://localhost:9200/app/kibana#discover',
subUrlBase: 'https://localhost:9200/app/kibana#discover'
},
{
url: 'https://localhost:9200/app/kibana#visualize',
subUrlBase: 'https://localhost:9200/app/kibana#visualize'
},
{
url: 'https://localhost:9200/app/kibana#dashboards',
subUrlBase: 'https://localhost:9200/app/kibana#dashboard'
},
].map(l => {
l.lastSubUrl = l.url;
return l;

View file

@ -1,5 +1,5 @@
import { parse, format } from 'url';
import { startsWith, isString } from 'lodash';
import { isString } from 'lodash';
export default function (chrome, internals) {
chrome.getNavLinks = function () {
@ -110,7 +110,7 @@ export default function (chrome, internals) {
const { appId, globalState: newGlobalState } = decodeKibanaUrl(url);
for (const link of internals.nav) {
link.active = startsWith(url, link.url);
link.active = url.startsWith(link.subUrlBase);
if (link.active) {
setLastUrl(link, url);
continue;
@ -124,11 +124,16 @@ export default function (chrome, internals) {
}
};
internals.nav.forEach(link => {
function relativeToAbsolute(url) {
// convert all link urls to absolute urls
const a = document.createElement('a');
a.setAttribute('href', link.url);
link.url = a.href;
a.setAttribute('href', url);
return a.href;
}
internals.nav.forEach(link => {
link.url = relativeToAbsolute(link.url);
link.subUrlBase = relativeToAbsolute(link.subUrlBase);
});
// simulate a possible change in url to initialize the

View file

@ -1,6 +1,6 @@
<div class="kuiLocalBreadcrumbs" data-test-subj="breadcrumbs">
<div class="kuiLocalBreadcrumb" ng-if="useLinks && (!omitPages || !omitPages.includes(breadcrumb.path))" ng-repeat="breadcrumb in breadcrumbs">
<a class=kuiLocalBreadcrumb__link" href="{{breadcrumb.url}}">{{breadcrumb.title}}</a>
<a class="kuiLocalBreadcrumb__link" href="{{breadcrumb.url}}">{{breadcrumb.title}}</a>
</div>
<div class="kuiLocalBreadcrumb" ng-if="!useLinks && (!omitPages || !omitPages.includes(breadcrumb.path))" ng-repeat="breadcrumb in breadcrumbs">
{{ breadcrumb.title }}

View file

@ -1,3 +1,5 @@
import { encodeQueryComponent } from '../../../utils';
const qs = {};
/*****
@ -12,26 +14,6 @@ function tryDecodeURIComponent(value) {
catch (e) {} // eslint-disable-line no-empty
}
/**
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
* encoded per http://tools.ietf.org/html/rfc3986:
* query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
function encodeUriQuery(val, pctEncodeSpaces) {
return encodeURIComponent(val)
.replace(/%40/gi, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}
/**
* Parses an escaped url query string into key-value pairs.
* @returns {Object.<string,boolean|Array>}
@ -82,7 +64,7 @@ qs.encode = function (obj) {
};
qs.param = function (key, val) {
return encodeUriQuery(key, true) + (val === true ? '' : '=' + encodeUriQuery(val, true));
return encodeQueryComponent(key, true) + (val === true ? '' : '=' + encodeQueryComponent(val, true));
};
/**

View file

@ -1,11 +1,10 @@
import { pick } from 'lodash';
export default class UiNavLink {
constructor(uiExports, spec) {
this.id = spec.id;
this.title = spec.title;
this.order = spec.order || 0;
this.url = `${uiExports.urlBasePath || ''}${spec.url}`;
this.subUrlBase = `${uiExports.urlBasePath || ''}${spec.subUrlBase || spec.url}`;
this.description = spec.description;
this.icon = spec.icon;
this.linkToLastSubUrl = spec.linkToLastSubUrl === false ? false : true;
@ -13,8 +12,4 @@ export default class UiNavLink {
this.disabled = spec.disabled || false;
this.tooltip = spec.tooltip || '';
}
toJSON() {
return pick(this, ['id', 'title', 'url', 'order', 'description', 'icon', 'linkToLastSubUrl', 'hidden', 'disabled', 'tooltip']);
}
}

View file

@ -0,0 +1,19 @@
/**
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
* encoded per http://tools.ietf.org/html/rfc3986:
* query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
*/
export function encodeQueryComponent(val, pctEncodeSpaces = false) {
return encodeURIComponent(val)
.replace(/%40/gi, '@')
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
}

View file

@ -5,6 +5,8 @@ export fromRoot from './from_root';
export pkg from './package_json';
export unset from './unset';
export { encodeQueryComponent } from './encode_query_component';
export {
createConcatStream,
createIntersperseStream,

View file

@ -1,22 +1,5 @@
import { existsSync } from 'fs';
import { join } from 'path';
import { dirname } from 'path';
let packageDir;
let packagePath;
while (!packagePath || !existsSync(packagePath)) {
const prev = packageDir;
packageDir = prev ? join(prev, '..') : __dirname;
packagePath = join(packageDir, 'package.json');
if (prev === packageDir) {
// if going up a directory doesn't work, we
// are already at the root of the filesystem
throw new Error('unable to find package.json');
}
}
module.exports = require(packagePath);
module.exports.__filename = packagePath;
module.exports.__dirname = packageDir;
module.exports = require('../../package.json');
module.exports.__filename = require.resolve('../../package.json');
module.exports.__dirname = dirname(module.exports.__filename);

View file

@ -1,5 +1,4 @@
const shield = require('./shield');
const kibanaURL = '/app/kibana';
module.exports = {
@ -44,7 +43,7 @@ module.exports = {
},
dashboard: {
pathname: kibanaURL,
hash: '/dashboard',
hash: '/dashboards',
},
settings: {
pathname: kibanaURL,

View file

@ -1,6 +1,9 @@
import _ from 'lodash';
import { defaultFindTimeout } from '../';
import {
DashboardConstants
} from '../../../src/core_plugins/kibana/public/dashboard/dashboard_constants';
import {
scenarioManager,
@ -35,7 +38,7 @@ export default class DashboardPage {
*/
async onDashboardLandingPage() {
PageObjects.common.debug(`onDashboardLandingPage`);
const exists = await PageObjects.common.doesCssSelectorExist('a[href="#/dashboard"]');
const exists = await PageObjects.common.doesCssSelectorExist(`a[href="#${DashboardConstants.LANDING_PAGE_PATH}"]`);
return !exists;
}
@ -44,7 +47,8 @@ export default class DashboardPage {
const onPage = await this.onDashboardLandingPage();
if (!onPage) {
await PageObjects.common.try(async () => {
const goToDashboardLink = await PageObjects.common.findByCssSelector('a[href="#/dashboard"]');
const goToDashboardLink =
await PageObjects.common.findByCssSelector(`a[href="#${DashboardConstants.LANDING_PAGE_PATH}"]`);
await goToDashboardLink.click();
// Once the searchFilter can be found, we know the page finished loading.
await PageObjects.common.findTestSubject('searchFilter');