mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-27 01:59:26 +00:00
Compare commits
112 Commits
20240529.0
...
dashboard_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ebe5207b6e | ||
![]() |
bd1ede4145 | ||
![]() |
321a085c0e | ||
![]() |
6a3041988a | ||
![]() |
23fcdf876c | ||
![]() |
00f325e961 | ||
![]() |
d00b3cfc61 | ||
![]() |
4cc9e74ea8 | ||
![]() |
a56b9a96ce | ||
![]() |
d4b5f4bc14 | ||
![]() |
cf1523ee73 | ||
![]() |
f5d571ca84 | ||
![]() |
362e92f313 | ||
![]() |
5ddf72b973 | ||
![]() |
6e78c28f51 | ||
![]() |
772f0bb669 | ||
![]() |
846c2a848f | ||
![]() |
8495757005 | ||
![]() |
9960d38b91 | ||
![]() |
d3222f8bb0 | ||
![]() |
2e5cce5409 | ||
![]() |
f78946447f | ||
![]() |
eb0579ddc5 | ||
![]() |
686424fc70 | ||
![]() |
039e9b40bd | ||
![]() |
8272bef890 | ||
![]() |
62528b2413 | ||
![]() |
fa24f529e0 | ||
![]() |
43a54f6cda | ||
![]() |
9c153bbd58 | ||
![]() |
27afe9ecb7 | ||
![]() |
72f989e2bd | ||
![]() |
a6ef46565f | ||
![]() |
a35ac09688 | ||
![]() |
27024135ea | ||
![]() |
8759ed740a | ||
![]() |
bb3e8ae33d | ||
![]() |
b5b60c9bf0 | ||
![]() |
3b6a2cf7d8 | ||
![]() |
7e10e14102 | ||
![]() |
a580abab4a | ||
![]() |
11523c08c4 | ||
![]() |
7a8988528b | ||
![]() |
2a6380f083 | ||
![]() |
29881c8bb4 | ||
![]() |
56254ddf03 | ||
![]() |
007ba70641 | ||
![]() |
3e1227b064 | ||
![]() |
067e179f26 | ||
![]() |
9a3f7df25e | ||
![]() |
c7b4e8f37c | ||
![]() |
bfa8b886ab | ||
![]() |
433c00b73a | ||
![]() |
a497f42f73 | ||
![]() |
165723cb5b | ||
![]() |
42b5fa696a | ||
![]() |
59062d96a8 | ||
![]() |
d36bbfe07d | ||
![]() |
0d489213a4 | ||
![]() |
c54acc9369 | ||
![]() |
562bc084f0 | ||
![]() |
6fce2f35a5 | ||
![]() |
f4e24bed2e | ||
![]() |
09969c0e2d | ||
![]() |
4b0181774b | ||
![]() |
272db5e9e8 | ||
![]() |
9ae3a824d9 | ||
![]() |
9db55c9391 | ||
![]() |
59697127c0 | ||
![]() |
565600e945 | ||
![]() |
721eebf367 | ||
![]() |
f5ae842167 | ||
![]() |
3575734ed0 | ||
![]() |
4e8de1f64d | ||
![]() |
cd73b8ac29 | ||
![]() |
74eca6b1f5 | ||
![]() |
9ef0bd6e46 | ||
![]() |
53eb7f771f | ||
![]() |
cd62f064cb | ||
![]() |
db82b856e0 | ||
![]() |
ab340e13e9 | ||
![]() |
22c54b3fea | ||
![]() |
2dd7e598d5 | ||
![]() |
d1ce06e368 | ||
![]() |
9717304b68 | ||
![]() |
c646f3c39a | ||
![]() |
0297ec5a7b | ||
![]() |
e48286c2a0 | ||
![]() |
f2b2da9877 | ||
![]() |
250f87cfd8 | ||
![]() |
9f6afb162a | ||
![]() |
0add65feff | ||
![]() |
c08d9a9166 | ||
![]() |
d9a9038cec | ||
![]() |
6bee3ef45c | ||
![]() |
3a855f95ad | ||
![]() |
e7f3393ec6 | ||
![]() |
d7cb4cb537 | ||
![]() |
c55720c933 | ||
![]() |
f78e757485 | ||
![]() |
fa03c58a93 | ||
![]() |
49f1ad633f | ||
![]() |
d449e10120 | ||
![]() |
474c8c243e | ||
![]() |
e9e53e9451 | ||
![]() |
cfa84f30be | ||
![]() |
7416ae7dfd | ||
![]() |
6f1fa139e7 | ||
![]() |
b38a348957 | ||
![]() |
f97971faf6 | ||
![]() |
c5ae9e8497 | ||
![]() |
c00287c401 |
@@ -126,6 +126,5 @@
|
||||
"lit-a11y/anchor-is-valid": "warn",
|
||||
"lit-a11y/role-has-required-aria-attrs": "warn"
|
||||
},
|
||||
"plugins": ["disable", "unused-imports"],
|
||||
"processor": "disable/disable"
|
||||
"plugins": ["unused-imports"]
|
||||
}
|
||||
|
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
with:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4.0.2
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
|
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@v2.1.10
|
||||
uses: relative-ci/agent-action@v2.1.11
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
4
.github/workflows/release.yaml
vendored
4
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
script/release
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2.0.5
|
||||
uses: softprops/action-gh-release@v2.0.6
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v4.1.6
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
File diff suppressed because one or more lines are too long
@@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.2.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
||||
|
@@ -92,8 +92,8 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: latestBuild ? false : "usage",
|
||||
corejs: latestBuild ? false : dependencies["core-js"],
|
||||
useBuiltIns: "usage",
|
||||
corejs: dependencies["core-js"],
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
@@ -157,7 +157,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
exclude: [
|
||||
path.join(paths.polymer_dir, "src/resources/polyfills"),
|
||||
...[
|
||||
"@formatjs/intl-\\w+",
|
||||
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
|
||||
"@lit-labs/virtualizer/polyfills",
|
||||
"@webcomponents/scoped-custom-element-registry",
|
||||
"element-internals-polyfill",
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-listbox/paper-listbox";
|
||||
import { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -28,6 +27,7 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
|
||||
import "./hc-layout";
|
||||
import "../../../../src/components/ha-list-item";
|
||||
|
||||
@customElement("hc-cast")
|
||||
class HcCast extends LitElement {
|
||||
@@ -83,37 +83,37 @@ class HcCast extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-path"
|
||||
.selected=${this.castManager.status.lovelacePath || ""}
|
||||
>
|
||||
<mwc-list @action=${this._handlePickView} activatable>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._handlePickView}
|
||||
data-path=${view.path || idx}
|
||||
(view, idx) =>
|
||||
html`<ha-list-item
|
||||
graphic="avatar"
|
||||
.activated=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
.selected=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
>
|
||||
${view.title || view.path || "Unnamed view"}
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="item-icon"
|
||||
slot="graphic"
|
||||
></ha-icon>
|
||||
`
|
||||
: html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiViewDashboard}
|
||||
></ha-svg-icon>`}
|
||||
${view.title || view.path || "Unnamed view"}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
></ha-svg-icon>`}</ha-list-item
|
||||
> `
|
||||
)}</mwc-list
|
||||
>
|
||||
`}
|
||||
|
||||
<div class="card-actions">
|
||||
${this.castManager.status
|
||||
? html`
|
||||
@@ -185,8 +185,8 @@ class HcCast extends LitElement {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: Event) {
|
||||
const path = (ev.currentTarget as any).getAttribute("data-path");
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
}
|
||||
@@ -249,26 +249,14 @@ class HcCast extends LitElement {
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
paper-listbox {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
paper-listbox ha-icon,
|
||||
paper-listbox ha-svg-icon {
|
||||
ha-list-item ha-icon,
|
||||
ha-list-item ha-svg-icon {
|
||||
padding: 12px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
paper-icon-item[disabled] {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
:host([hide-icons]) paper-icon-item {
|
||||
--paper-item-icon-width: 0px;
|
||||
:host([hide-icons]) ha-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import "@material/mwc-button";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-button";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
|
||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
demoConfigs,
|
||||
selectedDemoConfig,
|
||||
selectedDemoConfigIndex,
|
||||
setDemoConfig,
|
||||
} from "../configs/demo-configs";
|
||||
|
||||
@customElement("ha-demo-card")
|
||||
@@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
|
||||
<ha-button @click=${this._nextConfig} .disabled=${this._switching}>
|
||||
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p class="small-hidden">
|
||||
@@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
</div>
|
||||
<div class="actions small-hidden">
|
||||
<a href="https://www.home-assistant.io" target="_blank">
|
||||
<mwc-button>
|
||||
<ha-button>
|
||||
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
|
||||
</mwc-button>
|
||||
</ha-button>
|
||||
</a>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -113,13 +113,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
|
||||
private async _updateConfig(index: number) {
|
||||
this._switching = true;
|
||||
try {
|
||||
await setDemoConfig(this.hass, this.lovelace!, index);
|
||||
} catch (err: any) {
|
||||
alert("Failed to switch config :-(");
|
||||
} finally {
|
||||
this._switching = false;
|
||||
}
|
||||
fireEvent(this, "set-demo-config" as any, { index });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -149,7 +143,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.picker mwc-button {
|
||||
.picker ha-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
@@ -1,9 +1,12 @@
|
||||
import type { LocalizeFunc } from "../../../src/common/translations/localize";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
import { selectedDemoConfig } from "../configs/demo-configs";
|
||||
import {
|
||||
selectedDemoConfig,
|
||||
selectedDemoConfigIndex,
|
||||
setDemoConfig,
|
||||
} from "../configs/demo-configs";
|
||||
import "../custom-cards/cast-demo-row";
|
||||
import "../custom-cards/ha-demo-card";
|
||||
import type { HADemoCard } from "../custom-cards/ha-demo-card";
|
||||
|
||||
export const mockLovelace = (
|
||||
hass: MockHomeAssistant,
|
||||
@@ -19,17 +22,22 @@ export const mockLovelace = (
|
||||
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
|
||||
};
|
||||
|
||||
customElements.whenDefined("hui-view").then(() => {
|
||||
customElements.whenDefined("hui-root").then(() => {
|
||||
// eslint-disable-next-line
|
||||
const HUIView = customElements.get("hui-view");
|
||||
// Patch HUI-VIEW to make the lovelace object available to the demo card
|
||||
const oldCreateCard = HUIView!.prototype.createCardElement;
|
||||
const HUIRoot = customElements.get("hui-root")!;
|
||||
|
||||
HUIView!.prototype.createCardElement = function (config) {
|
||||
const el = oldCreateCard.call(this, config);
|
||||
if (el.tagName === "HA-DEMO-CARD") {
|
||||
(el as HADemoCard).lovelace = this.lovelace;
|
||||
}
|
||||
return el;
|
||||
const oldFirstUpdated = HUIRoot.prototype.firstUpdated;
|
||||
|
||||
HUIRoot.prototype.firstUpdated = function (changedProperties) {
|
||||
oldFirstUpdated.call(this, changedProperties);
|
||||
this.addEventListener("set-demo-config", async (ev) => {
|
||||
const index = (ev as CustomEvent).detail.index;
|
||||
try {
|
||||
await setDemoConfig(this.hass, this.lovelace!, index);
|
||||
} catch (err: any) {
|
||||
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
|
||||
alert("Failed to switch config :-(");
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { load } from "js-yaml";
|
||||
import { html, css, LitElement, PropertyValues } from "lit";
|
||||
import { LitElement, PropertyValueMap, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/panels/lovelace/cards/hui-card";
|
||||
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
export interface DemoCardConfig {
|
||||
@@ -19,7 +21,12 @@ class DemoCard extends LitElement {
|
||||
|
||||
@state() private _size?: number;
|
||||
|
||||
@query("#card") private _card!: HTMLElement;
|
||||
@query("hui-card", false) private _card?: HuiCard;
|
||||
|
||||
private _config = memoizeOne((config: string) => {
|
||||
const c = (load(config) as any)[0];
|
||||
return c;
|
||||
});
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -30,63 +37,32 @@ class DemoCard extends LitElement {
|
||||
: ""}
|
||||
</h2>
|
||||
<div class="root">
|
||||
<div id="card"></div>
|
||||
${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""}
|
||||
<hui-card
|
||||
.config=${this._config(this.config.config)}
|
||||
.hass=${this.hass}
|
||||
@card-updated=${this._cardUpdated}
|
||||
></hui-card>
|
||||
${this.showConfig
|
||||
? html`<pre>${this.config.config.trim()}</pre>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("config")) {
|
||||
const card = this._card;
|
||||
while (card.lastChild) {
|
||||
card.removeChild(card.lastChild);
|
||||
}
|
||||
|
||||
const el = this._createCardElement((load(this.config.config) as any)[0]);
|
||||
card.appendChild(el);
|
||||
this._getSize(el);
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
const card = this._card.lastChild;
|
||||
if (card) {
|
||||
(card as any).hass = this.hass;
|
||||
}
|
||||
}
|
||||
private async _cardUpdated(ev) {
|
||||
ev.stopPropagation();
|
||||
this._updateSize();
|
||||
}
|
||||
|
||||
async _getSize(el) {
|
||||
await customElements.whenDefined(el.localName);
|
||||
|
||||
if (!("getCardSize" in el)) {
|
||||
this._size = undefined;
|
||||
return;
|
||||
}
|
||||
this._size = await el.getCardSize();
|
||||
private async _updateSize() {
|
||||
this._size = await this._card?.getCardSize();
|
||||
}
|
||||
|
||||
_createCardElement(cardConfig) {
|
||||
const element = createCardElement(cardConfig);
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
_rebuildCard(cardElToReplace, config) {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
|
||||
protected update(
|
||||
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
super.update(_changedProperties);
|
||||
this._updateSize();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -101,7 +77,7 @@ class DemoCard extends LitElement {
|
||||
font-size: 0.5em;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
#card {
|
||||
hui-card {
|
||||
max-width: 400px;
|
||||
width: 100vw;
|
||||
}
|
||||
|
74
package.json
74
package.json
@@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.24.6",
|
||||
"@braintree/sanitize-url": "7.0.2",
|
||||
"@codemirror/autocomplete": "6.16.0",
|
||||
"@codemirror/commands": "6.5.0",
|
||||
"@codemirror/language": "6.10.1",
|
||||
"@babel/runtime": "7.24.7",
|
||||
"@braintree/sanitize-url": "7.0.3",
|
||||
"@codemirror/autocomplete": "6.16.3",
|
||||
"@codemirror/commands": "6.6.0",
|
||||
"@codemirror/language": "6.10.2",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.26.3",
|
||||
"@codemirror/view": "6.28.2",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
@@ -43,17 +43,17 @@
|
||||
"@formatjs/intl-numberformat": "8.10.3",
|
||||
"@formatjs/intl-pluralrules": "5.2.14",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.14",
|
||||
"@fullcalendar/core": "6.1.13",
|
||||
"@fullcalendar/daygrid": "6.1.13",
|
||||
"@fullcalendar/interaction": "6.1.13",
|
||||
"@fullcalendar/list": "6.1.13",
|
||||
"@fullcalendar/luxon3": "6.1.13",
|
||||
"@fullcalendar/timegrid": "6.1.13",
|
||||
"@fullcalendar/core": "6.1.11",
|
||||
"@fullcalendar/daygrid": "6.1.11",
|
||||
"@fullcalendar/interaction": "6.1.11",
|
||||
"@fullcalendar/list": "6.1.11",
|
||||
"@fullcalendar/luxon3": "6.1.11",
|
||||
"@fullcalendar/timegrid": "6.1.11",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.2",
|
||||
"@lit-labs/virtualizer": "2.0.12",
|
||||
"@lit-labs/virtualizer": "2.0.13",
|
||||
"@lrnwebcomponents/simple-tooltip": "8.0.2",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
@@ -88,8 +88,8 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.3.13",
|
||||
"@vaadin/vaadin-themable-mixin": "24.3.13",
|
||||
"@vaadin/combo-box": "24.4.0",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.0",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -110,7 +110,7 @@
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "patch:hls.js@npm%3A1.5.7#~/.yarn/patches/hls.js-npm-1.5.7-f5bbd3d060.patch",
|
||||
"home-assistant-js-websocket": "9.3.0",
|
||||
"home-assistant-js-websocket": "9.4.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.14",
|
||||
"js-yaml": "4.1.0",
|
||||
@@ -133,7 +133,7 @@
|
||||
"tinykeys": "2.1.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.37",
|
||||
"ua-parser-js": "1.0.38",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-network": "9.1.9",
|
||||
@@ -149,26 +149,26 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.24.6",
|
||||
"@babel/core": "7.24.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.6",
|
||||
"@babel/plugin-transform-runtime": "7.24.6",
|
||||
"@babel/preset-env": "7.24.6",
|
||||
"@babel/preset-typescript": "7.24.6",
|
||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/preset-env": "7.24.7",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.13.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.5.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "20.1.1",
|
||||
"@octokit/rest": "21.0.0",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "25.0.8",
|
||||
"@rollup/plugin-commonjs": "26.0.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.5",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.14",
|
||||
"@types/chromecast-caf-receiver": "6.0.15",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
@@ -185,8 +185,8 @@
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "7.10.0",
|
||||
"@typescript-eslint/parser": "7.10.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.13.1",
|
||||
"@typescript-eslint/parser": "7.13.1",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
@@ -198,15 +198,14 @@
|
||||
"eslint-config-airbnb-typescript": "18.0.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.13.0",
|
||||
"eslint-plugin-lit": "1.14.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.2",
|
||||
"eslint-plugin-unused-imports": "4.0.0",
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "10.4.1",
|
||||
"glob": "10.4.2",
|
||||
"gulp": "5.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
"gulp-rename": "2.0.0",
|
||||
@@ -215,7 +214,7 @@
|
||||
"husky": "9.0.11",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.5",
|
||||
"lint-staged": "15.2.7",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -225,7 +224,7 @@
|
||||
"object-hash": "3.0.0",
|
||||
"open": "10.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.2.5",
|
||||
"prettier": "3.3.2",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
@@ -234,18 +233,18 @@
|
||||
"sinon": "18.0.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.15.1",
|
||||
"tar": "7.1.0",
|
||||
"tar": "7.4.0",
|
||||
"terser-webpack-plugin": "5.3.10",
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.4.5",
|
||||
"webpack": "5.91.0",
|
||||
"webpack": "5.92.1",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "6.0.1",
|
||||
"workbox-build": "7.1.0"
|
||||
"workbox-build": "7.1.1"
|
||||
},
|
||||
"_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch",
|
||||
"resolutions": {
|
||||
@@ -254,8 +253,9 @@
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"@fullcalendar/daygrid": "6.1.11",
|
||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.2.2"
|
||||
"packageManager": "yarn@4.3.1"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240529.0"
|
||||
version = "20240610.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -31,6 +31,7 @@ import {
|
||||
mdiFormatListBulleted,
|
||||
mdiFormatListCheckbox,
|
||||
mdiFormTextbox,
|
||||
mdiForumOutline,
|
||||
mdiGauge,
|
||||
mdiGoogleAssistant,
|
||||
mdiGoogleCirclesCommunities,
|
||||
@@ -98,7 +99,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
conversation: mdiMicrophoneMessage,
|
||||
conversation: mdiForumOutline,
|
||||
counter: mdiCounter,
|
||||
date: mdiCalendar,
|
||||
datetime: mdiCalendarClock,
|
||||
@@ -235,6 +236,8 @@ export const SENSOR_ENTITIES = [
|
||||
"weather",
|
||||
];
|
||||
|
||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
|
||||
|
||||
/** Domains that render an input element instead of a text value when displayed in a row.
|
||||
* Those rows should then not show a cursor pointer when hovered (which would normally
|
||||
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
|
||||
|
1
src/common/dom/prevent_default.ts
Normal file
1
src/common/dom/prevent_default.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const preventDefault = (ev) => ev.preventDefault();
|
@@ -47,6 +47,8 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
|
||||
@property({ type: Boolean }) public readOnly = false;
|
||||
|
||||
@property({ type: Boolean }) public linewrap = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "autocomplete-entities" })
|
||||
public autocompleteEntities = false;
|
||||
|
||||
@@ -134,6 +136,13 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
),
|
||||
});
|
||||
}
|
||||
if (changedProps.has("linewrap")) {
|
||||
transactions.push({
|
||||
effects: this._loadedCodeMirror!.linewrapCompartment!.reconfigure(
|
||||
this.linewrap ? this._loadedCodeMirror!.EditorView.lineWrapping : []
|
||||
),
|
||||
});
|
||||
}
|
||||
if (changedProps.has("_value") && this._value !== this.value) {
|
||||
transactions.push({
|
||||
changes: {
|
||||
@@ -181,6 +190,9 @@ export class HaCodeEditor extends ReactiveElement {
|
||||
this._loadedCodeMirror.readonlyCompartment.of(
|
||||
this._loadedCodeMirror.EditorView.editable.of(!this.readOnly)
|
||||
),
|
||||
this._loadedCodeMirror.linewrapCompartment.of(
|
||||
this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
|
||||
),
|
||||
this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
|
||||
];
|
||||
|
||||
|
@@ -89,13 +89,18 @@ export class HaFilterDomains extends LitElement {
|
||||
});
|
||||
|
||||
return Array.from(domains.values())
|
||||
.map((domain) => ({
|
||||
domain,
|
||||
name: domainToName(this.hass.localize, domain),
|
||||
}))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!filter ||
|
||||
entry.toLowerCase().includes(filter) ||
|
||||
domainToName(this.hass.localize, entry).toLowerCase().includes(filter)
|
||||
entry.domain.toLowerCase().includes(filter) ||
|
||||
entry.name.toLowerCase().includes(filter)
|
||||
)
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language));
|
||||
.sort((a, b) => stringCompare(a.name, b.name, this.hass.locale.language))
|
||||
.map((entry) => entry.domain);
|
||||
});
|
||||
|
||||
protected updated(changed) {
|
||||
|
@@ -2,7 +2,7 @@ import type { Selector } from "../../data/selector";
|
||||
import type { HaFormSchema } from "./types";
|
||||
|
||||
export const computeInitialHaFormData = (
|
||||
schema: HaFormSchema[]
|
||||
schema: HaFormSchema[] | readonly HaFormSchema[]
|
||||
): Record<string, any> => {
|
||||
const data = {};
|
||||
schema.forEach((field) => {
|
||||
@@ -36,6 +36,8 @@ export const computeInitialHaFormData = (
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
};
|
||||
} else if (field.type === "expandable") {
|
||||
data[field.name] = computeInitialHaFormData(field.schema);
|
||||
} else if ("selector" in field) {
|
||||
const selector: Selector = field.selector;
|
||||
|
||||
|
233
src/components/ha-grid-size-picker.ts
Normal file
233
src/components/ha-grid-size-picker.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "./ha-icon-button";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
type GridSizeValue = {
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
};
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: GridSizeValue;
|
||||
|
||||
@property({ attribute: false }) public rows = 6;
|
||||
|
||||
@property({ attribute: false }) public columns = 4;
|
||||
|
||||
@property({ attribute: false }) public rowMin?: number;
|
||||
|
||||
@property({ attribute: false }) public rowMax?: number;
|
||||
|
||||
@property({ attribute: false }) public columnMin?: number;
|
||||
|
||||
@property({ attribute: false }) public columnMax?: number;
|
||||
|
||||
@property({ attribute: false }) public isDefault?: boolean;
|
||||
|
||||
@state() public _localValue?: GridSizeValue = undefined;
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("value")) {
|
||||
this._localValue = this.value;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="grid">
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.columns"
|
||||
)}
|
||||
id="columns"
|
||||
.min=${this.columnMin ?? 1}
|
||||
.max=${this.columnMax ?? this.columns}
|
||||
.range=${this.columns}
|
||||
.value=${this.value?.columns}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
></ha-grid-layout-slider>
|
||||
<ha-grid-layout-slider
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.rows"
|
||||
)}
|
||||
id="rows"
|
||||
.min=${this.rowMin ?? 1}
|
||||
.max=${this.rowMax ?? this.rows}
|
||||
.range=${this.rows}
|
||||
vertical
|
||||
.value=${this.value?.rows}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
></ha-grid-layout-slider>
|
||||
${!this.isDefault
|
||||
? html`
|
||||
<ha-icon-button
|
||||
@click=${this._reset}
|
||||
class="reset"
|
||||
.path=${mdiRestore}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.reset_default"
|
||||
)}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.grid-size-picker.reset_default"
|
||||
)}
|
||||
>
|
||||
</ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="preview"
|
||||
style=${styleMap({
|
||||
"--total-rows": this.rows,
|
||||
"--total-columns": this.columns,
|
||||
"--rows": this._localValue?.rows,
|
||||
"--columns": this._localValue?.columns,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
${Array(this.rows * this.columns)
|
||||
.fill(0)
|
||||
.map((_, index) => {
|
||||
const row = Math.floor(index / this.columns) + 1;
|
||||
const column = (index % this.columns) + 1;
|
||||
const disabled =
|
||||
(this.rowMin !== undefined && row < this.rowMin) ||
|
||||
(this.rowMax !== undefined && row > this.rowMax) ||
|
||||
(this.columnMin !== undefined && column < this.columnMin) ||
|
||||
(this.columnMax !== undefined && column > this.columnMax);
|
||||
return html`
|
||||
<div
|
||||
class="cell"
|
||||
data-row=${row}
|
||||
data-column=${column}
|
||||
?disabled=${disabled}
|
||||
@click=${this._cellClick}
|
||||
></div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
<div class="selected">
|
||||
<div class="cell"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
_cellClick(ev) {
|
||||
const cell = ev.currentTarget as HTMLElement;
|
||||
if (cell.getAttribute("disabled") !== null) return;
|
||||
const rows = Number(cell.getAttribute("data-row"));
|
||||
const columns = Number(cell.getAttribute("data-column"));
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { rows, columns },
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.currentTarget.id;
|
||||
const newValue = {
|
||||
...this.value,
|
||||
[key]: ev.detail.value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
|
||||
private _reset(ev) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
rows: undefined,
|
||||
columns: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _sliderMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.currentTarget.id;
|
||||
const value = ev.detail.value;
|
||||
if (value === undefined) return;
|
||||
this._localValue = {
|
||||
...this.value,
|
||||
[key]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"reset column-slider"
|
||||
"row-slider preview";
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
#columns {
|
||||
grid-area: column-slider;
|
||||
}
|
||||
#rows {
|
||||
grid-area: row-slider;
|
||||
}
|
||||
.reset {
|
||||
grid-area: reset;
|
||||
}
|
||||
.preview {
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
.preview > div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--total-columns), 1fr);
|
||||
grid-template-rows: repeat(var(--total-rows), 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
.preview .cell {
|
||||
background-color: var(--disabled-color);
|
||||
grid-column: span 1;
|
||||
grid-row: span 1;
|
||||
border-radius: 4px;
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.preview .cell[disabled] {
|
||||
opacity: 0.05;
|
||||
cursor: initial;
|
||||
}
|
||||
.selected {
|
||||
pointer-events: none;
|
||||
}
|
||||
.selected .cell {
|
||||
background-color: var(--primary-color);
|
||||
grid-column: 1 / span var(--columns, 0);
|
||||
grid-row: 1 / span var(--rows, 0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-grid-size-picker": HaGridSizeEditor;
|
||||
}
|
||||
}
|
@@ -198,8 +198,7 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-icon,
|
||||
ha-svg-icon {
|
||||
*[slot="icon"] {
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
bottom: 2px;
|
||||
|
@@ -64,6 +64,12 @@ const SELECTOR_SCHEMAS = {
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
floor: [
|
||||
{
|
||||
name: "multiple",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const,
|
||||
icon: [] as const,
|
||||
location: [] as const,
|
||||
media: [] as const,
|
||||
|
@@ -32,6 +32,7 @@ export class HaTemplateSelector extends LitElement {
|
||||
autocomplete-icons
|
||||
@value-changed=${this._handleChange}
|
||||
dir="ltr"
|
||||
linewrap
|
||||
></ha-code-editor>
|
||||
${this.helper
|
||||
? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
|
||||
|
@@ -327,6 +327,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
for (const entityId of Object.keys(this.hass.states)) {
|
||||
if (
|
||||
entityId.startsWith("update.") &&
|
||||
!this.hass.entities[entityId]?.hidden &&
|
||||
updateCanInstall(this.hass.states[entityId] as UpdateEntity)
|
||||
) {
|
||||
updateCount++;
|
||||
|
@@ -48,7 +48,7 @@ class HaEntityMarker extends LitElement {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: var(--ha-marker-font-size, 1.5em);
|
||||
border-radius: 50%;
|
||||
border-radius: var(--ha-marker-border-radius, 50%);
|
||||
border: 1px solid var(--ha-marker-color, var(--primary-color));
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--card-background-color);
|
||||
|
@@ -138,6 +138,17 @@ export const adminChangePassword = (
|
||||
password,
|
||||
});
|
||||
|
||||
export const adminChangeUsername = (
|
||||
hass: HomeAssistant,
|
||||
userId: string,
|
||||
username: string
|
||||
) =>
|
||||
hass.callWS<void>({
|
||||
type: "config/auth_provider/homeassistant/admin_change_username",
|
||||
user_id: userId,
|
||||
username,
|
||||
});
|
||||
|
||||
export const deleteAllRefreshTokens = (
|
||||
hass: HomeAssistant,
|
||||
token_type?: RefreshTokenType,
|
||||
|
@@ -901,7 +901,7 @@ const tryDescribeCondition = (
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (condition.above && condition.below) {
|
||||
if (condition.above !== undefined && condition.below !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
|
||||
{
|
||||
@@ -912,7 +912,7 @@ const tryDescribeCondition = (
|
||||
}
|
||||
);
|
||||
}
|
||||
if (condition.above) {
|
||||
if (condition.above !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
|
||||
{
|
||||
@@ -922,7 +922,7 @@ const tryDescribeCondition = (
|
||||
}
|
||||
);
|
||||
}
|
||||
if (condition.below) {
|
||||
if (condition.below !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
|
||||
{
|
||||
|
@@ -6,6 +6,7 @@ export interface ConfigUpdateValues {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
elevation: number;
|
||||
radius: number;
|
||||
unit_system: "metric" | "us_customary";
|
||||
time_zone: string;
|
||||
external_url?: string | null;
|
||||
|
@@ -249,6 +249,22 @@ export const localizeDeviceAutomationTrigger = (
|
||||
) ||
|
||||
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!);
|
||||
|
||||
export const localizeExtraFieldsComputeLabelCallback =
|
||||
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
(schema): string =>
|
||||
hass.localize(
|
||||
`component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
|
||||
export const localizeExtraFieldsComputeHelperCallback =
|
||||
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
|
||||
// Returns a callback for ha-form to calculate helper texts per schema object
|
||||
(schema): string | undefined =>
|
||||
hass.localize(
|
||||
`component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}`
|
||||
);
|
||||
|
||||
export const sortDeviceAutomations = (
|
||||
automationA: DeviceAutomation,
|
||||
automationB: DeviceAutomation
|
||||
|
@@ -30,6 +30,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
export interface LovelaceSectionElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
preview?: boolean;
|
||||
viewIndex?: number;
|
||||
index?: number;
|
||||
cards?: HuiCard[];
|
||||
|
@@ -2,6 +2,7 @@ import { HomeAssistant } from "../types";
|
||||
|
||||
export interface OTBRInfo {
|
||||
active_dataset_tlvs: string;
|
||||
border_agent_id: string;
|
||||
channel: number;
|
||||
extended_address: string;
|
||||
url: string;
|
||||
|
@@ -12,14 +12,6 @@ export interface RecorderInfo {
|
||||
thread_running: boolean;
|
||||
}
|
||||
|
||||
export interface RecordedEntities {
|
||||
entity_ids: string[];
|
||||
}
|
||||
export interface RecordedExcludedEntities {
|
||||
recorded_ids: string[];
|
||||
excluded_ids: string[];
|
||||
}
|
||||
|
||||
export type StatisticType = "change" | "state" | "sum" | "min" | "max" | "mean";
|
||||
|
||||
export interface Statistics {
|
||||
@@ -332,25 +324,3 @@ export const getDisplayUnit = (
|
||||
|
||||
export const isExternalStatistic = (statisticsId: string): boolean =>
|
||||
statisticsId.includes(":");
|
||||
|
||||
let recordedExcludedEntitiesCache: RecordedExcludedEntities | undefined;
|
||||
|
||||
export const getRecordedExcludedEntities = async (
|
||||
hass: HomeAssistant
|
||||
): Promise<RecordedExcludedEntities> => {
|
||||
if (recordedExcludedEntitiesCache) {
|
||||
return recordedExcludedEntitiesCache;
|
||||
}
|
||||
const recordedEntities = await hass.callWS<RecordedEntities>({
|
||||
type: "recorder/recorded_entities",
|
||||
});
|
||||
|
||||
recordedExcludedEntitiesCache = {
|
||||
recorded_ids: recordedEntities.entity_ids,
|
||||
excluded_ids: Object.keys(hass.states).filter(
|
||||
(id) => !recordedEntities.entity_ids.includes(id)
|
||||
),
|
||||
};
|
||||
|
||||
return recordedExcludedEntitiesCache;
|
||||
};
|
||||
|
21
src/data/threshold.ts
Normal file
21
src/data/threshold.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface ThresholdPreview {
|
||||
state: string;
|
||||
attributes: Record<string, any>;
|
||||
}
|
||||
|
||||
export const subscribePreviewThreshold = (
|
||||
hass: HomeAssistant,
|
||||
flow_id: string,
|
||||
flow_type: "config_flow" | "options_flow",
|
||||
user_input: Record<string, any>,
|
||||
callback: (preview: ThresholdPreview) => void
|
||||
): Promise<UnsubscribeFunc> =>
|
||||
hass.connection.subscribeMessage(callback, {
|
||||
type: "threshold/start_preview",
|
||||
flow_id,
|
||||
flow_type,
|
||||
user_input,
|
||||
});
|
@@ -144,7 +144,7 @@ export const checkForEntityUpdates = async (
|
||||
|
||||
// there is no reliable way to know if all the updates are done updating, so we just wait a bit for now...
|
||||
await new Promise((r) => {
|
||||
setTimeout(r, 10000);
|
||||
setTimeout(r, 15000);
|
||||
});
|
||||
|
||||
unsubscribeEvents();
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import {
|
||||
mdiAlertCircleOutline,
|
||||
mdiGauge,
|
||||
mdiThermometer,
|
||||
mdiThermometerWater,
|
||||
mdiSunWireless,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherCloudy,
|
||||
mdiWeatherFog,
|
||||
@@ -114,10 +117,15 @@ export const weatherIcons = {
|
||||
};
|
||||
|
||||
export const weatherAttrIcons = {
|
||||
apparent_temperature: mdiThermometer,
|
||||
cloud_coverage: mdiWeatherCloudy,
|
||||
dew_point: mdiThermometerWater,
|
||||
humidity: mdiWaterPercent,
|
||||
wind_bearing: mdiWeatherWindy,
|
||||
wind_speed: mdiWeatherWindy,
|
||||
pressure: mdiGauge,
|
||||
temperature: mdiThermometer,
|
||||
uv_index: mdiSunWireless,
|
||||
visibility: mdiWeatherFog,
|
||||
precipitation: mdiWeatherRainy,
|
||||
};
|
||||
@@ -221,6 +229,8 @@ export const getWeatherUnit = (
|
||||
stateObj.attributes.pressure_unit ||
|
||||
(lengthUnit === "km" ? "hPa" : "inHg")
|
||||
);
|
||||
case "apparent_temperature":
|
||||
case "dew_point":
|
||||
case "temperature":
|
||||
case "templow":
|
||||
return (
|
||||
@@ -228,6 +238,7 @@ export const getWeatherUnit = (
|
||||
);
|
||||
case "wind_speed":
|
||||
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
|
||||
case "cloud_coverage":
|
||||
case "humidity":
|
||||
case "precipitation_probability":
|
||||
return "%";
|
||||
|
@@ -14,6 +14,7 @@ export interface Zone {
|
||||
export interface HomeZoneMutableParams {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export interface ZoneMutableParams {
|
||||
|
@@ -487,14 +487,14 @@ export const stopZwaveExclusion = (hass: HomeAssistant, entry_id: string) =>
|
||||
export const zwaveGrantSecurityClasses = (
|
||||
hass: HomeAssistant,
|
||||
entry_id: string,
|
||||
security_classes: SecurityClass[],
|
||||
client_side_auth?: boolean
|
||||
securityClasses: SecurityClass[],
|
||||
clientSideAuth?: boolean
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "zwave_js/grant_security_classes",
|
||||
entry_id,
|
||||
security_classes,
|
||||
client_side_auth,
|
||||
securityClasses,
|
||||
clientSideAuth,
|
||||
});
|
||||
|
||||
export const zwaveTryParseDskFromQrCode = (
|
||||
|
108
src/dialogs/config-flow/previews/flow-preview-threshold.ts
Normal file
108
src/dialogs/config-flow/previews/flow-preview-threshold.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { FlowType } from "../../../data/data_entry_flow";
|
||||
import {
|
||||
ThresholdPreview,
|
||||
subscribePreviewThreshold,
|
||||
} from "../../../data/threshold";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import "./entity-preview-row";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
@customElement("flow-preview-threshold")
|
||||
class FlowPreviewThreshold extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public flowType!: FlowType;
|
||||
|
||||
public handler!: string;
|
||||
|
||||
@property() public stepId!: string;
|
||||
|
||||
@property() public flowId!: string;
|
||||
|
||||
@property() public stepData!: Record<string, any>;
|
||||
|
||||
@state() private _preview?: HassEntity;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _unsub?: Promise<UnsubscribeFunc>;
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("stepData")) {
|
||||
this._debouncedSubscribePreview();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (this._error) {
|
||||
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
|
||||
}
|
||||
return html`<entity-preview-row
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this._preview}
|
||||
></entity-preview-row>`;
|
||||
}
|
||||
|
||||
private _setPreview = (preview: ThresholdPreview) => {
|
||||
const now = new Date().toISOString();
|
||||
this._preview = {
|
||||
entity_id: `${this.stepId}.___flow_preview___`,
|
||||
last_changed: now,
|
||||
last_updated: now,
|
||||
context: { id: "", parent_id: null, user_id: null },
|
||||
...preview,
|
||||
};
|
||||
};
|
||||
|
||||
private _debouncedSubscribePreview = debounce(() => {
|
||||
this._subscribePreview();
|
||||
}, 250);
|
||||
|
||||
private async _subscribePreview() {
|
||||
if (this._unsub) {
|
||||
(await this._unsub)();
|
||||
this._unsub = undefined;
|
||||
}
|
||||
if (this.flowType === "repair_flow") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsub = subscribePreviewThreshold(
|
||||
this.hass,
|
||||
this.flowId,
|
||||
this.flowType,
|
||||
this.stepData,
|
||||
this._setPreview
|
||||
);
|
||||
await this._unsub;
|
||||
fireEvent(this, "set-flow-errors", { errors: {} });
|
||||
} catch (err: any) {
|
||||
if (typeof err.message === "string") {
|
||||
this._error = err.message;
|
||||
} else {
|
||||
this._error = undefined;
|
||||
fireEvent(this, "set-flow-errors", err.message);
|
||||
}
|
||||
this._unsub = undefined;
|
||||
this._preview = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"flow-preview-threshold": FlowPreviewThreshold;
|
||||
}
|
||||
}
|
@@ -126,7 +126,6 @@ class MoreInfoUpdate extends LitElement {
|
||||
></ha-checkbox>
|
||||
</ha-formfield> `
|
||||
: ""}
|
||||
<hr />
|
||||
<div class="actions">
|
||||
${this.stateObj.attributes.auto_update
|
||||
? ""
|
||||
@@ -240,10 +239,20 @@ class MoreInfoUpdate extends LitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.actions {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
background: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
padding: 12px 0;
|
||||
margin-bottom: -24px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.actions mwc-button {
|
||||
|
@@ -215,10 +215,10 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
<div class="messages">
|
||||
<div class="messages-container" id="scroll-container">
|
||||
${this._conversation!.map(
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
(message) => html`
|
||||
<div class=${this._computeMessageClasses(message)}>
|
||||
${message.text}
|
||||
</div>
|
||||
<div class=${this._computeMessageClasses(message)}>${message.text}</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -355,7 +355,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
private _handleSendMessage() {
|
||||
if (this._messageInput.value) {
|
||||
this._processText(this._messageInput.value);
|
||||
this._processText(this._messageInput.value.trim());
|
||||
this._messageInput.value = "";
|
||||
this._showSendButton = false;
|
||||
}
|
||||
@@ -427,34 +427,28 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
private async _showNotSupportedMessage() {
|
||||
this._addMessage({
|
||||
who: "hass",
|
||||
text: html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}
|
||||
</a>
|
||||
`,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
`,
|
||||
text:
|
||||
// New lines matter for messages
|
||||
// prettier-ignore
|
||||
html`${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_browser"
|
||||
)}
|
||||
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation",
|
||||
{
|
||||
documentation_link: html`<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/docs/configuration/securing/#remote-access"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
|
||||
)}</a>`,
|
||||
}
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -756,6 +750,7 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
max-height: 100%;
|
||||
}
|
||||
.message {
|
||||
white-space: pre-line;
|
||||
font-size: 18px;
|
||||
clear: both;
|
||||
margin: 8px 0;
|
||||
@@ -792,10 +787,14 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message a {
|
||||
.message.user a {
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
|
||||
.message.hass a {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.message img {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import "../resources/compatibility";
|
||||
import "../auth/ha-authorize";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
import "../resources/array.flat.polyfill";
|
||||
|
||||
import("../resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
|
@@ -25,7 +25,6 @@ import { subscribePanels } from "../data/ws-panels";
|
||||
import { subscribeThemes } from "../data/ws-themes";
|
||||
import { subscribeUser } from "../data/ws-user";
|
||||
import type { ExternalAuth } from "../external_app/external_auth";
|
||||
import "../resources/array.flat.polyfill";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
|
||||
window.name = MAIN_WINDOW_NAME;
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import "../resources/compatibility";
|
||||
import "../onboarding/ha-onboarding";
|
||||
import "../resources/safari-14-attachshadow-patch";
|
||||
import "../resources/array.flat.polyfill";
|
||||
|
||||
import("../resources/ha-style");
|
||||
import("@polymer/polymer/lib/utils/settings").then(
|
||||
|
@@ -5,6 +5,7 @@ export const demoConfig: HassConfig = {
|
||||
elevation: 300,
|
||||
latitude: 52.3731339,
|
||||
longitude: 4.8903147,
|
||||
radius: 100,
|
||||
unit_system: {
|
||||
length: "km",
|
||||
mass: "kg",
|
||||
|
@@ -96,7 +96,7 @@ export class HaConfigApplicationCredentials extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
backPath="/config"
|
||||
back-path="/config"
|
||||
.tabs=${configSections.devices}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._getApplicationCredentials(
|
||||
|
@@ -12,6 +12,8 @@ import {
|
||||
deviceAutomationsEqual,
|
||||
DeviceCapabilities,
|
||||
fetchDeviceActionCapabilities,
|
||||
localizeExtraFieldsComputeLabelCallback,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
@@ -84,8 +86,13 @@ export class HaDeviceAction extends LitElement {
|
||||
.data=${this._extraFieldsData(this.action, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
|
||||
this.hass,
|
||||
this.action
|
||||
)}
|
||||
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
|
||||
this.hass,
|
||||
this.action
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
@@ -152,14 +159,6 @@ export class HaDeviceAction extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.device_id.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-device-picker {
|
||||
display: block;
|
||||
|
@@ -147,7 +147,7 @@ class DialogAutomationMode extends LitElement implements HassDialog {
|
||||
type="number"
|
||||
name="max"
|
||||
.value=${this._newMax?.toString() ?? ""}
|
||||
@change=${this._valueChanged}
|
||||
@input=${this._valueChanged}
|
||||
class="max"
|
||||
>
|
||||
</ha-textfield>
|
||||
|
@@ -12,6 +12,8 @@ import {
|
||||
DeviceCapabilities,
|
||||
DeviceCondition,
|
||||
fetchDeviceConditionCapabilities,
|
||||
localizeExtraFieldsComputeLabelCallback,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -84,8 +86,13 @@ export class HaDeviceCondition extends LitElement {
|
||||
.data=${this._extraFieldsData(this.condition, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
|
||||
this.hass,
|
||||
this.condition
|
||||
)}
|
||||
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
|
||||
this.hass,
|
||||
this.condition
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
@@ -153,14 +160,6 @@ export class HaDeviceCondition extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.device.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-device-picker {
|
||||
display: block;
|
||||
|
@@ -145,6 +145,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _filteredAutomations?: string[] | null;
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "automation-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "automation-table-filters-full",
|
||||
@@ -547,6 +555,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.automation.picker.no_automations"
|
||||
)}
|
||||
@clear-filter=${this._clearFilter}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
hasFab
|
||||
clickable
|
||||
class=${this.narrow ? "narrow" : ""}
|
||||
@@ -924,6 +934,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
this._applyFilters();
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _filterChanged(ev) {
|
||||
const type = ev.target.localName;
|
||||
this._filters = { ...this._filters, [type]: ev.detail };
|
||||
|
@@ -596,6 +596,9 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
private _showTriggeredInfo() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.triggers.triggering_event_detail"
|
||||
),
|
||||
text: html`
|
||||
<ha-yaml-editor
|
||||
readOnly
|
||||
|
@@ -14,6 +14,8 @@ import {
|
||||
DeviceCapabilities,
|
||||
DeviceTrigger,
|
||||
fetchDeviceTriggerCapabilities,
|
||||
localizeExtraFieldsComputeLabelCallback,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
} from "../../../../../data/device_automation";
|
||||
import { EntityRegistryEntry } from "../../../../../data/entity_registry";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
@@ -88,8 +90,13 @@ export class HaDeviceTrigger extends LitElement {
|
||||
.data=${this._extraFieldsData(this.trigger, this._capabilities)}
|
||||
.schema=${this._capabilities.extra_fields}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._extraFieldsComputeLabelCallback(
|
||||
this.hass.localize
|
||||
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
|
||||
this.hass,
|
||||
this.trigger
|
||||
)}
|
||||
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
|
||||
this.hass,
|
||||
this.trigger
|
||||
)}
|
||||
@value-changed=${this._extraFieldsChanged}
|
||||
></ha-form>
|
||||
@@ -177,14 +184,6 @@ export class HaDeviceTrigger extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _extraFieldsComputeLabelCallback(localize) {
|
||||
// Returns a callback for ha-form to calculate labels per schema object
|
||||
return (schema) =>
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.triggers.type.device.extra_fields.${schema.name}`
|
||||
) || schema.name;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-device-picker {
|
||||
display: block;
|
||||
|
@@ -107,6 +107,14 @@ class HaBlueprintOverview extends LitElement {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "blueprint-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter: string = "";
|
||||
|
||||
private _processedBlueprints = memoizeOne(
|
||||
(
|
||||
blueprints: Record<string, Blueprints>,
|
||||
@@ -308,6 +316,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@@ -542,6 +552,10 @@ class HaBlueprintOverview extends LitElement {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return haStyle;
|
||||
}
|
||||
|
@@ -82,10 +82,13 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
${this._supervisorInfo && this._supervisorInfo.channel !== "dev"
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<mwc-list-item @request-selected=${this._toggleBeta}>
|
||||
<mwc-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
.disabled=${this._supervisorInfo.channel === "dev"}
|
||||
>
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
? this.hass.localize("ui.panel.config.updates.join_beta")
|
||||
: this.hass.localize(
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
mdiPower,
|
||||
mdiRefresh,
|
||||
} from "@mdi/js";
|
||||
import { HassEntities, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@@ -177,7 +177,10 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { updates: canInstallUpdates, total: totalUpdates } =
|
||||
this._filterUpdateEntitiesWithInstall(this.hass.states);
|
||||
this._filterUpdateEntitiesWithInstall(
|
||||
this.hass.states,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
const { issues: repairsIssues, total: totalRepairIssues } =
|
||||
this._repairsIssues;
|
||||
@@ -306,8 +309,13 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _filterUpdateEntitiesWithInstall = memoizeOne(
|
||||
(entities: HassEntities): { updates: UpdateEntity[]; total: number } => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities);
|
||||
(
|
||||
entities: HomeAssistant["states"],
|
||||
entityRegistry: HomeAssistant["entities"]
|
||||
): { updates: UpdateEntity[]; total: number } => {
|
||||
const updates = filterUpdateEntitiesWithInstall(entities).filter(
|
||||
(entity) => !entityRegistry[entity.entity_id]?.hidden
|
||||
);
|
||||
|
||||
return {
|
||||
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),
|
||||
|
@@ -24,7 +24,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -190,26 +190,42 @@ export class HaConfigDevicePage extends LitElement {
|
||||
|
||||
private _entitiesByCategory = memoizeOne(
|
||||
(entities: EntityRegistryEntry[]) => {
|
||||
const result = groupBy(entities, (entry) =>
|
||||
entry.entity_category
|
||||
? entry.entity_category
|
||||
: computeDomain(entry.entity_id) === "event"
|
||||
? "event"
|
||||
: SENSOR_ENTITIES.includes(computeDomain(entry.entity_id))
|
||||
? "sensor"
|
||||
: "control"
|
||||
) as Record<
|
||||
const result = groupBy(entities, (entry) => {
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
|
||||
if (entry.entity_category) {
|
||||
return entry.entity_category;
|
||||
}
|
||||
|
||||
if (domain === "event" || domain === "notify") {
|
||||
return domain;
|
||||
}
|
||||
|
||||
if (SENSOR_ENTITIES.includes(domain)) {
|
||||
return "sensor";
|
||||
}
|
||||
|
||||
if (ASSIST_ENTITIES.includes(domain)) {
|
||||
return "assist";
|
||||
}
|
||||
|
||||
return "control";
|
||||
}) as Record<
|
||||
| "control"
|
||||
| "event"
|
||||
| "sensor"
|
||||
| "assist"
|
||||
| "notify"
|
||||
| NonNullable<EntityRegistryEntry["entity_category"]>,
|
||||
EntityRegistryStateEntry[]
|
||||
>;
|
||||
for (const key of [
|
||||
"assist",
|
||||
"config",
|
||||
"control",
|
||||
"diagnostic",
|
||||
"event",
|
||||
"notify",
|
||||
"sensor",
|
||||
]) {
|
||||
if (!(key in result)) {
|
||||
@@ -854,7 +870,15 @@ export class HaConfigDevicePage extends LitElement {
|
||||
</div>
|
||||
<div class="column">
|
||||
${(
|
||||
["control", "sensor", "event", "config", "diagnostic"] as const
|
||||
[
|
||||
"control",
|
||||
"sensor",
|
||||
"notify",
|
||||
"event",
|
||||
"assist",
|
||||
"config",
|
||||
"diagnostic",
|
||||
] as const
|
||||
).map((category) =>
|
||||
// Make sure we render controls if no other cards will be rendered
|
||||
entitiesByCategory[category].length > 0 ||
|
||||
@@ -1004,6 +1028,9 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: this.hass.localize(
|
||||
`ui.panel.config.devices.confirm_delete`
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
|
@@ -122,7 +122,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _selected: string[] = [];
|
||||
|
||||
@state() private _filter: string = history.state?.filter || "";
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "devices-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter: string = history.state?.filter || "";
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
@@ -199,38 +205,33 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
if (this._searchParms.has("domain")) {
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-states": {
|
||||
value: [
|
||||
...((this._filters["ha-filter-states"]?.value as string[]) || []),
|
||||
"disabled",
|
||||
],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: [this._searchParms.get("domain")!],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (this._searchParms.has("config_entry")) {
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-states": {
|
||||
value: [
|
||||
...((this._filters["ha-filter-states"]?.value as string[]) || []),
|
||||
"disabled",
|
||||
],
|
||||
items: undefined,
|
||||
},
|
||||
config_entry: {
|
||||
value: [this._searchParms.get("config_entry")!],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
|
||||
if (!domain && !configEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._filter = history.state?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-states": {
|
||||
value: [
|
||||
...((this._filters["ha-filter-states"]?.value as string[]) || []),
|
||||
"disabled",
|
||||
],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: domain ? [domain] : [],
|
||||
items: undefined,
|
||||
},
|
||||
config_entry: {
|
||||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (this._searchParms.has("label")) {
|
||||
this._filterLabel();
|
||||
}
|
||||
|
@@ -164,6 +164,9 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
|
@@ -215,6 +215,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.confirm_delete"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
|
@@ -144,7 +144,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entities!: EntityRegistryEntry[];
|
||||
|
||||
@state() private _filter: string = history.state?.filter || "";
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "entities-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter: string = history.state?.filter || "";
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -703,7 +709,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
)
|
||||
).length
|
||||
}
|
||||
.filter=${this._filter}
|
||||
selectable
|
||||
.selected=${this._selected.length}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
@@ -715,6 +720,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
|
||||
@selection-changed=${this._handleSelectionChanged}
|
||||
clickable
|
||||
@clear-filter=${this._clearFilter}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._openEditEntry}
|
||||
id="entity_id"
|
||||
@@ -833,7 +839,7 @@ ${
|
||||
></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.button"
|
||||
"ui.panel.config.entities.picker.delete_selected.button"
|
||||
)}
|
||||
</div>
|
||||
</ha-menu-item>
|
||||
@@ -943,6 +949,7 @@ ${
|
||||
}
|
||||
|
||||
protected firstUpdated() {
|
||||
this._setFiltersFromUrl();
|
||||
if (Object.keys(this._filters).length) {
|
||||
return;
|
||||
}
|
||||
@@ -952,39 +959,36 @@ ${
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
this._setFiltersFromUrl();
|
||||
fetchEntitySourcesWithCache(this.hass).then((sources) => {
|
||||
this._entitySources = sources;
|
||||
});
|
||||
}
|
||||
|
||||
private _setFiltersFromUrl() {
|
||||
if (this._searchParms.has("domain")) {
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-states": {
|
||||
value: [],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: [this._searchParms.get("domain")!],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (this._searchParms.has("config_entry")) {
|
||||
this._filters = {
|
||||
...this._filters,
|
||||
"ha-filter-states": {
|
||||
value: [],
|
||||
items: undefined,
|
||||
},
|
||||
config_entry: {
|
||||
value: [this._searchParms.get("config_entry")!],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
const domain = this._searchParms.get("domain");
|
||||
const configEntry = this._searchParms.get("config_entry");
|
||||
|
||||
if (!domain && !configEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._filter = history.state?.filter || "";
|
||||
|
||||
this._filters = {
|
||||
"ha-filter-states": {
|
||||
value: [],
|
||||
items: undefined,
|
||||
},
|
||||
"ha-filter-integrations": {
|
||||
value: domain ? [domain] : [],
|
||||
items: undefined,
|
||||
},
|
||||
config_entry: {
|
||||
value: configEntry ? [configEntry] : [],
|
||||
items: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
if (this._searchParms.has("label")) {
|
||||
this._filterLabel();
|
||||
}
|
||||
@@ -1252,25 +1256,23 @@ ${rejected
|
||||
});
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
`ui.panel.config.entities.picker.remove_selected.confirm_${
|
||||
removeableEntities.length !== this._selected.length ? "partly_" : ""
|
||||
}title`,
|
||||
{ number: removeableEntities.length }
|
||||
`ui.panel.config.entities.picker.delete_selected.confirm_title`
|
||||
),
|
||||
text:
|
||||
removeableEntities.length === this._selected.length
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_text"
|
||||
"ui.panel.config.entities.picker.delete_selected.confirm_text"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.entities.picker.remove_selected.confirm_partly_text",
|
||||
"ui.panel.config.entities.picker.delete_selected.confirm_partly_text",
|
||||
{
|
||||
removable: removeableEntities.length,
|
||||
deletable: removeableEntities.length,
|
||||
selected: this._selected.length,
|
||||
}
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.remove"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: true,
|
||||
confirm: () => {
|
||||
removeableEntities.forEach((entity) =>
|
||||
removeEntityRegistryEntry(this.hass, entity)
|
||||
|
@@ -159,6 +159,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
private _activeCollapsed?: string;
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "helpers-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@state() private _stateItems: HassEntity[] = [];
|
||||
|
||||
@state() private _entityEntries?: Record<string, EntityRegistryEntry>;
|
||||
@@ -559,6 +567,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
.activeFilters=${this._activeFilters}
|
||||
@clear-filter=${this._clearFilter}
|
||||
@row-click=${this._openEditDialog}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
hasFab
|
||||
clickable
|
||||
.noDataText=${this.hass.localize(
|
||||
@@ -1070,6 +1080,10 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item-base";
|
||||
import "@material/web/divider/divider";
|
||||
import {
|
||||
mdiAlertCircle,
|
||||
mdiBookshelf,
|
||||
@@ -27,13 +27,13 @@ import {
|
||||
} from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -41,15 +41,15 @@ import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { isDevVersion } from "../../../common/config/version";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-button-menu-new";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-list-new";
|
||||
import "../../../components/ha-list-item-new";
|
||||
import "../../../components/ha-button-menu-new";
|
||||
import "../../../components/ha-list-new";
|
||||
import "../../../components/ha-menu-item";
|
||||
import {
|
||||
deleteApplicationCredential,
|
||||
fetchApplicationCredentialsConfigEntry,
|
||||
@@ -57,13 +57,13 @@ import {
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import {
|
||||
ConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
ERROR_STATES,
|
||||
RECOVERABLE_STATES,
|
||||
deleteConfigEntry,
|
||||
disableConfigEntry,
|
||||
DisableConfigEntryResult,
|
||||
enableConfigEntry,
|
||||
ERROR_STATES,
|
||||
getConfigEntries,
|
||||
RECOVERABLE_STATES,
|
||||
reloadConfigEntry,
|
||||
updateConfigEntry,
|
||||
} from "../../../data/config_entries";
|
||||
@@ -80,13 +80,13 @@ import {
|
||||
} from "../../../data/entity_registry";
|
||||
import { getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||
import {
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
LogSeverity,
|
||||
domainToName,
|
||||
fetchIntegrationManifest,
|
||||
integrationIssuesUrl,
|
||||
IntegrationLogInfo,
|
||||
IntegrationManifest,
|
||||
integrationsWithPanel,
|
||||
LogSeverity,
|
||||
setIntegrationLogLevel,
|
||||
subscribeLogInfo,
|
||||
} from "../../../data/integration";
|
||||
@@ -453,7 +453,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
const attention = ATTENTION_SOURCES.includes(
|
||||
flow.context.source
|
||||
);
|
||||
return html`<ha-list-item-new
|
||||
return html` <ha-list-item-new
|
||||
class="config_entry ${attention ? "attention" : ""}"
|
||||
>
|
||||
${flow.localized_title}
|
||||
@@ -477,8 +477,15 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
></ha-button>
|
||||
</ha-list-item-new>`;
|
||||
})}
|
||||
${attentionEntries.map((item) =>
|
||||
this._renderConfigEntry(item)
|
||||
${attentionEntries.map(
|
||||
(item, index) =>
|
||||
html`${this._renderConfigEntry(item)}
|
||||
${index < attentionEntries.length - 1
|
||||
? html` <md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></md-divider>`
|
||||
: ""} `
|
||||
)}
|
||||
</ha-list-new>
|
||||
</ha-card>`
|
||||
@@ -502,7 +509,16 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
</div>`
|
||||
: nothing}
|
||||
<ha-list-new>
|
||||
${normalEntries.map((item) => this._renderConfigEntry(item))}
|
||||
${normalEntries.map(
|
||||
(item, index) =>
|
||||
html`${this._renderConfigEntry(item)}
|
||||
${index < normalEntries.length - 1
|
||||
? html` <md-divider
|
||||
role="separator"
|
||||
tabindex="-1"
|
||||
></md-divider>`
|
||||
: ""} `
|
||||
)}
|
||||
</ha-list-new>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._addIntegration}>
|
||||
@@ -667,19 +683,21 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
|
||||
const configPanel = this._configPanel(item.domain, this.hass.panels);
|
||||
|
||||
return html`<ha-list-item-new
|
||||
return html` <ha-list-item-new
|
||||
class=${classMap({
|
||||
config_entry: true,
|
||||
"state-not-loaded": item!.state === "not_loaded",
|
||||
"state-failed-unload": item!.state === "failed_unload",
|
||||
"state-setup": item!.state === "setup_in_progress",
|
||||
"state-error": ERROR_STATES.includes(item!.state),
|
||||
"state-disabled": item.disabled_by !== null,
|
||||
})}
|
||||
data-entry-id=${item.entry_id}
|
||||
.disabled=${item.disabled_by}
|
||||
.configEntry=${item}
|
||||
>
|
||||
${item.title || domainToName(this.hass.localize, item.domain)}
|
||||
<div slot="headline">
|
||||
${item.title || domainToName(this.hass.localize, item.domain)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
<div>${devicesLine}</div>
|
||||
${stateText
|
||||
@@ -726,170 +744,159 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
${item.supports_options && stateText
|
||||
? html`<ha-list-item
|
||||
@request-selected=${this._showOptions}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
? html`<ha-menu-item @click=${this._showOptions}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.configure"
|
||||
)}
|
||||
</ha-list-item>`
|
||||
</ha-menu-item>`
|
||||
: ""}
|
||||
${item.disabled_by && devices.length
|
||||
? html`<a
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon .path=${mdiDevices} slot="graphic"></ha-svg-icon>
|
||||
? html`
|
||||
<ha-menu-item
|
||||
href=${devices.length === 1
|
||||
? `/config/devices/device/${devices[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDevices} slot="start"></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.devices`,
|
||||
{ count: devices.length }
|
||||
)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-menu-item>
|
||||
`
|
||||
: ""}
|
||||
${item.disabled_by && services.length
|
||||
? html`<a
|
||||
? html`<ha-menu-item
|
||||
href=${services.length === 1
|
||||
? `/config/devices/device/${services[0].id}`
|
||||
: `/config/devices/dashboard?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
<ha-svg-icon
|
||||
.path=${mdiHandExtendedOutline}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.services`,
|
||||
{ count: services.length }
|
||||
)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
<ha-svg-icon
|
||||
.path=${mdiHandExtendedOutline}
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.services`,
|
||||
{ count: services.length }
|
||||
)}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-menu-item> `
|
||||
: ""}
|
||||
${item.disabled_by && entities.length
|
||||
? html`<a
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-list-item hasMeta graphic="icon">
|
||||
? html`
|
||||
<ha-menu-item
|
||||
href=${`/config/entities?historyBack=1&config_entry=${item.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiShapeOutline}
|
||||
slot="graphic"
|
||||
slot="start"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.integrations.config_entry.entities`,
|
||||
{ count: entities.length }
|
||||
)}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-menu-item>
|
||||
`
|
||||
: ""}
|
||||
${!item.disabled_by &&
|
||||
RECOVERABLE_STATES.includes(item.state) &&
|
||||
item.supports_unload &&
|
||||
item.source !== "system"
|
||||
? html`<ha-list-item
|
||||
@request-selected=${this._handleReload}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiReload}></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
? html`
|
||||
<ha-menu-item @click=${this._handleReload}>
|
||||
<ha-svg-icon slot="start" .path=${mdiReload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reload"
|
||||
)}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-list-item @request-selected=${this._handleRename} graphic="icon">
|
||||
<ha-menu-item @click=${this._handleRename} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.rename"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-menu-item>
|
||||
|
||||
<li divider role="separator"></li>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
|
||||
${this._diagnosticHandler && item.state === "loaded"
|
||||
? html`<a
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-list-item graphic="icon">
|
||||
? html`
|
||||
<ha-menu-item
|
||||
href=${getConfigEntryDiagnosticsDownloadUrl(item.entry_id)}
|
||||
target="_blank"
|
||||
@click=${this._signUrl}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.download_diagnostics"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</a>`
|
||||
</ha-menu-item>
|
||||
`
|
||||
: ""}
|
||||
${!item.disabled_by &&
|
||||
item.supports_reconfigure &&
|
||||
item.source !== "system"
|
||||
? html`<ha-list-item
|
||||
@request-selected=${this._handleReconfigure}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiWrench}></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
? html`
|
||||
<ha-menu-item @click=${this._handleReconfigure}>
|
||||
<ha-svg-icon slot="start" .path=${mdiWrench}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.reconfigure"
|
||||
)}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-list-item
|
||||
@request-selected=${this._handleSystemOptions}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-menu-item @click=${this._handleSystemOptions} graphic="icon">
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.system_options"
|
||||
)}
|
||||
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
</ha-menu-item>
|
||||
${item.disabled_by === "user"
|
||||
? html`<ha-list-item
|
||||
@request-selected=${this._handleEnable}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: item.source !== "system"
|
||||
? html`<ha-list-item
|
||||
class="warning"
|
||||
@request-selected=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
? html`
|
||||
<ha-menu-item @click=${this._handleEnable}>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
class="warning"
|
||||
.path=${mdiStopCircleOutline}
|
||||
slot="start"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
${this.hass.localize("ui.common.enable")}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: item.source !== "system"
|
||||
? html`
|
||||
<ha-menu-item
|
||||
class="warning"
|
||||
@click=${this._handleDisable}
|
||||
graphic="icon"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiStopCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize("ui.common.disable")}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
${item.source !== "system"
|
||||
? html`<ha-list-item
|
||||
class="warning"
|
||||
@request-selected=${this._handleDelete}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>`
|
||||
: ""}
|
||||
? html`
|
||||
<ha-menu-item class="warning" @click=${this._handleDelete}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
class="warning"
|
||||
.path=${mdiDelete}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.delete"
|
||||
)}
|
||||
</ha-menu-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-button-menu-new>
|
||||
</ha-list-item-new>`;
|
||||
}
|
||||
@@ -1055,64 +1062,43 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _handleRename(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleRename(ev: Event): void {
|
||||
this._editEntryName(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleReload(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleReload(ev: Event): void {
|
||||
this._reloadIntegration(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleReconfigure(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleReconfigure(ev: Event): void {
|
||||
this._reconfigureIntegration(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDelete(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleDelete(ev: Event): void {
|
||||
this._removeIntegration(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleDisable(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleDisable(ev: Event): void {
|
||||
this._disableIntegration(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleEnable(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (ev.detail.source && !shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleEnable(ev: Event): void {
|
||||
this._enableIntegration(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSystemOptions(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
private _handleSystemOptions(ev: Event): void {
|
||||
this._showSystemOptions(
|
||||
((ev.target as HTMLElement).closest(".config_entry") as any).configEntry
|
||||
);
|
||||
@@ -1327,7 +1313,7 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _signUrl(ev) {
|
||||
const anchor = ev.target.closest("a");
|
||||
const anchor = ev.currentTarget;
|
||||
ev.preventDefault();
|
||||
const signedUrl = await getSignedPath(
|
||||
this.hass,
|
||||
@@ -1428,32 +1414,22 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
ha-alert:first-of-type {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-list-item-new {
|
||||
position: relative;
|
||||
}
|
||||
ha-list-item-new.discovered {
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-item-meta-display: flex;
|
||||
height: 72px;
|
||||
}
|
||||
ha-list-item-new.config_entry {
|
||||
overflow: visible;
|
||||
--mdc-list-item-meta-size: auto;
|
||||
--mdc-list-item-meta-display: flex;
|
||||
}
|
||||
ha-button-menu-new ha-list-item {
|
||||
--mdc-list-item-meta-size: 24px;
|
||||
}
|
||||
ha-list-item-new.config_entry::after {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 8px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
}
|
||||
ha-button-menu-new {
|
||||
flex: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -1510,6 +1486,10 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.state-disabled [slot="headline"],
|
||||
.state-disabled [slot="supporting-text"] {
|
||||
opacity: var(--md-list-item-disabled-opacity, 0.3);
|
||||
}
|
||||
ha-list-new {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
@@ -36,14 +36,8 @@ class DialogThreadDataset extends LitElement implements HassDialog {
|
||||
dataset.extended_pan_id &&
|
||||
otbrInfo.active_dataset_tlvs?.includes(dataset.extended_pan_id);
|
||||
|
||||
const canImportKeychain =
|
||||
hasOTBR &&
|
||||
!this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
|
||||
network.routers?.length;
|
||||
|
||||
return html`<ha-dialog
|
||||
open
|
||||
.hideActions=${!canImportKeychain}
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(this.hass, network.name)}
|
||||
>
|
||||
@@ -59,28 +53,8 @@ class DialogThreadDataset extends LitElement implements HassDialog {
|
||||
Active dataset TLVs: ${otbrInfo.active_dataset_tlvs}`
|
||||
: nothing}
|
||||
</div>
|
||||
${canImportKeychain
|
||||
? html`<ha-button slot="primary-action" @click=${this._sendCredentials}
|
||||
>Send credentials to phone</ha-button
|
||||
>`
|
||||
: nothing}
|
||||
</ha-dialog>`;
|
||||
}
|
||||
|
||||
private _sendCredentials() {
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "thread/store_in_platform_keychain",
|
||||
payload: {
|
||||
mac_extended_address:
|
||||
this._params?.network.dataset?.preferred_extended_address ||
|
||||
this._params!.network.routers![0]!.extended_address,
|
||||
border_agent_id:
|
||||
this._params?.network.dataset?.preferred_border_agent_id ||
|
||||
this._params!.network.routers![0]!.border_agent_id,
|
||||
active_operational_dataset: this._params!.otbrInfo!.active_dataset_tlvs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -151,7 +151,7 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
slot="fab"
|
||||
@click=${this._importExternalThreadCredentials}
|
||||
extended
|
||||
label="Import credentials"
|
||||
label="Send credentials to Home Assistant"
|
||||
><ha-svg-icon slot="icon" .path=${mdiCellphoneKey}></ha-svg-icon
|
||||
></ha-fab>`
|
||||
: nothing}
|
||||
@@ -160,6 +160,14 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _renderNetwork(network: ThreadNetwork) {
|
||||
const canImportKeychain =
|
||||
this.hass.auth.external?.config.canTransferThreadCredentialsToKeychain &&
|
||||
network.dataset?.extended_pan_id &&
|
||||
this._otbrInfo &&
|
||||
this._otbrInfo?.active_dataset_tlvs?.includes(
|
||||
network.dataset.extended_pan_id
|
||||
);
|
||||
|
||||
return html`<ha-card>
|
||||
<div class="card-header">
|
||||
${network.name}${network.dataset
|
||||
@@ -303,9 +311,30 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
${canImportKeychain
|
||||
? html`<div class="card-actions">
|
||||
<mwc-button @click=${this._sendCredentials}
|
||||
>Send credentials to phone</mwc-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private _sendCredentials() {
|
||||
if (!this._otbrInfo) {
|
||||
return;
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "thread/store_in_platform_keychain",
|
||||
payload: {
|
||||
mac_extended_address: this._otbrInfo.extended_address,
|
||||
border_agent_id: this._otbrInfo.border_agent_id ?? "",
|
||||
active_operational_dataset: this._otbrInfo.active_dataset_tlvs ?? "",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async _showDatasetInfo(ev: Event) {
|
||||
const network = (ev.currentTarget as any).network as ThreadNetwork;
|
||||
showThreadDatasetDialog(this, { network, otbrInfo: this._otbrInfo });
|
||||
|
@@ -3,6 +3,7 @@ import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-alert";
|
||||
import type { HaCheckbox } from "../../../../../components/ha-checkbox";
|
||||
@@ -60,7 +61,8 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
| "finished"
|
||||
| "provisioned"
|
||||
| "validate_dsk_enter_pin"
|
||||
| "grant_security_classes";
|
||||
| "grant_security_classes"
|
||||
| "waiting_for_device";
|
||||
|
||||
@state() private _device?: ZWaveJSAddNodeDevice;
|
||||
|
||||
@@ -86,6 +88,11 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
|
||||
private _qrProcessing = false;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("beforeunload", this._onBeforeUnload);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribe();
|
||||
@@ -106,14 +113,22 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Prevent accidentally closing the dialog in certain stages
|
||||
const preventClose = this._shouldPreventClose();
|
||||
|
||||
const heading = this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.title"
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.zwave_js.add_node.title")
|
||||
)}
|
||||
.heading=${preventClose
|
||||
? heading
|
||||
: createCloseHeading(this.hass, heading)}
|
||||
scrimClickAction=${ifDefined(preventClose ? "" : undefined)}
|
||||
escapeKeyAction=${ifDefined(preventClose ? "" : undefined)}
|
||||
>
|
||||
${this._status === "loading"
|
||||
? html`<div style="display: flex; justify-content: center;">
|
||||
@@ -122,81 +137,93 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
</div>`
|
||||
: this._status === "choose_strategy"
|
||||
? html`<h3>Choose strategy</h3>
|
||||
<div class="flex-column">
|
||||
<ha-formfield
|
||||
.label=${html`<b>Secure if possible</b>
|
||||
<div class="secondary">
|
||||
Requires user interaction during inclusion. Fast and
|
||||
secure with S2 when supported. Fallback to legacy S0 or
|
||||
no encryption when necessary.
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Default}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Default ||
|
||||
this._inclusionStrategy === undefined}
|
||||
: this._status === "waiting_for_device"
|
||||
? html`<div class="flex-container">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.waiting_for_device"
|
||||
)}
|
||||
</p>
|
||||
</div>`
|
||||
: this._status === "choose_strategy"
|
||||
? html`<h3>Choose strategy</h3>
|
||||
<div class="flex-column">
|
||||
<ha-formfield
|
||||
.label=${html`<b>Secure if possible</b>
|
||||
<div class="secondary">
|
||||
Requires user interaction during inclusion. Fast and
|
||||
secure with S2 when supported. Fallback to legacy S0
|
||||
or no encryption when necessary.
|
||||
</div>`}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Legacy Secure</b>
|
||||
<div class="secondary">
|
||||
Uses the older S0 security that is secure, but slow due
|
||||
to a lot of overhead. Allows securely including S2
|
||||
capable devices which fail to be included with S2.
|
||||
</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Security_S0}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Security_S0}
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Default}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Default ||
|
||||
this._inclusionStrategy === undefined}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Legacy Secure</b>
|
||||
<div class="secondary">
|
||||
Uses the older S0 security that is secure, but slow
|
||||
due to a lot of overhead. Allows securely including S2
|
||||
capable devices which fail to be included with S2.
|
||||
</div>`}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Insecure</b>
|
||||
<div class="secondary">Do not use encryption.</div>`}
|
||||
>
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Insecure}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Insecure}
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Security_S0}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Security_S0}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
<ha-formfield
|
||||
.label=${html`<b>Insecure</b>
|
||||
<div class="secondary">Do not use encryption.</div>`}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startManualInclusion}
|
||||
>
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "qr_scan"
|
||||
? html`${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
></ha-qr-scanner>
|
||||
<mwc-button slot="secondaryAction" @click=${this._startOver}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.back"
|
||||
)}
|
||||
<ha-radio
|
||||
name="strategy"
|
||||
@change=${this._handleStrategyChange}
|
||||
.value=${InclusionStrategy.Insecure}
|
||||
.checked=${this._inclusionStrategy ===
|
||||
InclusionStrategy.Insecure}
|
||||
>
|
||||
</ha-radio>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startManualInclusion}
|
||||
>
|
||||
Search device
|
||||
</mwc-button>`
|
||||
: this._status === "validate_dsk_enter_pin"
|
||||
? html`
|
||||
: this._status === "qr_scan"
|
||||
? html`${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<ha-qr-scanner
|
||||
.localize=${this.hass.localize}
|
||||
@qr-code-scanned=${this._qrCodeScanned}
|
||||
></ha-qr-scanner>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._startOver}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.back"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: this._status === "validate_dsk_enter_pin"
|
||||
? html`
|
||||
<p>
|
||||
Please enter the 5-digit PIN for your device and verify that
|
||||
the rest of the device-specific key matches the one that can
|
||||
@@ -225,198 +252,160 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: this._status === "grant_security_classes"
|
||||
? html`
|
||||
<h3>
|
||||
The device has requested the following security classes:
|
||||
</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<div class="flex-column">
|
||||
${this._requestedGrant?.securityClasses
|
||||
.sort((a, b) => {
|
||||
// Put highest security classes at the top, S0 at the bottom
|
||||
if (a === SecurityClass.S0_Legacy) return 1;
|
||||
if (b === SecurityClass.S0_Legacy) return -1;
|
||||
return b - a;
|
||||
})
|
||||
.map(
|
||||
(securityClass) =>
|
||||
html`<ha-formfield
|
||||
.label=${html`<b
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
|
||||
)}</b
|
||||
>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._handleSecurityClassChange}
|
||||
.value=${securityClass}
|
||||
.checked=${this._securityClasses.includes(
|
||||
securityClass
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
)}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._securityClasses.length}
|
||||
@click=${this._grantSecurityClasses}
|
||||
>
|
||||
Submit
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "timed_out"
|
||||
: this._status === "grant_security_classes"
|
||||
? html`
|
||||
<h3>Timed out!</h3>
|
||||
<p>
|
||||
We have not found any device in inclusion mode. Make
|
||||
sure the device is active and in inclusion mode.
|
||||
</p>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startOver}
|
||||
>
|
||||
Retry
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "started_specific"
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h3>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`
|
||||
: this._status === "started"
|
||||
? html`
|
||||
<div class="select-inclusion">
|
||||
<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h2>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._chooseInclusionStrategy}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
)}
|
||||
</h2>
|
||||
<ha-svg-icon
|
||||
.path=${mdiQrcodeScan}
|
||||
></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<mwc-button @click=${this._scanQRCode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||
)}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "interviewing"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<div class="status">
|
||||
<p>
|
||||
<b
|
||||
<h3>
|
||||
The device has requested the following security
|
||||
classes:
|
||||
</h3>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error"
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: ""}
|
||||
<div class="flex-column">
|
||||
${this._requestedGrant?.securityClasses
|
||||
.sort((a, b) => {
|
||||
// Put highest security classes at the top, S0 at the bottom
|
||||
if (a === SecurityClass.S0_Legacy) return 1;
|
||||
if (b === SecurityClass.S0_Legacy) return -1;
|
||||
return b - a;
|
||||
})
|
||||
.map(
|
||||
(securityClass) =>
|
||||
html`<ha-formfield
|
||||
.label=${html`<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.interview_started"
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.title`
|
||||
)}</b
|
||||
>
|
||||
<div class="secondary">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.security_classes.${SecurityClass[securityClass]}.description`
|
||||
)}
|
||||
</div>`}
|
||||
>
|
||||
<ha-checkbox
|
||||
@change=${this._handleSecurityClassChange}
|
||||
.value=${securityClass}
|
||||
.checked=${this._securityClasses.includes(
|
||||
securityClass
|
||||
)}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</ha-formfield>`
|
||||
)}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!this._securityClasses.length}
|
||||
@click=${this._grantSecurityClasses}
|
||||
>
|
||||
Submit
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "timed_out"
|
||||
? html`
|
||||
<h3>Timed out!</h3>
|
||||
<p>
|
||||
We have not found any device in inclusion mode. Make
|
||||
sure the device is active and in inclusion mode.
|
||||
</p>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this._startOver}
|
||||
>
|
||||
Retry
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "started_specific"
|
||||
? html`<h3>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h3>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>`
|
||||
: this._status === "started"
|
||||
? html`
|
||||
<div class="select-inclusion">
|
||||
<div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.searching_device"
|
||||
)}
|
||||
</h2>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<button
|
||||
class="link"
|
||||
@click=${this._chooseInclusionStrategy}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.choose_inclusion_strategy"
|
||||
)}
|
||||
</button>
|
||||
</p>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
(stage) => html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
${stage}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
${this._supportsSmartStart
|
||||
? html` <div class="outline">
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code"
|
||||
)}
|
||||
</h2>
|
||||
<ha-svg-icon
|
||||
.path=${mdiQrcodeScan}
|
||||
></ha-svg-icon>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.qr_code_paragraph"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<mwc-button @click=${this._scanQRCode}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.scan_qr_code"
|
||||
)}
|
||||
</mwc-button>
|
||||
</p>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "failed"
|
||||
: this._status === "interviewing"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
<div class="status">
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||
)}
|
||||
>
|
||||
${this._error ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||
)}
|
||||
</ha-alert>
|
||||
<p>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.interview_started"
|
||||
)}</b
|
||||
>
|
||||
</p>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -441,45 +430,21 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "finished"
|
||||
: this._status === "failed"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${this._lowSecurity
|
||||
? mdiAlertCircle
|
||||
: mdiCheckCircle}
|
||||
class=${this._lowSecurity
|
||||
? "warning"
|
||||
: "success"}
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_finished"
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_failed"
|
||||
)}
|
||||
</p>
|
||||
${this._lowSecurity
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
title="The device was added insecurely"
|
||||
>
|
||||
There was an error during secure
|
||||
inclusion. You can try again by
|
||||
excluding the device and adding it
|
||||
again.
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/config/devices/device/${
|
||||
this._device!.id
|
||||
}`}
|
||||
>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.view_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
${this._error ||
|
||||
this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.check_logs"
|
||||
)}
|
||||
</ha-alert>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
@@ -504,18 +469,60 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
? html` <div class="flex-container">
|
||||
: this._status === "finished"
|
||||
? html`
|
||||
<div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
.path=${this._lowSecurity
|
||||
? mdiAlertCircle
|
||||
: mdiCheckCircle}
|
||||
class=${this._lowSecurity
|
||||
? "warning"
|
||||
: "success"}
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
||||
"ui.panel.config.zwave_js.add_node.inclusion_finished"
|
||||
)}
|
||||
</p>
|
||||
${this._lowSecurity
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
title="The device was added insecurely"
|
||||
>
|
||||
There was an error during secure
|
||||
inclusion. You can try again by
|
||||
excluding the device and adding it
|
||||
again.
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
<a
|
||||
href=${`/config/devices/device/${
|
||||
this._device?.id
|
||||
}`}
|
||||
>
|
||||
<mwc-button>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.view_device"
|
||||
)}
|
||||
</mwc-button>
|
||||
</a>
|
||||
${this._stages
|
||||
? html` <div class="stages">
|
||||
${this._stages.map(
|
||||
(stage) => html`
|
||||
<span class="stage">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
${stage}
|
||||
</span>
|
||||
`
|
||||
)}
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
@@ -523,12 +530,42 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</mwc-button>
|
||||
`
|
||||
: this._status === "provisioned"
|
||||
? html` <div class="flex-container">
|
||||
<ha-svg-icon
|
||||
.path=${mdiCheckCircle}
|
||||
class="success"
|
||||
></ha-svg-icon>
|
||||
<div class="status">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.add_node.provisioning_finished"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this.hass.localize("ui.common.close")}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _shouldPreventClose(): boolean {
|
||||
return (
|
||||
this._status === "started_specific" ||
|
||||
this._status === "validate_dsk_enter_pin" ||
|
||||
this._status === "grant_security_classes" ||
|
||||
this._status === "waiting_for_device"
|
||||
);
|
||||
}
|
||||
|
||||
private _chooseInclusionStrategy(): void {
|
||||
this._unsubscribe();
|
||||
this._status = "choose_strategy";
|
||||
@@ -639,7 +676,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
|
||||
private async _validateDskAndEnterPin(): Promise<void> {
|
||||
this._status = "loading";
|
||||
this._status = "waiting_for_device";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await zwaveValidateDskAndEnterPin(
|
||||
@@ -656,7 +693,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}
|
||||
|
||||
private async _grantSecurityClasses(): Promise<void> {
|
||||
this._status = "loading";
|
||||
this._status = "waiting_for_device";
|
||||
this._error = undefined;
|
||||
try {
|
||||
await zwaveGrantSecurityClasses(
|
||||
@@ -719,6 +756,12 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
this._addNodeTimeoutHandle = undefined;
|
||||
}
|
||||
|
||||
if (message.event === "node found") {
|
||||
// The user may have to enter a PIN. Until then prevent accidentally
|
||||
// closing the dialog
|
||||
this._status = "waiting_for_device";
|
||||
}
|
||||
|
||||
if (message.event === "validate dsk and enter pin") {
|
||||
this._status = "validate_dsk_enter_pin";
|
||||
this._dsk = message.dsk;
|
||||
@@ -775,6 +818,13 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
}, 90000);
|
||||
}
|
||||
|
||||
private _onBeforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (this._shouldPreventClose()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
event.returnValue = true;
|
||||
};
|
||||
|
||||
private _unsubscribe(): void {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
@@ -791,6 +841,7 @@ class DialogZWaveJSAddNode extends LitElement {
|
||||
clearTimeout(this._addNodeTimeoutHandle);
|
||||
}
|
||||
this._addNodeTimeoutHandle = undefined;
|
||||
window.removeEventListener("beforeunload", this._onBeforeUnload);
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
|
@@ -51,6 +51,14 @@ export class HaConfigLabels extends LitElement {
|
||||
|
||||
@state() private _labels: LabelRegistryEntry[] = [];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "labels-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
key: "labels-table-sort",
|
||||
state: false,
|
||||
@@ -160,6 +168,8 @@ export class HaConfigLabels extends LitElement {
|
||||
hasFab
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editLabel}
|
||||
clickable
|
||||
id="label_id"
|
||||
@@ -283,6 +293,10 @@ export class HaConfigLabels extends LitElement {
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -2,17 +2,21 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiOpenInNew,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiStar,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, PropertyValues, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
DataTableColumnContainer,
|
||||
RowClickedEvent,
|
||||
@@ -22,6 +26,9 @@ import "../../../../components/ha-clickable-list-item";
|
||||
import "../../../../components/ha-fab";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-menu";
|
||||
import type { HaMenu } from "../../../../components/ha-menu";
|
||||
import "../../../../components/ha-menu-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||
import {
|
||||
@@ -41,13 +48,11 @@ import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { HomeAssistant, Route } from "../../../../types";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { getLovelaceStrategy } from "../../../lovelace/strategies/get-strategy";
|
||||
import { showNewDashboardDialog } from "../../dashboard/show-dialog-new-dashboard";
|
||||
import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
@@ -70,6 +75,14 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
@state() private _dashboards: LovelaceDashboard[] = [];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "lovelace-dashboards-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter: string = "";
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-sort",
|
||||
state: false,
|
||||
@@ -77,6 +90,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
})
|
||||
private _activeSorting?: SortingChangedEvent;
|
||||
|
||||
@state() private _overflowDashboard?: LovelaceDashboard;
|
||||
|
||||
@query("#overflow-menu") private _overflowMenu!: HaMenu;
|
||||
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadFragmentTranslation("lovelace");
|
||||
@@ -202,40 +219,36 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
columns.url_path = {
|
||||
columns.actions = {
|
||||
title: "",
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.url"
|
||||
),
|
||||
filterable: true,
|
||||
width: "100px",
|
||||
template: (dashboard) =>
|
||||
narrow
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiOpenInNew}
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: html`
|
||||
<mwc-button
|
||||
.urlPath=${dashboard.url_path}
|
||||
@click=${this._navigate}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.open"
|
||||
)}</mwc-button
|
||||
>
|
||||
`,
|
||||
width: "64px",
|
||||
type: "icon-button",
|
||||
template: (dashboard) => html`
|
||||
<ha-icon-button
|
||||
.dashboard=${dashboard}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._showOverflowMenu}
|
||||
></ha-icon-button>
|
||||
`,
|
||||
};
|
||||
|
||||
return columns;
|
||||
}
|
||||
);
|
||||
|
||||
private _showOverflowMenu = (ev) => {
|
||||
if (
|
||||
this._overflowMenu.open &&
|
||||
ev.target === this._overflowMenu.anchorElement
|
||||
) {
|
||||
this._overflowMenu.close();
|
||||
return;
|
||||
}
|
||||
this._overflowDashboard = ev.target.dashboard;
|
||||
this._overflowMenu.anchorElement = ev.target;
|
||||
this._overflowMenu.show();
|
||||
};
|
||||
|
||||
private _getItems = memoize((dashboards: LovelaceDashboard[]) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
@@ -304,7 +317,9 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
.data=${this._getItems(this._dashboards)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@row-click=${this._editDashboard}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._navigate}
|
||||
id="url_path"
|
||||
hasFab
|
||||
clickable
|
||||
@@ -336,6 +351,22 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-fab>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-menu id="overflow-menu" positioning="fixed">
|
||||
<ha-menu-item @click=${this._editDashboard}>
|
||||
<ha-svg-icon .path=${mdiPencil} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Edit</div>
|
||||
</ha-menu-item>
|
||||
|
||||
<ha-menu-item>
|
||||
<ha-svg-icon .path=${mdiStar} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Set to default</div>
|
||||
</ha-menu-item>
|
||||
<md-divider role="separator" tabindex="-1"></md-divider>
|
||||
<ha-menu-item class="warning">
|
||||
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
|
||||
<div slot="headline">Delete</div>
|
||||
</ha-menu-item>
|
||||
</ha-menu>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -348,21 +379,23 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._dashboards = await fetchDashboards(this.hass);
|
||||
}
|
||||
|
||||
private _navigate(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
navigate(`/${(ev.target as any).urlPath}`);
|
||||
private _navigate(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
navigate(`/${urlPath}`);
|
||||
}
|
||||
|
||||
private _editDashboard(ev: CustomEvent) {
|
||||
const urlPath = (ev.detail as RowClickedEvent).id;
|
||||
private _editDashboard = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const dashboard = ev.currentTarget.parentElement.anchorElement.automation;
|
||||
|
||||
const urlPath = (ev.currentTarget as any).urlPath;
|
||||
|
||||
if (urlPath === "energy") {
|
||||
navigate("/config/energy");
|
||||
return;
|
||||
}
|
||||
const dashboard = this._dashboards.find((res) => res.url_path === urlPath);
|
||||
this._openDetailDialog(dashboard, urlPath);
|
||||
}
|
||||
};
|
||||
|
||||
private async _addDashboard() {
|
||||
showNewDashboardDialog(this, {
|
||||
@@ -453,6 +486,10 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private _handleSortingChanged(ev: CustomEvent) {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -52,6 +52,14 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
|
||||
@state() private _resources: LovelaceResource[] = [];
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "lovelace-resources-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
key: "lovelace-resources-table-sort",
|
||||
state: false,
|
||||
@@ -138,6 +146,8 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
)}
|
||||
.initialSorting=${this._activeSorting}
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editResource}
|
||||
hasFab
|
||||
clickable
|
||||
@@ -252,6 +262,10 @@ export class HaConfigLovelaceRescources extends LitElement {
|
||||
this._activeSorting = ev.detail;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -8,6 +8,7 @@ import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-picture-upload";
|
||||
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-textfield";
|
||||
import { adminChangeUsername } from "../../../data/auth";
|
||||
import { PersonMutableParams } from "../../../data/person";
|
||||
import {
|
||||
deleteUser,
|
||||
@@ -19,10 +20,11 @@ import {
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../../types";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { showAddUserDialog } from "../users/show-dialog-add-user";
|
||||
import { showAdminChangePasswordDialog } from "../users/show-dialog-admin-change-password";
|
||||
@@ -136,9 +138,9 @@ class DialogPersonDetail extends LitElement {
|
||||
></ha-picture-upload>
|
||||
|
||||
<ha-formfield
|
||||
.label=${this.hass!.localize(
|
||||
.label=${`${this.hass!.localize(
|
||||
"ui.panel.config.person.detail.allow_login"
|
||||
)}
|
||||
)}${this._user ? ` (${this._user.username})` : ""}`}
|
||||
>
|
||||
<ha-switch
|
||||
@change=${this._allowLoginChanged}
|
||||
@@ -244,13 +246,21 @@ class DialogPersonDetail extends LitElement {
|
||||
</mwc-button>
|
||||
${this._user && this.hass.user?.is_owner
|
||||
? html`<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._changePassword}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
slot="secondaryAction"
|
||||
@click=${this._changeUsername}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this._changePassword}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
`
|
||||
: nothing}
|
||||
@@ -292,11 +302,14 @@ class DialogPersonDetail extends LitElement {
|
||||
userAddedCallback: async (user?: User) => {
|
||||
if (user) {
|
||||
target.checked = true;
|
||||
if (this._params!.entry) {
|
||||
await this._params!.updateEntry({ user_id: user.id });
|
||||
}
|
||||
this._params?.refreshUsers();
|
||||
this._user = user;
|
||||
this._userId = user.id;
|
||||
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
|
||||
this._localOnly = user.local_only;
|
||||
this._params?.refreshUsers();
|
||||
}
|
||||
},
|
||||
name: this._name,
|
||||
@@ -320,6 +333,9 @@ class DialogPersonDetail extends LitElement {
|
||||
await deleteUser(this.hass, this._userId);
|
||||
this._params?.refreshUsers();
|
||||
this._userId = undefined;
|
||||
this._user = undefined;
|
||||
this._isAdmin = undefined;
|
||||
this._localOnly = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,6 +365,53 @@ class DialogPersonDetail extends LitElement {
|
||||
showAdminChangePasswordDialog(this, { userId: this._user.id });
|
||||
}
|
||||
|
||||
private async _changeUsername() {
|
||||
if (!this._user) {
|
||||
return;
|
||||
}
|
||||
const credential = this._user.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
);
|
||||
if (!credential) {
|
||||
showAlertDialog(this, {
|
||||
title: "No Home Assistant credentials found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newUsername = await showPromptDialog(this, {
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.new_username"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.change"
|
||||
),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.caption"
|
||||
),
|
||||
defaultValue: this._user.username!,
|
||||
});
|
||||
if (newUsername) {
|
||||
try {
|
||||
await adminChangeUsername(this.hass, this._user.id, newUsername);
|
||||
this._params?.refreshUsers();
|
||||
this._user = { ...this._user, username: newUsername };
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.username_changed"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateEntry() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
|
@@ -31,8 +31,6 @@ class DialogIntegrationStartup extends LitElement {
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
hideActions
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
|
@@ -143,8 +143,6 @@ class DialogSystemInformation extends LitElement {
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.panel.config.repairs.system_information")
|
||||
|
@@ -137,6 +137,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _filteredScenes?: string[] | null;
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "scene-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "scene-table-filters-full",
|
||||
@@ -543,6 +551,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.scene.picker.no_scenes"
|
||||
)}
|
||||
@clear-filter=${this._clearFilter}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
hasFab
|
||||
clickable
|
||||
@row-click=${this._handleRowClicked}
|
||||
@@ -1141,6 +1151,10 @@ ${rejected
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
@@ -141,6 +141,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _filteredScripts?: string[] | null;
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "script-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "script-table-filters-full",
|
||||
@@ -558,6 +566,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
"ui.panel.config.script.picker.no_scripts"
|
||||
)}
|
||||
@clear-filter=${this._clearFilter}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
hasFab
|
||||
clickable
|
||||
class=${this.narrow ? "narrow" : ""}
|
||||
@@ -800,6 +810,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
private _filterExpanded(ev) {
|
||||
if (ev.detail.expanded) {
|
||||
this._expandedFilter = ev.target.localName;
|
||||
|
@@ -35,6 +35,7 @@ import { documentationUrl } from "../../../util/documentation-url";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import { showTagDetailDialog } from "./show-dialog-tag-detail";
|
||||
import "./tag-image";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
|
||||
export interface TagRowData extends Tag {
|
||||
display_name: string;
|
||||
@@ -57,6 +58,14 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
return this.hass.auth.external?.config.canWriteTag;
|
||||
}
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "tags-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne(
|
||||
(narrow: boolean, _language, localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<TagRowData> = {
|
||||
@@ -189,6 +198,8 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
.data=${this._data(this._tags)}
|
||||
.noDataText=${this.hass.localize("ui.panel.config.tag.no_tags")}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
hasFab
|
||||
>
|
||||
<ha-icon-button
|
||||
@@ -323,6 +334,10 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -10,12 +10,16 @@ import "../../../components/ha-label";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-switch";
|
||||
import "../../../components/ha-textfield";
|
||||
import { adminChangeUsername } from "../../../data/auth";
|
||||
import {
|
||||
computeUserBadges,
|
||||
SYSTEM_GROUP_ID_ADMIN,
|
||||
SYSTEM_GROUP_ID_USER,
|
||||
} from "../../../data/user";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { showAdminChangePasswordDialog } from "./show-dialog-admin-change-password";
|
||||
@@ -172,11 +176,15 @@ class DialogUserDetail extends LitElement {
|
||||
`
|
||||
: ""}
|
||||
${!user.system_generated && this.hass.user?.is_owner
|
||||
? html`<mwc-button @click=${this._changePassword}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
? html`<mwc-button @click=${this._changeUsername}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_username"
|
||||
)} </mwc-button
|
||||
><mwc-button @click=${this._changePassword}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.users.editor.change_password"
|
||||
)}
|
||||
</mwc-button>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
@@ -250,6 +258,56 @@ class DialogUserDetail extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _changeUsername() {
|
||||
const credential = this._params?.entry.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
);
|
||||
if (!credential) {
|
||||
showAlertDialog(this, {
|
||||
title: "No Home Assistant credentials found.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newUsername = await showPromptDialog(this, {
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.new_username"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.change"
|
||||
),
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.caption"
|
||||
),
|
||||
defaultValue: this._params!.entry.username!,
|
||||
});
|
||||
if (newUsername) {
|
||||
try {
|
||||
await adminChangeUsername(
|
||||
this.hass,
|
||||
this._params!.entry.id,
|
||||
newUsername
|
||||
);
|
||||
this._params = {
|
||||
...this._params!,
|
||||
entry: { ...this._params!.entry, username: newUsername },
|
||||
};
|
||||
this._params.replaceEntry(this._params.entry);
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.username_changed"
|
||||
),
|
||||
});
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.users.change_username.failed"
|
||||
),
|
||||
text: err.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _changePassword() {
|
||||
const credential = this._params?.entry.credentials.find(
|
||||
(cred) => cred.type === "homeassistant"
|
||||
|
@@ -46,6 +46,14 @@ export class HaConfigUsers extends LitElement {
|
||||
@storage({ key: "users-table-grouping", state: false, subscribe: false })
|
||||
private _activeGrouping?: string;
|
||||
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "users-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@storage({
|
||||
key: "users-table-collapsed",
|
||||
state: false,
|
||||
@@ -174,7 +182,7 @@ export class HaConfigUsers extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
backPath="/config"
|
||||
back-path="/config"
|
||||
.tabs=${configSections.persons}
|
||||
.columns=${this._columns(this.narrow, this.hass.localize)}
|
||||
.data=${this._userData(this._users, this.hass.localize)}
|
||||
@@ -184,6 +192,8 @@ export class HaConfigUsers extends LitElement {
|
||||
@sorting-changed=${this._handleSortingChanged}
|
||||
@grouping-changed=${this._handleGroupingChanged}
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editUser}
|
||||
hasFab
|
||||
clickable
|
||||
@@ -227,6 +237,11 @@ export class HaConfigUsers extends LitElement {
|
||||
|
||||
showUserDetailDialog(this, {
|
||||
entry,
|
||||
replaceEntry: (newEntry: User) => {
|
||||
this._users = this._users!.map((ent) =>
|
||||
ent.id === newEntry.id ? newEntry : ent
|
||||
);
|
||||
},
|
||||
updateEntry: async (values) => {
|
||||
const updated = await updateUser(this.hass!, entry!.id, values);
|
||||
this._users = this._users!.map((ent) =>
|
||||
@@ -283,6 +298,10 @@ export class HaConfigUsers extends LitElement {
|
||||
private _handleCollapseChanged(ev: CustomEvent) {
|
||||
this._activeCollapsed = ev.detail.value;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -4,6 +4,7 @@ import { UpdateUserParams, User } from "../../../data/user";
|
||||
export interface UserDetailDialogParams {
|
||||
entry: User;
|
||||
updateEntry: (updates: Partial<UpdateUserParams>) => Promise<unknown>;
|
||||
replaceEntry: (entry: User) => void;
|
||||
removeEntry: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
|
@@ -80,7 +80,13 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
|
||||
@state() private _extEntities?: Record<string, ExtEntityRegistryEntry>;
|
||||
|
||||
@state() private _filter: string = history.state?.filter || "";
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "voice-expose-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -634,7 +640,6 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
history.replaceState({ filter: this._filter }, "");
|
||||
}
|
||||
|
||||
private _handleSelectionChanged(
|
||||
|
@@ -14,7 +14,7 @@ const SCHEMA = [
|
||||
{
|
||||
name: "location",
|
||||
required: true,
|
||||
selector: { location: { radius: true, radius_readonly: true } },
|
||||
selector: { location: { radius: true } },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -35,6 +35,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
this._data = {
|
||||
latitude: this.hass.config.latitude,
|
||||
longitude: this.hass.config.longitude,
|
||||
radius: this.hass.config.radius,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,11 +74,6 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
.computeLabel=${this._computeLabel}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<p>
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.zone.detail.no_edit_home_zone_radius"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@@ -95,7 +91,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
location: {
|
||||
latitude: data.latitude,
|
||||
longitude: data.longitude,
|
||||
radius: this.hass.states["zone.home"]?.attributes?.radius || 100,
|
||||
radius: data.radius || 100,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -104,6 +100,7 @@ class DialogHomeZoneDetail extends LitElement {
|
||||
const value = { ...ev.detail.value };
|
||||
value.latitude = value.location.latitude;
|
||||
value.longitude = value.location.longitude;
|
||||
value.radius = value.location.radius;
|
||||
delete value.location;
|
||||
this._data = value;
|
||||
}
|
||||
|
@@ -101,7 +101,8 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
: zoneRadiusColor,
|
||||
location_editable:
|
||||
entityState.entity_id === "zone.home" && this._canEditCore,
|
||||
radius_editable: false,
|
||||
radius_editable:
|
||||
entityState.entity_id === "zone.home" && this._canEditCore,
|
||||
})
|
||||
);
|
||||
const storageLocations: MarkerLocation[] = storageItems.map((zone) => ({
|
||||
@@ -381,8 +382,14 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _radiusUpdated(ev: CustomEvent) {
|
||||
private async _radiusUpdated(ev: CustomEvent) {
|
||||
this._activeEntry = ev.detail.id;
|
||||
if (ev.detail.id === "zone.home" && this._canEditCore) {
|
||||
await saveCoreConfig(this.hass, {
|
||||
radius: Math.round(ev.detail.radius),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const entry = this._storageItems!.find((item) => item.id === ev.detail.id);
|
||||
if (!entry) {
|
||||
return;
|
||||
@@ -478,6 +485,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
await saveCoreConfig(this.hass, {
|
||||
latitude: values.latitude,
|
||||
longitude: values.longitude,
|
||||
radius: values.radius,
|
||||
});
|
||||
this._zoomZone("zone.home");
|
||||
}
|
||||
|
@@ -282,6 +282,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, [issue.data.statistic_id]);
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
@@ -314,7 +315,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
break;
|
||||
case "entity_no_longer_recorded":
|
||||
showAlertDialog(this, {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.title"
|
||||
),
|
||||
@@ -335,7 +336,17 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_3_link"
|
||||
)}</a
|
||||
>`,
|
||||
><br /><br />
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.statistics.fix_issue.entity_no_longer_recorded.info_text_4"
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, [issue.data.statistic_id]);
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
this._validateStatistics();
|
||||
},
|
||||
});
|
||||
break;
|
||||
case "unsupported_state_class":
|
||||
@@ -381,6 +392,7 @@ class HaPanelDevStatistics extends SubscribeMixin(LitElement) {
|
||||
{ statistic_id: issue.data.statistic_id }
|
||||
)}`,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
confirm: async () => {
|
||||
await clearStatistics(this.hass, [issue.data.statistic_id]);
|
||||
this._deletedStatistics.add(issue.data.statistic_id);
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
} from "@mdi/js";
|
||||
import { ActionDetail } from "@material/mwc-list";
|
||||
import { differenceInHours } from "date-fns";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
HassServiceTarget,
|
||||
UnsubscribeFunc,
|
||||
@@ -46,11 +45,7 @@ import {
|
||||
computeHistory,
|
||||
subscribeHistory,
|
||||
} from "../../data/history";
|
||||
import {
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
getRecordedExcludedEntities,
|
||||
} from "../../data/recorder";
|
||||
import { Statistics, fetchStatistics } from "../../data/recorder";
|
||||
import {
|
||||
expandAreaTarget,
|
||||
expandDeviceTarget,
|
||||
@@ -100,8 +95,6 @@ class HaPanelHistory extends LitElement {
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
private _excludedEntities?: string[];
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
@@ -192,7 +185,6 @@ class HaPanelHistory extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this._targetPickerValue}
|
||||
.disabled=${this._isLoading}
|
||||
.entityFilter=${this._entityFilter}
|
||||
addOnTop
|
||||
@value-changed=${this._targetsChanged}
|
||||
></ha-target-picker>
|
||||
@@ -219,10 +211,6 @@ class HaPanelHistory extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _entityFilter = (entity: HassEntity): boolean =>
|
||||
!this._excludedEntities ||
|
||||
!this._excludedEntities.includes(entity.entity_id);
|
||||
|
||||
private mergeHistoryResults(
|
||||
ltsResult: HistoryResult,
|
||||
historyResult: HistoryResult
|
||||
@@ -374,17 +362,8 @@ class HaPanelHistory extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _getRecordedExcludedEntities() {
|
||||
const { recorded_ids: _recordedIds, excluded_ids: excludedIds } =
|
||||
await getRecordedExcludedEntities(this.hass);
|
||||
this._excludedEntities = excludedIds;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this._getRecordedExcludedEntities();
|
||||
|
||||
const searchParams = extractSearchParamsObject();
|
||||
if (searchParams.back === "1" && history.length > 1) {
|
||||
this._showBack = true;
|
||||
|
@@ -1,7 +1,7 @@
|
||||
export const filterModes = (
|
||||
supportedModes: string[] | undefined,
|
||||
selectedModes: string[] | undefined
|
||||
): string[] =>
|
||||
export const filterModes = <T extends string = string>(
|
||||
supportedModes: T[] | undefined,
|
||||
selectedModes: T[] | undefined
|
||||
): T[] =>
|
||||
selectedModes
|
||||
? selectedModes.filter((mode) => (supportedModes || []).includes(mode))
|
||||
: supportedModes || [];
|
||||
|
@@ -6,7 +6,6 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-select";
|
||||
@@ -70,37 +69,18 @@ class HuiAlarmModeCardFeature
|
||||
}
|
||||
}
|
||||
|
||||
private _modes = memoizeOne(
|
||||
(
|
||||
stateObj: AlarmControlPanelEntity,
|
||||
selectedModes: AlarmMode[] | undefined
|
||||
) => {
|
||||
if (!selectedModes) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return (Object.keys(ALARM_MODES) as AlarmMode[]).filter((mode) => {
|
||||
const feature = ALARM_MODES[mode].feature;
|
||||
return (
|
||||
(!feature || supportsFeature(stateObj, feature)) &&
|
||||
selectedModes.includes(mode)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _getCurrentMode(stateObj: AlarmControlPanelEntity) {
|
||||
return this._modes(stateObj, this._config?.modes).find(
|
||||
(mode) => mode === stateObj.state
|
||||
);
|
||||
}
|
||||
private _getCurrentMode = memoizeOne((stateObj: AlarmControlPanelEntity) => {
|
||||
const supportedModes = supportedAlarmModes(stateObj);
|
||||
return supportedModes.find((mode) => mode === stateObj.state);
|
||||
});
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
if (!this.stateObj) return;
|
||||
const mode = (ev.detail as any).value as AlarmMode;
|
||||
|
||||
if (mode === this.stateObj!.state) return;
|
||||
if (mode === this.stateObj.state) return;
|
||||
|
||||
const oldMode = this._getCurrentMode(this.stateObj!);
|
||||
const oldMode = this._getCurrentMode(this.stateObj);
|
||||
this._currentMode = mode;
|
||||
|
||||
try {
|
||||
@@ -153,6 +133,7 @@ class HuiAlarmModeCardFeature
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<ha-control-select
|
||||
|
@@ -24,7 +24,7 @@ import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyGridGaugeCardConfig } from "../types";
|
||||
import type { EnergyGridNeutralityGaugeCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
|
||||
const LEVELS: LevelDefinition[] = [
|
||||
@@ -39,7 +39,7 @@ class HuiEnergyGridGaugeCard
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: EnergyGridGaugeCardConfig;
|
||||
@state() private _config?: EnergyGridNeutralityGaugeCardConfig;
|
||||
|
||||
@state() private _data?: EnergyData;
|
||||
|
||||
@@ -59,7 +59,7 @@ class HuiEnergyGridGaugeCard
|
||||
return 4;
|
||||
}
|
||||
|
||||
public setConfig(config: EnergyGridGaugeCardConfig): void {
|
||||
public setConfig(config: EnergyGridNeutralityGaugeCardConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { PropertyValueMap, PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { MediaQueriesListener } from "../../../common/dom/media_query";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
@@ -10,15 +11,41 @@ import {
|
||||
checkConditionsMet,
|
||||
} from "../common/validate-condition";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import type { Lovelace, LovelaceCard, LovelaceLayoutOptions } from "../types";
|
||||
import { createErrorCardConfig } from "../create-element/create-element-base";
|
||||
import type { LovelaceCard, LovelaceLayoutOptions } from "../types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"card-visibility-changed": { value: boolean };
|
||||
"card-updated": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("hui-card")
|
||||
export class HuiCard extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() public _config?: LovelaceCardConfig;
|
||||
@property({ type: Boolean }) public isPanel = false;
|
||||
|
||||
set config(config: LovelaceCardConfig | undefined) {
|
||||
if (!config) return;
|
||||
if (config.type !== this._config?.type) {
|
||||
this._buildElement(config);
|
||||
} else if (config !== this.config) {
|
||||
this._element?.setConfig(config);
|
||||
fireEvent(this, "card-updated");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
public get config() {
|
||||
return this._config;
|
||||
}
|
||||
|
||||
private _config?: LovelaceCardConfig;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
|
||||
@@ -36,7 +63,7 @@ export class HuiCard extends ReactiveElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._listenMediaQueries();
|
||||
this._updateElement();
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
public getCardSize(): number | Promise<number> {
|
||||
@@ -48,7 +75,7 @@ export class HuiCard extends ReactiveElement {
|
||||
}
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
const configOptions = this._config?.layout_options ?? {};
|
||||
const configOptions = this.config?.layout_options ?? {};
|
||||
if (this._element) {
|
||||
const cardOptions = this._element.getLayoutOptions?.() ?? {};
|
||||
return {
|
||||
@@ -59,37 +86,84 @@ export class HuiCard extends ReactiveElement {
|
||||
return configOptions;
|
||||
}
|
||||
|
||||
public setConfig(config: LovelaceCardConfig): void {
|
||||
if (this._config === config) {
|
||||
return;
|
||||
}
|
||||
this._config = config;
|
||||
this._element = createCardElement(config);
|
||||
this._element.hass = this.hass;
|
||||
this._element.editMode = this.lovelace.editMode;
|
||||
public getElementLayoutOptions(): LovelaceLayoutOptions {
|
||||
return this._element?.getLayoutOptions?.() ?? {};
|
||||
}
|
||||
|
||||
private _createElement(config: LovelaceCardConfig) {
|
||||
const element = createCardElement(config);
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(element as any).editMode = this.preview;
|
||||
// Update element when the visibility of the card changes (e.g. conditional card or filter card)
|
||||
element.addEventListener("card-visibility-changed", (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._updateVisibility();
|
||||
});
|
||||
element.addEventListener(
|
||||
"ll-upgrade",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
this._buildElement(config);
|
||||
fireEvent(this, "card-updated");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _buildElement(config: LovelaceCardConfig) {
|
||||
this._element = this._createElement(config);
|
||||
|
||||
while (this.lastChild) {
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
this.appendChild(this._element!);
|
||||
this._updateVisibility();
|
||||
}
|
||||
|
||||
protected update(changedProperties: PropertyValues<typeof this>) {
|
||||
super.update(changedProperties);
|
||||
protected update(changedProps: PropertyValues<typeof this>) {
|
||||
super.update(changedProps);
|
||||
|
||||
if (this._element) {
|
||||
if (changedProperties.has("hass")) {
|
||||
this._element.hass = this.hass;
|
||||
if (changedProps.has("hass")) {
|
||||
try {
|
||||
this._element.hass = this.hass;
|
||||
} catch (e: any) {
|
||||
this._buildElement(createErrorCardConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("lovelace")) {
|
||||
this._element.editMode = this.lovelace.editMode;
|
||||
if (changedProps.has("preview")) {
|
||||
try {
|
||||
this._element.preview = this.preview;
|
||||
// For backwards compatibility
|
||||
(this._element as any).editMode = this.preview;
|
||||
} catch (e: any) {
|
||||
this._buildElement(createErrorCardConfig(e.message, null));
|
||||
}
|
||||
}
|
||||
if (changedProperties.has("hass") || changedProperties.has("lovelace")) {
|
||||
this._updateElement();
|
||||
if (changedProps.has("isPanel")) {
|
||||
this._element.isPanel = this.isPanel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(
|
||||
changedProps: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
||||
): void {
|
||||
if (changedProps.has("hass") || changedProps.has("preview")) {
|
||||
this._updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private _clearMediaQueries() {
|
||||
this._listeners.forEach((unsub) => unsub());
|
||||
this._listeners = [];
|
||||
@@ -97,35 +171,50 @@ export class HuiCard extends ReactiveElement {
|
||||
|
||||
private _listenMediaQueries() {
|
||||
this._clearMediaQueries();
|
||||
if (!this._config?.visibility) {
|
||||
if (!this.config?.visibility) {
|
||||
return;
|
||||
}
|
||||
const conditions = this._config.visibility;
|
||||
const conditions = this.config.visibility;
|
||||
const hasOnlyMediaQuery =
|
||||
conditions.length === 1 &&
|
||||
conditions[0].condition === "screen" &&
|
||||
!!conditions[0].media_query;
|
||||
|
||||
this._listeners = attachConditionMediaQueriesListeners(
|
||||
this._config.visibility,
|
||||
this.config.visibility,
|
||||
(matches) => {
|
||||
this._updateElement(hasOnlyMediaQuery && matches);
|
||||
this._updateVisibility(hasOnlyMediaQuery && matches);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _updateElement(forceVisible?: boolean) {
|
||||
if (!this._element) {
|
||||
private _updateVisibility(forceVisible?: boolean) {
|
||||
if (!this._element || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._element.hidden) {
|
||||
this._setElementVisibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const visible =
|
||||
forceVisible ||
|
||||
this.lovelace.editMode ||
|
||||
!this._config?.visibility ||
|
||||
checkConditionsMet(this._config.visibility, this.hass);
|
||||
this.preview ||
|
||||
!this.config?.visibility ||
|
||||
checkConditionsMet(this.config.visibility, this.hass);
|
||||
this._setElementVisibility(visible);
|
||||
}
|
||||
|
||||
private _setElementVisibility(visible: boolean) {
|
||||
if (!this._element) return;
|
||||
|
||||
if (this.hidden !== !visible) {
|
||||
this.style.setProperty("display", visible ? "" : "none");
|
||||
this.toggleAttribute("hidden", !visible);
|
||||
fireEvent(this, "card-visibility-changed", { value: visible });
|
||||
}
|
||||
|
||||
this.style.setProperty("display", visible ? "" : "none");
|
||||
this.toggleAttribute("hidden", !visible);
|
||||
if (!visible && this._element.parentElement) {
|
||||
this.removeChild(this._element);
|
||||
} else if (visible && !this._element.parentElement) {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { customElement } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
import { HuiConditionalBase } from "../components/hui-conditional-base";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { ConditionalCardConfig } from "./types";
|
||||
|
||||
@@ -37,25 +37,19 @@ class HuiConditionalCard extends HuiConditionalBase implements LovelaceCard {
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(cardConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
return element;
|
||||
}
|
||||
|
||||
private _rebuildCard(config: LovelaceCardConfig): void {
|
||||
this._element = this._createCardElement(config);
|
||||
if (this.lastChild) {
|
||||
this.replaceChild(this._element, this.lastChild);
|
||||
protected setVisibility(conditionMet: boolean): void {
|
||||
const visible = this.preview || conditionMet;
|
||||
const previouslyHidden = this.hidden;
|
||||
super.setVisibility(conditionMet);
|
||||
if (previouslyHidden !== this.hidden) {
|
||||
fireEvent(this, "card-visibility-changed", { value: visible });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { PropertyValues, ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { computeCardSize } from "../common/compute-card-size";
|
||||
@@ -11,9 +12,9 @@ import {
|
||||
checkConditionsMet,
|
||||
extractConditionEntityIds,
|
||||
} from "../common/validate-condition";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||
import { LovelaceCard } from "../types";
|
||||
import { HuiCard } from "./hui-card";
|
||||
import { EntityFilterCardConfig } from "./types";
|
||||
|
||||
@customElement("hui-entity-filter-card")
|
||||
@@ -54,11 +55,11 @@ export class HuiEntityFilterCard
|
||||
|
||||
@property({ type: Boolean }) public isPanel = false;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() private _config?: EntityFilterCardConfig;
|
||||
|
||||
private _element?: LovelaceCard;
|
||||
private _element?: HuiCard;
|
||||
|
||||
private _configEntities?: EntityFilterEntityConfig[];
|
||||
|
||||
@@ -116,7 +117,7 @@ export class HuiEntityFilterCard
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (this._element) {
|
||||
this._element.hass = this.hass;
|
||||
this._element.editMode = this.editMode;
|
||||
this._element.preview = this.preview;
|
||||
this._element.isPanel = this.isPanel;
|
||||
}
|
||||
|
||||
@@ -163,18 +164,21 @@ export class HuiEntityFilterCard
|
||||
});
|
||||
|
||||
if (entitiesList.length === 0 && this._config.show_empty === false) {
|
||||
this.style.display = "none";
|
||||
this.toggleAttribute("hidden", true);
|
||||
if (!this.hidden) {
|
||||
this.style.display = "none";
|
||||
this.toggleAttribute("hidden", true);
|
||||
fireEvent(this, "card-visibility-changed", { value: false });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.lastChild) {
|
||||
this._element.setConfig({
|
||||
this._element.config = {
|
||||
...this._baseCardConfig!,
|
||||
entities: entitiesList,
|
||||
});
|
||||
};
|
||||
this._oldEntities = entitiesList;
|
||||
} else if (this._element.tagName !== "HUI-ERROR-CARD") {
|
||||
} else {
|
||||
const isSame =
|
||||
this._oldEntities &&
|
||||
entitiesList.length === this._oldEntities.length &&
|
||||
@@ -182,10 +186,10 @@ export class HuiEntityFilterCard
|
||||
|
||||
if (!isSame) {
|
||||
this._oldEntities = entitiesList;
|
||||
this._element.setConfig({
|
||||
this._element.config = {
|
||||
...this._baseCardConfig!,
|
||||
entities: entitiesList,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +198,11 @@ export class HuiEntityFilterCard
|
||||
this.appendChild(this._element);
|
||||
}
|
||||
|
||||
this.style.display = "block";
|
||||
this.toggleAttribute("hidden", false);
|
||||
if (this.hidden) {
|
||||
this.style.display = "block";
|
||||
this.toggleAttribute("hidden", false);
|
||||
fireEvent(this, "card-visibility-changed", { value: true });
|
||||
}
|
||||
}
|
||||
|
||||
private _haveEntitiesChanged(oldHass: HomeAssistant | null): boolean {
|
||||
@@ -238,33 +245,12 @@ export class HuiEntityFilterCard
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.isPanel = this.isPanel;
|
||||
element.editMode = this.editMode;
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
return element;
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: LovelaceCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
if (cardElToReplace.parentElement) {
|
||||
cardElToReplace.parentElement!.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
this._element = newCardEl;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { dump } from "js-yaml";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceCard } from "../types";
|
||||
@@ -10,6 +10,8 @@ import { ErrorCardConfig } from "./types";
|
||||
export class HuiErrorCard extends LitElement implements LovelaceCard {
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public preview = false;
|
||||
|
||||
@state() private _config?: ErrorCardConfig;
|
||||
|
||||
public getCardSize(): number {
|
||||
|
@@ -38,7 +38,7 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() private _config?: MarkdownCardConfig;
|
||||
|
||||
@@ -163,12 +163,12 @@ export class HuiMarkdownCard extends LitElement implements LovelaceCard {
|
||||
user: this.hass.user!.name,
|
||||
},
|
||||
strict: true,
|
||||
report_errors: this.editMode,
|
||||
report_errors: this.preview,
|
||||
}
|
||||
);
|
||||
await this._unsubRenderTemplate;
|
||||
} catch (e: any) {
|
||||
if (this.editMode) {
|
||||
if (this.preview) {
|
||||
this._error = e.message;
|
||||
this._errorLevel = undefined;
|
||||
}
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { createCardElement } from "../create-element/create-card-element";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import "./hui-card";
|
||||
import type { HuiCard } from "./hui-card";
|
||||
import { StackCardConfig } from "./types";
|
||||
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||
|
||||
export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
extends LitElement
|
||||
@@ -30,9 +23,9 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public editMode = false;
|
||||
@property({ type: Boolean }) public preview = false;
|
||||
|
||||
@state() protected _cards?: LovelaceCard[];
|
||||
@state() protected _cards?: HuiCard[];
|
||||
|
||||
@state() protected _config?: T;
|
||||
|
||||
@@ -49,30 +42,36 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
}
|
||||
this._config = config;
|
||||
this._cards = config.cards.map((card) => {
|
||||
const element = this._createCardElement(card) as LovelaceCard;
|
||||
const element = this._createCardElement(card);
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (
|
||||
!this._cards ||
|
||||
(!changedProps.has("hass") && !changedProps.has("editMode"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
protected update(changedProperties) {
|
||||
super.update(changedProperties);
|
||||
|
||||
for (const element of this._cards) {
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
if (this._cards) {
|
||||
if (changedProperties.has("hass")) {
|
||||
this._cards.forEach((card) => {
|
||||
card.hass = this.hass;
|
||||
});
|
||||
}
|
||||
if (this.editMode !== undefined) {
|
||||
element.editMode = this.editMode;
|
||||
if (changedProperties.has("editMode")) {
|
||||
this._cards.forEach((card) => {
|
||||
card.preview = this.preview;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = document.createElement("hui-card");
|
||||
element.hass = this.hass;
|
||||
element.preview = this.preview;
|
||||
element.config = cardConfig;
|
||||
return element;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config || !this._cards) {
|
||||
return nothing;
|
||||
@@ -110,34 +109,4 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig>
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _createCardElement(cardConfig: LovelaceCardConfig) {
|
||||
const element = createCardElement(cardConfig) as LovelaceCard;
|
||||
if (this.hass) {
|
||||
element.hass = this.hass;
|
||||
}
|
||||
element.addEventListener(
|
||||
"ll-rebuild",
|
||||
(ev) => {
|
||||
ev.stopPropagation();
|
||||
this._rebuildCard(element, cardConfig);
|
||||
fireEvent(this, "ll-rebuild");
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _rebuildCard(
|
||||
cardElToReplace: LovelaceCard,
|
||||
config: LovelaceCardConfig
|
||||
): void {
|
||||
const newCardEl = this._createCardElement(config);
|
||||
if (cardElToReplace.parentElement) {
|
||||
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
|
||||
}
|
||||
this._cards = this._cards!.map((curCardEl) =>
|
||||
curCardEl === cardElToReplace ? newCardEl : curCardEl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-checkbox";
|
||||
@@ -104,6 +105,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubItems?.then((unsub) => unsub());
|
||||
this._unsubItems = undefined;
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
@@ -242,7 +244,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.MOVE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu>
|
||||
? html`<ha-button-menu @closed=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
@@ -286,7 +288,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
|
||||
${this.todoListSupportsFeature(
|
||||
TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
)
|
||||
? html`<ha-button-menu>
|
||||
? html`<ha-button-menu @closed=${stopPropagation}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
|
@@ -118,85 +118,74 @@ export interface EnergyCardBaseConfig extends LovelaceCardConfig {
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySummaryCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySummaryCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-summary";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDistributionCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-distribution";
|
||||
title?: string;
|
||||
link_dashboard?: boolean;
|
||||
collection_key?: string;
|
||||
}
|
||||
export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyUsageGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-usage-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySolarGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySolarGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-solar-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyGasGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyGasGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-gas-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyWaterGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyWaterGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-water-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDevicesGraphCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-devices-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
max_devices?: number;
|
||||
}
|
||||
|
||||
export interface EnergyDevicesDetailGraphCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyDevicesDetailGraphCardConfig
|
||||
extends EnergyCardBaseConfig {
|
||||
type: "energy-devices-detail-graph";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
max_devices?: number;
|
||||
}
|
||||
|
||||
export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySourcesTableCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-sources-table";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergySolarGaugeCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-solar-consumed-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergySelfSufficiencyGaugeCardConfig
|
||||
extends LovelaceCardConfig {
|
||||
extends EnergyCardBaseConfig {
|
||||
type: "energy-self-sufficiency-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig {
|
||||
type: "energy-grid-result-gauge";
|
||||
export interface EnergyGridNeutralityGaugeCardConfig
|
||||
extends EnergyCardBaseConfig {
|
||||
type: "energy-grid-neutrality-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig {
|
||||
export interface EnergyCarbonGaugeCardConfig extends EnergyCardBaseConfig {
|
||||
type: "energy-carbon-consumed-gauge";
|
||||
title?: string;
|
||||
collection_key?: string;
|
||||
}
|
||||
|
||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { SENSOR_ENTITIES } from "../../../common/const";
|
||||
import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
@@ -35,17 +35,16 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types";
|
||||
const HIDE_DOMAIN = new Set([
|
||||
"automation",
|
||||
"configurator",
|
||||
"conversation",
|
||||
"device_tracker",
|
||||
"event",
|
||||
"geo_location",
|
||||
"notify",
|
||||
"persistent_notification",
|
||||
"script",
|
||||
"sun",
|
||||
"zone",
|
||||
"event",
|
||||
"tts",
|
||||
"stt",
|
||||
"todo",
|
||||
"zone",
|
||||
...ASSIST_ENTITIES,
|
||||
]);
|
||||
|
||||
const HIDE_PLATFORM = new Set(["mobile_app"]);
|
||||
|
@@ -353,7 +353,7 @@ export class HuiCardOptions extends LitElement {
|
||||
allowDashboardChange: true,
|
||||
header: this.hass!.localize("ui.panel.lovelace.editor.move_card.header"),
|
||||
viewSelectedCallback: async (urlPath, selectedDashConfig, viewIndex) => {
|
||||
const view = this.lovelace!.config.views[viewIndex];
|
||||
const view = selectedDashConfig.views[viewIndex];
|
||||
|
||||
if (!isStrategyView(view) && view.type === SECTION_VIEW_LAYOUT) {
|
||||
showAlertDialog(this, {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user