Files
gerrit/polygerrit-ui/app/elements/core/gr-router/gr-router.js
Logan Hanks 2931aa0e2c Link to project dashboards instead of custom
The admin page that lists project dashboards constructs custom dashboard
links that only approximate the intended behavior of each dashboard.

Bug: Issue 9903
Change-Id: Ic911d307416ee4e5eab6020e6c12e6d0554204fb
2018-11-13 11:55:13 -08:00

1481 lines
46 KiB
JavaScript

/**
* @license
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed 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.
*/
(function() {
'use strict';
const RoutePattern = {
ROOT: '/',
DASHBOARD: /^\/dashboard\/(.+)$/,
CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
AGREEMENTS: /^\/settings\/agreements\/?/,
NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/,
REGISTER: /^\/register(\/.*)?$/,
// Pattern for login and logout URLs intended to be passed-through. May
// include a return URL.
LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/,
// Pattern for a catchall route when no other pattern is matched.
DEFAULT: /.*/,
// Matches /admin/groups/[uuid-]<group>
GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/,
// Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui)
// Redirects to /admin/groups/[uuid-]<group>
GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/,
// Matches /admin/groups/<group>,audit-log
GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/,
// Matches /admin/groups/[uuid-]<group>,members
GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/,
// Matches /admin/groups[,<offset>][/].
GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/,
GROUP_LIST_FILTER: '/admin/groups/q/filter::filter',
GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset',
// Matches /admin/create-project
LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/,
// Matches /admin/create-project
LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/,
PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/,
// Matches /admin/repos/<repo>
REPO: /^\/admin\/repos\/([^,]+)$/,
// Matches /admin/repos/<repo>,commands.
REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/,
// Matches /admin/repos/<repos>,access.
REPO_ACCESS: /^\/admin\/repos\/(.+),access$/,
// Matches /admin/repos/<repos>,access.
REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/,
// Matches /admin/repos[,<offset>][/].
REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/,
REPO_LIST_FILTER: '/admin/repos/q/filter::filter',
REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset',
// Matches /admin/repos/<repo>,branches[,<offset>].
BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/,
BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter',
BRANCH_LIST_FILTER_OFFSET:
'/admin/repos/:repo,branches/q/filter::filter,:offset',
// Matches /admin/repos/<repo>,tags[,<offset>].
TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/,
TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter',
TAG_LIST_FILTER_OFFSET:
'/admin/repos/:repo,tags/q/filter::filter,:offset',
PLUGINS: /^\/plugins\/(.+)$/,
PLUGIN_LIST: /^\/admin\/plugins(\/)?$/,
// Matches /admin/plugins[,<offset>][/].
PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/,
PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter',
PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset',
QUERY: /^\/q\/([^,]+)(,(\d+))?$/,
/**
* Support vestigial params from GWT UI.
* @see Issue 7673.
* @type {!RegExp}
*/
QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/,
// Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/].
CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/,
// Matches
// /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>].
// TODO(kaspern): Migrate completely to project based URLs, with backwards
// compatibility for change-only.
CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/,
// Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit
CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/,
// Matches
// /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>.
// TODO(kaspern): Migrate completely to project based URLs, with backwards
// compatibility for change-only.
// eslint-disable-next-line max-len
DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
// Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit
DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit$/,
// Matches non-project-relative
// /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/,
// Matches diff routes using @\d+ to specify a file name (whether or not
// the project name is included).
// eslint-disable-next-line max-len
DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/,
SETTINGS: /^\/settings\/?/,
SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/,
// Matches /c/<changeNum>/ /<URL tail>
// Catches improperly encoded URLs (context: Issue 7100)
IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter',
DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/,
DOCUMENTATION: /^\/Documentation(\/)?(.+)?/,
};
/**
* Pattern to recognize and parse the diff line locations as they appear in
* the hash of diff URLs. In this format, a number on its own indicates that
* line number in the revision of the diff. A number prefixed by either an 'a'
* or a 'b' indicates that line number of the base of the diff.
* @type {RegExp}
*/
const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/;
/**
* Pattern to recognize '+' in url-encoded strings for replacement with ' '.
*/
const PLUS_PATTERN = /\+/g;
/**
* Pattern to recognize leading '?' in window.location.search, for stripping.
*/
const QUESTION_PATTERN = /^\?*/;
/**
* GWT UI would use @\d+ at the end of a path to indicate linenum.
*/
const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/;
const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
// Polymer makes `app` intrinsically defined on the window by virtue of the
// custom element having the id "app", but it is made explicit here.
const app = document.querySelector('#app');
if (!app) {
console.log('No gr-app found (running tests)');
}
// Setup listeners outside of the router component initialization.
(function() {
const reporting = document.createElement('gr-reporting');
window.addEventListener('load', () => {
reporting.pageLoaded();
});
window.addEventListener('WebComponentsReady', () => {
reporting.timeEnd('WebComponentsReady');
});
})();
Polymer({
is: 'gr-router',
properties: {
_app: {
type: Object,
value: app,
},
_isRedirecting: Boolean,
},
behaviors: [
Gerrit.BaseUrlBehavior,
Gerrit.PatchSetBehavior,
Gerrit.URLEncodingBehavior,
],
start() {
if (!this._app) { return; }
this._startRouter();
},
_setParams(params) {
this._app.params = params;
},
_redirect(url) {
this._isRedirecting = true;
page.redirect(url);
},
/**
* @param {!Object} params
* @return {string}
*/
_generateUrl(params) {
const base = this.getBaseUrl();
let url = '';
const Views = Gerrit.Nav.View;
if (params.view === Views.SEARCH) {
url = this._generateSearchUrl(params);
} else if (params.view === Views.CHANGE) {
url = this._generateChangeUrl(params);
} else if (params.view === Views.DASHBOARD) {
url = this._generateDashboardUrl(params);
} else if (params.view === Views.DIFF || params.view === Views.EDIT) {
url = this._generateDiffOrEditUrl(params);
} else if (params.view === Views.GROUP) {
url = this._generateGroupUrl(params);
} else if (params.view === Views.REPO) {
url = this._generateRepoUrl(params);
} else if (params.view === Views.ROOT) {
url = '/';
} else if (params.view === Views.SETTINGS) {
url = this._generateSettingsUrl(params);
} else {
throw new Error('Can\'t generate');
}
return base + url;
},
_generateWeblinks(params) {
const type = params.type;
switch (type) {
case Gerrit.Nav.WeblinkType.FILE:
return this._getFileWebLinks(params);
case Gerrit.Nav.WeblinkType.CHANGE:
return this._getChangeWeblinks(params);
case Gerrit.Nav.WeblinkType.PATCHSET:
return this._getPatchSetWeblink(params);
default:
console.warn(`Unsupported weblink ${type}!`);
}
},
_getPatchSetWeblink(params) {
const {repo, commit, options} = params;
const {weblinks, config} = options || {};
const name = commit && commit.slice(0, 7);
const gitwebConfigUrl = this._configBasedCommitUrl(repo, commit, config);
if (gitwebConfigUrl) {
return {
name,
url: gitwebConfigUrl,
};
}
const url = this._getSupportedWeblinkUrl(weblinks);
if (!url) {
return {name};
} else {
return {name, url};
}
},
_configBasedCommitUrl(repo, commit, config) {
if (config && config.gitweb && config.gitweb.url &&
config.gitweb.type && config.gitweb.type.revision) {
return config.gitweb.url + config.gitweb.type.revision
.replace('${project}', repo)
.replace('${commit}', commit);
}
},
_isDirectCommit(link) {
// This is a whitelist of web link types that provide direct links to
// the commit in the url property.
return link.name === 'gitiles' || link.name === 'gitweb';
},
_getSupportedWeblinkUrl(weblinks) {
if (!weblinks) { return null; }
const weblink = weblinks.find(this._isDirectCommit);
if (!weblink) { return null; }
return weblink.url;
},
_getChangeWeblinks({repo, commit, options: {weblinks}}) {
if (!weblinks || !weblinks.length) return [];
return weblinks.filter(weblink => !this._isDirectCommit(weblink)).map(
({name, url}) => {
if (url.startsWith('https:') || url.startsWith('http:')) {
return {name, url};
} else {
return {
name,
url: `../../${url}`,
};
}
});
},
_getFileWebLinks({repo, commit, file, options: {weblinks}}) {
return weblinks;
},
/**
* @param {!Object} params
* @return {string}
*/
_generateSearchUrl(params) {
let offsetExpr = '';
if (params.offset && params.offset > 0) {
offsetExpr = ',' + params.offset;
}
if (params.query) {
return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
}
const operators = [];
if (params.owner) {
operators.push('owner:' + this.encodeURL(params.owner, false));
}
if (params.project) {
operators.push('project:' + this.encodeURL(params.project, false));
}
if (params.branch) {
operators.push('branch:' + this.encodeURL(params.branch, false));
}
if (params.topic) {
operators.push('topic:"' + this.encodeURL(params.topic, false) + '"');
}
if (params.hashtag) {
operators.push('hashtag:"' +
this.encodeURL(params.hashtag.toLowerCase(), false) + '"');
}
if (params.statuses) {
if (params.statuses.length === 1) {
operators.push(
'status:' + this.encodeURL(params.statuses[0], false));
} else if (params.statuses.length > 1) {
operators.push(
'(' +
params.statuses.map(s => `status:${this.encodeURL(s, false)}`)
.join(' OR ') +
')');
}
}
return '/q/' + operators.join('+') + offsetExpr;
},
/**
* @param {!Object} params
* @return {string}
*/
_generateChangeUrl(params) {
let range = this._getPatchRangeExpression(params);
if (range.length) { range = '/' + range; }
let suffix = `${range}`;
if (params.querystring) {
suffix += '?' + params.querystring;
} else if (params.edit) {
suffix += ',edit';
}
if (params.messageHash) {
suffix += params.messageHash;
}
if (params.project) {
const encodedProject = this.encodeURL(params.project, true);
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
} else {
return `/c/${params.changeNum}${suffix}`;
}
},
/**
* @param {!Object} params
* @return {string}
*/
_generateDashboardUrl(params) {
const repoName = params.repo || params.project || null;
if (params.sections) {
// Custom dashboard.
const queryParams = this._sectionsToEncodedParams(params.sections,
repoName);
if (params.title) {
queryParams.push('title=' + encodeURIComponent(params.title));
}
const user = params.user ? params.user : '';
return `/dashboard/${user}?${queryParams.join('&')}`;
} else if (repoName) {
// Project dashboard.
const encodedRepo = this.encodeURL(repoName, true);
return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`;
} else {
// User dashboard.
return `/dashboard/${params.user || 'self'}`;
}
},
/**
* @param {!Array<!{name: string, query: string}>} sections
* @param {string=} opt_repoName
* @return {!Array<string>}
*/
_sectionsToEncodedParams(sections, opt_repoName) {
return sections.map(section => {
// If there is a repo name provided, make sure to substitute it into the
// ${repo} (or legacy ${project}) query tokens.
const query = opt_repoName ?
section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) :
section.query;
return encodeURIComponent(section.name) + '=' +
encodeURIComponent(query);
});
},
/**
* @param {!Object} params
* @return {string}
*/
_generateDiffOrEditUrl(params) {
let range = this._getPatchRangeExpression(params);
if (range.length) { range = '/' + range; }
let suffix = `${range}/${this.encodeURL(params.path, true)}`;
if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; }
if (params.lineNum) {
suffix += '#';
if (params.leftSide) { suffix += 'b'; }
suffix += params.lineNum;
}
if (params.project) {
const encodedProject = this.encodeURL(params.project, true);
return `/c/${encodedProject}/+/${params.changeNum}${suffix}`;
} else {
return `/c/${params.changeNum}${suffix}`;
}
},
/**
* @param {!Object} params
* @return {string}
*/
_generateGroupUrl(params) {
let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`;
if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) {
url += ',members';
} else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) {
url += ',audit-log';
}
return url;
},
/**
* @param {!Object} params
* @return {string}
*/
_generateRepoUrl(params) {
let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`;
if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) {
url += ',access';
} else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) {
url += ',branches';
} else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) {
url += ',tags';
} else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) {
url += ',commands';
} else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) {
url += ',dashboards';
}
return url;
},
/**
* @param {!Object} params
* @return {string}
*/
_generateSettingsUrl(params) {
return '/settings';
},
/**
* Given an object of parameters, potentially including a `patchNum` or a
* `basePatchNum` or both, return a string representation of that range. If
* no range is indicated in the params, the empty string is returned.
* @param {!Object} params
* @return {string}
*/
_getPatchRangeExpression(params) {
let range = '';
if (params.patchNum) { range = '' + params.patchNum; }
if (params.basePatchNum) { range = params.basePatchNum + '..' + range; }
return range;
},
/**
* Given a set of params without a project, gets the project from the rest
* API project lookup and then sets the app params.
*
* @param {?Object} params
*/
_normalizeLegacyRouteParams(params) {
if (!params.changeNum) { return Promise.resolve(); }
return this.$.restAPI.getFromProjectLookup(params.changeNum)
.then(project => {
// Show a 404 and terminate if the lookup request failed. Attempting
// to redirect after failing to get the project loops infinitely.
if (!project) {
this._show404();
return;
}
params.project = project;
this._normalizePatchRangeParams(params);
this._redirect(this._generateUrl(params));
});
},
/**
* Normalizes the params object, and determines if the URL needs to be
* modified to fit the proper schema.
*
* @param {*} params
* @return {boolean} whether or not the URL needs to be upgraded.
*/
_normalizePatchRangeParams(params) {
const hasBasePatchNum = params.basePatchNum !== null &&
params.basePatchNum !== undefined;
const hasPatchNum = params.patchNum !== null &&
params.patchNum !== undefined;
let needsRedirect = false;
// Diffing a patch against itself is invalid, so if the base and revision
// patches are equal clear the base.
if (hasBasePatchNum &&
this.patchNumEquals(params.basePatchNum, params.patchNum)) {
needsRedirect = true;
params.basePatchNum = null;
} else if (hasBasePatchNum && !hasPatchNum) {
// Regexes set basePatchNum instead of patchNum when only one is
// specified. Redirect is not needed in this case.
params.patchNum = params.basePatchNum;
params.basePatchNum = null;
}
// In GWTUI, edits are represented in URLs with either 0 or 'edit'.
// TODO(kaspern): Remove this normalization when GWT UI is gone.
if (this.patchNumEquals(params.basePatchNum, 0)) {
params.basePatchNum = this.EDIT_NAME;
needsRedirect = true;
}
if (this.patchNumEquals(params.patchNum, 0)) {
params.patchNum = this.EDIT_NAME;
needsRedirect = true;
}
return needsRedirect;
},
/**
* Redirect the user to login using the given return-URL for redirection
* after authentication success.
* @param {string} returnUrl
*/
_redirectToLogin(returnUrl) {
const basePath = this.getBaseUrl() || '';
page(
'/login/' + encodeURIComponent(returnUrl.substring(basePath.length)));
},
/**
* Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c"
* is parsed to have a hash of "b" rather than "b#c". Instead, this method
* parses hashes correctly. Will return an empty string if there is no hash.
* @param {!string} canonicalPath
* @return {!string} Everything after the first '#' ("a#b#c" -> "b#c").
*/
_getHashFromCanonicalPath(canonicalPath) {
return canonicalPath.split('#').slice(1).join('#');
},
_parseLineAddress(hash) {
const match = hash.match(LINE_ADDRESS_PATTERN);
if (!match) { return null; }
return {
leftSide: !!match[1],
lineNum: parseInt(match[2], 10),
};
},
/**
* Check to see if the user is logged in and return a promise that only
* resolves if the user is logged in. If the user us not logged in, the
* promise is rejected and the page is redirected to the login flow.
* @param {!Object} data The parsed route data.
* @return {!Promise<!Object>} A promise yielding the original route data
* (if it resolves).
*/
_redirectIfNotLoggedIn(data) {
return this.$.restAPI.getLoggedIn().then(loggedIn => {
if (loggedIn) {
return Promise.resolve();
} else {
this._redirectToLogin(data.canonicalPath);
return Promise.reject();
}
});
},
/** Page.js middleware that warms the REST API's logged-in cache line. */
_loadUserMiddleware(ctx, next) {
this.$.restAPI.getLoggedIn().then(() => { next(); });
},
/**
* Map a route to a method on the router.
*
* @param {!string|!RegExp} pattern The page.js pattern for the route.
* @param {!string} handlerName The method name for the handler. If the
* route is matched, the handler will be executed with `this` referring
* to the component. Its return value will be discarded so that it does
* not interfere with page.js.
* @param {?boolean=} opt_authRedirect If true, then auth is checked before
* executing the handler. If the user is not logged in, it will redirect
* to the login flow and the handler will not be executed. The login
* redirect specifies the matched URL to be used after successfull auth.
*/
_mapRoute(pattern, handlerName, opt_authRedirect) {
if (!this[handlerName]) {
console.error('Attempted to map route to unknown method: ',
handlerName);
return;
}
page(pattern, this._loadUserMiddleware.bind(this), data => {
this.$.reporting.locationChanged(handlerName);
const promise = opt_authRedirect ?
this._redirectIfNotLoggedIn(data) : Promise.resolve();
promise.then(() => { this[handlerName](data); });
});
},
_startRouter() {
const base = this.getBaseUrl();
if (base) {
page.base(base);
}
Gerrit.Nav.setup(
url => { page.show(url); },
this._generateUrl.bind(this),
params => this._generateWeblinks(params),
x => x
);
page.exit('*', (ctx, next) => {
if (!this._isRedirecting) {
this.$.reporting.beforeLocationChanged();
}
this._isRedirecting = false;
next();
});
// Middleware
page((ctx, next) => {
document.body.scrollTop = 0;
if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) {
// Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen
// This is needed to allow plugins to add basic #/x/ screen links to
// any location.
this._redirect(ctx.hash);
return;
}
// Fire asynchronously so that the URL is changed by the time the event
// is processed.
this.async(() => {
this.fire('location-change', {
hash: window.location.hash,
pathname: window.location.pathname,
});
}, 1);
next();
});
this._mapRoute(RoutePattern.ROOT, '_handleRootRoute');
this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute');
this._mapRoute(RoutePattern.CUSTOM_DASHBOARD,
'_handleCustomDashboardRoute');
this._mapRoute(RoutePattern.PROJECT_DASHBOARD,
'_handleProjectDashboardRoute');
this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true);
this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute',
true);
this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute',
true);
this._mapRoute(RoutePattern.GROUP_LIST_OFFSET,
'_handleGroupListOffsetRoute', true);
this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET,
'_handleGroupListFilterOffsetRoute', true);
this._mapRoute(RoutePattern.GROUP_LIST_FILTER,
'_handleGroupListFilterRoute', true);
this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true);
this._mapRoute(RoutePattern.PROJECT_OLD,
'_handleProjectsOldRoute');
this._mapRoute(RoutePattern.REPO_COMMANDS,
'_handleRepoCommandsRoute', true);
this._mapRoute(RoutePattern.REPO_ACCESS,
'_handleRepoAccessRoute');
this._mapRoute(RoutePattern.REPO_DASHBOARDS,
'_handleRepoDashboardsRoute');
this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET,
'_handleBranchListOffsetRoute');
this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET,
'_handleBranchListFilterOffsetRoute');
this._mapRoute(RoutePattern.BRANCH_LIST_FILTER,
'_handleBranchListFilterRoute');
this._mapRoute(RoutePattern.TAG_LIST_OFFSET,
'_handleTagListOffsetRoute');
this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET,
'_handleTagListFilterOffsetRoute');
this._mapRoute(RoutePattern.TAG_LIST_FILTER,
'_handleTagListFilterRoute');
this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP,
'_handleCreateGroupRoute', true);
this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT,
'_handleCreateProjectRoute', true);
this._mapRoute(RoutePattern.REPO_LIST_OFFSET,
'_handleRepoListOffsetRoute');
this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET,
'_handleRepoListFilterOffsetRoute');
this._mapRoute(RoutePattern.REPO_LIST_FILTER,
'_handleRepoListFilterRoute');
this._mapRoute(RoutePattern.REPO, '_handleRepoRoute');
this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute');
this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET,
'_handlePluginListOffsetRoute', true);
this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET,
'_handlePluginListFilterOffsetRoute', true);
this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER,
'_handlePluginListFilterRoute', true);
this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true);
this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX,
'_handleQueryLegacySuffixRoute');
this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute');
this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum');
this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY,
'_handleChangeNumberLegacyRoute');
this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true);
this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true);
this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute');
this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute');
this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute');
this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute');
this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true);
this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute',
true);
this._mapRoute(RoutePattern.SETTINGS_LEGACY,
'_handleSettingsLegacyRoute', true);
this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true);
this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute');
this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute');
this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS,
'_handleImproperlyEncodedPlusRoute');
this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen');
this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER,
'_handleDocumentationSearchRoute');
// redirects /Documentation/q/* to /Documentation/q/filter:*
this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH,
'_handleDocumentationSearchRedirectRoute');
// Makes sure /Documentation/* links work (doin't return 404)
this._mapRoute(RoutePattern.DOCUMENTATION,
'_handleDocumentationRedirectRoute');
// Note: this route should appear last so it only catches URLs unmatched
// by other patterns.
this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute');
page.start();
},
/**
* @param {!Object} data
* @return {Promise|null} if handling the route involves asynchrony, then a
* promise is returned. Otherwise, synchronous handling returns null.
*/
_handleRootRoute(data) {
if (data.querystring.match(/^closeAfterLogin/)) {
// Close child window on redirect after login.
window.close();
return null;
}
let hash = this._getHashFromCanonicalPath(data.canonicalPath);
// For backward compatibility with GWT links.
if (hash) {
// In certain login flows the server may redirect to a hash without
// a leading slash, which page.js doesn't handle correctly.
if (hash[0] !== '/') {
hash = '/' + hash;
}
if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) {
// Path decodes all '+' to ' ' -- this breaks project-based URLs.
// See Issue 6888.
hash = hash.replace('/ /', '/+/');
}
const base = this.getBaseUrl();
let newUrl = base + hash;
if (hash.startsWith('/VE/')) {
newUrl = base + '/settings' + hash;
}
this._redirect(newUrl);
return null;
}
return this.$.restAPI.getLoggedIn().then(loggedIn => {
if (loggedIn) {
this._redirect('/dashboard/self');
} else {
this._redirect('/q/status:open');
}
});
},
/**
* Decode an application/x-www-form-urlencoded string.
*
* @param {string} qs The application/x-www-form-urlencoded string.
* @return {string} The decoded string.
*/
_decodeQueryString(qs) {
return decodeURIComponent(qs.replace(PLUS_PATTERN, ' '));
},
/**
* Parse a query string (e.g. window.location.search) into an array of
* name/value pairs.
*
* @param {string} qs The application/x-www-form-urlencoded query string.
* @return {!Array<!Array<string>>} An array of name/value pairs, where each
* element is a 2-element array.
*/
_parseQueryString(qs) {
qs = qs.replace(QUESTION_PATTERN, '');
if (!qs) {
return [];
}
const params = [];
qs.split('&').forEach(param => {
const idx = param.indexOf('=');
let name;
let value;
if (idx < 0) {
name = this._decodeQueryString(param);
value = '';
} else {
name = this._decodeQueryString(param.substring(0, idx));
value = this._decodeQueryString(param.substring(idx + 1));
}
if (name) {
params.push([name, value]);
}
});
return params;
},
/**
* Handle dashboard routes. These may be user, or project dashboards.
*
* @param {!Object} data The parsed route data.
*/
_handleDashboardRoute(data) {
// User dashboard. We require viewing user to be logged in, else we
// redirect to login for self dashboard or simple owner search for
// other user dashboard.
return this.$.restAPI.getLoggedIn().then(loggedIn => {
if (!loggedIn) {
if (data.params[0].toLowerCase() === 'self') {
this._redirectToLogin(data.canonicalPath);
} else {
this._redirect('/q/owner:' + encodeURIComponent(data.params[0]));
}
} else {
this._setParams({
view: Gerrit.Nav.View.DASHBOARD,
user: data.params[0],
});
}
});
},
/**
* Handle custom dashboard routes.
*
* @param {!Object} data The parsed route data.
* @param {string=} opt_qs Optional query string associated with the route.
* If not given, window.location.search is used. (Used by tests).
*/
_handleCustomDashboardRoute(data, opt_qs) {
// opt_qs may be provided by a test, and it may have a falsy value
const qs = opt_qs !== undefined ? opt_qs : window.location.search;
const queryParams = this._parseQueryString(qs);
let title = 'Custom Dashboard';
const titleParam = queryParams.find(
elem => elem[0].toLowerCase() === 'title');
if (titleParam) {
title = titleParam[1];
}
// Dashboards support a foreach param which adds a base query to any
// additional query.
const forEachParam = queryParams.find(
elem => elem[0].toLowerCase() === 'foreach');
let forEachQuery = null;
if (forEachParam) {
forEachQuery = forEachParam[1];
}
const sectionParams = queryParams.filter(
elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title'
&& elem[0].toLowerCase() !== 'foreach');
const sections = sectionParams.map(elem => {
const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1];
return {
name: elem[0],
query,
};
});
if (sections.length > 0) {
// Custom dashboard view.
this._setParams({
view: Gerrit.Nav.View.DASHBOARD,
user: 'self',
sections,
title,
});
return Promise.resolve();
}
// Redirect /dashboard/ -> /dashboard/self.
this._redirect('/dashboard/self');
return Promise.resolve();
},
_handleProjectDashboardRoute(data) {
this._setParams({
view: Gerrit.Nav.View.DASHBOARD,
project: data.params[0],
dashboard: decodeURIComponent(data.params[1]),
});
},
_handleGroupInfoRoute(data) {
this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
},
_handleGroupRoute(data) {
this._setParams({
view: Gerrit.Nav.View.GROUP,
groupId: data.params[0],
});
},
_handleGroupAuditLogRoute(data) {
this._setParams({
view: Gerrit.Nav.View.GROUP,
detail: Gerrit.Nav.GroupDetailView.LOG,
groupId: data.params[0],
});
},
_handleGroupMembersRoute(data) {
this._setParams({
view: Gerrit.Nav.View.GROUP,
detail: Gerrit.Nav.GroupDetailView.MEMBERS,
groupId: data.params[0],
});
},
_handleGroupListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-admin-group-list',
offset: data.params[1] || 0,
filter: null,
openCreateModal: data.hash === 'create',
});
},
_handleGroupListFilterOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-admin-group-list',
offset: data.params.offset,
filter: data.params.filter,
});
},
_handleGroupListFilterRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-admin-group-list',
filter: data.params.filter || null,
});
},
_handleProjectsOldRoute(data) {
if (data.params[1]) {
this._redirect('/admin/repos/' + encodeURIComponent(data.params[1]));
} else {
this._redirect('/admin/repos');
}
},
_handleRepoCommandsRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.COMMANDS,
repo: data.params[0],
});
},
_handleRepoAccessRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.ACCESS,
repo: data.params[0],
});
},
_handleRepoDashboardsRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
repo: data.params[0],
});
},
_handleBranchListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.BRANCHES,
repo: data.params[0],
offset: data.params[2] || 0,
filter: null,
});
},
_handleBranchListFilterOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.BRANCHES,
repo: data.params.repo,
offset: data.params.offset,
filter: data.params.filter,
});
},
_handleBranchListFilterRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.BRANCHES,
repo: data.params.repo,
filter: data.params.filter || null,
});
},
_handleTagListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.TAGS,
repo: data.params[0],
offset: data.params[2] || 0,
filter: null,
});
},
_handleTagListFilterOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.TAGS,
repo: data.params.repo,
offset: data.params.offset,
filter: data.params.filter,
});
},
_handleTagListFilterRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.TAGS,
repo: data.params.repo,
filter: data.params.filter || null,
});
},
_handleRepoListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-repo-list',
offset: data.params[1] || 0,
filter: null,
openCreateModal: data.hash === 'create',
});
},
_handleRepoListFilterOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-repo-list',
offset: data.params.offset,
filter: data.params.filter,
});
},
_handleRepoListFilterRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-repo-list',
filter: data.params.filter || null,
});
},
_handleCreateProjectRoute(data) {
// Redirects the legacy route to the new route, which displays the project
// list with a hash 'create'.
this._redirect('/admin/projects#create');
},
_handleCreateGroupRoute(data) {
// Redirects the legacy route to the new route, which displays the group
// list with a hash 'create'.
this._redirect('/admin/groups#create');
},
_handleRepoRoute(data) {
this._setParams({
view: Gerrit.Nav.View.REPO,
repo: data.params[0],
});
},
_handlePluginListOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-plugin-list',
offset: data.params[1] || 0,
filter: null,
});
},
_handlePluginListFilterOffsetRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-plugin-list',
offset: data.params.offset,
filter: data.params.filter,
});
},
_handlePluginListFilterRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-plugin-list',
filter: data.params.filter || null,
});
},
_handlePluginListRoute(data) {
this._setParams({
view: Gerrit.Nav.View.ADMIN,
adminView: 'gr-plugin-list',
});
},
_handleQueryRoute(data) {
this._setParams({
view: Gerrit.Nav.View.SEARCH,
query: data.params[0],
offset: data.params[2],
});
},
_handleQueryLegacySuffixRoute(ctx) {
this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, ''));
},
_handleChangeNumberLegacyRoute(ctx) {
this._redirect('/c/' + encodeURIComponent(ctx.params[0]));
},
_handleChangeRoute(ctx) {
// Parameter order is based on the regex group number matched.
const params = {
project: ctx.params[0],
changeNum: ctx.params[1],
basePatchNum: ctx.params[4],
patchNum: ctx.params[6],
view: Gerrit.Nav.View.CHANGE,
};
this._redirectOrNavigate(params);
},
_handleDiffRoute(ctx) {
// Parameter order is based on the regex group number matched.
const params = {
project: ctx.params[0],
changeNum: ctx.params[1],
basePatchNum: ctx.params[4],
patchNum: ctx.params[6],
path: ctx.params[8],
view: Gerrit.Nav.View.DIFF,
};
const address = this._parseLineAddress(ctx.hash);
if (address) {
params.leftSide = address.leftSide;
params.lineNum = address.lineNum;
}
this._redirectOrNavigate(params);
},
_handleChangeLegacyRoute(ctx) {
// Parameter order is based on the regex group number matched.
const params = {
changeNum: ctx.params[0],
basePatchNum: ctx.params[3],
patchNum: ctx.params[5],
view: Gerrit.Nav.View.CHANGE,
querystring: ctx.querystring,
};
this._normalizeLegacyRouteParams(params);
},
_handleLegacyLinenum(ctx) {
this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1'));
},
_handleDiffLegacyRoute(ctx) {
// Parameter order is based on the regex group number matched.
const params = {
changeNum: ctx.params[0],
basePatchNum: ctx.params[2],
patchNum: ctx.params[4],
path: ctx.params[5],
view: Gerrit.Nav.View.DIFF,
};
const address = this._parseLineAddress(ctx.hash);
if (address) {
params.leftSide = address.leftSide;
params.lineNum = address.lineNum;
}
this._normalizeLegacyRouteParams(params);
},
_handleDiffEditRoute(ctx) {
// Parameter order is based on the regex group number matched.
this._redirectOrNavigate({
project: ctx.params[0],
changeNum: ctx.params[1],
patchNum: ctx.params[2],
path: ctx.params[3],
view: Gerrit.Nav.View.EDIT,
});
},
_handleChangeEditRoute(ctx) {
// Parameter order is based on the regex group number matched.
this._redirectOrNavigate({
project: ctx.params[0],
changeNum: ctx.params[1],
patchNum: ctx.params[3],
view: Gerrit.Nav.View.CHANGE,
edit: true,
});
},
/**
* Normalize the patch range params for a the change or diff view and
* redirect if URL upgrade is needed.
*/
_redirectOrNavigate(params) {
const needsRedirect = this._normalizePatchRangeParams(params);
if (needsRedirect) {
this._redirect(this._generateUrl(params));
} else {
this._setParams(params);
}
},
// TODO fix this so it properly redirects
// to /settings#Agreements (Scrolls down)
_handleAgreementsRoute(data) {
this._redirect('/settings/#Agreements');
},
_handleNewAgreementsRoute(data) {
data.params.view = Gerrit.Nav.View.AGREEMENTS;
this._setParams(data.params);
},
_handleSettingsLegacyRoute(data) {
this._setParams({
view: Gerrit.Nav.View.SETTINGS,
emailToken: data.params[0],
});
},
_handleSettingsRoute(data) {
this._setParams({view: Gerrit.Nav.View.SETTINGS});
},
_handleRegisterRoute(ctx) {
this._setParams({justRegistered: true});
let path = ctx.params[0] || '/';
// Prevent redirect looping.
if (path.startsWith('/register')) { path = '/'; }
if (path[0] !== '/') { return; }
this._redirect(this.getBaseUrl() + path);
},
/**
* Handler for routes that should pass through the router and not be caught
* by the catchall _handleDefaultRoute handler.
*/
_handlePassThroughRoute() {
location.reload();
},
/**
* URL may sometimes have /+/ encoded to / /.
* Context: Issue 6888, Issue 7100
*/
_handleImproperlyEncodedPlusRoute(ctx) {
let hash = this._getHashFromCanonicalPath(ctx.canonicalPath);
if (hash.length) { hash = '#' + hash; }
this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`);
},
_handlePluginScreen(ctx) {
const view = Gerrit.Nav.View.PLUGIN_SCREEN;
const plugin = ctx.params[0];
const screen = ctx.params[1];
this._setParams({view, plugin, screen});
},
_handleDocumentationSearchRoute(data) {
this._setParams({
view: Gerrit.Nav.View.DOCUMENTATION_SEARCH,
filter: data.params.filter || null,
});
},
_handleDocumentationSearchRedirectRoute(data) {
this._redirect('/Documentation/q/filter:' +
encodeURIComponent(data.params[0]));
},
_handleDocumentationRedirectRoute(data) {
if (data.params[1]) {
location.reload();
} else {
// Redirect /Documentation to /Documentation/index.html
this._redirect('/Documentation/index.html');
}
},
/**
* Catchall route for when no other route is matched.
*/
_handleDefaultRoute() {
this._show404();
},
_show404() {
// Note: the app's 404 display is tightly-coupled with catching 404
// network responses, so we simulate a 404 response status to display it.
// TODO: Decouple the gr-app error view from network responses.
this._app.dispatchEvent(new CustomEvent('page-error',
{detail: {response: {status: 404}}}));
},
});
})();