Finish cloud login flow (#420)

* Finish cloud login flow

* Address comments

* Fix cache travis
This commit is contained in:
Paulus Schoutsen 2017-09-09 19:47:07 -07:00 committed by GitHub
parent 3912347f3d
commit 4c96240ff6
14 changed files with 795 additions and 120 deletions

View File

@ -39,6 +39,7 @@
"no-param-reassign": 0,
"no-multi-assign": 0,
"radix": 0,
"no-alert": 0,
"import/prefer-default-export": 0,
"react/jsx-no-bind": [2, { "ignoreRefs": true }],
"react/jsx-no-duplicate-props": 2,

View File

@ -1,8 +1,8 @@
sudo: false
language: node_js
cache:
yarn: true
directories:
- node_modules
- bower_components
install:
- yarn install

View File

@ -3,12 +3,16 @@
<link rel="import" href="../../../bower_components/paper-item/paper-item-body.html">
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
<link rel="import" href="../../../src/layouts/hass-subpage.html">
<link rel="import" href="../../../src/util/hass-mixins.html">
<link rel="import" href='../../../src/resources/ha-style.html'>
<dom-module id="ha-config-cloud-account">
<template>
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
}
paper-card {
display: block;
}
@ -25,20 +29,36 @@
text-align: center;
}
</style>
<paper-card>
<div class='account'>
<paper-item-body two-line>
[[account.first_name]] [[account.last_name]]
<div secondary>[[account.email]]</div>
</paper-item-body>
<paper-button
class='warning'
on-tap='handleLogout'
>Sign out</paper-button>
</div>
</paper-card>
<hass-subpage title='Cloud Account'>
<div class='content'>
<ha-config-section
is-wide='[[isWide]]'
>
<span slot='header'>Home Assistant Cloud</span>
<span slot='introduction'>
The Home Assistant Cloud allows you to opt-in to functions that will bring your Home Assistant experience to the next level.
<div class='soon'>More configuration options coming soon.</div>
<p><i>
Home Assistant will never share information with our cloud without your prior permission.
</i></p>
</span>
<paper-card>
<div class='account'>
<paper-item-body>
[[account.email]]
</paper-item-body>
<paper-button
class='warning'
on-tap='handleLogout'
>Sign out</paper-button>
</div>
</paper-card>
<div class='soon'>More configuration options coming soon.</div>
</ha-config-section>
</div>
</hass-subpage>
</template>
</dom-module>

View File

@ -0,0 +1,236 @@
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
<link rel="import" href='../../../bower_components/paper-card/paper-card.html'>
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
<link rel="import" href="../../../src/layouts/hass-subpage.html">
<link rel="import" href="../../../src/util/hass-mixins.html">
<link rel="import" href='../../../src/resources/ha-style.html'>
<link rel="import" href='../../../src/components/buttons/ha-progress-button.html'>
<dom-module id="ha-config-cloud-forgot-password">
<template>
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
}
paper-card {
display: block;
max-width: 600px;
margin: 0 auto;
margin-top: 24px;
}
h1 {
@apply(--paper-font-headline);
margin: 0;
}
.error {
color: var(--google-red-500);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.card-actions a {
color: var(--primary-text-color);
}
[hidden] {
display: none;
}
</style>
<hass-subpage title="Forgot Password">
<div class='content'>
<template is='dom-if' if='[[!_hasToken]]'>
<paper-card>
<div class='card-content'>
<h1>Forgot Password</h1>
<p>
Enter your email address and we will send you a link to reset your password.
</p>
<paper-input
autofocus
label='E-mail'
value='{{email}}'
type='email'
on-keydown='_keyDown'
></paper-input>
<div class='error' hidden$='[[!error]]'>[[error]]</div>
</div>
<div class='card-actions'>
<ha-progress-button
on-tap='_handleEmailPasswordReset'
progress='[[_requestInProgress]]'
>Send reset email</ha-progress-button>
<button
class='link'
hidden='[[_requestInProgress]]'
on-click='_handleHaveToken'
>have a token?</button>
</div>
</paper-card>
</template>
<template is='dom-if' if='[[_hasToken]]'>
<paper-card>
<div class='card-content'>
<h1>Confirm new password</h1>
<template is='dom-if' if='[[_showEmailInputForConfirmation]]'>
<paper-input
label='E-mail'
type='email'
value='{{email}}'
on-keydown='_keyDown'
></paper-input>
</template>
<paper-input
label='Confirmation code'
value='{{_confirmationCode}}'
on-keydown='_keyDown'
type='number'
></paper-input>
<paper-input
label='New password'
value='{{_newPassword}}'
on-keydown='_keyDown'
type='password'
></paper-input>
<div class='error' hidden$='[[!error]]'>[[error]]</div>
</div>
<div class='card-actions'>
<ha-progress-button
on-tap='_handleConfirmPasswordReset'
progress='[[_requestInProgress]]'
>Reset Password</ha-progress-button>
</div>
</paper-card>
</template>
</div>
</hass-subpage>
</template>
</dom-module>
<script>
class HaConfigCloudForgotPassword extends
window.hassMixins.NavigateMixin(
window.hassMixins.EventsMixin(Polymer.Element)) {
static get is() { return 'ha-config-cloud-forgot-password'; }
static get properties() {
return {
hass: Object,
email: {
type: String,
notify: true,
},
_hasToken: {
type: Boolean,
value: false,
},
_newPassword: {
type: String,
value: '',
},
_confirmationCode: {
type: String,
value: '',
},
_showEmailInputForConfirmation: {
type: Boolean,
value: false,
},
_requestInProgress: {
type: Boolean,
value: false,
},
};
}
static get observers() {
return [
'_inputChanged(email, _newPassword)',
];
}
_inputChanged() {
this.error = false;
}
_keyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
if (this._hasToken) {
this._handleConfirmPasswordReset();
} else {
this._handleEmailPasswordReset();
}
ev.preventDefault();
}
}
_handleEmailPasswordReset() {
if (!this.email) {
this.error = 'Email is required.';
}
if (this.error) return;
this._requestInProgress = true;
this.hass.callApi('post', 'cloud/forgot_password', {
email: this.email,
}).then(
() => {
this._hasToken = true;
this._requestInProgress = false;
}, (err) => {
this._requestInProgress = false;
this.error = err && err.body && err.body.message ?
err.body.message : 'Unknown error';
});
}
_handleHaveToken() {
this._error = '';
this._showEmailInputForConfirmation = true;
this._hasToken = true;
}
_handleConfirmPasswordReset() {
this.error = '';
if (!this.email) {
this.error += 'Email is required. ';
}
if (!this._confirmationCode) {
this.error += 'Confirmation code is required. ';
}
if (!this._newPassword) {
this.error += 'New password is required. ';
} else if (this._newPassword.length < 6) {
this.error += 'New password should be at least 6 characters.';
}
if (this.error) return;
this._requestInProgress = true;
this.hass.callApi('post', 'cloud/confirm_forgot_password', {
email: this.email,
confirmation_code: this._confirmationCode,
new_password: this._newPassword,
}).then(
() => {
// eslint-disable-next-line
alert('Password reset successful! You can now login.');
this.navigate('config/cloud/login');
}, (err) => {
this._requestInProgress = false;
this.error = err && err.body && err.body.message ?
err.body.message : 'Unknown error';
});
}
}
customElements.define(HaConfigCloudForgotPassword.is, HaConfigCloudForgotPassword);
</script>

View File

@ -3,61 +3,130 @@
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
<link rel="import" href="../../../src/layouts/hass-subpage.html">
<link rel="import" href="../../../src/util/hass-mixins.html">
<link rel="import" href='../../../src/resources/ha-style.html'>
<link rel="import" href='../../../src/components/buttons/ha-progress-button.html'>
<dom-module id="ha-config-cloud-login">
<template>
<style include="iron-flex ha-style">
.content {
padding-bottom: 24px;
}
paper-card {
display: block;
}
paper-item {
cursor: pointer;
}
paper-card:last-child {
margin-top: 24px;
}
h1 {
@apply(--paper-font-headline);
margin: 0;
}
.error {
color: var(--google-red-500);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
[hidden] {
display: none;
}
</style>
<paper-card>
<div class='card-content'>
<h1>Sign In</h1>
<paper-input
label='Username'
value='{{username}}'
error-message='Failed to login'
invalid='[[error]]'
on-keydown='_keyDown'
></paper-input>
<paper-input
label='Password'
value='{{password}}'
type='password'
on-keydown='_keyDown'
></paper-input>
<hass-subpage title='Cloud Login'>
<div class='content'>
<ha-config-section
is-wide='[[isWide]]'
>
<span slot='header'>Home Assistant Cloud</span>
<span slot='introduction'>
The Home Assistant Cloud allows you to opt-in to functions that will bring your Home Assistant experience to the next level.
<p><i>
Home Assistant will never share information with our cloud without your prior permission.
</i></p>
</span>
<paper-card>
<div class='card-content'>
<h1>Sign In</h1>
<paper-input
label='Email'
id='emailInput'
type='email'
value='{{email}}'
on-keydown='_keyDown'
></paper-input>
<paper-input
label='Password'
value='{{_password}}'
type='password'
on-keydown='_keyDown'
></paper-input>
<div class='error' hidden$='[[!error]]'>[[error]]</div>
</div>
<div class='card-actions'>
<ha-progress-button
on-tap='_handleLogin'
progress='[[_requestInProgress]]'
>Sign in</ha-progress-button>
<button
class='link'
hidden='[[_requestInProgress]]'
on-click='_handleForgotPassword'
>forgot password?</button>
</div>
</paper-card>
<paper-card>
<paper-item on-tap='_handleRegister'>
<paper-item-body two-line>
Create Account
<div secondary>It is free and allows easy integration with voice assistants.</div>
</paper-item-body>
<iron-icon icon='mdi:chevron-right'></iron-icon>
</paper-item>
</paper-card>
</ha-config-section>
</div>
<div class='card-actions'>
<paper-button
on-tap='_handleLogin'
>Sign in</paper-button>
</div>
</paper-card>
</hass-subpage>
</template>
</dom-module>
<script>
class HaConfigCloudLogin extends window.hassMixins.EventsMixin(Polymer.Element) {
class HaConfigCloudLogin extends
window.hassMixins.NavigateMixin(
window.hassMixins.EventsMixin(Polymer.Element)) {
static get is() { return 'ha-config-cloud-login'; }
static get properties() {
return {
hass: Object,
username: String,
password: String,
isWide: Boolean,
email: {
type: String,
notify: true,
},
_password: {
type: String,
value: '',
},
_requestInProgress: {
type: Boolean,
value: false,
},
};
}
static get observers() {
return [
'_inputChanged(username, password)'
'_inputChanged(email, _password)'
];
}
@ -74,19 +143,49 @@ class HaConfigCloudLogin extends window.hassMixins.EventsMixin(Polymer.Element)
}
_handleLogin() {
if (!this.email) {
this.error = 'Email is required.';
} else if (!this._password) {
this.error = 'Password is required.';
}
if (this.error) return;
this._requestInProgress = true;
this.hass.callApi('post', 'cloud/login', {
username: this.username,
password: this.password,
email: this.email,
password: this._password,
}).then(
(account) => {
this.fire('ha-account-refreshed', { account: account });
this.username = '';
this.password = '';
}, () => {
this.password = '';
this.error = true;
this.email = '';
this._password = '';
}, (err) => {
this._password = '';
this._requestInProgress = false;
if (!err || !err.body || !err.body.message) {
this.error = 'Unknown error';
return;
} else if (err.body.code === 'UserNotConfirmed') {
alert('You need to confirm your email before logging in.');
this.navigate('/config/cloud/register#confirm');
return;
} else if (err.body.code === 'PasswordChangeRequired') {
alert('You need to change your password before logging in.');
this.navigate('/config/cloud/forgot-password');
}
this.error = err.body.message;
});
}
_handleRegister() {
this.navigate('/config/cloud/register');
}
_handleForgotPassword() {
this.navigate('/config/cloud/forgot-password');
}
}
customElements.define(HaConfigCloudLogin.is, HaConfigCloudLogin);

View File

@ -0,0 +1,252 @@
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
<link rel="import" href='../../../bower_components/paper-card/paper-card.html'>
<link rel="import" href='../../../bower_components/paper-button/paper-button.html'>
<link rel="import" href='../../../bower_components/paper-input/paper-input.html'>
<link rel="import" href="../../../src/layouts/hass-subpage.html">
<link rel="import" href="../../../src/util/hass-mixins.html">
<link rel="import" href='../../../src/resources/ha-style.html'>
<link rel="import" href='../../../src/components/buttons/ha-progress-button.html'>
<dom-module id="ha-config-cloud-register">
<template>
<style include="iron-flex ha-style">
paper-card {
display: block;
}
paper-item {
cursor: pointer;
}
paper-card:last-child {
margin-top: 24px;
}
h1 {
@apply(--paper-font-headline);
margin: 0;
}
.error {
color: var(--google-red-500);
}
.card-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
[hidden] {
display: none;
}
</style>
<hass-subpage title="Register Account">
<div class='content'>
<ha-config-section
is-wide='[[isWide]]'
>
<span slot='header'>Register with the Home Assistant Cloud</span>
<span slot='introduction'>
Register today to easily connect your Home Assistant to cloud-only services.
<p>
By registering an account you agree to the following terms and conditions.
<ul>
<li><a href='#'>Terms and Conditions</a></li>
<li><a href='#'>Privacy Policy</a></li>
</ul>
</p>
<p><i>
Home Assistant will never share information with our cloud without your prior permission.
</i></p>
</span>
<template is='dom-if' if='[[!_hasConfirmationCode]]'>
<paper-card>
<div class='card-content'>
<div class='header'>
<h1>Register</h1>
<div class='error' hidden$='[[!_error]]'>[[_error]]</div>
</div>
<paper-input
autofocus
label='Email address'
type='email'
value='{{email}}'
on-keydown='_keyDown'
></paper-input>
<paper-input
label='Password'
value='{{_password}}'
type='password'
on-keydown='_keyDown'
></paper-input>
</div>
<div class='card-actions'>
<ha-progress-button
on-tap='_handleRegister'
progress='[[_requestInProgress]]'
>Create Account</ha-progress-button>
<button
class='link'
hidden='[[_requestInProgress]]'
on-click='_handleShowVerifyAccount'
>have confirmation code?</button>
</div>
</paper-card>
</template>
<template is='dom-if' if='[[_hasConfirmationCode]]'>
<paper-card>
<div class='card-content'>
<div class='header'>
<h1>Verify email</h1>
<div class='error' hidden$='[[!_error]]'>[[_error]]</div>
</div>
<p>
Check your email address, we've emailed you a verification code to activate your account.
</p>
<template is='dom-if' if='[[_showEmailInputForConfirmation]]'>
<paper-input
label='Email address'
type='email'
value='{{email}}'
on-keydown='_keyDown'
></paper-input>
</template>
<paper-input
label='Confirmation code'
value='{{_confirmationCode}}'
on-keydown='_keyDown'
type='number'
></paper-input>
</div>
<div class='card-actions'>
<ha-progress-button
on-tap='_handleVerifyEmail'
progress='[[_requestInProgress]]'
>Verify Email</ha-progress-button>
</div>
</paper-card>
</template>
</ha-config-section>
</div>
</hass-subpage>
</template>
</dom-module>
<script>
class HaConfigCloudRegister extends
window.hassMixins.NavigateMixin(
window.hassMixins.EventsMixin(Polymer.Element)) {
static get is() { return 'ha-config-cloud-register'; }
static get properties() {
return {
hass: Object,
isWide: Boolean,
email: {
type: String,
notify: true,
},
_requestInProgress: {
type: Boolean,
value: false,
},
_password: {
type: String,
value: '',
},
_showEmailInputForConfirmation: {
type: Boolean,
value: false,
},
_hasConfirmationCode: {
type: Boolean,
value: () => document.location.hash === '#confirm'
}
};
}
static get observers() {
return [
'_inputChanged(email, _password)'
];
}
_inputChanged() {
this._error = false;
}
_keyDown(ev) {
// validate on enter
if (ev.keyCode === 13) {
if (this._hasConfirmationCode) {
this._handleVerifyEmail();
} else {
this._handleRegister();
}
ev.preventDefault();
}
}
_handleRegister() {
if (!this.email) {
this._error = 'Email is required.';
} else if (!this._password) {
this._error = 'Password is required.';
}
if (this._error) return;
this._requestInProgress = true;
this.hass.callApi('post', 'cloud/register', {
email: this.email,
password: this._password,
}).then(
() => {
this._requestInProgress = false;
this._hasConfirmationCode = true;
}, (err) => {
this._password = '';
this._requestInProgress = false;
this._error = err && err.body && err.body.message ?
err.body.message : 'Unknown error';
});
}
_handleShowVerifyAccount() {
this._error = '';
this._showEmailInputForConfirmation = true;
this._hasConfirmationCode = true;
}
_handleVerifyEmail() {
if (!this.email) {
this._error = 'Email is required.';
} else if (!this._confirmationCode) {
this._error = 'Confirmation code is required.';
}
if (this._error) return;
this._requestInProgress = true;
this.hass.callApi('post', 'cloud/confirm_register', {
email: this.email,
confirmation_code: this._confirmationCode,
}).then(
() => {
// eslint-disable-next-line
alert('Confirmation successful. You can now login.');
this.navigate('config/cloud/login');
}, (err) => {
this._confirmationCode = '';
this._error = err && err.body && err.body.message ?
err.body.message : 'Unknown error';
this._requestInProgress = false;
});
}
}
customElements.define(HaConfigCloudRegister.is, HaConfigCloudRegister);
</script>

View File

@ -1,66 +1,68 @@
<link rel="import" href='../../../bower_components/polymer/polymer-element.html'>
<link rel="import" href="../../../bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="../../../bower_components/app-layout/app-header/app-header.html">
<link rel="import" href="../../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../../bower_components/paper-icon-button/paper-icon-button.html">
<link rel='import' href='../../../bower_components/app-route/app-route.html'>
<link rel="import" href="../ha-config-section.html">
<link rel="import" href="./ha-config-cloud-login.html">
<link rel="import" href="./ha-config-cloud-register.html">
<link rel="import" href="./ha-config-cloud-forgot-password.html">
<link rel="import" href="./ha-config-cloud-account.html">
<dom-module id="ha-config-cloud">
<template>
<style include="iron-flex ha-style">
.content {
padding-bottom: 32px;
<style>
iron-pages {
height: 100%;
}
</style>
<app-route
route='[[route]]'
pattern='/:page'
data="{{_routeData}}"
tail="{{_routeTail}}"
></app-route>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<paper-icon-button
icon='mdi:arrow-left'
on-tap='_backTapped'
></paper-icon-button>
<div main-title>Cloud</div>
</app-toolbar>
</app-header>
<template is='dom-if' if='[[account]]' restamp>
<ha-config-cloud-account
hass='[[hass]]'
account='[[account]]'
is-wide='[[isWide]]'
></ha-config-cloud-account>
</template>
<div class$='[[computeClasses(isWide)]]'>
<ha-config-section
<template is='dom-if' if='[[!account]]' restamp>
<template is='dom-if' if='[[_isLoginPage(_routeData.page)]]' restamp>
<ha-config-cloud-login
page-name='login'
hass='[[hass]]'
is-wide='[[isWide]]'
>
<span slot='header'>Home Assistant Cloud</span>
<span slot='introduction'>
The Home Assistant Cloud allows you to opt-in to functions that will bring your Home Assistant experience to the next level.
email='{{_loginEmail}}'
></ha-config-cloud-login>
</template>
<p><i>
Home Assistant will never share information with our cloud without your prior permission.
</i></p>
</span>
<template is='dom-if' if='[[_isRegisterPage(_routeData.page)]]' restamp>
<ha-config-cloud-register
page-name='register'
hass='[[hass]]'
is-wide='[[isWide]]'
email='{{_loginEmail}}'
></ha-config-cloud-register>
</template>
<template is='dom-if' if='[[account]]'>
<ha-config-cloud-account
hass='[[hass]]'
account='[[account]]'
></ha-config-cloud-account>
</template>
<template is='dom-if' if='[[!account]]'>
<ha-config-cloud-login
hass='[[hass]]'
></ha-config-cloud-login>
</template>
</ha-config-section>
</div>
</app-header-layout>
<template is='dom-if' if='[[_isForgotPasswordPage(_routeData.page)]]' restamp>
<ha-config-cloud-forgot-password
page-name='forgot-password'
hass='[[hass]]'
is-wide='[[isWide]]'
email='{{_loginEmail}}'
></ha-config-cloud-forgot-password>
</template>
</template>
</template>
</dom-module>
<script>
class HaConfigCloud extends Polymer.Element {
class HaConfigCloud extends window.hassMixins.NavigateMixin(Polymer.Element) {
static get is() { return 'ha-config-cloud'; }
static get properties() {
@ -75,16 +77,35 @@ class HaConfigCloud extends Polymer.Element {
type: Object,
value: null,
},
route: Object,
_routeData: Object,
_routeTail: Object,
_loginEmail: String,
};
}
computeClasses(isWide) {
return isWide ? 'content' : 'content narrow';
static get observers() {
return [
'_checkRoute(route, account)'
];
}
_backTapped() {
history.back();
_checkRoute(route, account) {
if (!route || route.prefix !== '/config/cloud') return;
if (!account && ['/forgot-password', '/register'].indexOf(route.path) === -1) {
this.navigate('/config/cloud/login', true);
} else if (account &&
['/login', '/register', '/forgot-password'].indexOf(route.path) !== -1) {
this.navigate('/config/cloud/account', true);
}
}
_isRegisterPage(page) { return page === 'register'; }
_isForgotPasswordPage(page) { return page === 'forgot-password'; }
_isLoginPage(page) { return page === 'login'; }
}
customElements.define(HaConfigCloud.is, HaConfigCloud);

View File

@ -21,7 +21,7 @@
<paper-item-body two-line>
Home Assistant Cloud
<template is='dom-if' if='[[account]]'>
<div secondary>Logged in as [[account.first_name]] [[account.last_name]]</div>
<div secondary>Logged in as [[account.email]]</div>
</template>
<template is='dom-if' if='[[!account]]'>
<div secondary>Not logged in</div>

View File

@ -25,7 +25,7 @@
</app-toolbar>
</app-header>
<div class$='[[computeClasses(isWide)]]'>
<div class='content'>
<ha-config-section
is-wide='[[isWide]]'
>
@ -58,17 +58,12 @@ class HaConfigDashboard extends Polymer.Element {
return {
hass: Object,
isWide: Boolean,
account: {
type: Object,
value: null,
}
account: Object,
narrow: Boolean,
showMenu: Boolean,
};
}
computeClasses(isWide) {
return isWide ? 'content' : 'content narrow';
}
computeIsCloudLoaded(hass) {
return window.hassUtil.isComponentLoaded(hass, 'cloud');
}

View File

@ -47,6 +47,7 @@
<ha-config-cloud
page-name='cloud'
route='[[_routeTail]]'
hass='[[hass]]'
is-wide='[[isWide]]'
account='[[account]]'
@ -57,6 +58,8 @@
hass='[[hass]]'
is-wide='[[isWide]]'
account='[[account]]'
narrow='[[narrow]]'
show-menu='[[showMenu]]'
></ha-config-dashboard>
<ha-config-automation
@ -103,7 +106,10 @@ class HaPanelConfig extends window.hassMixins.EventsMixin(Polymer.Element) {
hass: Object,
narrow: Boolean,
showMenu: Boolean,
account: Object,
account: {
type: Object,
value: null,
},
route: {
type: Object,

View File

@ -71,19 +71,6 @@ Polymer({
value: false,
},
domain: {
type: String,
},
service: {
type: String,
},
serviceData: {
type: Object,
value: {},
},
disabled: {
type: Boolean,
value: false,

View File

@ -0,0 +1,42 @@
<link rel="import" href='../../bower_components/polymer/polymer-element.html'>
<link rel="import" href="../../bower_components/app-layout/app-header-layout/app-header-layout.html">
<link rel="import" href="../../bower_components/app-layout/app-header/app-header.html">
<link rel="import" href="../../bower_components/app-layout/app-toolbar/app-toolbar.html">
<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html">
<dom-module id="hass-subpage">
<template>
<style include="ha-style"></style>
<app-header-layout has-scrolling-region>
<app-header slot="header" fixed>
<app-toolbar>
<paper-icon-button
icon='mdi:arrow-left'
on-tap='_backTapped'
></paper-icon-button>
<div main-title>[[title]]</div>
</app-toolbar>
</app-header>
<slot></slot>
</app-header-layout>
</template>
</dom-module>
<script>
class HassSubpage extends Polymer.Element {
static get is() { return 'hass-subpage'; }
static get properties() {
return {
title: String
};
}
_backTapped() {
history.back();
}
}
customElements.define(HassSubpage.is, HassSubpage);
</script>

View File

@ -103,7 +103,19 @@
@apply(--paper-font-title);
}
button.link {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
text-align: left;
text-decoration: underline;
cursor: pointer;
}
.card-actions paper-button:not([disabled]),
.card-actions ha-progress-button:not([disabled]),
.card-actions ha-call-api-button:not([disabled]),
.card-actions ha-call-service-button:not([disabled]) {
color: var(--primary-color);

View File

@ -70,8 +70,12 @@ window.hassMixins.EventsMixin = Polymer.dedupingMixin(
/* @polymerMixin */
window.hassMixins.NavigateMixin = Polymer.dedupingMixin(
superClass => class extends window.hassMixins.EventsMixin(superClass) {
navigate(path) {
history.pushState(null, null, path);
navigate(path, replace = false) {
if (replace) {
history.replaceState(null, null, path);
} else {
history.pushState(null, null, path);
}
this.fire('location-changed');
}
});