mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-31 03:50:26 +00:00
Compare commits
10 Commits
combine-ap
...
20210222.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
216526e391 | ||
![]() |
d76af2cb61 | ||
![]() |
b7d4c40736 | ||
![]() |
6092af8de6 | ||
![]() |
627424b8b9 | ||
![]() |
e33aff7cf3 | ||
![]() |
ef0bfb237a | ||
![]() |
c042c5568b | ||
![]() |
d84a7ee358 | ||
![]() |
311e1cfb00 |
@@ -1,7 +1,7 @@
|
||||
const webpack = require("webpack");
|
||||
const path = require("path");
|
||||
const TerserPlugin = require("terser-webpack-plugin");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
|
||||
const paths = require("./paths.js");
|
||||
const bundle = require("./bundle");
|
||||
const log = require("fancy-log");
|
||||
@@ -68,7 +68,7 @@ const createWebpackConfig = ({
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new ManifestPlugin({
|
||||
new WebpackManifestPlugin({
|
||||
// Only include the JS of entrypoints
|
||||
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
|
||||
}),
|
||||
|
@@ -101,7 +101,7 @@
|
||||
"fuse.js": "^6.0.0",
|
||||
"google-timezones-json": "^1.0.2",
|
||||
"hls.js": "^0.13.2",
|
||||
"home-assistant-js-websocket": "^5.8.1",
|
||||
"home-assistant-js-websocket": "^5.9.0",
|
||||
"idb-keyval": "^3.2.0",
|
||||
"intl-messageformat": "^8.3.9",
|
||||
"js-yaml": "^3.13.1",
|
||||
@@ -222,7 +222,7 @@
|
||||
"webpack": "5.1.3",
|
||||
"webpack-cli": "4.1.0",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-manifest-plugin": "3.0.0-rc.0",
|
||||
"webpack-manifest-plugin": "~3.0.0",
|
||||
"workbox-build": "^5.1.3"
|
||||
},
|
||||
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20210208.0",
|
||||
version="20210222.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
@@ -6,6 +6,7 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
@@ -107,8 +108,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
|
||||
|
||||
@@ -290,6 +292,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
: this.label}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
|
@@ -117,6 +117,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
|
||||
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@internalProperty() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@internalProperty() private _devices?: DeviceRegistryEntry[];
|
||||
@@ -339,6 +341,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
.disabled=${this.disabled}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._areaChanged}
|
||||
>
|
||||
@@ -349,6 +352,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
|
||||
.placeholder=${this.placeholder
|
||||
? this._area(this.placeholder)?.name
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
|
@@ -10,6 +10,7 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
@@ -67,8 +68,9 @@ export class HaComboBox extends LitElement {
|
||||
model: { item: any }
|
||||
) => void;
|
||||
|
||||
@property({ type: Boolean })
|
||||
private _opened?: boolean;
|
||||
@property({ type: Boolean }) public disabled?: boolean;
|
||||
|
||||
@internalProperty() private _opened?: boolean;
|
||||
|
||||
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
|
||||
|
||||
@@ -95,12 +97,14 @@ export class HaComboBox extends LitElement {
|
||||
.filteredItems=${this.filteredItems}
|
||||
.renderer=${this.renderer || defaultRowRenderer}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<paper-input
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
class="input"
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
|
@@ -21,8 +21,11 @@ export class HaActionSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-automation-action
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
></ha-automation-action>`;
|
||||
@@ -34,6 +37,10 @@ export class HaActionSelector extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
:host([disabled]) ha-automation-action {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,8 @@ export class HaAreaSelector extends LitElement {
|
||||
|
||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
@@ -50,6 +52,7 @@ export class HaAreaSelector extends LitElement {
|
||||
.includeDomains=${this.selector.area.entity?.domain
|
||||
? [this.selector.area.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-area-picker>`;
|
||||
}
|
||||
|
||||
|
@@ -19,11 +19,14 @@ export class HaBooleanSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
|
||||
<ha-switch
|
||||
.checked=${this.value}
|
||||
@change=${this._handleChange}
|
||||
.disabled=${this.disabled}
|
||||
></ha-switch>
|
||||
</ha-formfield>`;
|
||||
}
|
||||
|
@@ -23,10 +23,12 @@ export class HaDeviceSelector extends LitElement {
|
||||
|
||||
@internalProperty() public _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected updated(changedProperties) {
|
||||
if (changedProperties.has("selector")) {
|
||||
const oldSelector = changedProperties.get("selector");
|
||||
if (oldSelector !== this.selector && this.selector.device.integration) {
|
||||
if (oldSelector !== this.selector && this.selector.device?.integration) {
|
||||
this._loadConfigEntries();
|
||||
}
|
||||
}
|
||||
@@ -44,24 +46,25 @@ export class HaDeviceSelector extends LitElement {
|
||||
.includeDomains=${this.selector.device.entity?.domain
|
||||
? [this.selector.device.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-device-picker>`;
|
||||
}
|
||||
|
||||
private _filterDevices(device: DeviceRegistryEntry): boolean {
|
||||
if (
|
||||
this.selector.device.manufacturer &&
|
||||
this.selector.device?.manufacturer &&
|
||||
device.manufacturer !== this.selector.device.manufacturer
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
this.selector.device.model &&
|
||||
this.selector.device?.model &&
|
||||
device.model !== this.selector.device.model
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (this.selector.device.integration) {
|
||||
if (this.selector.device?.integration) {
|
||||
if (
|
||||
this._configEntries &&
|
||||
!this._configEntries.some((entry) =>
|
||||
|
@@ -25,12 +25,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.entityFilter=${(entity) => this._filterEntities(entity)}
|
||||
.disabled=${this.disabled}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>`;
|
||||
}
|
||||
@@ -51,12 +54,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _filterEntities(entity: HassEntity): boolean {
|
||||
if (this.selector.entity.domain) {
|
||||
if (this.selector.entity?.domain) {
|
||||
if (computeStateDomain(entity) !== this.selector.entity.domain) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.selector.entity.device_class) {
|
||||
if (this.selector.entity?.device_class) {
|
||||
if (
|
||||
!entity.attributes.device_class ||
|
||||
entity.attributes.device_class !== this.selector.entity.device_class
|
||||
@@ -64,7 +67,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.selector.entity.integration) {
|
||||
if (this.selector.entity?.integration) {
|
||||
if (
|
||||
!this._entityPlaformLookup ||
|
||||
this._entityPlaformLookup[entity.entity_id] !==
|
||||
|
@@ -21,8 +21,12 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property() public value?: number;
|
||||
|
||||
@property() public placeholder?: number;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`${this.label}
|
||||
${this.selector.number.mode === "slider"
|
||||
@@ -31,6 +35,7 @@ export class HaNumberSelector extends LitElement {
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this._value}
|
||||
.step=${this.selector.number.step}
|
||||
.disabled=${this.disabled}
|
||||
pin
|
||||
ignore-bar-touch
|
||||
@change=${this._handleSliderChange}
|
||||
@@ -42,12 +47,14 @@ export class HaNumberSelector extends LitElement {
|
||||
.label=${this.selector.number.mode === "slider"
|
||||
? undefined
|
||||
: this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
.noLabelFloat=${this.selector.number.mode === "slider"}
|
||||
class=${classMap({ single: this.selector.number.mode === "box" })}
|
||||
.min=${this.selector.number.min}
|
||||
.max=${this.selector.number.max}
|
||||
.value=${this.value}
|
||||
.step=${this.selector.number.step}
|
||||
.disabled=${this.disabled}
|
||||
type="number"
|
||||
auto-validate
|
||||
@value-changed=${this._handleInputChange}
|
||||
|
@@ -11,8 +11,14 @@ export class HaObjectSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-yaml-editor
|
||||
.disabled=${this.disabled}
|
||||
.placeholder=${this.placeholder}
|
||||
.defaultValue=${this.value}
|
||||
@value-changed=${this._handleChange}
|
||||
></ha-yaml-editor>`;
|
||||
|
@@ -21,8 +21,13 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
return html`<ha-paper-dropdown-menu .label=${this.label}>
|
||||
return html`<ha-paper-dropdown-menu
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.label}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
attr-for-selected="item-value"
|
||||
@@ -41,7 +46,7 @@ export class HaSelectSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
if (!ev.detail.value) {
|
||||
if (this.disabled || !ev.detail.value) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
|
@@ -42,6 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
|
||||
@internalProperty() private _configEntries?: ConfigEntry[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
@@ -84,6 +86,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
|
||||
.includeDomains=${this.selector.target.entity?.domain
|
||||
? [this.selector.target.entity.domain]
|
||||
: undefined}
|
||||
.disabled=${this.disabled}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
|
@@ -13,14 +13,20 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public selector!: StringSelector;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
if (this.selector.text?.multiline) {
|
||||
return html`<paper-textarea
|
||||
.label=${this.label}
|
||||
.value="${this.value}"
|
||||
@value-changed="${this._handleChange}"
|
||||
.placeholder=${this.placeholder}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._handleChange}
|
||||
autocapitalize="none"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
@@ -29,6 +35,8 @@ export class HaTextSelector extends LitElement {
|
||||
return html`<paper-input
|
||||
required
|
||||
.value=${this.value}
|
||||
.placeholder=${this.placeholder}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._handleChange}
|
||||
.label=${this.label}
|
||||
></paper-input>`;
|
||||
|
@@ -17,6 +17,8 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
protected render() {
|
||||
const parts = this.value?.split(":") || [];
|
||||
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
|
||||
@@ -29,6 +31,7 @@ export class HaTimeSelector extends LitElement {
|
||||
.sec=${parts[2] ?? "00"}
|
||||
.format=${useAMPM ? 12 : 24}
|
||||
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._timeChanged}
|
||||
@am-pm-changed=${this._timeChanged}
|
||||
hide-label
|
||||
|
@@ -24,6 +24,10 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: any;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
public focus() {
|
||||
const input = this.shadowRoot!.getElementById("selector");
|
||||
if (!input) {
|
||||
@@ -43,6 +47,8 @@ export class HaSelector extends LitElement {
|
||||
selector: this.selector,
|
||||
value: this.value,
|
||||
label: this.label,
|
||||
placeholder: this.placeholder,
|
||||
disabled: this.disabled,
|
||||
id: "selector",
|
||||
})}
|
||||
`;
|
||||
|
@@ -22,6 +22,7 @@ import "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import "./ha-checkbox";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
|
||||
interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
@@ -30,6 +31,7 @@ interface ExtHassService extends Omit<HassService, "fields"> {
|
||||
name?: string;
|
||||
description: string;
|
||||
required?: boolean;
|
||||
advanced?: boolean;
|
||||
default?: any;
|
||||
example?: any;
|
||||
selector?: Selector;
|
||||
@@ -48,14 +50,26 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ type: Boolean }) public showAdvanced?: boolean;
|
||||
|
||||
@internalProperty() private _serviceData?: ExtHassService;
|
||||
|
||||
@internalProperty() private _checkedKeys = new Set();
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("value")) {
|
||||
return;
|
||||
}
|
||||
const oldValue = changedProperties.get("value") as
|
||||
| undefined
|
||||
| this["value"];
|
||||
|
||||
if (oldValue?.service !== this.value?.service) {
|
||||
this._checkedKeys = new Set();
|
||||
}
|
||||
|
||||
this._serviceData = this.value?.service
|
||||
? this._getServiceInfo(this.value.service)
|
||||
: undefined;
|
||||
@@ -63,13 +77,33 @@ export class HaServiceControl extends LitElement {
|
||||
if (
|
||||
this._serviceData &&
|
||||
"target" in this._serviceData &&
|
||||
this.value?.data?.entity_id
|
||||
(this.value?.data?.entity_id ||
|
||||
this.value?.data?.area_id ||
|
||||
this.value?.data?.device_id)
|
||||
) {
|
||||
const target = {
|
||||
...this.value.target,
|
||||
};
|
||||
|
||||
if (this.value.data.entity_id && !this.value.target?.entity_id) {
|
||||
target.entity_id = this.value.data.entity_id;
|
||||
}
|
||||
if (this.value.data.area_id && !this.value.target?.area_id) {
|
||||
target.area_id = this.value.data.area_id;
|
||||
}
|
||||
if (this.value.data.device_id && !this.value.target?.device_id) {
|
||||
target.device_id = this.value.data.device_id;
|
||||
}
|
||||
|
||||
this.value = {
|
||||
...this.value,
|
||||
target: { ...this.value.target, entity_id: this.value.data.entity_id },
|
||||
target,
|
||||
data: { ...this.value.data },
|
||||
};
|
||||
|
||||
delete this.value.data!.entity_id;
|
||||
delete this.value.data!.device_id;
|
||||
delete this.value.data!.area_id;
|
||||
}
|
||||
|
||||
if (this.value?.data) {
|
||||
@@ -125,24 +159,46 @@ export class HaServiceControl extends LitElement {
|
||||
legacy &&
|
||||
this._serviceData?.fields.find((field) => field.key === "entity_id");
|
||||
|
||||
const hasOptional = Boolean(
|
||||
!legacy &&
|
||||
this._serviceData?.fields.some(
|
||||
(field) => field.selector && !field.required
|
||||
)
|
||||
);
|
||||
|
||||
return html`<ha-service-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value?.service}
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-picker>
|
||||
<p>${this._serviceData?.description}</p>
|
||||
${this._serviceData && "target" in this._serviceData
|
||||
? html`<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._serviceData.target
|
||||
? { target: this._serviceData.target }
|
||||
: {
|
||||
target: {
|
||||
entity: { domain: computeDomain(this.value!.service) },
|
||||
},
|
||||
}}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.value?.target}
|
||||
></ha-selector>`
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_description"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._serviceData.target
|
||||
? { target: this._serviceData.target }
|
||||
: {
|
||||
target: {
|
||||
entity: { domain: computeDomain(this.value!.service) },
|
||||
},
|
||||
}}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.value?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.hass=${this.hass}
|
||||
@@ -156,38 +212,76 @@ export class HaServiceControl extends LitElement {
|
||||
${legacy
|
||||
? html`<ha-yaml-editor
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.service.service_data"
|
||||
"ui.components.service-control.service_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.defaultValue=${this.value?.data}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: this._serviceData?.fields.map((dataField) =>
|
||||
dataField.selector
|
||||
dataField.selector && (!dataField.advanced || this.showAdvanced)
|
||||
? html`<ha-settings-row .narrow=${this.narrow}>
|
||||
${dataField.required
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: ""
|
||||
: html`<ha-checkbox
|
||||
.key=${dataField.key}
|
||||
.checked=${this._checkedKeys.has(dataField.key) ||
|
||||
(this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined)}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading">${dataField.name || dataField.key}</span>
|
||||
<span slot="description">${dataField?.description}</span
|
||||
><ha-selector
|
||||
.disabled=${!dataField.required &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this.value?.data ||
|
||||
this.value.data[dataField.key] === undefined)}
|
||||
.hass=${this.hass}
|
||||
.selector=${dataField.selector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${(this.value?.data &&
|
||||
this.value.data[dataField.key]) ||
|
||||
dataField.default}
|
||||
.value=${this.value?.data &&
|
||||
this.value.data[dataField.key] !== undefined
|
||||
? this.value.data[dataField.key]
|
||||
: dataField.default}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: ""
|
||||
)} `;
|
||||
}
|
||||
|
||||
private _checkboxChanged(ev) {
|
||||
const checked = ev.currentTarget.checked;
|
||||
const key = ev.currentTarget.key;
|
||||
if (checked) {
|
||||
this._checkedKeys.add(key);
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
const data = { ...this.value?.data };
|
||||
|
||||
delete data[key];
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.value,
|
||||
data,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.requestUpdate("_checkedKeys");
|
||||
}
|
||||
|
||||
private _serviceChanged(ev: PolymerChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.value === this.value?.service) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { service: ev.detail.value || "", data: {} },
|
||||
value: { service: ev.detail.value || "" },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,10 +362,27 @@ export class HaServiceControl extends LitElement {
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-settings-row {
|
||||
padding: 0;
|
||||
padding: var(--service-control-padding, 0 16px);
|
||||
}
|
||||
ha-settings-row {
|
||||
--paper-time-input-justify-content: flex-end;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
ha-service-picker,
|
||||
ha-entity-picker,
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: 16px 0;
|
||||
}
|
||||
p {
|
||||
margin: var(--service-control-padding, 0 16px);
|
||||
padding: 16px 0;
|
||||
}
|
||||
:host(:not([narrow])) ha-settings-row paper-input {
|
||||
width: 60%;
|
||||
@@ -279,6 +390,12 @@ export class HaServiceControl extends LitElement {
|
||||
:host(:not([narrow])) ha-settings-row ha-selector {
|
||||
width: 60%;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-left: -16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { html, internalProperty, LitElement, property } from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { domainToName } from "../data/integration";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
|
||||
const rowRenderer = (
|
||||
root: HTMLElement,
|
||||
_owner,
|
||||
model: { item: { service: string; description: string } }
|
||||
model: { item: { service: string; name: string } }
|
||||
) => {
|
||||
if (!root.firstElementChild) {
|
||||
root.innerHTML = `
|
||||
@@ -19,15 +21,16 @@ const rowRenderer = (
|
||||
</style>
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div class='name'>[[item.description]]</div>
|
||||
<div class='name'>[[item.name]]</div>
|
||||
<div secondary>[[item.service]]</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
`;
|
||||
}
|
||||
|
||||
root.querySelector(".name")!.textContent = model.item.description;
|
||||
root.querySelector("[secondary]")!.textContent = model.item.service;
|
||||
root.querySelector(".name")!.textContent = model.item.name;
|
||||
root.querySelector("[secondary]")!.textContent =
|
||||
model.item.name === model.item.service ? "" : model.item.service;
|
||||
};
|
||||
|
||||
class HaServicePicker extends LitElement {
|
||||
@@ -43,13 +46,14 @@ class HaServicePicker extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-picker.service")}
|
||||
.filteredItems=${this._filteredServices(
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._filter
|
||||
)}
|
||||
.value=${this.value}
|
||||
.renderer=${rowRenderer}
|
||||
item-value-path="service"
|
||||
item-label-path="description"
|
||||
item-label-path="name"
|
||||
allow-custom-value
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -57,38 +61,48 @@ class HaServicePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _services = memoizeOne((services: HomeAssistant["services"]): {
|
||||
service: string;
|
||||
description: string;
|
||||
}[] => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const result: { service: string; description: string }[] = [];
|
||||
|
||||
Object.keys(services)
|
||||
.sort()
|
||||
.forEach((domain) => {
|
||||
const services_keys = Object.keys(services[domain]).sort();
|
||||
|
||||
for (const service of services_keys) {
|
||||
result.push({
|
||||
service: `${domain}.${service}`,
|
||||
description:
|
||||
services[domain][service].description || `${domain}.${service}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
private _filteredServices = memoizeOne(
|
||||
(services: HomeAssistant["services"], filter?: string) => {
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): {
|
||||
service: string;
|
||||
name: string;
|
||||
}[] => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const processedServices = this._services(services);
|
||||
const result: { service: string; name: string }[] = [];
|
||||
|
||||
Object.keys(services)
|
||||
.sort()
|
||||
.forEach((domain) => {
|
||||
const services_keys = Object.keys(services[domain]).sort();
|
||||
|
||||
for (const service of services_keys) {
|
||||
result.push({
|
||||
service: `${domain}.${service}`,
|
||||
name: `${domainToName(localize, domain)}: ${
|
||||
services[domain][service].name || service
|
||||
}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private _filteredServices = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
filter?: string
|
||||
) => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const processedServices = this._services(localize, services);
|
||||
|
||||
if (!filter) {
|
||||
return processedServices;
|
||||
@@ -96,7 +110,7 @@ class HaServicePicker extends LitElement {
|
||||
return processedServices.filter(
|
||||
(service) =>
|
||||
service.service.toLowerCase().includes(filter) ||
|
||||
service.description.toLowerCase().includes(filter)
|
||||
service.name?.toLowerCase().includes(filter)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
SVGTemplateResult,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
|
||||
@customElement("ha-settings-row")
|
||||
@@ -16,15 +16,18 @@ export class HaSettingsRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "three-line" })
|
||||
public threeLine = false;
|
||||
|
||||
protected render(): SVGTemplateResult {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-item-body
|
||||
?two-line=${!this.threeLine}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
<div class="prefix-wrap">
|
||||
<slot name="prefix"></slot>
|
||||
<paper-item-body
|
||||
?two-line=${!this.threeLine}
|
||||
?three-line=${this.threeLine}
|
||||
>
|
||||
<slot name="heading"></slot>
|
||||
<div secondary><slot name="description"></slot></div>
|
||||
</paper-item-body>
|
||||
</div>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
@@ -59,6 +62,13 @@ export class HaSettingsRow extends LitElement {
|
||||
div[secondary] {
|
||||
white-space: normal;
|
||||
}
|
||||
.prefix-wrap {
|
||||
display: contents;
|
||||
}
|
||||
:host([narrow]) .prefix-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -84,6 +84,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
|
||||
|
||||
@internalProperty() private _devices?: {
|
||||
@@ -438,7 +440,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
type: string,
|
||||
id: string
|
||||
): this["value"] {
|
||||
const newVal = ensureArray(value![type])!.filter((val) => val !== id);
|
||||
const newVal = ensureArray(value![type])!.filter(
|
||||
(val) => String(val) !== id
|
||||
);
|
||||
if (newVal.length) {
|
||||
return {
|
||||
...value,
|
||||
@@ -599,6 +603,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
paper-tooltip.expand {
|
||||
min-width: 200px;
|
||||
}
|
||||
:host([disabled]) .mdc-chip {
|
||||
opacity: var(--light-disabled-opacity);
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -44,14 +44,14 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
@internalProperty() private _yaml = "";
|
||||
|
||||
@query("ha-code-editor", true) private _editor?: HaCodeEditor;
|
||||
@query("ha-code-editor") private _editor?: HaCodeEditor;
|
||||
|
||||
public setValue(value): void {
|
||||
try {
|
||||
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
console.error(err, value);
|
||||
alert(`There was an error converting to YAML: ${err}`);
|
||||
}
|
||||
afterNextRender(() => {
|
||||
@@ -73,7 +73,7 @@ export class HaYamlEditor extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
${this.label ? html` <p>${this.label}</p> ` : ""}
|
||||
${this.label ? html`<p>${this.label}</p>` : ""}
|
||||
<ha-code-editor
|
||||
.value=${this._yaml}
|
||||
mode="yaml"
|
||||
@@ -85,13 +85,13 @@ export class HaYamlEditor extends LitElement {
|
||||
|
||||
private _onChange(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
this._yaml = ev.detail.value;
|
||||
let parsed;
|
||||
let isValid = true;
|
||||
|
||||
if (value) {
|
||||
if (this._yaml) {
|
||||
try {
|
||||
parsed = safeLoad(value);
|
||||
parsed = safeLoad(this._yaml);
|
||||
} catch (err) {
|
||||
// Invalid YAML
|
||||
isValid = false;
|
||||
@@ -107,7 +107,7 @@ export class HaYamlEditor extends LitElement {
|
||||
}
|
||||
|
||||
get yaml() {
|
||||
return this._editor?.value;
|
||||
return this._yaml;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -65,16 +65,18 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
|
||||
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
|
||||
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
|
||||
|
||||
const fetchConfigFlowInProgress = (conn) =>
|
||||
export const fetchConfigFlowInProgress = (
|
||||
conn: Connection
|
||||
): Promise<DataEntryFlowProgress[]> =>
|
||||
conn.sendMessagePromise({
|
||||
type: "config_entries/flow/progress",
|
||||
});
|
||||
|
||||
const subscribeConfigFlowInProgressUpdates = (conn, store) =>
|
||||
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchConfigFlowInProgress(conn).then((flows) =>
|
||||
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
|
||||
store.setState(flows, true)
|
||||
),
|
||||
500,
|
||||
|
@@ -37,8 +37,9 @@ export const validateHassioSession = async (
|
||||
type: "supervisor/api",
|
||||
endpoint: "/ingress/validate_session",
|
||||
method: "post",
|
||||
data: session,
|
||||
data: { session },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
|
@@ -22,7 +22,9 @@ import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import { fetchConfigFlowInProgress } from "../../data/config_flow";
|
||||
import type {
|
||||
DataEntryFlowProgress,
|
||||
DataEntryFlowProgressedEvent,
|
||||
DataEntryFlowStep,
|
||||
} from "../../data/data_entry_flow";
|
||||
@@ -41,6 +43,7 @@ import "./step-flow-form";
|
||||
import "./step-flow-loading";
|
||||
import "./step-flow-pick-handler";
|
||||
import "./step-flow-progress";
|
||||
import "./step-flow-pick-flow";
|
||||
|
||||
let instance = 0;
|
||||
|
||||
@@ -76,6 +79,10 @@ class DataEntryFlowDialog extends LitElement {
|
||||
|
||||
@internalProperty() private _handlers?: string[];
|
||||
|
||||
@internalProperty() private _handler?: string;
|
||||
|
||||
@internalProperty() private _flowsInProgress?: DataEntryFlowProgress[];
|
||||
|
||||
private _unsubAreas?: UnsubscribeFunc;
|
||||
|
||||
private _unsubDevices?: UnsubscribeFunc;
|
||||
@@ -84,59 +91,93 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._params = params;
|
||||
this._instance = instance++;
|
||||
|
||||
if (params.startFlowHandler) {
|
||||
this._checkFlowsInProgress(params.startFlowHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.continueFlowId) {
|
||||
this._loading = true;
|
||||
const curInstance = this._instance;
|
||||
let step: DataEntryFlowStep;
|
||||
try {
|
||||
step = await params.flowConfig.fetchFlow(
|
||||
this.hass,
|
||||
params.continueFlowId
|
||||
);
|
||||
} catch (err) {
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Happens if second showDialog called
|
||||
if (curInstance !== this._instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processStep(step);
|
||||
this._loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new config flow. Show picker
|
||||
if (!params.continueFlowId && !params.startFlowHandler) {
|
||||
if (!params.flowConfig.getFlowHandlers) {
|
||||
throw new Error("No getFlowHandlers defined in flow config");
|
||||
if (!params.flowConfig.getFlowHandlers) {
|
||||
throw new Error("No getFlowHandlers defined in flow config");
|
||||
}
|
||||
this._step = null;
|
||||
|
||||
// We only load the handlers once
|
||||
if (this._handlers === undefined) {
|
||||
this._loading = true;
|
||||
try {
|
||||
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
this._step = null;
|
||||
|
||||
// We only load the handlers once
|
||||
if (this._handlers === undefined) {
|
||||
this._loading = true;
|
||||
try {
|
||||
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
await this.updateComplete;
|
||||
return;
|
||||
}
|
||||
|
||||
this._loading = true;
|
||||
const curInstance = this._instance;
|
||||
let step: DataEntryFlowStep;
|
||||
try {
|
||||
step = await (params.continueFlowId
|
||||
? params.flowConfig.fetchFlow(this.hass, params.continueFlowId)
|
||||
: params.flowConfig.createFlow(this.hass, params.startFlowHandler!));
|
||||
} catch (err) {
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
showAlertDialog(this, {
|
||||
title: "Error",
|
||||
text: "Config flow could not be loaded",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Happens if second showDialog called
|
||||
if (curInstance !== this._instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processStep(step);
|
||||
this._loading = false;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
if (this._step) {
|
||||
this._flowDone();
|
||||
} else if (this._step === null) {
|
||||
// Flow aborted during picking flow
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const flowFinished = Boolean(
|
||||
this._step && ["create_entry", "abort"].includes(this._step.type)
|
||||
);
|
||||
|
||||
// If we created this flow, delete it now.
|
||||
if (this._step && !flowFinished && !this._params.continueFlowId) {
|
||||
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
|
||||
}
|
||||
|
||||
if (this._step !== null && this._params.dialogClosedCallback) {
|
||||
this._params.dialogClosedCallback({
|
||||
flowFinished,
|
||||
});
|
||||
}
|
||||
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this._devices = undefined;
|
||||
this._flowsInProgress = undefined;
|
||||
this._handler = undefined;
|
||||
if (this._unsubAreas) {
|
||||
this._unsubAreas();
|
||||
this._unsubAreas = undefined;
|
||||
}
|
||||
if (this._unsubDevices) {
|
||||
this._unsubDevices();
|
||||
this._unsubDevices = undefined;
|
||||
}
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -156,7 +197,9 @@ class DataEntryFlowDialog extends LitElement {
|
||||
>
|
||||
<div>
|
||||
${this._loading ||
|
||||
(this._step === null && this._handlers === undefined)
|
||||
(this._step === null &&
|
||||
this._handlers === undefined &&
|
||||
this._handler === undefined)
|
||||
? html`
|
||||
<step-flow-loading
|
||||
.label=${this.hass.localize(
|
||||
@@ -178,15 +221,22 @@ class DataEntryFlowDialog extends LitElement {
|
||||
?rtl=${computeRTL(this.hass)}
|
||||
></ha-icon-button>
|
||||
${this._step === null
|
||||
? // Show handler picker
|
||||
html`
|
||||
<step-flow-pick-handler
|
||||
? this._handler
|
||||
? html`<step-flow-pick-flow
|
||||
.flowConfig=${this._params.flowConfig}
|
||||
.hass=${this.hass}
|
||||
.handlers=${this._handlers}
|
||||
.showAdvanced=${this._params.showAdvanced}
|
||||
></step-flow-pick-handler>
|
||||
`
|
||||
.handler=${this._handler}
|
||||
.flowsInProgress=${this._flowsInProgress}
|
||||
></step-flow-pick-flow>`
|
||||
: // Show handler picker
|
||||
html`
|
||||
<step-flow-pick-handler
|
||||
.hass=${this.hass}
|
||||
.handlers=${this._handlers}
|
||||
.showAdvanced=${this._params.showAdvanced}
|
||||
@handler-picked=${this._handlerPicked}
|
||||
></step-flow-pick-handler>
|
||||
`
|
||||
: this._step.type === "form"
|
||||
? html`
|
||||
<step-flow-form
|
||||
@@ -291,6 +341,43 @@ class DataEntryFlowDialog extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _checkFlowsInProgress(handler: string) {
|
||||
this._loading = true;
|
||||
|
||||
const flowsInProgress = (
|
||||
await fetchConfigFlowInProgress(this.hass.connection)
|
||||
).filter((flow) => flow.handler === handler);
|
||||
|
||||
if (!flowsInProgress.length) {
|
||||
let step: DataEntryFlowStep;
|
||||
try {
|
||||
step = await this._params!.flowConfig.createFlow(this.hass, handler);
|
||||
} catch (err) {
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.error"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.could_not_load"
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._processStep(step);
|
||||
} else {
|
||||
this._step = null;
|
||||
this._handler = handler;
|
||||
this._flowsInProgress = flowsInProgress;
|
||||
}
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
private _handlerPicked(ev) {
|
||||
this._checkFlowsInProgress(ev.detail.handler);
|
||||
}
|
||||
|
||||
private async _processStep(
|
||||
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
|
||||
): Promise<void> {
|
||||
@@ -305,7 +392,7 @@ class DataEntryFlowDialog extends LitElement {
|
||||
}
|
||||
|
||||
if (step === undefined) {
|
||||
this._flowDone();
|
||||
this.closeDialog();
|
||||
return;
|
||||
}
|
||||
this._step = undefined;
|
||||
@@ -313,38 +400,6 @@ class DataEntryFlowDialog extends LitElement {
|
||||
this._step = step;
|
||||
}
|
||||
|
||||
private _flowDone(): void {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const flowFinished = Boolean(
|
||||
this._step && ["create_entry", "abort"].includes(this._step.type)
|
||||
);
|
||||
|
||||
// If we created this flow, delete it now.
|
||||
if (this._step && !flowFinished && !this._params.continueFlowId) {
|
||||
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
|
||||
}
|
||||
|
||||
if (this._params.dialogClosedCallback) {
|
||||
this._params.dialogClosedCallback({
|
||||
flowFinished,
|
||||
});
|
||||
}
|
||||
|
||||
this._step = undefined;
|
||||
this._params = undefined;
|
||||
this._devices = undefined;
|
||||
if (this._unsubAreas) {
|
||||
this._unsubAreas();
|
||||
this._unsubAreas = undefined;
|
||||
}
|
||||
if (this._unsubDevices) {
|
||||
this._unsubDevices();
|
||||
this._unsubDevices = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyleDialog,
|
||||
|
130
src/dialogs/config-flow/step-flow-pick-flow.ts
Normal file
130
src/dialogs/config-flow/step-flow-pick-flow.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-icon-next";
|
||||
import { localizeConfigFlowTitle } from "../../data/config_flow";
|
||||
import { DataEntryFlowProgress } from "../../data/data_entry_flow";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
@customElement("step-flow-pick-flow")
|
||||
class StepFlowPickFlow extends LitElement {
|
||||
public flowConfig!: FlowConfig;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public flowsInProgress!: DataEntryFlowProgress[];
|
||||
|
||||
@property() public handler!: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.pick_flow_step.title"
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<div>
|
||||
${this.flowsInProgress.map(
|
||||
(flow) => html` <paper-icon-item
|
||||
@click=${this._flowInProgressPicked}
|
||||
.flow=${flow}
|
||||
>
|
||||
<img
|
||||
slot="item-icon"
|
||||
loading="lazy"
|
||||
src=${brandsUrl(flow.handler, "icon", true)}
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
|
||||
<paper-item-body>
|
||||
${localizeConfigFlowTitle(this.hass.localize, flow)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-icon-item>`
|
||||
)}
|
||||
<paper-item @click=${this._startNewFlowPicked} .handler=${this.handler}>
|
||||
<paper-item-body>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.pick_flow_step.new_flow",
|
||||
"integration",
|
||||
domainToName(this.hass.localize, this.handler)
|
||||
)}
|
||||
</paper-item-body>
|
||||
<ha-icon-next></ha-icon-next>
|
||||
</paper-item>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _startNewFlowPicked(ev) {
|
||||
this._startFlow(ev.currentTarget.handler);
|
||||
}
|
||||
|
||||
private _startFlow(handler: string) {
|
||||
fireEvent(this, "flow-update", {
|
||||
stepPromise: this.flowConfig.createFlow(this.hass, handler),
|
||||
});
|
||||
}
|
||||
|
||||
private _flowInProgressPicked(ev) {
|
||||
const flow: DataEntryFlowProgress = ev.currentTarget.flow;
|
||||
fireEvent(this, "flow-update", {
|
||||
stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
configFlowContentStyles,
|
||||
css`
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
ha-icon-next {
|
||||
margin-right: 8px;
|
||||
}
|
||||
div {
|
||||
overflow: auto;
|
||||
max-height: 600px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
h2 {
|
||||
padding-right: 66px;
|
||||
}
|
||||
@media all and (max-height: 900px) {
|
||||
div {
|
||||
max-height: calc(100vh - 134px);
|
||||
}
|
||||
}
|
||||
paper-icon-item,
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"step-flow-pick-flow": StepFlowPickFlow;
|
||||
}
|
||||
}
|
@@ -22,7 +22,6 @@ import { domainToName } from "../../data/integration";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { FlowConfig } from "./show-dialog-data-entry-flow";
|
||||
import { configFlowContentStyles } from "./styles";
|
||||
|
||||
interface HandlerObj {
|
||||
@@ -30,17 +29,24 @@ interface HandlerObj {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"handler-picked": {
|
||||
handler: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("step-flow-pick-handler")
|
||||
class StepFlowPickHandler extends LitElement {
|
||||
public flowConfig!: FlowConfig;
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public handlers!: string[];
|
||||
|
||||
@property() public showAdvanced?: boolean;
|
||||
|
||||
@internalProperty() private filter?: string;
|
||||
@internalProperty() private _filter?: string;
|
||||
|
||||
private _width?: number;
|
||||
|
||||
@@ -74,7 +80,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const handlers = this._getHandlers(
|
||||
this.handlers,
|
||||
this.filter,
|
||||
this._filter,
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
@@ -82,7 +88,7 @@ class StepFlowPickHandler extends LitElement {
|
||||
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
|
||||
<search-input
|
||||
autofocus
|
||||
.filter=${this.filter}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.integrations.search")}
|
||||
></search-input>
|
||||
@@ -164,15 +170,12 @@ class StepFlowPickHandler extends LitElement {
|
||||
}
|
||||
|
||||
private async _filterChanged(e) {
|
||||
this.filter = e.detail.value;
|
||||
this._filter = e.detail.value;
|
||||
}
|
||||
|
||||
private async _handlerPicked(ev) {
|
||||
fireEvent(this, "flow-update", {
|
||||
stepPromise: this.flowConfig.createFlow(
|
||||
this.hass,
|
||||
ev.currentTarget.handler.slug
|
||||
),
|
||||
fireEvent(this, "handler-picked", {
|
||||
handler: ev.currentTarget.handler.slug,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,6 +198,9 @@ class StepFlowPickHandler extends LitElement {
|
||||
overflow: auto;
|
||||
max-height: 600px;
|
||||
}
|
||||
h2 {
|
||||
padding-right: 66px;
|
||||
}
|
||||
@media all and (max-height: 900px) {
|
||||
div {
|
||||
max-height: calc(100vh - 134px);
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
@@ -62,6 +64,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.value=${this._action}
|
||||
.showAdvanced=${this.hass.userData?.showAdvanced}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-service-control>
|
||||
`;
|
||||
@@ -72,6 +75,15 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
ev.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-service-control {
|
||||
display: block;
|
||||
margin: 0 -16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -211,7 +211,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
"ui.panel.config.blueprint.overview.add_blueprint"
|
||||
)}
|
||||
extended
|
||||
@click=${this._addBlueprint}
|
||||
@click=${this._addBlueprintClicked}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
@@ -249,6 +249,10 @@ class HaBlueprintOverview extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _addBlueprintClicked(): void {
|
||||
this._addBlueprint();
|
||||
}
|
||||
|
||||
private _reload() {
|
||||
fireEvent(this, "reload-blueprints");
|
||||
}
|
||||
|
@@ -1,371 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { safeDump, safeLoad } from "js-yaml";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-code-editor";
|
||||
import "../../../components/ha-service-picker";
|
||||
import { ENTITY_COMPONENT_DOMAINS } from "../../../data/entity";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import LocalizeMixin from "../../../mixins/localize-mixin";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import "../../../util/app-localstorage-document";
|
||||
|
||||
const ERROR_SENTINEL = {};
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaPanelDevService extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ha-form {
|
||||
margin-right: 16px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
ha-progress-button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.attributes th {
|
||||
text-align: left;
|
||||
background-color: var(--card-background-color);
|
||||
border-bottom: 1px solid var(--primary-text-color);
|
||||
}
|
||||
|
||||
:host([rtl]) .attributes th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.attributes tr {
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.attributes tr:nth-child(odd) {
|
||||
background-color: var(--table-row-background-color, #eee);
|
||||
}
|
||||
|
||||
.attributes tr:nth-child(even) {
|
||||
background-color: var(--table-row-alternative-background-color, #eee);
|
||||
}
|
||||
|
||||
.attributes td:nth-child(3) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
:host([rtl]) .desc-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
:host([rtl]) .desc-container h3 {
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
|
||||
<app-localstorage-document
|
||||
key="panel-dev-service-state-domain-service"
|
||||
data="{{domainService}}"
|
||||
>
|
||||
</app-localstorage-document>
|
||||
<app-localstorage-document
|
||||
key="[[_computeServiceDataKey(domainService)]]"
|
||||
data="{{serviceData}}"
|
||||
>
|
||||
</app-localstorage-document>
|
||||
|
||||
<div class="content">
|
||||
<p>
|
||||
[[localize('ui.panel.developer-tools.tabs.services.description')]]
|
||||
</p>
|
||||
|
||||
<div class="ha-form">
|
||||
<ha-service-picker
|
||||
hass="[[hass]]"
|
||||
value="{{domainService}}"
|
||||
></ha-service-picker>
|
||||
<template is="dom-if" if="[[_computeHasEntity(_attributes)]]">
|
||||
<ha-entity-picker
|
||||
hass="[[hass]]"
|
||||
value="[[_computeEntityValue(parsedJSON)]]"
|
||||
on-change="_entityPicked"
|
||||
disabled="[[!validJSON]]"
|
||||
include-domains="[[_computeEntityDomainFilter(_domain)]]"
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
</template>
|
||||
<p>[[localize('ui.panel.developer-tools.tabs.services.data')]]</p>
|
||||
<ha-code-editor
|
||||
mode="yaml"
|
||||
value="[[serviceData]]"
|
||||
error="[[!validJSON]]"
|
||||
on-value-changed="_yamlChanged"
|
||||
></ha-code-editor>
|
||||
<ha-progress-button
|
||||
on-click="_callService"
|
||||
raised
|
||||
disabled="[[!validJSON]]"
|
||||
>
|
||||
[[localize('ui.panel.developer-tools.tabs.services.call_service')]]
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
|
||||
<ha-card>
|
||||
<div class="card-header">
|
||||
<template is="dom-if" if="[[!domainService]]">
|
||||
[[localize('ui.panel.developer-tools.tabs.services.select_service')]]
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[domainService]]">
|
||||
<template is="dom-if" if="[[!_description]]">
|
||||
[[localize('ui.panel.developer-tools.tabs.services.no_description')]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[_description]]">
|
||||
[[_description]]
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<template is="dom-if" if="[[_description]]">
|
||||
<template is="dom-if" if="[[!_attributes.length]]">
|
||||
[[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_attributes.length]]">
|
||||
<table class="attributes">
|
||||
<tr>
|
||||
<th>
|
||||
[[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
|
||||
</th>
|
||||
<th>
|
||||
[[localize('ui.panel.developer-tools.tabs.services.column_description')]]
|
||||
</th>
|
||||
<th>
|
||||
[[localize('ui.panel.developer-tools.tabs.services.column_example')]]
|
||||
</th>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[_attributes]]" as="attribute">
|
||||
<tr>
|
||||
<td><pre>[[attribute.key]]</pre></td>
|
||||
<td>[[attribute.description]]</td>
|
||||
<td>[[attribute.example]]</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_attributes.length]]">
|
||||
<mwc-button on-click="_fillExampleData">
|
||||
[[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
|
||||
</mwc-button>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
domainService: {
|
||||
type: String,
|
||||
observer: "_domainServiceChanged",
|
||||
},
|
||||
|
||||
_domain: {
|
||||
type: String,
|
||||
computed: "_computeDomain(domainService)",
|
||||
},
|
||||
|
||||
_service: {
|
||||
type: String,
|
||||
computed: "_computeService(domainService)",
|
||||
},
|
||||
|
||||
serviceData: {
|
||||
type: String,
|
||||
value: "",
|
||||
},
|
||||
|
||||
parsedJSON: {
|
||||
type: Object,
|
||||
computed: "_computeParsedServiceData(serviceData)",
|
||||
},
|
||||
|
||||
validJSON: {
|
||||
type: Boolean,
|
||||
computed: "_computeValidJSON(parsedJSON)",
|
||||
},
|
||||
|
||||
_attributes: {
|
||||
type: Array,
|
||||
computed: "_computeAttributesArray(hass, _domain, _service)",
|
||||
},
|
||||
|
||||
_description: {
|
||||
type: String,
|
||||
computed: "_computeDescription(hass, _domain, _service)",
|
||||
},
|
||||
|
||||
rtl: {
|
||||
reflectToAttribute: true,
|
||||
computed: "_computeRTL(hass)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_domainServiceChanged() {
|
||||
this.serviceData = "";
|
||||
}
|
||||
|
||||
_computeAttributesArray(hass, domain, service) {
|
||||
const serviceDomains = hass.services;
|
||||
if (!(domain in serviceDomains)) return [];
|
||||
if (!(service in serviceDomains[domain])) return [];
|
||||
|
||||
const fields = serviceDomains[domain][service].fields;
|
||||
return Object.keys(fields).map(function (field) {
|
||||
return { key: field, ...fields[field] };
|
||||
});
|
||||
}
|
||||
|
||||
_computeDescription(hass, domain, service) {
|
||||
const serviceDomains = hass.services;
|
||||
if (!(domain in serviceDomains)) return undefined;
|
||||
if (!(service in serviceDomains[domain])) return undefined;
|
||||
return serviceDomains[domain][service].description;
|
||||
}
|
||||
|
||||
_computeServiceDataKey(domainService) {
|
||||
return `panel-dev-service-state-servicedata.${domainService}`;
|
||||
}
|
||||
|
||||
_computeDomain(domainService) {
|
||||
return domainService.split(".", 1)[0];
|
||||
}
|
||||
|
||||
_computeService(domainService) {
|
||||
return domainService.split(".", 2)[1] || null;
|
||||
}
|
||||
|
||||
_computeParsedServiceData(serviceData) {
|
||||
try {
|
||||
return serviceData.trim() ? safeLoad(serviceData) : {};
|
||||
} catch (err) {
|
||||
return ERROR_SENTINEL;
|
||||
}
|
||||
}
|
||||
|
||||
_computeValidJSON(parsedJSON) {
|
||||
return parsedJSON !== ERROR_SENTINEL;
|
||||
}
|
||||
|
||||
_computeHasEntity(attributes) {
|
||||
return attributes.some((attr) => attr.key === "entity_id");
|
||||
}
|
||||
|
||||
_computeEntityValue(parsedJSON) {
|
||||
return parsedJSON === ERROR_SENTINEL ? "" : parsedJSON.entity_id;
|
||||
}
|
||||
|
||||
_computeEntityDomainFilter(domain) {
|
||||
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
|
||||
}
|
||||
|
||||
_callService(ev) {
|
||||
const button = ev.target;
|
||||
if (this.parsedJSON === ERROR_SENTINEL) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.alert_parsing_yaml",
|
||||
"data",
|
||||
this.serviceData
|
||||
),
|
||||
});
|
||||
button.actionError();
|
||||
return;
|
||||
}
|
||||
this.hass
|
||||
.callService(this._domain, this._service, this.parsedJSON)
|
||||
.then(() => {
|
||||
button.actionSuccess();
|
||||
})
|
||||
.catch(() => {
|
||||
button.actionError();
|
||||
});
|
||||
}
|
||||
|
||||
_fillExampleData() {
|
||||
const example = {};
|
||||
this._attributes.forEach((attribute) => {
|
||||
if (attribute.example) {
|
||||
let value = "";
|
||||
try {
|
||||
value = safeLoad(attribute.example);
|
||||
} catch (err) {
|
||||
value = attribute.example;
|
||||
}
|
||||
example[attribute.key] = value;
|
||||
}
|
||||
});
|
||||
this.serviceData = safeDump(example);
|
||||
}
|
||||
|
||||
_entityPicked(ev) {
|
||||
this.serviceData = safeDump({
|
||||
...this.parsedJSON,
|
||||
entity_id: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
_yamlChanged(ev) {
|
||||
this.serviceData = ev.detail.value;
|
||||
}
|
||||
|
||||
_computeRTL(hass) {
|
||||
return computeRTL(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("developer-tools-service", HaPanelDevService);
|
350
src/panels/developer-tools/service/developer-tools-service.ts
Normal file
350
src/panels/developer-tools/service/developer-tools-service.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { safeLoad } from "js-yaml";
|
||||
import {
|
||||
css,
|
||||
CSSResultArray,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { LocalStorage } from "../../../common/decorators/local-storage";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import "../../../components/entity/ha-entity-picker";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-service-control";
|
||||
import "../../../components/ha-service-picker";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
|
||||
import { ServiceAction } from "../../../data/script";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import "../../../styles/polymer-ha-style";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "../../../util/app-localstorage-document";
|
||||
|
||||
class HaPanelDevService extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public narrow!: boolean;
|
||||
|
||||
@LocalStorage("panel-dev-service-state-service-data", true)
|
||||
private _serviceData?: ServiceAction = { service: "", target: {}, data: {} };
|
||||
|
||||
@LocalStorage("panel-dev-service-state-yaml-mode", true)
|
||||
private _yamlMode = false;
|
||||
|
||||
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
|
||||
|
||||
protected firstUpdated(params) {
|
||||
super.firstUpdated(params);
|
||||
if (!this._serviceData?.service) {
|
||||
const domain = Object.keys(this.hass.services).sort()[0];
|
||||
const service = Object.keys(this.hass.services[domain]).sort()[0];
|
||||
this._serviceData = {
|
||||
service: `${domain}.${service}`,
|
||||
target: {},
|
||||
data: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const { target, fields } = this._fields(
|
||||
this.hass.services,
|
||||
this._serviceData?.service
|
||||
);
|
||||
|
||||
const isValid = this._isValid(this._serviceData, fields, target);
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.description"
|
||||
)}
|
||||
</p>
|
||||
|
||||
${this._yamlMode
|
||||
? html`<ha-yaml-editor
|
||||
.defaultValue=${this._serviceData}
|
||||
@value-changed=${this._yamlChanged}
|
||||
></ha-yaml-editor>`
|
||||
: html`<ha-card
|
||||
><div>
|
||||
<ha-service-control
|
||||
.hass=${this.hass}
|
||||
.value=${this._serviceData}
|
||||
.narrow=${this.narrow}
|
||||
showAdvanced
|
||||
@value-changed=${this._serviceChanged}
|
||||
></ha-service-control></div
|
||||
></ha-card>`}
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<div class="buttons">
|
||||
<mwc-button @click=${this._toggleYaml}>
|
||||
${this._yamlMode
|
||||
? this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.ui_mode"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.yaml_mode"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button .disabled=${!isValid} raised @click=${this._callService}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.call_service"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${(this._yamlMode ? fields : this._filterSelectorFields(fields)).length
|
||||
? html`<div class="content">
|
||||
<ha-expansion-panel
|
||||
.header=${this._yamlMode
|
||||
? this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.all_parameters"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.yaml_parameters"
|
||||
)}
|
||||
outlined
|
||||
.expanded=${this._yamlMode}
|
||||
>
|
||||
${this._yamlMode && target
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.accepts_target"
|
||||
)}
|
||||
</h3>`
|
||||
: ""}
|
||||
<table class="attributes">
|
||||
<tr>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.column_parameter"
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.column_description"
|
||||
)}
|
||||
</th>
|
||||
<th>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.column_example"
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
${fields.map(
|
||||
(field) => html` <tr>
|
||||
<td><pre>${field.key}</pre></td>
|
||||
<td>${field.description}</td>
|
||||
<td>${field.example}</td>
|
||||
</tr>`
|
||||
)}
|
||||
</table>
|
||||
${this._yamlMode
|
||||
? html`<mwc-button @click=${this._fillExampleData}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.services.fill_example_data"
|
||||
)}</mwc-button
|
||||
>`
|
||||
: ""}
|
||||
</ha-expansion-panel>
|
||||
</div>`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
private _filterSelectorFields = memoizeOne((fields) =>
|
||||
fields.filter((field) => !field.selector)
|
||||
);
|
||||
|
||||
private _isValid = memoizeOne((serviceData, fields, target): boolean => {
|
||||
if (!serviceData?.service) {
|
||||
return false;
|
||||
}
|
||||
const domain = computeDomain(serviceData.service);
|
||||
const service = computeObjectId(serviceData.service);
|
||||
if (!domain || !service) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
target &&
|
||||
!serviceData.target &&
|
||||
!serviceData.data?.entity_id &&
|
||||
!serviceData.data?.device_id &&
|
||||
!serviceData.data?.area_id
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
for (const field of fields) {
|
||||
if (
|
||||
field.required &&
|
||||
(!serviceData.data || serviceData.data[field.key] === undefined)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
private _fields = memoizeOne(
|
||||
(
|
||||
serviceDomains: HomeAssistant["services"],
|
||||
domainService: string | undefined
|
||||
): { target: boolean; fields: any[] } => {
|
||||
if (!domainService) {
|
||||
return { target: false, fields: [] };
|
||||
}
|
||||
const domain = computeDomain(domainService);
|
||||
const service = computeObjectId(domainService);
|
||||
if (!(domain in serviceDomains)) {
|
||||
return { target: false, fields: [] };
|
||||
}
|
||||
if (!(service in serviceDomains[domain])) {
|
||||
return { target: false, fields: [] };
|
||||
}
|
||||
const target = "target" in serviceDomains[domain][service];
|
||||
const fields = serviceDomains[domain][service].fields;
|
||||
const result = Object.keys(fields).map((field) => {
|
||||
return { key: field, ...fields[field] };
|
||||
});
|
||||
|
||||
return {
|
||||
target,
|
||||
fields: result,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private _callService() {
|
||||
const domain = computeDomain(this._serviceData!.service);
|
||||
const service = computeObjectId(this._serviceData!.service);
|
||||
if (!domain || !service) {
|
||||
return;
|
||||
}
|
||||
this.hass.callService(
|
||||
domain,
|
||||
service,
|
||||
this._serviceData!.data,
|
||||
this._serviceData!.target
|
||||
);
|
||||
}
|
||||
|
||||
private _toggleYaml() {
|
||||
this._yamlMode = !this._yamlMode;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev) {
|
||||
if (!ev.detail.isValid) {
|
||||
return;
|
||||
}
|
||||
this._serviceChanged(ev);
|
||||
}
|
||||
|
||||
private _serviceChanged(ev) {
|
||||
this._serviceData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _fillExampleData() {
|
||||
const { fields } = this._fields(
|
||||
this.hass.services,
|
||||
this._serviceData?.service
|
||||
);
|
||||
const example = {};
|
||||
fields.forEach((field) => {
|
||||
if (field.example) {
|
||||
let value = "";
|
||||
try {
|
||||
value = safeLoad(field.example);
|
||||
} catch (err) {
|
||||
value = field.example;
|
||||
}
|
||||
example[field.key] = value;
|
||||
}
|
||||
});
|
||||
this._serviceData = { ...this._serviceData!, data: example };
|
||||
this._yamlEditor?.setValue(this._serviceData);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultArray {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 16px;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
.button-row {
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
background: var(--card-background-color);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-row .buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.attributes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.attributes th {
|
||||
text-align: left;
|
||||
background-color: var(--card-background-color);
|
||||
border-bottom: 1px solid var(--primary-text-color);
|
||||
}
|
||||
|
||||
:host([rtl]) .attributes th {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.attributes tr {
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.attributes tr:nth-child(odd) {
|
||||
background-color: var(--table-row-background-color, #eee);
|
||||
}
|
||||
|
||||
.attributes tr:nth-child(even) {
|
||||
background-color: var(--table-row-alternative-background-color, #eee);
|
||||
}
|
||||
|
||||
.attributes td:nth-child(3) {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.attributes td {
|
||||
padding: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("developer-tools-service", HaPanelDevService);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"developer-tools-service": HaPanelDevService;
|
||||
}
|
||||
}
|
@@ -194,6 +194,9 @@ export class HuiActionEditor extends LitElement {
|
||||
.dropdown {
|
||||
display: flex;
|
||||
}
|
||||
ha-service-control {
|
||||
--service-control-padding: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,11 @@ export const configElementStyle = css`
|
||||
}
|
||||
.side-by-side > * {
|
||||
flex: 1;
|
||||
padding-right: 4px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.side-by-side > *:last-child {
|
||||
flex: 1;
|
||||
padding-right: 0;
|
||||
}
|
||||
.suffix {
|
||||
margin: 0 8px;
|
||||
|
281
src/panels/profile/dialog-ha-mfa-module-setup-flow.ts
Normal file
281
src/panels/profile/dialog-ha-mfa-module-setup-flow.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import "@material/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
} from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { localizeKey } from "../../common/translations/localize";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import "../../components/ha-markdown";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepForm,
|
||||
} from "../../data/data_entry_flow";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../../components/ha-dialog";
|
||||
|
||||
let instance = 0;
|
||||
|
||||
@customElement("ha-mfa-module-setup-flow")
|
||||
class HaMfaModuleSetupFlow extends LitElement {
|
||||
@property() public hass!: HomeAssistant;
|
||||
|
||||
@internalProperty() private _dialogClosedCallback?: (params: {
|
||||
flowFinished: boolean;
|
||||
}) => void;
|
||||
|
||||
@internalProperty() private _instance?: number;
|
||||
|
||||
@internalProperty() private _loading = false;
|
||||
|
||||
@internalProperty() private _opened = false;
|
||||
|
||||
@internalProperty() private _stepData: any = {};
|
||||
|
||||
@internalProperty() private _step?: DataEntryFlowStep;
|
||||
|
||||
@internalProperty() private _errorMessage?: string;
|
||||
|
||||
public showDialog({ continueFlowId, mfaModuleId, dialogClosedCallback }) {
|
||||
this._instance = instance++;
|
||||
this._dialogClosedCallback = dialogClosedCallback;
|
||||
this._opened = true;
|
||||
|
||||
const fetchStep = continueFlowId
|
||||
? this.hass.callWS({
|
||||
type: "auth/setup_mfa",
|
||||
flow_id: continueFlowId,
|
||||
})
|
||||
: this.hass.callWS({
|
||||
type: "auth/setup_mfa",
|
||||
mfa_module_id: mfaModuleId,
|
||||
});
|
||||
|
||||
const curInstance = this._instance;
|
||||
|
||||
fetchStep.then((step) => {
|
||||
if (curInstance !== this._instance) return;
|
||||
|
||||
this._processStep(step);
|
||||
});
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
// Closed dialog by clicking on the overlay
|
||||
if (this._step) {
|
||||
this._flowDone();
|
||||
}
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._opened) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
.heading=${this._computeStepTitle()}
|
||||
@closing=${this.closeDialog}
|
||||
>
|
||||
<div>
|
||||
${this._errorMessage
|
||||
? html`<div class="error">${this._errorMessage}</div>`
|
||||
: ""}
|
||||
${!this._step
|
||||
? html`<div class="init-spinner">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>`
|
||||
: html`${this._step.type === "abort"
|
||||
? html` <ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${this.hass.localize(
|
||||
`component.auth.mfa_setup.${this._step.handler}.abort.${this._step.reason}`
|
||||
)}
|
||||
></ha-markdown>`
|
||||
: this._step.type === "create_entry"
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.step_done",
|
||||
"step",
|
||||
this._step.title
|
||||
)}
|
||||
</p>`
|
||||
: this._step.type === "form"
|
||||
? html` <ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
.content=${localizeKey(
|
||||
this.hass.localize,
|
||||
`component.auth.mfa_setup.${this._step!.handler}.step.${
|
||||
(this._step! as DataEntryFlowStepForm).step_id
|
||||
}.description`,
|
||||
this._step!.description_placeholders
|
||||
)}
|
||||
></ha-markdown>
|
||||
<ha-form
|
||||
.data=${this._stepData}
|
||||
.schema=${this._step.data_schema}
|
||||
.error=${this._step.errors}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeError=${this._computeError}
|
||||
@value-changed=${this._stepDataChanged}
|
||||
></ha-form>`
|
||||
: ""}`}
|
||||
</div>
|
||||
${["abort", "create_entry"].includes(this._step?.type || "")
|
||||
? html`<mwc-button slot="primaryAction" @click=${this.closeDialog}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.close"
|
||||
)}</mwc-button
|
||||
>`
|
||||
: ""}
|
||||
${this._step?.type === "form"
|
||||
? html`<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${this._loading}
|
||||
@click=${this._submitStep}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa_setup.submit"
|
||||
)}</mwc-button
|
||||
>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
ha-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-svg-background-color: white;
|
||||
--markdown-svg-color: black;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.init-spinner {
|
||||
padding: 10px 100px 34px;
|
||||
text-align: center;
|
||||
}
|
||||
.submit-spinner {
|
||||
margin-right: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this.hass.loadBackendTranslation("mfa_setup", "auth");
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
this._submitStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _stepDataChanged(ev: CustomEvent) {
|
||||
this._stepData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _submitStep() {
|
||||
this._loading = true;
|
||||
this._errorMessage = undefined;
|
||||
|
||||
const curInstance = this._instance;
|
||||
|
||||
this.hass
|
||||
.callWS({
|
||||
type: "auth/setup_mfa",
|
||||
flow_id: this._step!.flow_id,
|
||||
user_input: this._stepData,
|
||||
})
|
||||
.then(
|
||||
(step) => {
|
||||
if (curInstance !== this._instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._processStep(step);
|
||||
this._loading = false;
|
||||
},
|
||||
(err) => {
|
||||
this._errorMessage =
|
||||
(err && err.body && err.body.message) || "Unknown error occurred";
|
||||
this._loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _processStep(step) {
|
||||
if (!step.errors) step.errors = {};
|
||||
this._step = step;
|
||||
// We got a new form if there are no errors.
|
||||
if (Object.keys(step.errors).length === 0) {
|
||||
this._stepData = {};
|
||||
}
|
||||
}
|
||||
|
||||
private _flowDone() {
|
||||
const flowFinished = Boolean(
|
||||
this._step && ["create_entry", "abort"].includes(this._step.type)
|
||||
);
|
||||
|
||||
this._dialogClosedCallback!({
|
||||
flowFinished,
|
||||
});
|
||||
|
||||
this._errorMessage = undefined;
|
||||
this._step = undefined;
|
||||
this._stepData = {};
|
||||
this._dialogClosedCallback = undefined;
|
||||
this.closeDialog();
|
||||
}
|
||||
|
||||
private _computeStepTitle() {
|
||||
return this._step?.type === "abort"
|
||||
? this.hass.localize("ui.panel.profile.mfa_setup.title_aborted")
|
||||
: this._step?.type === "create_entry"
|
||||
? this.hass.localize("ui.panel.profile.mfa_setup.title_success")
|
||||
: this._step?.type === "form"
|
||||
? this.hass.localize(
|
||||
`component.auth.mfa_setup.${this._step.handler}.step.${this._step.step_id}.title`
|
||||
)
|
||||
: "";
|
||||
}
|
||||
|
||||
private _computeLabel = (schema) =>
|
||||
this.hass.localize(
|
||||
`component.auth.mfa_setup.${this._step!.handler}.step.${
|
||||
(this._step! as DataEntryFlowStepForm).step_id
|
||||
}.data.${schema.name}`
|
||||
) || schema.name;
|
||||
|
||||
private _computeError = (error) =>
|
||||
this.hass.localize(
|
||||
`component.auth.mfa_setup.${this._step!.handler}.error.${error}`
|
||||
) || error;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-mfa-module-setup-flow": HaMfaModuleSetupFlow;
|
||||
}
|
||||
}
|
@@ -1,322 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../components/dialog/ha-paper-dialog";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-form/ha-form";
|
||||
import "../../components/ha-markdown";
|
||||
import { EventsMixin } from "../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import "../../styles/polymer-ha-style-dialog";
|
||||
|
||||
let instance = 0;
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaMfaModuleSetupFlow extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
ha-paper-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
h2 {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-markdown {
|
||||
--markdown-svg-background-color: white;
|
||||
--markdown-svg-color: black;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.init-spinner {
|
||||
padding: 10px 100px 34px;
|
||||
text-align: center;
|
||||
}
|
||||
.submit-spinner {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<ha-paper-dialog
|
||||
id="dialog"
|
||||
with-backdrop=""
|
||||
opened="{{_opened}}"
|
||||
on-opened-changed="_openedChanged"
|
||||
>
|
||||
<h2>
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
||||
[[localize('ui.panel.profile.mfa_setup.title_aborted')]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
||||
[[localize('ui.panel.profile.mfa_setup.title_success')]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
||||
[[_computeStepTitle(localize, _step)]]
|
||||
</template>
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<template is="dom-if" if="[[_errorMsg]]">
|
||||
<div class="error">[[_errorMsg]]</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_step]]">
|
||||
<div class="init-spinner">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_step]]">
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
content="[[_computeStepAbortedReason(localize, _step)]]"
|
||||
></ha-markdown>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
||||
<p>
|
||||
[[localize('ui.panel.profile.mfa_setup.step_done', 'step',
|
||||
_step.title)]]
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_computeStepDescription(localize, _step)]]"
|
||||
>
|
||||
<ha-markdown
|
||||
allowsvg
|
||||
breaks
|
||||
content="[[_computeStepDescription(localize, _step)]]"
|
||||
></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>
|
||||
</template>
|
||||
</paper-dialog-scrollable>
|
||||
<div class="buttons">
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'abort')]]">
|
||||
<mwc-button on-click="_flowDone"
|
||||
>[[localize('ui.panel.profile.mfa_setup.close')]]</mwc-button
|
||||
>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'create_entry')]]">
|
||||
<mwc-button on-click="_flowDone"
|
||||
>[[localize('ui.panel.profile.mfa_setup.close')]]</mwc-button
|
||||
>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_step.type, 'form')]]">
|
||||
<template is="dom-if" if="[[_loading]]">
|
||||
<div class="submit-spinner">
|
||||
<ha-circular-progress active></ha-circular-progress>
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_loading]]">
|
||||
<mwc-button on-click="_submitStep"
|
||||
>[[localize('ui.panel.profile.mfa_setup.submit')]]</mwc-button
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</ha-paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_hass: Object,
|
||||
_dialogClosedCallback: Function,
|
||||
_instance: Number,
|
||||
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
// Error message when can't talk to server etc
|
||||
_errorMsg: String,
|
||||
|
||||
_opened: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
_step: {
|
||||
type: Object,
|
||||
value: null,
|
||||
},
|
||||
|
||||
/*
|
||||
* Store user entered data.
|
||||
*/
|
||||
_stepData: Object,
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
this.hass.loadBackendTranslation("mfa_setup", "auth");
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.keyCode === 13) {
|
||||
this._submitStep();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showDialog({ hass, continueFlowId, mfaModuleId, dialogClosedCallback }) {
|
||||
this.hass = hass;
|
||||
this._instance = instance++;
|
||||
this._dialogClosedCallback = dialogClosedCallback;
|
||||
this._createdFromHandler = !!mfaModuleId;
|
||||
this._loading = true;
|
||||
this._opened = true;
|
||||
|
||||
const fetchStep = continueFlowId
|
||||
? this.hass.callWS({
|
||||
type: "auth/setup_mfa",
|
||||
flow_id: continueFlowId,
|
||||
})
|
||||
: this.hass.callWS({
|
||||
type: "auth/setup_mfa",
|
||||
mfa_module_id: mfaModuleId,
|
||||
});
|
||||
|
||||
const curInstance = this._instance;
|
||||
|
||||
fetchStep.then((step) => {
|
||||
if (curInstance !== this._instance) return;
|
||||
|
||||
this._processStep(step);
|
||||
this._loading = false;
|
||||
// When the flow changes, center the dialog.
|
||||
// Don't do it on each step or else the dialog keeps bouncing.
|
||||
setTimeout(() => this.$.dialog.center(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
_submitStep() {
|
||||
this._loading = true;
|
||||
this._errorMsg = null;
|
||||
|
||||
const curInstance = this._instance;
|
||||
|
||||
this.hass
|
||||
.callWS({
|
||||
type: "auth/setup_mfa",
|
||||
flow_id: this._step.flow_id,
|
||||
user_input: this._stepData,
|
||||
})
|
||||
.then(
|
||||
(step) => {
|
||||
if (curInstance !== this._instance) return;
|
||||
|
||||
this._processStep(step);
|
||||
this._loading = false;
|
||||
},
|
||||
(err) => {
|
||||
this._errorMsg =
|
||||
(err && err.body && err.body.message) || "Unknown error occurred";
|
||||
this._loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_processStep(step) {
|
||||
if (!step.errors) step.errors = {};
|
||||
this._step = step;
|
||||
// We got a new form if there are no errors.
|
||||
if (Object.keys(step.errors).length === 0) {
|
||||
this._stepData = {};
|
||||
}
|
||||
}
|
||||
|
||||
_flowDone() {
|
||||
this._opened = false;
|
||||
const flowFinished =
|
||||
this._step && ["create_entry", "abort"].includes(this._step.type);
|
||||
|
||||
if (this._step && !flowFinished && this._createdFromHandler) {
|
||||
// console.log('flow not finish');
|
||||
}
|
||||
|
||||
this._dialogClosedCallback({
|
||||
flowFinished,
|
||||
});
|
||||
|
||||
this._errorMsg = null;
|
||||
this._step = null;
|
||||
this._stepData = {};
|
||||
this._dialogClosedCallback = null;
|
||||
}
|
||||
|
||||
_equals(a, b) {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
_openedChanged(ev) {
|
||||
// Closed dialog by clicking on the overlay
|
||||
if (this._step && !ev.detail.value) {
|
||||
this._flowDone();
|
||||
}
|
||||
}
|
||||
|
||||
_computeStepAbortedReason(localize, step) {
|
||||
return localize(
|
||||
`component.auth.mfa_setup.${step.handler}.abort.${step.reason}`
|
||||
);
|
||||
}
|
||||
|
||||
_computeStepTitle(localize, step) {
|
||||
return (
|
||||
localize(
|
||||
`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.title`
|
||||
) || "Setup Multi-factor Authentication"
|
||||
);
|
||||
}
|
||||
|
||||
_computeStepDescription(localize, step) {
|
||||
const args = [
|
||||
`component.auth.mfa_setup.${step.handler}.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(
|
||||
`component.auth.mfa_setup.${step.handler}.step.${step.step_id}.data.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
_computeErrorCallback(localize, step) {
|
||||
// Returns a callback for ha-form to calculate error messages
|
||||
return (error) =>
|
||||
localize(`component.auth.mfa_setup.${step.handler}.error.${error}`) ||
|
||||
error;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-mfa-module-setup-flow", HaMfaModuleSetupFlow);
|
@@ -1,130 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../components/ha-card";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { EventsMixin } from "../../mixins/events-mixin";
|
||||
import LocalizeMixin from "../../mixins/localize-mixin";
|
||||
import "../../styles/polymer-ha-style";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaMfaModulesCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
.status {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.error,
|
||||
.status {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-right: -0.57em;
|
||||
}
|
||||
</style>
|
||||
<ha-card header="[[localize('ui.panel.profile.mfa.header')]]">
|
||||
<template is="dom-repeat" items="[[mfaModules]]" as="module">
|
||||
<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div>[[module.name]]</div>
|
||||
<div secondary="">[[module.id]]</div>
|
||||
</paper-item-body>
|
||||
<template is="dom-if" if="[[module.enabled]]">
|
||||
<mwc-button on-click="_disable"
|
||||
>[[localize('ui.panel.profile.mfa.disable')]]</mwc-button
|
||||
>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!module.enabled]]">
|
||||
<mwc-button on-click="_enable"
|
||||
>[[localize('ui.panel.profile.mfa.enable')]]</mwc-button
|
||||
>
|
||||
</template>
|
||||
</paper-item>
|
||||
</template>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
|
||||
_loading: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
|
||||
// Error message when can't talk to server etc
|
||||
_statusMsg: String,
|
||||
_errorMsg: String,
|
||||
|
||||
mfaModules: Array,
|
||||
};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
this.fire("register-dialog", {
|
||||
dialogShowEvent: "show-mfa-module-setup-flow",
|
||||
dialogTag: "ha-mfa-module-setup-flow",
|
||||
dialogImport: () => import("./ha-mfa-module-setup-flow"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_enable(ev) {
|
||||
this.fire("show-mfa-module-setup-flow", {
|
||||
hass: this.hass,
|
||||
mfaModuleId: ev.model.module.id,
|
||||
dialogClosedCallback: () => this._refreshCurrentUser(),
|
||||
});
|
||||
}
|
||||
|
||||
async _disable(ev) {
|
||||
const mfamodule = ev.model.module;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.localize(
|
||||
"ui.panel.profile.mfa.confirm_disable",
|
||||
"name",
|
||||
mfamodule.name
|
||||
),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mfaModuleId = mfamodule.id;
|
||||
|
||||
this.hass
|
||||
.callWS({
|
||||
type: "auth/depose_mfa",
|
||||
mfa_module_id: mfaModuleId,
|
||||
})
|
||||
.then(() => {
|
||||
this._refreshCurrentUser();
|
||||
});
|
||||
}
|
||||
|
||||
_refreshCurrentUser() {
|
||||
this.fire("hass-refresh-current-user");
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-mfa-modules-card", HaMfaModulesCard);
|
101
src/panels/profile/ha-mfa-modules-card.ts
Normal file
101
src/panels/profile/ha-mfa-modules-card.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import "@material/mwc-button";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-card";
|
||||
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant, MFAModule } from "../../types";
|
||||
import { showMfaModuleSetupFlowDialog } from "./show-ha-mfa-module-setup-flow-dialog";
|
||||
|
||||
@customElement("ha-mfa-modules-card")
|
||||
class HaMfaModulesCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public mfaModules!: MFAModule[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card .header=${this.hass.localize("ui.panel.profile.mfa.header")}>
|
||||
${this.mfaModules.map(
|
||||
(module) => html`<paper-item>
|
||||
<paper-item-body two-line="">
|
||||
<div>${module.name}</div>
|
||||
<div secondary>${module.id}</div>
|
||||
</paper-item-body>
|
||||
${module.enabled
|
||||
? html`<mwc-button .module=${module} @click=${this._disable}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa.disable"
|
||||
)}</mwc-button
|
||||
>`
|
||||
: html`<mwc-button .module=${module} @click=${this._enable}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.profile.mfa.enable"
|
||||
)}</mwc-button
|
||||
>`}
|
||||
</paper-item>`
|
||||
)}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
mwc-button {
|
||||
margin-right: -0.57em;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _enable(ev) {
|
||||
showMfaModuleSetupFlowDialog(this, {
|
||||
mfaModuleId: ev.currentTarget.module.id,
|
||||
dialogClosedCallback: () => this._refreshCurrentUser(),
|
||||
});
|
||||
}
|
||||
|
||||
private async _disable(ev) {
|
||||
const mfamodule = ev.currentTarget.module;
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.profile.mfa.confirm_disable",
|
||||
"name",
|
||||
mfamodule.name
|
||||
),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mfaModuleId = mfamodule.id;
|
||||
|
||||
this.hass
|
||||
.callWS({
|
||||
type: "auth/depose_mfa",
|
||||
mfa_module_id: mfaModuleId,
|
||||
})
|
||||
.then(() => {
|
||||
this._refreshCurrentUser();
|
||||
});
|
||||
}
|
||||
|
||||
private _refreshCurrentUser() {
|
||||
fireEvent(this, "hass-refresh-current-user");
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-mfa-modules-card": HaMfaModulesCard;
|
||||
}
|
||||
}
|
21
src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts
Normal file
21
src/panels/profile/show-ha-mfa-module-setup-flow-dialog.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export interface MfaModuleSetupFlowDialogParams {
|
||||
continueFlowId?: string;
|
||||
mfaModuleId?: string;
|
||||
dialogClosedCallback: (params: { flowFinished: boolean }) => void;
|
||||
}
|
||||
|
||||
export const loadMfaModuleSetupFlowDialog = () =>
|
||||
import("./dialog-ha-mfa-module-setup-flow");
|
||||
|
||||
export const showMfaModuleSetupFlowDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: MfaModuleSetupFlowDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "ha-mfa-module-setup-flow",
|
||||
dialogImport: loadMfaModuleSetupFlowDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
@@ -424,6 +424,12 @@
|
||||
"service-picker": {
|
||||
"service": "Service"
|
||||
},
|
||||
"service-control": {
|
||||
"required": "This field is required",
|
||||
"target": "Target",
|
||||
"target_description": "What should this service call target",
|
||||
"service_data": "Service data"
|
||||
},
|
||||
"related-items": {
|
||||
"no_related_found": "No related items found.",
|
||||
"integration": "Integration",
|
||||
@@ -1401,8 +1407,7 @@
|
||||
"type_select": "Action type",
|
||||
"type": {
|
||||
"service": {
|
||||
"label": "Call service",
|
||||
"service_data": "Service data"
|
||||
"label": "Call service"
|
||||
},
|
||||
"delay": {
|
||||
"label": "Delay",
|
||||
@@ -1425,7 +1430,7 @@
|
||||
"event": {
|
||||
"label": "Fire event",
|
||||
"event": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::event%]",
|
||||
"service_data": "[%key:ui::panel::config::automation::editor::actions::type::service::service_data%]"
|
||||
"service_data": "[%key:ui::components::service-control::service_data%]"
|
||||
},
|
||||
"device_id": {
|
||||
"label": "Device",
|
||||
@@ -2069,7 +2074,13 @@
|
||||
"description": "This step requires you to visit an external website to be completed.",
|
||||
"open_site": "Open website"
|
||||
},
|
||||
"loading_first_time": "Please wait while the integration is being installed"
|
||||
"pick_flow_step": {
|
||||
"title": "We discovered these, want to set them up?",
|
||||
"new_flow": "No, set up an other instance of {integration}"
|
||||
},
|
||||
"loading_first_time": "Please wait while the integration is being installed",
|
||||
"error": "Error",
|
||||
"could_not_load": "Config flow could not be loaded"
|
||||
}
|
||||
},
|
||||
"users": {
|
||||
@@ -2694,7 +2705,6 @@
|
||||
"action-editor": {
|
||||
"navigation_path": "Navigation Path",
|
||||
"url_path": "URL Path",
|
||||
"editor_service_data": "Service data can only be entered in the code editor",
|
||||
"actions": {
|
||||
"default_action": "Default Action",
|
||||
"call-service": "Call Service",
|
||||
@@ -3273,16 +3283,16 @@
|
||||
"services": {
|
||||
"title": "Services",
|
||||
"description": "The service dev tool allows you to call any available service in Home Assistant.",
|
||||
"data": "Service Data (YAML, optional)",
|
||||
"call_service": "Call Service",
|
||||
"select_service": "Select a service to see the description",
|
||||
"no_description": "No description is available",
|
||||
"no_parameters": "This service takes no parameters.",
|
||||
"column_parameter": "Parameter",
|
||||
"column_description": "Description",
|
||||
"column_example": "Example",
|
||||
"fill_example_data": "Fill Example Data",
|
||||
"alert_parsing_yaml": "Error parsing YAML: {data}"
|
||||
"yaml_mode": "Go to YAML mode",
|
||||
"ui_mode": "Go to UI mode",
|
||||
"yaml_parameters": "Parameters only available in YAML mode",
|
||||
"all_parameters": "All available parameters",
|
||||
"accepts_target": "This service accepts a target, for example: `entity_id: light.bed_light`"
|
||||
},
|
||||
"states": {
|
||||
"title": "States",
|
||||
|
@@ -3409,6 +3409,7 @@
|
||||
"playback_title": "Odtwarzanie wiadomości"
|
||||
},
|
||||
"my": {
|
||||
"component_not_loaded": "To przekierowanie nie jest obsługiwane przez Twoją instancję Home Assistanta. Aby skorzystać z tego przekierowania, potrzebujesz integracji {integration}.",
|
||||
"error": "Wystąpił nieznany błąd",
|
||||
"faq_link": "Mój Home Assistant - często zadawane pytania",
|
||||
"not_supported": "To przekierowanie nie jest obsługiwane przez Twoją instancję Home Assistanta. Sprawdź {link} aby znaleźć obsługiwane przekierowania i wersję, w której zostały wprowadzone."
|
||||
|
@@ -3409,6 +3409,7 @@
|
||||
"playback_title": "消息回放"
|
||||
},
|
||||
"my": {
|
||||
"component_not_loaded": "您的 Home Assistant 不支持此重定向。需要使用 {integration} 集成才能使用它。",
|
||||
"error": "发生未知错误",
|
||||
"faq_link": "我的 Home Assistant 常见问题",
|
||||
"not_supported": "您的 Home Assistant 不支持此重定向。请查阅{link}以获取受支持的重定向及其引入的版本。"
|
||||
|
44
yarn.lock
44
yarn.lock
@@ -8174,10 +8174,10 @@ hmac-drbg@^1.0.0:
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
home-assistant-js-websocket@^5.8.1:
|
||||
version "5.8.1"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.8.1.tgz#4c5930aa47e7089f5806bb3d190ebe53697d2edc"
|
||||
integrity sha512-2H3q8NK3WrT50iYODv95iz0E2E+nAUOD452V6lhBxhUTQlVFBsuxNMRTTbIZp+6Xab7ad84uF0z+hHFmBMq/Sw==
|
||||
home-assistant-js-websocket@^5.9.0:
|
||||
version "5.9.0"
|
||||
resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-5.9.0.tgz#85f73cc7aa23362e93d7e8208026fbcf25934022"
|
||||
integrity sha512-HSAhX+s2JgsE77sYKKqcNsukiO6Zm4CcCIwugq17MwHcEyLoecChsbQtgtbvg1dHctUAk+IHxuZ0JBx10B1YGQ==
|
||||
|
||||
homedir-polyfill@^1.0.1:
|
||||
version "1.0.3"
|
||||
@@ -9617,16 +9617,16 @@ lodash.values@~2.4.1:
|
||||
dependencies:
|
||||
lodash.keys "~2.4.1"
|
||||
|
||||
lodash@^4, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.17.10, lodash@^4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
|
||||
lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
||||
lodash@^4.17.19:
|
||||
version "4.17.20"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
@@ -12854,11 +12854,6 @@ tapable@^0.1.8:
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
|
||||
integrity sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=
|
||||
|
||||
tapable@^1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
||||
|
||||
tapable@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.0.0.tgz#a49c3d6a8a2bb606e7db372b82904c970d537a08"
|
||||
@@ -13753,14 +13748,13 @@ webpack-log@^2.0.0:
|
||||
ansi-colors "^3.0.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
webpack-manifest-plugin@3.0.0-rc.0:
|
||||
version "3.0.0-rc.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-3.0.0-rc.0.tgz#d488cc34d9509aa4ffcf98eee8559d9106195e2a"
|
||||
integrity sha512-KL4W5wh/ZnYF1pIzK/beFSgPPINbqD/oQeVS/5YwiZX486wcyZ+ZfeQZ1/8LfxHFG/s27G2hGcwkQ/H/amQCfQ==
|
||||
webpack-manifest-plugin@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-manifest-plugin/-/webpack-manifest-plugin-3.0.0.tgz#426644300e5dc41a75a9c996c4d4f876eb3c2b5b"
|
||||
integrity sha512-nbORTdky2HxD8XSaaT+zrsHb30AAgyWAWgCLWaAeQO21VGCScGb52ipqlHA/njix1Z8OW8IOlo4+XK0OKr1fkw==
|
||||
dependencies:
|
||||
fs-extra "^8.1.0"
|
||||
lodash "^4"
|
||||
tapable "^1.1.3"
|
||||
tapable "^2.0.0"
|
||||
webpack-sources "^2.2.0"
|
||||
|
||||
webpack-merge@^4.2.2:
|
||||
version "4.2.2"
|
||||
@@ -13777,6 +13771,14 @@ webpack-sources@^2.0.1:
|
||||
source-list-map "^2.0.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
webpack-sources@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-2.2.0.tgz#058926f39e3d443193b6c31547229806ffd02bac"
|
||||
integrity sha512-bQsA24JLwcnWGArOKUxYKhX3Mz/nK1Xf6hxullKERyktjNMC4x8koOeaDNTA2fEJ09BdWLbM/iTW0ithREUP0w==
|
||||
dependencies:
|
||||
source-list-map "^2.0.1"
|
||||
source-map "^0.6.1"
|
||||
|
||||
webpack@5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.1.3.tgz#a6e4fd250ef2513f94844ae5d8f7570215a2ac49"
|
||||
|
Reference in New Issue
Block a user