chromium-dashboard/client-src/elements/chromedash-header.ts

382 строки
11 KiB
TypeScript

import {LitElement, html, css, nothing} from 'lit';
import {showToastMessage, IS_MOBILE} from './utils';
import {SHARED_STYLES} from '../css/shared-css.js';
import {customElement, property, state} from 'lit/decorators.js';
import {User} from '../js-src/cs-client';
@customElement('chromedash-header')
export class ChromedashHeader extends LitElement {
static get styles() {
return [
...SHARED_STYLES,
css`
:host {
--md-gray-100-alpha: hsla(0, 0%, 0%, 0.12);
--md-gray-700-alpha: hsla(0, 0%, 0%, 0.62);
--md-blue-900: #01579b;
--nav-link-color: var(--md-gray-700-alpha);
--nav-link-font-size: 16px;
--nav-link-hover-background: var(--md-gray-100-alpha);
--nav-link-border: 2px solid transparent;
--nav-link-active-color: var(--md-blue-900);
--nav-link-active-border: 2px solid var(--nav-link-active-color);
}
header {
display: flex;
align-items: center;
user-select: none;
background: var(--card-background);
border-bottom: var(--card-border);
box-shadow: var(--card-box-shadow);
}
header a {
text-decoration: none !important;
}
header nav {
display: flex;
flex: 1;
align-items: center;
margin: 0 var(--content-padding);
-webkit-font-smoothing: initial;
}
header nav a {
cursor: pointer;
font-size: var(--nav-link-font-size);
text-align: center;
padding: var(--content-padding-half) var(--content-padding);
color: var(--nav-link-color);
white-space: nowrap;
border-bottom: var(--nav-link-border);
}
header nav a:hover {
color: black;
background: var(--nav-link-hover-background);
}
header nav a.disabled {
opacity: 0.5;
pointer-events: none;
}
header nav [active] {
color: var(--nav-link-active-color);
border-bottom: var(--nav-link-active-border);
}
header nav [active] a {
color: var(--nav-link-active-color);
}
header nav .nav-dropdown-container {
position: relative;
}
header nav .nav-dropdown-container ul {
display: none;
position: absolute;
top: 80%;
left: 0;
list-style: none;
z-index: 1;
background: var(--card-background);
border-bottom: var(--card-border);
box-shadow: var(--card-box-shadow);
}
header nav .nav-dropdown-container a {
display: block;
}
header nav .nav-dropdown-container .nav-dropdown-trigger:hover + ul,
header nav .nav-dropdown-container ul:hover {
display: block;
}
header aside a {
color: var(--logo-color);
}
header aside h1 {
line-height: 1;
}
header aside img {
height: 24px;
width: 24px;
}
.flex-container {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
align-items: center;
width: 100%;
}
.menu {
margin-left: 15px;
margin-right: 7px;
align-items: center;
}
.menu:hover {
color: black;
background: var(--nav-link-hover-background);
}
.menu [active] {
color: var(--nav-link-active-color);
border-bottom: var(--nav-link-active-border);
}
@media only screen and (max-width: 700px) {
header {
--logoSize: 24px;
margin: 0;
display: flex;
}
header aside {
display: flex;
padding: var(--content-padding-half);
border-radius: 0;
background: inherit;
}
}
`,
];
}
@property({type: String})
appTitle = '';
@property({type: String})
googleSignInClientId = '';
@property({type: String})
devMode = '';
@property({type: String})
currentPage = '';
@property({type: Object})
user = {} as User;
@state()
loading = false;
connectedCallback() {
super.connectedCallback();
if (IS_MOBILE) {
// Login UI will be handled by chromedash-drawer instead.
return;
}
// user is passed in from chromedash-app
if (this.user && this.user.email) return;
// user is passed in from chromedash-app, but the user is not logged in
if (!this.user) {
if (!window['isPlaywright']) {
// Insert the google signin button first.
// Only insert if not running playwright.
this.initializeGoogleSignIn();
}
if (this.devMode == 'True') {
// Insert the testing signin second, so it appears to the left
// of the google signin button, with a large margin on the right.
this.initializeTestingSignIn();
}
return;
}
// user is not passed in from anywhere, i.e. this.user is still {}
// this is for MPA pages where this component is initialized in _base.html
this.loading = true;
window.csClient
.getPermissions()
.then(user => {
this.user = user;
if (!this.user) {
if (!window['isPlaywright']) {
this.initializeGoogleSignIn();
}
if (this.devMode == 'True') {
this.initializeTestingSignIn();
}
}
})
.catch(() => {
showToastMessage(
'Some errors occurred. Please refresh the page or try again later.'
);
})
.finally(() => {
this.loading = false;
});
}
initializeGoogleSignIn() {
google.accounts.id.initialize({
client_id: this.googleSignInClientId,
callback: this.handleCredentialResponse,
use_fedcm_for_prompt: true,
});
google.accounts.id.prompt();
// Google Identity Services Library cannot find elements in a shadow DOM,
// so we create signInButton element at the document level and insert it
// in the light DOM of the header, which will be rendered in the <slot> below
const signInButton = document.createElement('div');
google.accounts.id.renderButton(signInButton, {type: 'standard'});
const appComponent = document.querySelector('chromedash-app');
if (appComponent) {
appComponent.insertAdjacentElement('afterbegin', signInButton); // for SPA
} else {
this.insertAdjacentElement('afterbegin', signInButton); // for MPA
}
}
initializeTestingSignIn() {
// Create DEV_MODE login button for testing
const signInTestingButton = document.createElement('button');
signInTestingButton.innerText = 'Sign in as example@chromium.org';
signInTestingButton.setAttribute('type', 'button');
signInTestingButton.setAttribute('data-testid', 'dev-mode-sign-in-button');
signInTestingButton.setAttribute(
'style',
'margin-right: 300px; z-index:1000; background: lightblue; border: 1px solid blue;'
);
signInTestingButton.addEventListener('click', () => {
// POST to '/dev/mock_login' to login as example@chromium.
fetch('/dev/mock_login', {method: 'POST'})
.then(response => {
if (!response.ok) {
throw new Error(`Sign in failed! Response: ${response}`);
}
})
.then(() => {
setTimeout(() => {
const url = window.location.href.split('?')[0];
window.location.href = url;
}, 1000);
})
.catch(error => {
console.error('Sign in failed.', error);
});
});
const signInButtonContainer = document.querySelector('chromedash-app');
if (signInButtonContainer) {
signInButtonContainer.insertAdjacentElement(
'afterbegin',
signInTestingButton
); // for SPA
} else {
this.insertAdjacentElement('afterbegin', signInTestingButton); // for MPA
}
}
handleCredentialResponse(credentialResponse) {
window.csClient
.signIn(credentialResponse)
.then(() => {
setTimeout(() => {
const url = window.location.href.split('?')[0];
window.location = url as string & Location;
}, 1000);
})
.catch(() => {
console.error('Sign in failed, so signing out to allow retry');
this.signOut();
});
}
handleSignOutClick(e) {
e.preventDefault();
this.signOut();
}
signOut() {
window.csClient.signOut().then(() => {
window.location.reload();
});
}
isCurrentPage(href) {
return this.currentPage.startsWith(href);
}
_fireEvent(eventName, detail) {
const event = new CustomEvent(eventName, {
bubbles: true,
composed: true,
detail,
});
this.dispatchEvent(event);
}
handleDrawer() {
this._fireEvent('drawer-clicked', {});
}
renderAccountMenu() {
return html`
${this.user
? html`
${this.user.can_create_feature && !this.isCurrentPage('/guide/new')
? html`
<sl-button
data-testid="create-feature-button"
href="/guide/new"
variant="primary"
size="small"
>
Create feature
</sl-button>
`
: nothing}
<div class="nav-dropdown-container" data-testid="account-indicator">
<a class="nav-dropdown-trigger">
${this.user.email}
<iron-icon icon="chromestatus:arrow-drop-down"></iron-icon>
</a>
<ul>
<li><a href="/settings">Settings</a></li>
<li>
<a
href="#"
id="sign-out-link"
data-testid="sign-out-link"
@click=${this.handleSignOutClick}
>Sign out</a
>
</li>
</ul>
</div>
`
: html` <slot></slot> `}
`;
}
render() {
let accountMenu = html``;
if (!IS_MOBILE && !this.loading) {
accountMenu = html` <div class="flex-container">
${this.renderAccountMenu()}
</div>`;
}
return html`
<header data-testid="header">
<sl-icon-button
data-testid="menu"
variant="text"
library="material"
class="menu"
style="font-size: 2.4rem;"
name="menu_20px"
@click="${this.handleDrawer}"
>
</sl-icon-button>
<aside>
<a href="/roadmap" target="_top">
<h1>
<img src="/static/img/chrome_logo.svg" />
${this.appTitle}
</h1>
</a>
</aside>
<nav>${accountMenu}</nav>
</header>
`;
}
}