blob: be6afad54d08149fc24eedb3557b987888c19556 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {LitElement, html, css} from 'lit-element';
import {connectStore} from 'reducers/base.js';
import * as userV0 from 'reducers/userV0.js';
import * as projectV0 from 'reducers/projectV0.js';
import * as sitewide from 'reducers/sitewide.js';
import {prpcClient} from 'prpc-client-instance.js';
import 'elements/framework/mr-keystrokes/mr-keystrokes.js';
import '../mr-dropdown/mr-dropdown.js';
import '../mr-dropdown/mr-account-dropdown.js';
import './mr-search-bar.js';
import {SHARED_STYLES} from 'shared/shared-styles.js';
import {logEvent} from 'monitoring/client-logger.js';
/**
* @type {Object<string, string>} JS coding of enum values from
* appengine/monorail/api/v3/api_proto/project_objects.proto.
*/
const projectRoles = Object.freeze({
OWNER: 'Owner',
MEMBER: 'Member',
CONTRIBUTOR: 'Contributor',
NONE: '',
});
/**
* `<mr-header>`
*
* The header for Monorail.
*
*/
export class MrHeader extends connectStore(LitElement) {
/** @override */
static get styles() {
return [
SHARED_STYLES,
css`
:host {
color: var(--chops-header-text-color);
box-sizing: border-box;
background: hsl(221, 67%, 92%);
width: 100%;
height: var(--monorail-header-height);
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
z-index: 800;
background-color: var(--chops-primary-header-bg);
border-bottom: var(--chops-normal-border);
top: 0;
position: fixed;
padding: 0 4px;
font-size: var(--chops-large-font-size);
}
@media (max-width: 840px) {
:host {
position: static;
}
}
a {
font-size: inherit;
color: var(--chops-link-color);
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 4px;
flex-grow: 0;
flex-shrink: 0;
}
a[hidden] {
display: none;
}
a.button {
font-size: inherit;
height: auto;
margin: 0 8px;
border: 0;
height: 30px;
}
.home-link {
color: var(--chops-gray-900);
letter-spacing: 0.5px;
font-size: 18px;
font-weight: 400;
display: flex;
font-stretch: 100%;
padding-left: 8px;
}
a.home-link img {
/** Cover up default padding with the custom logo. */
margin-left: -8px;
}
a.home-link:hover {
text-decoration: none;
}
mr-search-bar {
margin-left: 8px;
flex-grow: 2;
max-width: 1000px;
}
i.material-icons {
font-size: var(--chops-icon-font-size);
color: var(--chops-primary-icon-color);
}
i.material-icons[hidden] {
display: none;
}
.right-section {
font-size: inherit;
display: flex;
align-items: center;
height: 100%;
margin-left: auto;
justify-content: flex-end;
}
.hamburger-icon:hover {
text-decoration: none;
}
`,
];
}
/** @override */
render() {
return this.projectName ?
this._renderProjectScope() : this._renderNonProjectScope();
}
/**
* @return {TemplateResult}
*/
_renderProjectScope() {
return html`
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<mr-keystrokes
.issueId=${this.queryParams.id}
.queryParams=${this.queryParams}
.issueEntryUrl=${this.issueEntryUrl}
></mr-keystrokes>
<a href="/p/${this.projectName}/issues/list" class="home-link">
${this.projectThumbnailUrl ? html`
<img
class="project-logo"
src=${this.projectThumbnailUrl}
title=${this.projectName}
/>
` : this.projectName}
</a>
<mr-dropdown
class="project-selector"
.text=${this.projectName}
.items=${this._projectDropdownItems}
menuAlignment="left"
title=${this.presentationConfig.projectSummary}
></mr-dropdown>
<a class="button emphasized new-issue-link" href=${this.issueEntryUrl}>
New issue
</a>
<mr-search-bar
.projectName=${this.projectName}
.userDisplayName=${this.userDisplayName}
.projectSavedQueries=${this.presentationConfig.savedQueries}
.initialCan=${this._currentCan}
.initialQuery=${this._currentQuery}
.queryParams=${this.queryParams}
></mr-search-bar>
<div class="right-section">
<mr-dropdown
icon="settings"
label="Project Settings"
.items=${this._projectSettingsItems}
></mr-dropdown>
${this._renderAccount()}
</div>
`;
}
/**
* @return {TemplateResult}
*/
_renderNonProjectScope() {
return html`
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<a class="hamburger-icon" title="Main menu" hidden>
<i class="material-icons">menu</i>
</a>
${this._headerTitle ?
html`<span class="home-link">${this._headerTitle}</span>` :
html`<a href="/" class="home-link">avm99963 bugs</a>`}
<div class="right-section">
${this._renderAccount()}
</div>
`;
}
/**
* @return {TemplateResult}
*/
_renderAccount() {
if (!this.userDisplayName) {
return html`<a href=${this.loginUrl}>Sign in</a>`;
}
return html`
<mr-account-dropdown
.userDisplayName=${this.userDisplayName}
.logoutUrl=${this.logoutUrl}
.loginUrl=${this.loginUrl}
></mr-account-dropdown>
`;
}
/** @override */
static get properties() {
return {
loginUrl: {type: String},
logoutUrl: {type: String},
projectName: {type: String},
// Project thumbnail is set separately from presentationConfig to prevent
// "flashing" logo when navigating EZT pages.
projectThumbnailUrl: {type: String},
userDisplayName: {type: String},
isSiteAdmin: {type: Boolean},
userProjects: {type: Object},
presentationConfig: {type: Object},
queryParams: {type: Object},
// TODO(zhangtiff): Change this to be dynamically computed by the
// frontend with logic similar to ComputeIssueEntryURL().
issueEntryUrl: {type: String},
clientLogger: {type: Object},
_headerTitle: {type: String},
_currentQuery: {type: String},
_currentCan: {type: String},
};
}
/** @override */
constructor() {
super();
this.presentationConfig = {};
this.userProjects = {};
this.isSiteAdmin = false;
this._headerTitle = '';
}
/** @override */
stateChanged(state) {
this.projectName = projectV0.viewedProjectName(state);
this.userProjects = userV0.projects(state);
const currentUser = userV0.currentUser(state);
this.isSiteAdmin = currentUser ? currentUser.isSiteAdmin : false;
const presentationConfig = projectV0.viewedPresentationConfig(state);
this.presentationConfig = presentationConfig;
// Set separately in order allow EZT pages to load project logo before
// the GetPresentationConfig pRPC request.
this.projectThumbnailUrl = presentationConfig.projectThumbnailUrl;
this._headerTitle = sitewide.headerTitle(state);
this._currentQuery = sitewide.currentQuery(state);
this._currentCan = sitewide.currentCan(state);
this.queryParams = sitewide.queryParams(state);
}
/**
* @return {boolean} whether the currently logged in user has admin
* privileges for the currently viewed project.
*/
get canAdministerProject() {
if (!this.userDisplayName) return false; // Not logged in.
if (this.isSiteAdmin) return true;
if (!this.userProjects || !this.userProjects.ownerOf) return false;
return this.userProjects.ownerOf.includes(this.projectName);
}
/**
* @return {string} The name of the role the user has in the viewed project.
*/
get roleInCurrentProject() {
if (!this.userProjects || !this.projectName) return projectRoles.NONE;
const {ownerOf = [], memberOf = [], contributorTo = []} = this.userProjects;
if (ownerOf.includes(this.projectName)) return projectRoles.OWNER;
if (memberOf.includes(this.projectName)) return projectRoles.MEMBER;
if (contributorTo.includes(this.projectName)) {
return projectRoles.CONTRIBUTOR;
}
return projectRoles.NONE;
}
// TODO(crbug.com/monorail/6891): Remove once we deprecate the old issue
// filing wizard.
/**
* @return {string} A URL for the page the issue filing wizard posts to.
*/
get _wizardPostUrl() {
// The issue filing wizard posts to the legacy issue entry page's ".do"
// endpoint.
return `${this._origin}/p/${this.projectName}/issues/entry.do`;
}
/**
* @return {string} The domain name of the current page.
*/
get _origin() {
return window.location.origin;
}
/**
* Computes the URL the user should see to a file an issue, accounting
* for the case where a project has a customIssueEntryUrl to navigate to
* the wizard as well.
* @return {string} The URL that "New issue" button goes to.
*/
get issueEntryUrl() {
const config = this.presentationConfig;
const role = this.roleInCurrentProject;
const mayBeRedirectedToWizard = role === projectRoles.NONE;
if (!this.userDisplayName || !config || !config.customIssueEntryUrl ||
!mayBeRedirectedToWizard) {
return `/p/${this.projectName}/issues/entry`;
}
const token = prpcClient.token;
const customUrl = this.presentationConfig.customIssueEntryUrl;
return `${customUrl}?token=${token}&role=${
role}&continue=${this._wizardPostUrl}`;
}
/**
* @return {Array<MenuItem>} the dropdown items for the project selector,
* showing which projects a user can switch to.
*/
get _projectDropdownItems() {
const {userProjects, loginUrl} = this;
if (!this.userDisplayName) {
return [{text: 'Sign in to see your projects', url: loginUrl}];
}
const items = [];
const starredProjects = userProjects.starredProjects || [];
const projects = (userProjects.ownerOf || [])
.concat(userProjects.memberOf || [])
.concat(userProjects.contributorTo || []);
if (projects.length) {
projects.sort();
items.push({text: 'My Projects', separator: true});
projects.forEach((project) => {
items.push({text: project, url: `/p/${project}/issues/list`});
});
}
if (starredProjects.length) {
starredProjects.sort();
items.push({text: 'Starred Projects', separator: true});
starredProjects.forEach((project) => {
items.push({text: project, url: `/p/${project}/issues/list`});
});
}
if (items.length) {
items.push({separator: true});
}
items.push({text: 'All projects', url: '/hosting/'});
items.forEach((item) => {
item.handler = () => this._projectChangedHandler(item.url);
});
return items;
}
/**
* @return {Array<MenuItem>} dropdown menu items to show in the project
* settings menu.
*/
get _projectSettingsItems() {
const {projectName, canAdministerProject} = this;
const items = [
{text: 'People', url: `/p/${projectName}/people/list`},
{text: 'Development Process', url: `/p/${projectName}/adminIntro`},
{text: 'History', url: `/p/${projectName}/updates/list`},
];
if (canAdministerProject) {
items.push({separator: true});
items.push({text: 'Administer', url: `/p/${projectName}/admin`});
}
return items;
}
/**
* Records Google Analytics events for when users change projects using
* the selector.
* @param {string} url which project URL the user is navigating to.
*/
_projectChangedHandler(url) {
// Just log it to GA and continue.
logEvent('mr-header', 'project-change', url);
}
}
customElements.define('mr-header', MrHeader);