BWC link fixes (#11062)

This commit is contained in:
Stacey Gammon 2017-04-05 19:48:36 -04:00 committed by GitHub
parent def2a7ecf3
commit 7604c5f8f6
17 changed files with 111 additions and 103 deletions

View file

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

@ -13,12 +13,14 @@ import DocTitleProvider from 'ui/doc_title';
import stateMonitorFactory from 'ui/state_management/state_monitor_factory';
import { getTopNavConfig } from './top_nav/get_top_nav_config';
import { createPanelState } from 'plugins/kibana/dashboard/panel/panel_state';
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 { FilterUtils } from './filter_utils';
import { getPersistedStateId } from 'plugins/kibana/dashboard/panel/panel_state';
import errors from 'ui/errors';
import notify from 'ui/notify';
const app = uiModules.get('app/dashboard', [
'elasticsearch',
@ -30,24 +32,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 errors.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
}));
}
}
@ -99,6 +114,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
$scope.$watch('state.options.darkTheme', setDarkTheme);
$scope.topNavMenu = getTopNavConfig(kbnUrl);
$scope.landingPageUrl = () => `#${DashboardConstants.LANDING_PAGE_PATH}`;
$scope.refresh = _.bindKey(courier, 'fetch');
@ -200,6 +216,7 @@ app.directive('dashboardApp', function (Notifier, courier, AppState, timefilter,
$state.save();
};
$scope.brushEvent = brushEvent;
$scope.filterBarClickHandler = filterBarClickHandler;
$scope.expandedPanel = null;

View file

@ -1,6 +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'
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__icon kuiIcon fa-plus"></span>
Create a dashboard

View file

@ -1,8 +1,8 @@
import SavedObjectRegistryProvider from 'ui/saved_objects/saved_object_registry';
import 'ui/pager_control';
import 'ui/pager';
import { DashboardConstants } from '../dashboard_constants';
import _ from 'lodash';
import { DashboardConstants, createDashboardEditUrl } from '../dashboard_constants';
export function DashboardListingController($injector, $scope) {
const $filter = $injector.get('$filter');
@ -147,6 +147,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

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

@ -91,9 +91,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, find } 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,12 +1,10 @@
import { pick } from 'lodash';
import { join } from 'path';
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;
@ -14,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

@ -4,3 +4,5 @@ export deepCloneWithBuffers from './deep_clone_with_buffers';
export fromRoot from './from_root';
export pkg from './package_json';
export unset from './unset';
export { encodeQueryComponent } from './encode_query_component';

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 = {
@ -40,7 +39,7 @@ module.exports = {
},
dashboard: {
pathname: kibanaURL,
hash: '/dashboard',
hash: '/dashboards',
},
settings: {
pathname: kibanaURL,

View file

@ -1,7 +1,8 @@
import { defaultFindTimeout } from '../';
import {
defaultFindTimeout,
} from '../';
DashboardConstants
} from '../../../src/core_plugins/kibana/public/dashboard/dashboard_constants';
import PageObjects from './';
@ -14,7 +15,7 @@ export default class DashboardPage {
gotoDashboardLandingPage() {
return this.findTimeout
.findByCssSelector('a[href="#/dashboard"]')
.findByCssSelector(`a[href="#${DashboardConstants.LANDING_PAGE_PATH}"]`)
.click();
}