Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/elements/framework/mr-header/mr-header.js b/static_src/elements/framework/mr-header/mr-header.js
new file mode 100644
index 0000000..6603c85
--- /dev/null
+++ b/static_src/elements/framework/mr-header/mr-header.js
@@ -0,0 +1,427 @@
+// 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">Monorail</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);