
* stable-3.1: Update git submodules Update git submodules Update git submodules Update git submodules Update git submodules Rely on a released version of gatling-git in stable branch Document Gerrit log files and their format Update git submodules Update git submodules Update git submodules Update git submodules Upgrade soy to 2020-08-24 config-reverseproxy.txt: Document X-Forwarded-For header ElasticVersionTest: Use latest V6 in V7 assertions ElasticContainer: Upgrade V6_8 to elasticsearch 6.8.12 e2e-tests: Add presentation links to documentation Redirect GWT links to project dashboard to Polygerit Doc: Make command for configuring bazel copyable Update git submodules Update git submodules Update git submodules Introduce sshd.gracefulStopTimeout ChangeEdits: Don't wrap Response.none() in Response.ok() Update git submodules Update git submodules Upgrade caffeine to 2.8.5 Add Eclipse support for Gatling tests Document how to mitigate the issue of broken Eclipse project on MacOS Change-Id: I0e650fbf0fbab42f5df77e9c16357125528a4160
1574 lines
47 KiB
JavaScript
1574 lines
47 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.
|
|
*/
|
|
import '../../../scripts/bundled-polymer.js';
|
|
|
|
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
|
|
import '../gr-reporting/gr-reporting.js';
|
|
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
|
|
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
|
|
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
|
|
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
|
|
import page from 'page/page.mjs';
|
|
import {htmlTemplate} from './gr-router_html.js';
|
|
import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
|
|
import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
|
|
import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
|
|
import {GerritNav} from '../gr-navigation/gr-navigation.js';
|
|
|
|
const RoutePattern = {
|
|
ROOT: '/',
|
|
|
|
DASHBOARD: /^\/dashboard\/(.+)$/,
|
|
CUSTOM_DASHBOARD: /^\/dashboard\/?$/,
|
|
PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/,
|
|
LEGACY_PROJECT_DASHBOARD: /^\/projects\/(.+),dashboards\/(.+)/,
|
|
|
|
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-)?([^,]+)$/,
|
|
|
|
// Redirects /groups/self to /settings/#Groups for GWT compatibility
|
|
GROUP_SELF: /^\/groups\/self/,
|
|
|
|
// 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[#lineNum]
|
|
DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
|
|
|
|
// 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.
|
|
// If you move this code to other place, please update comment about
|
|
// gr-router and gr-app in the PolyGerritIndexHtml.soy file if needed
|
|
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('WebComponentsReady', () => {
|
|
reporting.timeEnd('WebComponentsReady');
|
|
});
|
|
})();
|
|
|
|
/**
|
|
* @extends Polymer.Element
|
|
*/
|
|
class GrRouter extends mixinBehaviors( [
|
|
BaseUrlBehavior,
|
|
PatchSetBehavior,
|
|
URLEncodingBehavior,
|
|
], GestureEventListeners(
|
|
LegacyElementMixin(
|
|
PolymerElement))) {
|
|
static get template() { return htmlTemplate; }
|
|
|
|
static get is() { return 'gr-router'; }
|
|
|
|
static get properties() {
|
|
return {
|
|
_app: {
|
|
type: Object,
|
|
value: app,
|
|
},
|
|
_isRedirecting: Boolean,
|
|
// This variable is to differentiate between internal navigation (false)
|
|
// and for first navigation in app after loaded from server (true).
|
|
_isInitialLoad: {
|
|
type: Boolean,
|
|
value: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
start() {
|
|
if (!this._app) { return; }
|
|
this._startRouter();
|
|
}
|
|
|
|
_setParams(params) {
|
|
this._appElement().params = params;
|
|
}
|
|
|
|
_appElement() {
|
|
// In Polymer2 you have to reach through the shadow root of the app
|
|
// element. This obviously breaks encapsulation.
|
|
// TODO(brohlfs): Make this more elegant, e.g. by exposing app-element
|
|
// explicitly in app, or by delegating to it.
|
|
return document.getElementById('app-element') ||
|
|
document.getElementById('app').shadowRoot.getElementById(
|
|
'app-element');
|
|
}
|
|
|
|
_redirect(url) {
|
|
this._isRedirecting = true;
|
|
page.redirect(url);
|
|
}
|
|
|
|
/**
|
|
* @param {!Object} params
|
|
* @return {string}
|
|
*/
|
|
_generateUrl(params) {
|
|
const base = this.getBaseUrl();
|
|
let url = '';
|
|
const Views = GerritNav.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 GerritNav.WeblinkType.FILE:
|
|
return this._getFileWebLinks(params);
|
|
case GerritNav.WeblinkType.CHANGE:
|
|
return this._getChangeWeblinks(params);
|
|
case GerritNav.WeblinkType.PATCHSET:
|
|
return this._getPatchSetWeblink(params);
|
|
default:
|
|
console.warn(`Unsupported weblink ${type}!`);
|
|
}
|
|
}
|
|
|
|
_getPatchSetWeblink(params) {
|
|
const {commit, options} = params;
|
|
const {weblinks, config} = options || {};
|
|
const name = commit && commit.slice(0, 7);
|
|
const weblink = this._getBrowseCommitWeblink(weblinks, config);
|
|
if (!weblink || !weblink.url) {
|
|
return {name};
|
|
} else {
|
|
return {name, url: weblink.url};
|
|
}
|
|
}
|
|
|
|
_firstCodeBrowserWeblink(weblinks) {
|
|
// This is an ordered whitelist of web link types that provide direct
|
|
// links to the commit in the url property.
|
|
const codeBrowserLinks = ['gitiles', 'browse', 'gitweb'];
|
|
for (let i = 0; i < codeBrowserLinks.length; i++) {
|
|
const weblink =
|
|
weblinks.find(weblink => weblink.name === codeBrowserLinks[i]);
|
|
if (weblink) { return weblink; }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
_getBrowseCommitWeblink(weblinks, config) {
|
|
if (!weblinks) { return null; }
|
|
let weblink;
|
|
// Use primary weblink if configured and exists.
|
|
if (config && config.gerrit && config.gerrit.primary_weblink_name) {
|
|
weblink = weblinks.find(
|
|
weblink => weblink.name === config.gerrit.primary_weblink_name
|
|
);
|
|
}
|
|
if (!weblink) {
|
|
weblink = this._firstCodeBrowserWeblink(weblinks);
|
|
}
|
|
if (!weblink) { return null; }
|
|
return weblink;
|
|
}
|
|
|
|
_getChangeWeblinks({repo, commit, options: {weblinks, config}}) {
|
|
if (!weblinks || !weblinks.length) return [];
|
|
const commitWeblink = this._getBrowseCommitWeblink(weblinks, config);
|
|
return weblinks.filter(weblink =>
|
|
!commitWeblink ||
|
|
!commitWeblink.name ||
|
|
weblink.name !== commitWeblink.name);
|
|
}
|
|
|
|
_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 === GerritNav.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 === GerritNav.GroupDetailView.MEMBERS) {
|
|
url += ',members';
|
|
} else if (params.detail === GerritNav.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 === GerritNav.RepoDetailView.ACCESS) {
|
|
url += ',access';
|
|
} else if (params.detail === GerritNav.RepoDetailView.BRANCHES) {
|
|
url += ',branches';
|
|
} else if (params.detail === GerritNav.RepoDetailView.TAGS) {
|
|
url += ',tags';
|
|
} else if (params.detail === GerritNav.RepoDetailView.COMMANDS) {
|
|
url += ',commands';
|
|
} else if (params.detail === GerritNav.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;
|
|
}
|
|
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(new Error());
|
|
}
|
|
});
|
|
}
|
|
|
|
/** Page.js middleware that warms the REST API's logged-in cache line. */
|
|
_loadUserMiddleware(ctx, next) {
|
|
this.$.restAPI.getLoggedIn().then(() => { next(); });
|
|
}
|
|
|
|
/** Page.js middleware that try parse the querystring into queryMap. */
|
|
_queryStringMiddleware(ctx, next) {
|
|
let queryMap = new Map();
|
|
if (ctx.querystring) {
|
|
// https://caniuse.com/#search=URLSearchParams
|
|
if (window.URLSearchParams) {
|
|
queryMap = new URLSearchParams(ctx.querystring);
|
|
} else {
|
|
queryMap = new Map(this._parseQueryString(ctx.querystring));
|
|
}
|
|
}
|
|
ctx.queryMap = queryMap;
|
|
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,
|
|
(ctx, next) => this._loadUserMiddleware(ctx, next),
|
|
(ctx, next) => this._queryStringMiddleware(ctx, next),
|
|
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);
|
|
}
|
|
|
|
GerritNav.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;
|
|
this._isInitialLoad = 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.dispatchEvent(new CustomEvent('location-change', {
|
|
detail: {
|
|
hash: window.location.hash,
|
|
pathname: window.location.pathname,
|
|
},
|
|
composed: true, bubbles: true,
|
|
}));
|
|
}, 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.LEGACY_PROJECT_DASHBOARD,
|
|
'_handleLegacyProjectDashboardRoute');
|
|
|
|
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_SELF, '_handleGroupSelfRedirectRoute',
|
|
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+-is:wip');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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: GerritNav.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: GerritNav.View.DASHBOARD,
|
|
user: 'self',
|
|
sections,
|
|
title,
|
|
});
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// Redirect /dashboard/ -> /dashboard/self.
|
|
this._redirect('/dashboard/self');
|
|
return Promise.resolve();
|
|
}
|
|
|
|
_handleProjectDashboardRoute(data) {
|
|
const project = data.params[0];
|
|
this._setParams({
|
|
view: GerritNav.View.DASHBOARD,
|
|
project,
|
|
dashboard: decodeURIComponent(data.params[1]),
|
|
});
|
|
this.$.reporting.setRepoName(project);
|
|
}
|
|
|
|
_handleLegacyProjectDashboardRoute(data) {
|
|
this._redirect('/p/' + data.params[0] + '/+/dashboard/' + data.params[1]);
|
|
}
|
|
|
|
_handleGroupInfoRoute(data) {
|
|
this._redirect('/admin/groups/' + encodeURIComponent(data.params[0]));
|
|
}
|
|
|
|
_handleGroupSelfRedirectRoute(data) {
|
|
this._redirect('/settings/#Groups');
|
|
}
|
|
|
|
_handleGroupRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.GROUP,
|
|
groupId: data.params[0],
|
|
});
|
|
}
|
|
|
|
_handleGroupAuditLogRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.GROUP,
|
|
detail: GerritNav.GroupDetailView.LOG,
|
|
groupId: data.params[0],
|
|
});
|
|
}
|
|
|
|
_handleGroupMembersRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.GROUP,
|
|
detail: GerritNav.GroupDetailView.MEMBERS,
|
|
groupId: data.params[0],
|
|
});
|
|
}
|
|
|
|
_handleGroupListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-admin-group-list',
|
|
offset: data.params[1] || 0,
|
|
filter: null,
|
|
openCreateModal: data.hash === 'create',
|
|
});
|
|
}
|
|
|
|
_handleGroupListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-admin-group-list',
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
}
|
|
|
|
_handleGroupListFilterRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-admin-group-list',
|
|
filter: data.params.filter || null,
|
|
});
|
|
}
|
|
|
|
_handleProjectsOldRoute(data) {
|
|
let params = '';
|
|
if (data.params[1]) {
|
|
params = encodeURIComponent(data.params[1]);
|
|
if (data.params[1].includes(',')) {
|
|
params =
|
|
encodeURIComponent(data.params[1]).replace('%2C', ',');
|
|
}
|
|
}
|
|
|
|
this._redirect(`/admin/repos/${params}`);
|
|
}
|
|
|
|
_handleRepoCommandsRoute(data) {
|
|
const repo = data.params[0];
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.COMMANDS,
|
|
repo,
|
|
});
|
|
this.$.reporting.setRepoName(repo);
|
|
}
|
|
|
|
_handleRepoAccessRoute(data) {
|
|
const repo = data.params[0];
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.ACCESS,
|
|
repo,
|
|
});
|
|
this.$.reporting.setRepoName(repo);
|
|
}
|
|
|
|
_handleRepoDashboardsRoute(data) {
|
|
const repo = data.params[0];
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.DASHBOARDS,
|
|
repo,
|
|
});
|
|
this.$.reporting.setRepoName(repo);
|
|
}
|
|
|
|
_handleBranchListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.BRANCHES,
|
|
repo: data.params[0],
|
|
offset: data.params[2] || 0,
|
|
filter: null,
|
|
});
|
|
}
|
|
|
|
_handleBranchListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.BRANCHES,
|
|
repo: data.params.repo,
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
}
|
|
|
|
_handleBranchListFilterRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.BRANCHES,
|
|
repo: data.params.repo,
|
|
filter: data.params.filter || null,
|
|
});
|
|
}
|
|
|
|
_handleTagListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.TAGS,
|
|
repo: data.params[0],
|
|
offset: data.params[2] || 0,
|
|
filter: null,
|
|
});
|
|
}
|
|
|
|
_handleTagListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.TAGS,
|
|
repo: data.params.repo,
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
}
|
|
|
|
_handleTagListFilterRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
detail: GerritNav.RepoDetailView.TAGS,
|
|
repo: data.params.repo,
|
|
filter: data.params.filter || null,
|
|
});
|
|
}
|
|
|
|
_handleRepoListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-repo-list',
|
|
offset: data.params[1] || 0,
|
|
filter: null,
|
|
openCreateModal: data.hash === 'create',
|
|
});
|
|
}
|
|
|
|
_handleRepoListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-repo-list',
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
}
|
|
|
|
_handleRepoListFilterRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.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/repos#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) {
|
|
const repo = data.params[0];
|
|
this._setParams({
|
|
view: GerritNav.View.REPO,
|
|
repo,
|
|
});
|
|
this.$.reporting.setRepoName(repo);
|
|
}
|
|
|
|
_handlePluginListOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
offset: data.params[1] || 0,
|
|
filter: null,
|
|
});
|
|
}
|
|
|
|
_handlePluginListFilterOffsetRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
offset: data.params.offset,
|
|
filter: data.params.filter,
|
|
});
|
|
}
|
|
|
|
_handlePluginListFilterRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
filter: data.params.filter || null,
|
|
});
|
|
}
|
|
|
|
_handlePluginListRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.View.ADMIN,
|
|
adminView: 'gr-plugin-list',
|
|
});
|
|
}
|
|
|
|
_handleQueryRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.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: GerritNav.View.CHANGE,
|
|
queryMap: ctx.queryMap,
|
|
};
|
|
|
|
this.$.reporting.setRepoName(params.project);
|
|
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: GerritNav.View.DIFF,
|
|
};
|
|
|
|
const address = this._parseLineAddress(ctx.hash);
|
|
if (address) {
|
|
params.leftSide = address.leftSide;
|
|
params.lineNum = address.lineNum;
|
|
}
|
|
this.$.reporting.setRepoName(params.project);
|
|
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: GerritNav.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: GerritNav.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.
|
|
const project = ctx.params[0];
|
|
this._redirectOrNavigate({
|
|
project,
|
|
changeNum: ctx.params[1],
|
|
patchNum: ctx.params[2],
|
|
path: ctx.params[3],
|
|
lineNum: ctx.hash,
|
|
view: GerritNav.View.EDIT,
|
|
});
|
|
this.$.reporting.setRepoName(project);
|
|
}
|
|
|
|
_handleChangeEditRoute(ctx) {
|
|
// Parameter order is based on the regex group number matched.
|
|
const project = ctx.params[0];
|
|
this._redirectOrNavigate({
|
|
project,
|
|
changeNum: ctx.params[1],
|
|
patchNum: ctx.params[3],
|
|
view: GerritNav.View.CHANGE,
|
|
edit: true,
|
|
});
|
|
this.$.reporting.setRepoName(project);
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
_handleAgreementsRoute() {
|
|
this._redirect('/settings/#Agreements');
|
|
}
|
|
|
|
_handleNewAgreementsRoute(data) {
|
|
data.params.view = GerritNav.View.AGREEMENTS;
|
|
this._setParams(data.params);
|
|
}
|
|
|
|
_handleSettingsLegacyRoute(data) {
|
|
// email tokens may contain '+' but no space.
|
|
// The parameter parsing replaces all '+' with a space,
|
|
// undo that to have valid tokens.
|
|
const token = data.params[0].replace(/ /g, '+');
|
|
this._setParams({
|
|
view: GerritNav.View.SETTINGS,
|
|
emailToken: token,
|
|
});
|
|
}
|
|
|
|
_handleSettingsRoute(data) {
|
|
this._setParams({view: GerritNav.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 = GerritNav.View.PLUGIN_SCREEN;
|
|
const plugin = ctx.params[0];
|
|
const screen = ctx.params[1];
|
|
this._setParams({view, plugin, screen});
|
|
}
|
|
|
|
_handleDocumentationSearchRoute(data) {
|
|
this._setParams({
|
|
view: GerritNav.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() {
|
|
if (this._isInitialLoad) {
|
|
// Server recognized this route as polygerrit, so we show 404.
|
|
this._show404();
|
|
} else {
|
|
// Route can be recognized by server, so we pass it to server.
|
|
this._handlePassThroughRoute();
|
|
}
|
|
}
|
|
|
|
_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._appElement().dispatchEvent(new CustomEvent('page-error',
|
|
{detail: {response: {status: 404}}}));
|
|
}
|
|
}
|
|
|
|
customElements.define(GrRouter.is, GrRouter);
|