mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-25 05:47:20 +00:00
20190508.0 (#3176)
* Prevent default form action (#3172) * Use areas when generating Lovelace config (#3175) * Use areas when generating Lovelace config * Add missing return type * Convert auth-flow to TypeScript/Lit (#3174) * Change ha-pick-auth-provider to typescript * Convert auth-flow to TypeScript/Lit * Make sure we block emulated mouse events when touch is used (#3173) * Trim text * Update translations * Bumped version to 20190508.0
This commit is contained in:
parent
484b1c8444
commit
746ad588ef
@ -75,7 +75,7 @@
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"fecha": "^3.0.2",
|
||||
"hls.js": "^0.12.4",
|
||||
"home-assistant-js-websocket": "^4.1.1",
|
||||
"home-assistant-js-websocket": "^4.1.2",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "^3.13.0",
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20190507.0",
|
||||
version="20190508.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@ -1,270 +0,0 @@
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "@material/mwc-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import "../components/ha-form";
|
||||
import "../components/ha-markdown";
|
||||
import { localizeLiteMixin } from "../mixins/localize-lite-mixin";
|
||||
|
||||
class HaAuthFlow extends localizeLiteMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
/* So we can set min-height to avoid jumping during loading */
|
||||
display: block;
|
||||
}
|
||||
.action {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<form>
|
||||
<template is="dom-if" if='[[_equals(_state, "loading")]]'>
|
||||
[[localize('ui.panel.page-authorize.form.working')]]:
|
||||
</template>
|
||||
<template is="dom-if" if='[[_equals(_state, "error")]]'>
|
||||
<div class="error">Error: [[_errorMsg]]</div>
|
||||
</template>
|
||||
<template is="dom-if" if='[[_equals(_state, "step")]]'>
|
||||
<template is="dom-if" if='[[_equals(_step.type, "abort")]]'>
|
||||
[[localize('ui.panel.page-authorize.abort_intro')]]:
|
||||
<ha-markdown
|
||||
content="[[_computeStepAbortedReason(localize, _step)]]"
|
||||
></ha-markdown>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if='[[_equals(_step.type, "form")]]'>
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_computeStepDescription(localize, _step)]]"
|
||||
>
|
||||
<ha-markdown
|
||||
content="[[_computeStepDescription(localize, _step)]]"
|
||||
allow-svg
|
||||
></ha-markdown>
|
||||
</template>
|
||||
|
||||
<ha-form
|
||||
data="{{_stepData}}"
|
||||
schema="[[_step.data_schema]]"
|
||||
error="[[_step.errors]]"
|
||||
compute-label="[[_computeLabelCallback(localize, _step)]]"
|
||||
compute-error="[[_computeErrorCallback(localize, _step)]]"
|
||||
></ha-form>
|
||||
</template>
|
||||
<div class="action">
|
||||
<mwc-button raised on-click="_handleSubmit"
|
||||
>[[_computeSubmitCaption(_step.type)]]</mwc-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
authProvider: {
|
||||
type: Object,
|
||||
observer: "_providerChanged",
|
||||
},
|
||||
clientId: String,
|
||||
redirectUri: String,
|
||||
oauth2State: String,
|
||||
_state: {
|
||||
type: String,
|
||||
value: "loading",
|
||||
},
|
||||
_stepData: {
|
||||
type: Object,
|
||||
value: () => ({}),
|
||||
},
|
||||
_step: {
|
||||
type: Object,
|
||||
notify: true,
|
||||
},
|
||||
_errorMsg: String,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._handleSubmit(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _providerChanged(newProvider, oldProvider) {
|
||||
if (oldProvider && this._step && this._step.type === "form") {
|
||||
fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/auth/login_flow", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
client_id: this.clientId,
|
||||
handler: [newProvider.type, newProvider.id],
|
||||
redirect_uri: this.redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// allow auth provider bypass the login form
|
||||
if (data.type === "create_entry") {
|
||||
this._redirect(data.result);
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateStep(data);
|
||||
} else {
|
||||
this.setProperties({
|
||||
_state: "error",
|
||||
_errorMsg: data.message,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error("Error starting auth flow", err);
|
||||
this.setProperties({
|
||||
_state: "error",
|
||||
_errorMsg: this.localize("ui.panel.page-authorize.form.unknown_error"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_redirect(authCode) {
|
||||
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
|
||||
let url = this.redirectUri;
|
||||
if (!url.includes("?")) {
|
||||
url += "?";
|
||||
} else if (!url.endsWith("&")) {
|
||||
url += "&";
|
||||
}
|
||||
|
||||
url += `code=${encodeURIComponent(authCode)}`;
|
||||
|
||||
if (this.oauth2State) {
|
||||
url += `&state=${encodeURIComponent(this.oauth2State)}`;
|
||||
}
|
||||
|
||||
document.location = url;
|
||||
}
|
||||
|
||||
_updateStep(step) {
|
||||
const props = {
|
||||
_step: step,
|
||||
_state: "step",
|
||||
};
|
||||
|
||||
if (
|
||||
this._step &&
|
||||
(step.flow_id !== this._step.flow_id ||
|
||||
step.step_id !== this._step.step_id)
|
||||
) {
|
||||
props._stepData = {};
|
||||
}
|
||||
|
||||
this.setProperties(props);
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_computeSubmitCaption(stepType) {
|
||||
return stepType === "form" ? "Next" : "Start over";
|
||||
}
|
||||
|
||||
_computeStepAbortedReason(localize, step) {
|
||||
return localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.abort.${
|
||||
step.reason
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
_computeStepDescription(localize, step) {
|
||||
const args = [
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
|
||||
step.step_id
|
||||
}.description`,
|
||||
];
|
||||
const placeholders = step.description_placeholders || {};
|
||||
Object.keys(placeholders).forEach((key) => {
|
||||
args.push(key);
|
||||
args.push(placeholders[key]);
|
||||
});
|
||||
return localize(...args);
|
||||
}
|
||||
|
||||
_computeLabelCallback(localize, step) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
|
||||
step.step_id
|
||||
}.data.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
_computeErrorCallback(localize, step) {
|
||||
// Returns a callback for ha-form to calculate error messages
|
||||
return (error) =>
|
||||
localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.error.${error}`
|
||||
);
|
||||
}
|
||||
|
||||
async _handleSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
if (this._step.type !== "form") {
|
||||
this._providerChanged(this.authProvider, null);
|
||||
return;
|
||||
}
|
||||
this._state = "loading";
|
||||
// To avoid a jumping UI.
|
||||
this.style.setProperty("min-height", `${this.offsetHeight}px`);
|
||||
|
||||
const postData = Object.assign({}, this._stepData, {
|
||||
client_id: this.clientId,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
const newStep = await response.json();
|
||||
|
||||
if (newStep.type === "create_entry") {
|
||||
this._redirect(newStep.result);
|
||||
return;
|
||||
}
|
||||
this._updateStep(newStep);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.error("Error submitting step", err);
|
||||
this._state = "error-loading";
|
||||
} finally {
|
||||
this.style.setProperty("min-height", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("ha-auth-flow", HaAuthFlow);
|
296
src/auth/ha-auth-flow.ts
Normal file
296
src/auth/ha-auth-flow.ts
Normal file
@ -0,0 +1,296 @@
|
||||
import { LitElement, html, property, PropertyValues } from "lit-element";
|
||||
import "@material/mwc-button";
|
||||
import "../components/ha-form";
|
||||
import "../components/ha-markdown";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
import { ConfigFlowStep, ConfigFlowStepForm } from "../data/config_entries";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
|
||||
@property() public authProvider?: AuthProvider;
|
||||
@property() public clientId?: string;
|
||||
@property() public redirectUri?: string;
|
||||
@property() public oauth2State?: string;
|
||||
@property() private _state: State = "loading";
|
||||
@property() private _stepData: any = {};
|
||||
@property() private _step?: ConfigFlowStep;
|
||||
@property() private _errorMessage?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
/* So we can set min-height to avoid jumping during loading */
|
||||
display: block;
|
||||
}
|
||||
.action {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<form>
|
||||
${this._renderForm()}
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (this.clientId == null || this.redirectUri == null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error(
|
||||
"clientId and redirectUri must not be null",
|
||||
this.clientId,
|
||||
this.redirectUri
|
||||
);
|
||||
this._state = "error";
|
||||
this._errorMessage = this._unknownError();
|
||||
return;
|
||||
}
|
||||
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._handleSubmit(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("authProvider")) {
|
||||
this._providerChanged(this.authProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderForm() {
|
||||
switch (this._state) {
|
||||
case "step":
|
||||
if (this._step == null) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
${this._renderStep(this._step)}
|
||||
<div class="action">
|
||||
<mwc-button raised @click=${this._handleSubmit}
|
||||
>${this._step.type === "form" ? "Next" : "Start over"}</mwc-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
case "error":
|
||||
return html`
|
||||
<div class="error">Error: ${this._errorMessage}</div>
|
||||
`;
|
||||
case "loading":
|
||||
return html`
|
||||
${this.localize("ui.panel.page-authorize.form.working")}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
private _renderStep(step: ConfigFlowStep) {
|
||||
switch (step.type) {
|
||||
case "abort":
|
||||
return html`
|
||||
${this.localize("ui.panel.page-authorize.abort_intro")}:
|
||||
<ha-markdown
|
||||
.content=${this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.abort.${step.reason}`
|
||||
)}
|
||||
></ha-markdown>
|
||||
`;
|
||||
case "form":
|
||||
return html`
|
||||
${this._computeStepDescription(step)
|
||||
? html`
|
||||
<ha-markdown
|
||||
.content=${this._computeStepDescription(step)}
|
||||
allow-svg
|
||||
></ha-markdown>
|
||||
`
|
||||
: html``}
|
||||
<ha-form
|
||||
.data=${this._stepData}
|
||||
.schema=${step.data_schema}
|
||||
.error=${step.errors}
|
||||
.computeLabel=${this._computeLabelCallback(step)}
|
||||
.computeError=${this._computeErrorCallback(step)}
|
||||
></ha-form>
|
||||
`;
|
||||
default:
|
||||
return html``;
|
||||
}
|
||||
}
|
||||
|
||||
private async _providerChanged(newProvider?: AuthProvider) {
|
||||
if (this._step && this._step.type === "form") {
|
||||
fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
}).catch((err) => {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error("Error delete obsoleted auth flow", err);
|
||||
});
|
||||
}
|
||||
|
||||
if (newProvider == null) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error("No auth provider");
|
||||
this._state = "error";
|
||||
this._errorMessage = this._unknownError();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/auth/login_flow", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
client_id: this.clientId,
|
||||
handler: [newProvider.type, newProvider.id],
|
||||
redirect_uri: this.redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// allow auth provider bypass the login form
|
||||
if (data.type === "create_entry") {
|
||||
this._redirect(data.result);
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateStep(data);
|
||||
} else {
|
||||
this._state = "error";
|
||||
this._errorMessage = data.message;
|
||||
}
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error("Error starting auth flow", err);
|
||||
this._state = "error";
|
||||
this._errorMessage = this._unknownError();
|
||||
}
|
||||
}
|
||||
|
||||
private _redirect(authCode: string) {
|
||||
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
|
||||
let url = this.redirectUri!!;
|
||||
if (!url.includes("?")) {
|
||||
url += "?";
|
||||
} else if (!url.endsWith("&")) {
|
||||
url += "&";
|
||||
}
|
||||
|
||||
url += `code=${encodeURIComponent(authCode)}`;
|
||||
|
||||
if (this.oauth2State) {
|
||||
url += `&state=${encodeURIComponent(this.oauth2State)}`;
|
||||
}
|
||||
|
||||
document.location.assign(url);
|
||||
}
|
||||
|
||||
private _updateStep(step: ConfigFlowStep) {
|
||||
let stepData: any = null;
|
||||
if (
|
||||
this._step &&
|
||||
(step.flow_id !== this._step.flow_id ||
|
||||
(step.type === "form" &&
|
||||
this._step.type === "form" &&
|
||||
step.step_id !== this._step.step_id))
|
||||
) {
|
||||
stepData = {};
|
||||
}
|
||||
this._step = step;
|
||||
this._state = "step";
|
||||
if (stepData != null) {
|
||||
this._stepData = stepData;
|
||||
}
|
||||
}
|
||||
|
||||
private _computeStepDescription(step: ConfigFlowStepForm) {
|
||||
const resourceKey = `ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.step.${step.step_id}.description`;
|
||||
const args: string[] = [];
|
||||
const placeholders = step.description_placeholders || {};
|
||||
Object.keys(placeholders).forEach((key) => {
|
||||
args.push(key);
|
||||
args.push(placeholders[key]);
|
||||
});
|
||||
return this.localize(resourceKey, ...args);
|
||||
}
|
||||
|
||||
private _computeLabelCallback(step: ConfigFlowStepForm) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${step.handler[0]}.step.${
|
||||
step.step_id
|
||||
}.data.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
private _computeErrorCallback(step: ConfigFlowStepForm) {
|
||||
// Returns a callback for ha-form to calculate error messages
|
||||
return (error) =>
|
||||
this.localize(
|
||||
`ui.panel.page-authorize.form.providers.${
|
||||
step.handler[0]
|
||||
}.error.${error}`
|
||||
);
|
||||
}
|
||||
|
||||
private _unknownError() {
|
||||
return this.localize("ui.panel.page-authorize.form.unknown_error");
|
||||
}
|
||||
|
||||
private async _handleSubmit(ev: Event) {
|
||||
ev.preventDefault();
|
||||
if (this._step == null) {
|
||||
return;
|
||||
}
|
||||
if (this._step.type !== "form") {
|
||||
this._providerChanged(this.authProvider);
|
||||
return;
|
||||
}
|
||||
this._state = "loading";
|
||||
// To avoid a jumping UI.
|
||||
this.style.setProperty("min-height", `${this.offsetHeight}px`);
|
||||
|
||||
const postData = { ...this._stepData, client_id: this.clientId };
|
||||
|
||||
try {
|
||||
const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
const newStep = await response.json();
|
||||
|
||||
if (newStep.type === "create_entry") {
|
||||
this._redirect(newStep.result);
|
||||
return;
|
||||
}
|
||||
this._updateStep(newStep);
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.error("Error submitting step", err);
|
||||
this._state = "error";
|
||||
this._errorMessage = this._unknownError();
|
||||
} finally {
|
||||
this.style.setProperty("min-height", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("ha-auth-flow", HaAuthFlow);
|
@ -108,7 +108,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
.resources="${this.resources}"
|
||||
.clientId="${this.clientId}"
|
||||
.authProviders="${inactiveProviders}"
|
||||
@pick="${this._handleAuthProviderPick}"
|
||||
@pick-auth-provider="${this._handleAuthProviderPick}"
|
||||
></ha-pick-auth-provider>
|
||||
`
|
||||
: ""}
|
||||
|
@ -1,54 +0,0 @@
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import { localizeLiteMixin } from "../mixins/localize-lite-mixin";
|
||||
import "../components/ha-icon-next";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaPickAuthProvider extends EventsMixin(
|
||||
localizeLiteMixin(PolymerElement)
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<p>[[localize('ui.panel.page-authorize.pick_auth_provider')]]:</p>
|
||||
<template is="dom-repeat" items="[[authProviders]]">
|
||||
<paper-item on-click="_handlePick">
|
||||
<paper-item-body>[[item.name]]</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_state: {
|
||||
type: String,
|
||||
value: "loading",
|
||||
},
|
||||
authProviders: Array,
|
||||
};
|
||||
}
|
||||
|
||||
_handlePick(ev) {
|
||||
this.fire("pick", ev.model.item);
|
||||
}
|
||||
|
||||
_equal(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-pick-auth-provider", HaPickAuthProvider);
|
44
src/auth/ha-pick-auth-provider.ts
Normal file
44
src/auth/ha-pick-auth-provider.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { LitElement, html, property } from "lit-element";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "../components/ha-icon-next";
|
||||
import { AuthProvider } from "../data/auth";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"pick-auth-provider": AuthProvider;
|
||||
}
|
||||
}
|
||||
|
||||
class HaPickAuthProvider extends litLocalizeLiteMixin(LitElement) {
|
||||
@property() public authProviders: AuthProvider[] = [];
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
<paper-item .auth_provider=${provider} @click=${this._handlePick}>
|
||||
<paper-item-body>${provider.name}</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
`
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _handlePick(ev) {
|
||||
fireEvent(this, "pick-auth-provider", ev.currentTarget.auth_provider);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-pick-auth-provider", HaPickAuthProvider);
|
16
src/common/util/subscribe-one.ts
Normal file
16
src/common/util/subscribe-one.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
|
||||
export const subscribeOne = async <T>(
|
||||
hass: HomeAssistant,
|
||||
subscribe: (
|
||||
hass: HomeAssistant,
|
||||
onChange: (items: T) => void
|
||||
) => UnsubscribeFunc
|
||||
) =>
|
||||
new Promise<T>((resolve) => {
|
||||
const unsub = subscribe(hass, (items) => {
|
||||
unsub();
|
||||
resolve(items);
|
||||
});
|
||||
});
|
@ -111,6 +111,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
on-mousedown="handleVolumeDown"
|
||||
on-touchstart="handleVolumeDown"
|
||||
on-touchend="handleVolumeTouchEnd"
|
||||
icon="hass:volume-medium"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
@ -118,6 +119,7 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
disabled$="[[playerObj.isMuted]]"
|
||||
on-mousedown="handleVolumeUp"
|
||||
on-touchstart="handleVolumeUp"
|
||||
on-touchend="handleVolumeTouchEnd"
|
||||
icon="hass:volume-high"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
@ -357,6 +359,12 @@ class MoreInfoMediaPlayer extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
this.playerObj.volumeMute(!this.playerObj.isMuted);
|
||||
}
|
||||
|
||||
handleVolumeTouchEnd(ev) {
|
||||
/* when touch ends, we must prevent this from
|
||||
* becoming a mousedown, up, click by emulation */
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
handleVolumeUp() {
|
||||
const obj = this.$.volumeUp;
|
||||
this.handleVolumeWorker("volume_up", obj, true);
|
||||
|
@ -130,7 +130,7 @@ class OnboardingCreateUser extends LitElement {
|
||||
);
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._submitForm();
|
||||
this._submitForm(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -152,7 +152,8 @@ class OnboardingCreateUser extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _submitForm(): Promise<void> {
|
||||
private async _submitForm(ev): Promise<void> {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) {
|
||||
this._errorMsg = "required_fields";
|
||||
return;
|
||||
|
@ -108,7 +108,7 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
|
||||
super.ready();
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._createUser();
|
||||
this._createUser(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -131,7 +131,8 @@ class HaDialogAddUser extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
}
|
||||
|
||||
async _createUser() {
|
||||
async _createUser(ev) {
|
||||
ev.preventDefault();
|
||||
if (!this._name || !this._username || !this._password) return;
|
||||
|
||||
this._loading = true;
|
||||
|
@ -17,6 +17,19 @@ import computeDomain from "../../../common/entity/compute_domain";
|
||||
import { EntityRowConfig, WeblinkConfig } from "../entity-rows/types";
|
||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { EntitiesCardConfig } from "../cards/types";
|
||||
import {
|
||||
subscribeAreaRegistry,
|
||||
AreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
import { subscribeOne } from "../../../common/util/subscribe-one";
|
||||
import {
|
||||
subscribeDeviceRegistry,
|
||||
DeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
subscribeEntityRegistry,
|
||||
EntityRegistryEntry,
|
||||
} from "../../../data/entity_registry";
|
||||
|
||||
const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
|
||||
const DOMAINS_BADGES = [
|
||||
@ -34,6 +47,55 @@ const HIDE_DOMAIN = new Set([
|
||||
"geo_location",
|
||||
]);
|
||||
|
||||
interface Registries {
|
||||
areas: AreaRegistryEntry[];
|
||||
devices: DeviceRegistryEntry[];
|
||||
entities: EntityRegistryEntry[];
|
||||
}
|
||||
|
||||
let subscribedRegistries = false;
|
||||
|
||||
interface SplittedByAreas {
|
||||
areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>;
|
||||
otherEntities: HassEntities;
|
||||
}
|
||||
|
||||
const splitByAreas = (
|
||||
registries: Registries,
|
||||
entities: HassEntities
|
||||
): SplittedByAreas => {
|
||||
const allEntities = { ...entities };
|
||||
const areasWithEntities: SplittedByAreas["areasWithEntities"] = [];
|
||||
|
||||
for (const area of registries.areas) {
|
||||
const areaEntities: HassEntity[] = [];
|
||||
const areaDevices = new Set(
|
||||
registries.devices
|
||||
.filter((device) => device.area_id === area.area_id)
|
||||
.map((device) => device.id)
|
||||
);
|
||||
for (const entity of registries.entities) {
|
||||
if (
|
||||
areaDevices.has(
|
||||
// @ts-ignore
|
||||
entity.device_id
|
||||
) &&
|
||||
entity.entity_id in allEntities
|
||||
) {
|
||||
areaEntities.push(allEntities[entity.entity_id]);
|
||||
delete allEntities[entity.entity_id];
|
||||
}
|
||||
}
|
||||
if (areaEntities.length > 0) {
|
||||
areasWithEntities.push([area, areaEntities]);
|
||||
}
|
||||
}
|
||||
return {
|
||||
areasWithEntities,
|
||||
otherEntities: allEntities,
|
||||
};
|
||||
};
|
||||
|
||||
const computeCards = (
|
||||
states: Array<[string, HassEntity]>,
|
||||
entityCardOptions: Partial<EntitiesCardConfig>
|
||||
@ -124,6 +186,51 @@ const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => {
|
||||
return states;
|
||||
};
|
||||
|
||||
const generateDefaultViewConfig = (
|
||||
hass: HomeAssistant,
|
||||
registries: Registries
|
||||
): LovelaceViewConfig => {
|
||||
const states = computeDefaultViewStates(hass);
|
||||
const path = "default_view";
|
||||
const title = "Home";
|
||||
const icon = undefined;
|
||||
|
||||
// In the case of a default view, we want to use the group order attribute
|
||||
const groupOrders = {};
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
const stateObj = states[entityId];
|
||||
if (stateObj.attributes.order) {
|
||||
groupOrders[entityId] = stateObj.attributes.order;
|
||||
}
|
||||
});
|
||||
|
||||
const splittedByAreas = splitByAreas(registries, states);
|
||||
|
||||
const config = generateViewConfig(
|
||||
hass.localize,
|
||||
path,
|
||||
title,
|
||||
icon,
|
||||
splittedByAreas.otherEntities,
|
||||
groupOrders
|
||||
);
|
||||
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
|
||||
splittedByAreas.areasWithEntities.forEach(([area, entities]) => {
|
||||
areaCards.push(
|
||||
...computeCards(entities.map((entity) => [entity.entity_id, entity]), {
|
||||
title: area.name,
|
||||
show_header_toggle: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
config.cards!.unshift(...areaCards);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const generateViewConfig = (
|
||||
localize: LocalizeFunc,
|
||||
path: string,
|
||||
@ -208,10 +315,10 @@ const generateViewConfig = (
|
||||
return view;
|
||||
};
|
||||
|
||||
export const generateLovelaceConfig = (
|
||||
export const generateLovelaceConfig = async (
|
||||
hass: HomeAssistant,
|
||||
localize: LocalizeFunc
|
||||
): LovelaceConfig => {
|
||||
): Promise<LovelaceConfig> => {
|
||||
const viewEntities = extractViews(hass.states);
|
||||
|
||||
const views = viewEntities.map((viewEntity: GroupEntity) => {
|
||||
@ -241,27 +348,23 @@ export const generateLovelaceConfig = (
|
||||
viewEntities.length === 0 ||
|
||||
viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID
|
||||
) {
|
||||
const states = computeDefaultViewStates(hass);
|
||||
// We want to keep the registry subscriptions alive after generating the UI
|
||||
// so that we don't serve up stale data after changing areas.
|
||||
if (!subscribedRegistries) {
|
||||
subscribedRegistries = true;
|
||||
subscribeAreaRegistry(hass, () => undefined);
|
||||
subscribeDeviceRegistry(hass, () => undefined);
|
||||
subscribeEntityRegistry(hass, () => undefined);
|
||||
}
|
||||
|
||||
// In the case of a default view, we want to use the group order attribute
|
||||
const groupOrders = {};
|
||||
Object.keys(states).forEach((entityId) => {
|
||||
const stateObj = states[entityId];
|
||||
if (stateObj.attributes.order) {
|
||||
groupOrders[entityId] = stateObj.attributes.order;
|
||||
}
|
||||
});
|
||||
const [areas, devices, entities] = await Promise.all([
|
||||
subscribeOne(hass, subscribeAreaRegistry),
|
||||
subscribeOne(hass, subscribeDeviceRegistry),
|
||||
subscribeOne(hass, subscribeEntityRegistry),
|
||||
]);
|
||||
const registries = { areas, devices, entities };
|
||||
|
||||
views.unshift(
|
||||
generateViewConfig(
|
||||
localize,
|
||||
"default_view",
|
||||
"Home",
|
||||
undefined,
|
||||
states,
|
||||
groupOrders
|
||||
)
|
||||
);
|
||||
views.unshift(generateDefaultViewConfig(hass, registries));
|
||||
|
||||
// Add map of geo locations to default view if loaded
|
||||
if (hass.config.components.includes("geo_location")) {
|
||||
|
@ -123,18 +123,14 @@ class HuiInputSelectEntityRow extends LitElement implements EntityRow {
|
||||
|
||||
private _selectedChanged(ev): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity];
|
||||
const option = ev.detail.item.innerText;
|
||||
const option = ev.target.selectedItem.innerText.trim();
|
||||
if (option === stateObj.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
forwardHaptic("light");
|
||||
|
||||
setInputSelectOption(
|
||||
this.hass!,
|
||||
stateObj.entity_id,
|
||||
ev.target.selectedItem.innerText
|
||||
);
|
||||
setInputSelectOption(this.hass!, stateObj.entity_id, option);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,7 +171,7 @@ class LovelacePanel extends LitElement {
|
||||
this._errorMsg = err.message;
|
||||
return;
|
||||
}
|
||||
conf = generateLovelaceConfig(this.hass!, this.hass!.localize);
|
||||
conf = await generateLovelaceConfig(this.hass!, this.hass!.localize);
|
||||
confMode = "generated";
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ class StateCardInputSelect extends LitElement {
|
||||
private async _selectedOptionChanged(
|
||||
ev: PolymerIronSelectEvent<PaperItemElement>
|
||||
) {
|
||||
const option = ev.detail.item.innerText;
|
||||
const option = ev.detail.item.innerText.trim();
|
||||
if (option === this.stateObj.state) {
|
||||
return;
|
||||
}
|
||||
|
@ -855,6 +855,11 @@
|
||||
"required_fields": "Vyplňte všechna povinná pole",
|
||||
"password_not_match": "Hesla se neshodují"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"intro": "Zařízení a služby jsou v programu Home Assistant reprezentovány jako integrace. Můžete je nyní nastavit nebo provést později z konfigurační obrazovky.",
|
||||
"more_integrations": "Více",
|
||||
"finish": "Dokončit"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
@ -868,6 +873,14 @@
|
||||
"title": "Vítejte doma",
|
||||
"no_devices": "Tato stránka umožňuje ovládat vaše zařízení; zdá se však, že ještě nemáte žádné zařízení nastavené. Nejprve tedy přejděte na stránku integrace.",
|
||||
"go_to_integrations_page": "Přejděte na stránku integrace."
|
||||
},
|
||||
"picture-elements": {
|
||||
"hold": "Podržte:",
|
||||
"tap": "Klepněte:",
|
||||
"navigate_to": "Přejděte na {location}",
|
||||
"toggle": "Přepnout {name}",
|
||||
"call_service": "Zavolat službu {name}",
|
||||
"more_info": "Zobrazit více informací: {name}"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -925,7 +938,8 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"log_out": "Odhlásit se",
|
||||
"developer_tools": "Vývojářské nástroje"
|
||||
"developer_tools": "Vývojářské nástroje",
|
||||
"external_app_configuration": "Konfigurace aplikace"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Načítání",
|
||||
|
@ -855,6 +855,11 @@
|
||||
"required_fields": "필수 입력란을 모두 채워주세요",
|
||||
"password_not_match": "비밀번호가 일치하지 않습니다"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"intro": "기기 및 서비스는 Home Assistant 에서 통합 구성요소로 표시됩니다. 지금 구성하거나 설정 화면에서 나중에 구성할 수 있습니다.",
|
||||
"more_integrations": "더보기",
|
||||
"finish": "완료"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
|
@ -855,6 +855,11 @@
|
||||
"required_fields": "Fëllt all néideg Felder aus",
|
||||
"password_not_match": "Passwierder stëmmen net iwwereneen"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"intro": "Apparaten a Servicë ginn am Home Assistant als Integratioune representéiert. Dir kënnt si elo astellen, oder méi spéit vun der Konfiguratioun's Säit aus.",
|
||||
"more_integrations": "Méi",
|
||||
"finish": "Ofschléissen"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
|
@ -855,6 +855,11 @@
|
||||
"required_fields": "Vul alle verplichte velden in",
|
||||
"password_not_match": "Wachtwoorden komen niet overeen"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"intro": "Apparaten en services worden in Home Assistant weergegeven als integraties. U kunt ze nu instellen of later via het configuratiescherm.",
|
||||
"more_integrations": "Meer",
|
||||
"finish": "Voltooien"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
@ -870,8 +875,11 @@
|
||||
"go_to_integrations_page": "Ga naar de integraties pagina."
|
||||
},
|
||||
"picture-elements": {
|
||||
"hold": "Vasthouden:",
|
||||
"tap": "Tik:",
|
||||
"navigate_to": "Navigeer naar {location}",
|
||||
"toggle": "Omschakelen {name}",
|
||||
"call_service": "Roep service {name} aan",
|
||||
"more_info": "Meer informatie weergeven: {name}"
|
||||
}
|
||||
},
|
||||
@ -930,7 +938,8 @@
|
||||
},
|
||||
"sidebar": {
|
||||
"log_out": "Uitloggen",
|
||||
"developer_tools": "Ontwikkelaarstools"
|
||||
"developer_tools": "Ontwikkelaarstools",
|
||||
"external_app_configuration": "App configuratie"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Bezig met laden",
|
||||
|
@ -575,7 +575,7 @@
|
||||
"name": "Имя",
|
||||
"username": "Логин",
|
||||
"password": "Пароль",
|
||||
"create": "Создать"
|
||||
"create": "Добавить"
|
||||
}
|
||||
},
|
||||
"cloud": {
|
||||
@ -589,7 +589,7 @@
|
||||
"description": "Управляйте подключенными устройствами и службами",
|
||||
"discovered": "Обнаружено",
|
||||
"configured": "Настроено",
|
||||
"new": "Создать новую интеграцию",
|
||||
"new": "Интеграции",
|
||||
"configure": "Настроить",
|
||||
"none": "Пока ничего не настроено",
|
||||
"config_entry": {
|
||||
@ -745,8 +745,8 @@
|
||||
},
|
||||
"page-authorize": {
|
||||
"initializing": "Инициализация",
|
||||
"authorizing_client": "Вы собираетесь предоставить доступ {clientId} к вашему Home Assistant.",
|
||||
"logging_in_with": "Вход с помощью **{authProviderName}**.",
|
||||
"authorizing_client": "Получение доступа к Home Assistant через {clientId}.",
|
||||
"logging_in_with": "Провайдер аутентификации: **{authProviderName}**.",
|
||||
"pick_auth_provider": "Или войти с помощью",
|
||||
"abort_intro": "Вход прерван",
|
||||
"form": {
|
||||
@ -855,6 +855,11 @@
|
||||
"required_fields": "Заполните все обязательные поля",
|
||||
"password_not_match": "Пароли не совпадают"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"intro": "Устройства и сервисы представлены в Home Assistant как интеграции. Вы можете добавить их сейчас или сделать это позже в разделе настроек.",
|
||||
"more_integrations": "Все интеграции",
|
||||
"finish": "Готово"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
|
@ -855,6 +855,11 @@
|
||||
"required_fields": "填寫所有所需欄位",
|
||||
"password_not_match": "密碼不相符"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
"intro": "將會於 Home Assistant 整合中呈現的裝置與服務。可以現在進行設定,或者稍後於設定選單中進行。",
|
||||
"more_integrations": "更多",
|
||||
"finish": "完成"
|
||||
}
|
||||
},
|
||||
"lovelace": {
|
||||
|
@ -7237,10 +7237,10 @@ hoek@6.x.x:
|
||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c"
|
||||
integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==
|
||||
|
||||
home-assistant-js-websocket@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.1.tgz#b85152c223a20bfe8827b817b927fd97cc021157"
|
||||
integrity sha512-hNk8bj9JObd3NpgQ1+KtQCbSoz/TWockC8T/L8KvsPrDtkl1oQddajirumaMDgrJg/su4QsxFNUcDPGJyJ05UA==
|
||||
home-assistant-js-websocket@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.2.tgz#dbcdb4b67df8d189d29bbf5603771d5bc80ef031"
|
||||
integrity sha512-/I0m6FTDEq3LkzFc4tmgHJHTj9gWA6Wn/fgaa1ghIJJY0Yqb3x6whovN5pRNFsl6bnKzOCR+nmJ2ruVTBa5mVQ==
|
||||
|
||||
homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
|
||||
version "1.0.3"
|
||||
|
Loading…
x
Reference in New Issue
Block a user