mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-02 13:00:27 +00:00
Compare commits
1 Commits
initial-fo
...
dashboard_
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1b44c7ce64 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -9,7 +9,7 @@ body:
|
||||
|
||||
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
|
||||
|
||||
**Please do not report issues for custom cards.**
|
||||
**Please not not report issues for custom cards.**
|
||||
|
||||
[fr]: https://github.com/home-assistant/frontend/discussions
|
||||
[releases]: https://github.com/home-assistant/home-assistant/releases
|
||||
|
2
.github/workflows/labeler.yaml
vendored
2
.github/workflows/labeler.yaml
vendored
@@ -10,6 +10,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply labels
|
||||
uses: actions/labeler@v5.0.0
|
||||
uses: actions/labeler@v4.3.0
|
||||
with:
|
||||
sync-labels: true
|
||||
|
3
.github/workflows/lock.yml
vendored
3
.github/workflows/lock.yml
vendored
@@ -9,10 +9,9 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v5.0.1
|
||||
- uses: dessant/lock-threads@v4.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-lock-reason: ""
|
||||
|
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.1
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
|
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@v9.0.0
|
||||
uses: actions/stale@v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
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.0.2.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.0.1.cjs
|
||||
|
@@ -1,56 +0,0 @@
|
||||
import defineProvider from "@babel/helper-define-polyfill-provider";
|
||||
|
||||
// List of polyfill keys with supported browser targets for the functionality
|
||||
const PolyfillSupport = {
|
||||
fetch: {
|
||||
android: 42,
|
||||
chrome: 42,
|
||||
edge: 14,
|
||||
firefox: 39,
|
||||
ios: 10.3,
|
||||
opera: 29,
|
||||
opera_mobile: 29,
|
||||
safari: 10.1,
|
||||
samsung: 4.0,
|
||||
},
|
||||
proxy: {
|
||||
android: 49,
|
||||
chrome: 49,
|
||||
edge: 12,
|
||||
firefox: 18,
|
||||
ios: 10.0,
|
||||
opera: 36,
|
||||
opera_mobile: 36,
|
||||
safari: 10.0,
|
||||
samsung: 5.0,
|
||||
},
|
||||
};
|
||||
|
||||
// Map of global variables and/or instance and static properties to the
|
||||
// corresponding polyfill key and actual module to import
|
||||
const polyfillMap = {
|
||||
global: {
|
||||
Proxy: { key: "proxy", module: "proxy-polyfill" },
|
||||
fetch: { key: "fetch", module: "unfetch/polyfill" },
|
||||
},
|
||||
instance: {},
|
||||
static: {},
|
||||
};
|
||||
|
||||
// Create plugin using the same factory as for CoreJS
|
||||
export default defineProvider(
|
||||
({ createMetaResolver, debug, shouldInjectPolyfill }) => {
|
||||
const resolvePolyfill = createMetaResolver(polyfillMap);
|
||||
return {
|
||||
name: "HA Custom",
|
||||
polyfills: PolyfillSupport,
|
||||
usageGlobal(meta, utils) {
|
||||
const polyfill = resolvePolyfill(meta);
|
||||
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
|
||||
debug(polyfill.desc.key);
|
||||
utils.injectGlobalImport(polyfill.desc.module);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
@@ -31,6 +31,8 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
|
||||
require.resolve(
|
||||
path.resolve(paths.polymer_dir, "src/resources/compatibility.ts")
|
||||
),
|
||||
// This polyfill is loaded in workers to support ES5, filter it out.
|
||||
latestBuild && require.resolve("proxy-polyfill/src/index.js"),
|
||||
// Icons in supervisor conflict with icons in HA so we don't load.
|
||||
isHassioBuild &&
|
||||
require.resolve(
|
||||
@@ -85,12 +87,14 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
setSpreadProperties: true,
|
||||
},
|
||||
browserslistEnv: latestBuild ? "modern" : "legacy",
|
||||
// Must be unambiguous because some dependencies are CommonJS only
|
||||
sourceType: "unambiguous",
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: latestBuild ? false : "usage",
|
||||
corejs: latestBuild ? false : "3.33",
|
||||
useBuiltIns: latestBuild ? false : "entry",
|
||||
corejs: latestBuild ? false : { version: "3.33", proposals: true },
|
||||
bugfixes: true,
|
||||
shippedProposals: true,
|
||||
},
|
||||
@@ -108,33 +112,21 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
ignoreModuleNotFound: true,
|
||||
},
|
||||
],
|
||||
[
|
||||
path.resolve(
|
||||
paths.polymer_dir,
|
||||
"build-scripts/babel-plugins/custom-polyfill-plugin.js"
|
||||
),
|
||||
{ method: "usage-global" },
|
||||
],
|
||||
// Minify template literals for production
|
||||
isProdBuild && [
|
||||
"template-html-minifier",
|
||||
{
|
||||
modules: {
|
||||
...Object.fromEntries(
|
||||
["lit", "lit-element", "lit-html"].map((m) => [
|
||||
m,
|
||||
[
|
||||
"html",
|
||||
{ name: "svg", encapsulation: "svg" },
|
||||
{ name: "css", encapsulation: "style" },
|
||||
],
|
||||
])
|
||||
),
|
||||
"@polymer/polymer/lib/utils/html-tag.js": ["html"],
|
||||
lit: [
|
||||
"html",
|
||||
{ name: "svg", encapsulation: "svg" },
|
||||
{ name: "css", encapsulation: "style" },
|
||||
],
|
||||
"@polymer/polymer/lib/utils/html-tag": ["html"],
|
||||
},
|
||||
strictCSS: true,
|
||||
htmlMinifier: module.exports.htmlMinifierOptions,
|
||||
failOnError: false, // we can turn this off in case of false positives
|
||||
failOnError: true, // we can turn this off in case of false positives
|
||||
},
|
||||
],
|
||||
// Import helpers and regenerator from runtime package
|
||||
@@ -151,18 +143,6 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
/node_modules[\\/]webpack[\\/]buildin/,
|
||||
],
|
||||
sourceMaps: !isTestBuild,
|
||||
overrides: [
|
||||
{
|
||||
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
|
||||
// Exclusions are needed in some cases where ES modules have no static imports or exports, such as polyfills
|
||||
sourceType: "unambiguous",
|
||||
include: /\/node_modules\//,
|
||||
exclude: [
|
||||
"element-internals-polyfill",
|
||||
"@?lit(?:-labs|-element|-html)?",
|
||||
].map((p) => new RegExp(`/node_modules/${p}/`)),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const nameSuffix = (latestBuild) => (latestBuild ? "-modern" : "-legacy");
|
||||
|
@@ -30,8 +30,8 @@ gulp.task(
|
||||
env.useWDS()
|
||||
? "wds-watch-app"
|
||||
: env.useRollup()
|
||||
? "rollup-watch-app"
|
||||
: "webpack-watch-app"
|
||||
? "rollup-watch-app"
|
||||
: "webpack-watch-app"
|
||||
)
|
||||
);
|
||||
|
||||
|
@@ -161,10 +161,6 @@ gulp.task("fetch-lokalise", async function () {
|
||||
})
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
@@ -51,8 +51,8 @@ const createWebpackConfig = ({
|
||||
devtool: isTestBuild
|
||||
? false
|
||||
: isProdBuild
|
||||
? "nosources-source-map"
|
||||
: "eval-cheap-module-source-map",
|
||||
? "nosources-source-map"
|
||||
: "eval-cheap-module-source-map",
|
||||
entry,
|
||||
node: false,
|
||||
module: {
|
||||
@@ -191,11 +191,11 @@ const createWebpackConfig = ({
|
||||
filename: ({ chunk }) =>
|
||||
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
|
||||
? "[name].js"
|
||||
: "[name].[contenthash].js",
|
||||
: "[name]-[contenthash].js",
|
||||
chunkFilename:
|
||||
isProdBuild && !isStatsBuild ? "[name].[contenthash].js" : "[name].js",
|
||||
isProdBuild && !isStatsBuild ? "[id]-[contenthash].js" : "[name].js",
|
||||
assetModuleFilename:
|
||||
isProdBuild && !isStatsBuild ? "[id].[contenthash][ext]" : "[id][ext]",
|
||||
isProdBuild && !isStatsBuild ? "[id]-[contenthash][ext]" : "[id][ext]",
|
||||
crossOriginLoading: "use-credentials",
|
||||
hashFunction: "xxhash64",
|
||||
hashDigest: "base64url",
|
||||
|
@@ -73,44 +73,44 @@ class HcCast extends LitElement {
|
||||
${error
|
||||
? html` <div class="card-content">${error}</div> `
|
||||
: !this.castManager.status
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<mwc-button raised @click=${this._handleLaunch}>
|
||||
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
|
||||
Start Casting
|
||||
</mwc-button>
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-path"
|
||||
.selected=${this.castManager.status.lovelacePath || ""}
|
||||
>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._handlePickView}
|
||||
data-path=${view.path || idx}
|
||||
>
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="item-icon"
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${view.title || view.path}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
`}
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<mwc-button raised @click=${this._handleLaunch}>
|
||||
<ha-svg-icon .path=${mdiCast}></ha-svg-icon>
|
||||
Start Casting
|
||||
</mwc-button>
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-path"
|
||||
.selected=${this.castManager.status.lovelacePath || ""}
|
||||
>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<paper-icon-item
|
||||
@click=${this._handlePickView}
|
||||
data-path=${view.path || idx}
|
||||
>
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="item-icon"
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
${view.title || view.path}
|
||||
</paper-icon-item>
|
||||
`
|
||||
)}
|
||||
</paper-listbox>
|
||||
`}
|
||||
<div class="card-actions">
|
||||
${this.castManager.status
|
||||
? html`
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCastConnected, mdiCast } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
Auth,
|
||||
Connection,
|
||||
@@ -23,7 +24,6 @@ import "../../../../src/components/ha-svg-icon";
|
||||
import "../../../../src/layouts/hass-loading-screen";
|
||||
import { registerServiceWorker } from "../../../../src/util/register-service-worker";
|
||||
import "./hc-layout";
|
||||
import "../../../../src/components/ha-textfield";
|
||||
|
||||
const seeFAQ = (qid) => html`
|
||||
See <a href="./faq.html${qid ? `#${qid}` : ""}">the FAQ</a> for more
|
||||
@@ -33,13 +33,13 @@ const translateErr = (err) =>
|
||||
err === ERR_CANNOT_CONNECT
|
||||
? "Unable to connect"
|
||||
: err === ERR_HASS_HOST_REQUIRED
|
||||
? "Please enter a Home Assistant URL."
|
||||
: err === ERR_INVALID_HTTPS_TO_HTTP
|
||||
? html`
|
||||
Cannot connect to Home Assistant instances over "http://".
|
||||
${seeFAQ("https")}
|
||||
`
|
||||
: `Unknown error (${err}).`;
|
||||
? "Please enter a Home Assistant URL."
|
||||
: err === ERR_INVALID_HTTPS_TO_HTTP
|
||||
? html`
|
||||
Cannot connect to Home Assistant instances over "http://".
|
||||
${seeFAQ("https")}
|
||||
`
|
||||
: `Unknown error (${err}).`;
|
||||
|
||||
const INTRO = html`
|
||||
<p>
|
||||
@@ -116,11 +116,13 @@ export class HcConnect extends LitElement {
|
||||
To get started, enter your Home Assistant URL and click authorize.
|
||||
If you want a preview instead, click the show demo button.
|
||||
</p>
|
||||
<ha-textfield
|
||||
label="Home Assistant URL"
|
||||
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
></ha-textfield>
|
||||
<p>
|
||||
<paper-input
|
||||
label="Home Assistant URL"
|
||||
placeholder="https://abcdefghijklmnop.ui.nabu.casa"
|
||||
@keydown=${this._handleInputKeyDown}
|
||||
></paper-input>
|
||||
</p>
|
||||
${this.error ? html` <p class="error">${this.error}</p> ` : ""}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -194,7 +196,7 @@ export class HcConnect extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleConnect() {
|
||||
const inputEl = this.shadowRoot!.querySelector("ha-textfield")!;
|
||||
const inputEl = this.shadowRoot!.querySelector("paper-input")!;
|
||||
const value = inputEl.value || "";
|
||||
this.error = undefined;
|
||||
|
||||
@@ -313,10 +315,6 @@ export class HcConnect extends LitElement {
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -39,9 +39,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
<div class="picker">
|
||||
<div class="label">
|
||||
${this._switching
|
||||
? html`<ha-circular-progress
|
||||
indeterminate
|
||||
></ha-circular-progress>`
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: until(
|
||||
selectedDemoConfig.then(
|
||||
(conf) => html`
|
||||
@@ -50,7 +48,8 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
<a target="_blank" href=${conf.authorUrl}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.page-demo.cards.demo.demo_by",
|
||||
{ name: conf.authorName }
|
||||
"name",
|
||||
conf.authorName
|
||||
)}
|
||||
</a>
|
||||
</small>
|
||||
|
@@ -43,8 +43,8 @@ const generateMeanStatistics = (
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
@@ -80,8 +80,8 @@ const generateSumStatistics = (
|
||||
period === "day"
|
||||
? addDays(currentDate, 1)
|
||||
: period === "month"
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
? addMonths(currentDate, 1)
|
||||
: addHours(currentDate, 1);
|
||||
}
|
||||
return statistics;
|
||||
};
|
||||
|
@@ -23,7 +23,7 @@ class DemoMoreInfo extends LitElement {
|
||||
<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
inDialog
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
|
@@ -1,4 +0,0 @@
|
||||
---
|
||||
title: Circular Progress
|
||||
subtitle: Can be used to indicate an ongoing task.
|
||||
---
|
@@ -1,64 +0,0 @@
|
||||
import { html, css, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-bar";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import "@material/web/progress/circular-progress";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
@customElement("demo-components-ha-circular-progress")
|
||||
export class DemoHaCircularProgress extends LitElement {
|
||||
@property({ attribute: false }) hass!: HomeAssistant;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`<ha-card header="Basic circular progress">
|
||||
<div class="card-content">
|
||||
<ha-circular-progress indeterminate></ha-circular-progress></div
|
||||
></ha-card>
|
||||
<ha-card header="Different circular progress sizes">
|
||||
<div class="card-content">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="tiny"
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="small"
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="medium"
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
size="large"
|
||||
></ha-circular-progress></div
|
||||
></ha-card>
|
||||
<ha-card header="Circular progress with an aria-label">
|
||||
<div class="card-content">
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
aria-label="Doing something..."
|
||||
></ha-circular-progress>
|
||||
<ha-circular-progress
|
||||
indeterminate
|
||||
.ariaLabel=${"Doing something..."}
|
||||
></ha-circular-progress></div
|
||||
></ha-card>`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-circular-progress": DemoHaCircularProgress;
|
||||
}
|
||||
}
|
@@ -18,7 +18,7 @@ The Home Assistant interface is based on Material Design. It's a design system c
|
||||
|
||||
We want to make it as easy for designers to contribute as it is for developers. There’s a lot a designer can contribute to:
|
||||
|
||||
- Meet us at <a href="https://www.home-assistant.io/join-chat" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Meet us at <a href="https://discord.gg/BPBc8rZ9" rel="noopener noreferrer" target="_blank">devs_ux Discord</a>. Feel free to share your designs, user test or strategic ideas.
|
||||
- Start designing with our <a href="https://www.figma.com/community/file/967153512097289521/Home-Assistant-DesignKit" rel="noopener noreferrer" target="_blank">Figma DesignKit</a>.
|
||||
- Find the latest UX <a href="https://github.com/home-assistant/frontend/discussions?discussions_q=label%3Aux" rel="noopener noreferrer" target="_blank">discussions</a> and <a href="https://github.com/home-assistant/frontend/labels/ux" rel="noopener noreferrer" target="_blank">issues</a> on GitHub. Everyone can start a new issue or discussion!
|
||||
|
||||
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Input Text
|
||||
---
|
@@ -1,46 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("input_text", "text", "Inspiration", {
|
||||
friendly_name: "Text",
|
||||
mode: "text",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-input-text")
|
||||
class DemoMoreInfoInputText extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-input-text": DemoMoreInfoInputText;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Lock
|
||||
---
|
@@ -1,49 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("lock", "lock", "locked", {
|
||||
friendly_name: "Lock",
|
||||
device_class: "lock",
|
||||
}),
|
||||
getEntity("lock", "unavailable", "unavailable", {
|
||||
friendly_name: "Unavailable lock",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-lock")
|
||||
class DemoMoreInfoLock extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-lock": DemoMoreInfoLock;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Media Player
|
||||
---
|
@@ -1,41 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
import { createMediaPlayerEntities } from "../../data/media_players";
|
||||
|
||||
const ENTITIES = createMediaPlayerEntities();
|
||||
|
||||
@customElement("demo-more-info-media-player")
|
||||
class DemoMoreInfoMediaPlayer extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-media-player": DemoMoreInfoMediaPlayer;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Number
|
||||
---
|
@@ -1,78 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("number", "box1", 0, {
|
||||
friendly_name: "Box1",
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
initial: 0,
|
||||
mode: "box",
|
||||
unit_of_measurement: "items",
|
||||
}),
|
||||
getEntity("number", "slider1", 0, {
|
||||
friendly_name: "Slider1",
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
initial: 0,
|
||||
mode: "slider",
|
||||
unit_of_measurement: "items",
|
||||
}),
|
||||
getEntity("number", "auto1", 0, {
|
||||
friendly_name: "Auto1",
|
||||
min: 0,
|
||||
max: 1000,
|
||||
step: 1,
|
||||
initial: 0,
|
||||
mode: "auto",
|
||||
unit_of_measurement: "items",
|
||||
}),
|
||||
getEntity("number", "auto2", 0, {
|
||||
friendly_name: "Auto2",
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
initial: 0,
|
||||
mode: "auto",
|
||||
unit_of_measurement: "items",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-number")
|
||||
class DemoMoreInfoNumber extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-number": DemoMoreInfoNumber;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Scene
|
||||
---
|
@@ -1,49 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("scene", "romantic_lights", "scening", {
|
||||
entity_id: ["light.bed_light", "light.ceiling_lights"],
|
||||
friendly_name: "Romantic Scene",
|
||||
}),
|
||||
getEntity("scene", "unavailable", "unavailable", {
|
||||
friendly_name: "Romantic Scene",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-scene")
|
||||
class DemoMoreInfoScene extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-scene": DemoMoreInfoScene;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Timer
|
||||
---
|
@@ -1,46 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("timer", "timer", "idle", {
|
||||
friendly_name: "Timer",
|
||||
duration: "0:05:00",
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-timer")
|
||||
class DemoMoreInfoTimer extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-timer": DemoMoreInfoTimer;
|
||||
}
|
||||
}
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Vacuum
|
||||
---
|
@@ -1,50 +0,0 @@
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/dialogs/more-info/more-info-content";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import {
|
||||
MockHomeAssistant,
|
||||
provideHass,
|
||||
} from "../../../../src/fake_data/provide_hass";
|
||||
import "../../components/demo-more-infos";
|
||||
import { VacuumEntityFeature } from "../../../../src/data/vacuum";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("vacuum", "first_floor_vacuum", "docked", {
|
||||
friendly_name: "First floor vacuum",
|
||||
supported_features:
|
||||
VacuumEntityFeature.START +
|
||||
VacuumEntityFeature.STOP +
|
||||
VacuumEntityFeature.RETURN_HOME,
|
||||
}),
|
||||
];
|
||||
|
||||
@customElement("demo-more-info-vacuum")
|
||||
class DemoMoreInfoVacuum extends LitElement {
|
||||
@property() public hass!: MockHomeAssistant;
|
||||
|
||||
@query("demo-more-infos") private _demoRoot!: HTMLElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
.hass=${this.hass}
|
||||
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-more-info-vacuum": DemoMoreInfoVacuum;
|
||||
}
|
||||
}
|
@@ -49,9 +49,11 @@ export class HassioAddonRepositoryEl extends LitElement {
|
||||
return html`
|
||||
<div class="content">
|
||||
<p class="description">
|
||||
${this.supervisor.localize("store.no_results_found", {
|
||||
repository: repo.name,
|
||||
})}
|
||||
${this.supervisor.localize(
|
||||
"store.no_results_found",
|
||||
"repository",
|
||||
repo.name
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
@@ -84,15 +86,15 @@ export class HassioAddonRepositoryEl extends LitElement {
|
||||
)
|
||||
: this.supervisor.localize("addon.state.installed")
|
||||
: addon.available
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
? this.supervisor.localize("addon.state.not_installed")
|
||||
: this.supervisor.localize("addon.state.not_available")}
|
||||
.iconClass=${addon.installed
|
||||
? addon.update_available
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "not_available"
|
||||
: ""}
|
||||
? "not_available"
|
||||
: ""}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
@@ -106,8 +108,8 @@ export class HassioAddonRepositoryEl extends LitElement {
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
? "unavailable"
|
||||
: ""}
|
||||
? "unavailable"
|
||||
: ""}
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
@@ -20,7 +20,7 @@ class HassioAddonConfigDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
const hasConfiguration =
|
||||
(this.addon.options && Object.keys(this.addon.options).length) ||
|
||||
|
@@ -104,50 +104,50 @@ class HassioAddonConfig extends LitElement {
|
||||
selector: { select: { options: entry.options } },
|
||||
}
|
||||
: entry.type === "string"
|
||||
? entry.multiple
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
select: { options: [], multiple: true, custom_value: true },
|
||||
? entry.multiple
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
select: { options: [], multiple: true, custom_value: true },
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
text: {
|
||||
type:
|
||||
entry.format || MASKED_FIELDS.includes(entry.name)
|
||||
? "password"
|
||||
: "text",
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
text: {
|
||||
type:
|
||||
entry.format || MASKED_FIELDS.includes(entry.name)
|
||||
? "password"
|
||||
: "text",
|
||||
},
|
||||
},
|
||||
}
|
||||
: entry.type === "boolean"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { boolean: {} },
|
||||
}
|
||||
: entry.type === "schema"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { object: {} },
|
||||
}
|
||||
: entry.type === "float" || entry.type === "integer"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: entry.type === "float" ? "any" : undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
: entry
|
||||
},
|
||||
}
|
||||
: entry.type === "boolean"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { boolean: {} },
|
||||
}
|
||||
: entry.type === "schema"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: { object: {} },
|
||||
}
|
||||
: entry.type === "float" || entry.type === "integer"
|
||||
? {
|
||||
name: entry.name,
|
||||
required: entry.required,
|
||||
selector: {
|
||||
number: {
|
||||
mode: "box",
|
||||
step: entry.type === "float" ? "any" : undefined,
|
||||
},
|
||||
},
|
||||
}
|
||||
: entry
|
||||
)
|
||||
);
|
||||
|
||||
@@ -340,9 +340,11 @@ class HassioAddonConfig extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_reset",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
button.progress = false;
|
||||
}
|
||||
@@ -379,9 +381,11 @@ class HassioAddonConfig extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
eventdata.success = false;
|
||||
}
|
||||
button.progress = false;
|
||||
|
@@ -180,9 +180,11 @@ class HassioAddonNetwork extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_reset", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_reset",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
button.actionError();
|
||||
}
|
||||
}
|
||||
@@ -218,9 +220,11 @@ class HassioAddonNetwork extends LitElement {
|
||||
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
button.actionError();
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
@@ -85,7 +85,8 @@ class HassioAddonDocumentationDashboard extends LitElement {
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.documentation.get_documentation",
|
||||
{ error: extractApiErrorMessage(err) }
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ class HassioAddonInfoDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
return html`<ha-circular-progress active></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
|
@@ -451,14 +451,13 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
<div class="description light-color">
|
||||
${this.addon.description}.<br />
|
||||
${this.supervisor.localize("addon.dashboard.visit_addon_page", {
|
||||
name: html`<a
|
||||
href=${this.addon.url!}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.visit_addon_page",
|
||||
"name",
|
||||
html`<a href=${this.addon.url!} target="_blank" rel="noreferrer"
|
||||
>${this.addon.name}</a
|
||||
>`,
|
||||
})}
|
||||
>`
|
||||
)}
|
||||
</div>
|
||||
<div class="addon-container">
|
||||
<div>
|
||||
@@ -625,10 +624,10 @@ class HassioAddonInfo extends LitElement {
|
||||
<ha-alert alert-type="warning">
|
||||
${this.supervisor.localize(
|
||||
"addon.dashboard.not_available_version",
|
||||
{
|
||||
core_version_installed: this.supervisor.core.version,
|
||||
core_version_needed: addonStoreInfo!.homeassistant,
|
||||
}
|
||||
"core_version_installed",
|
||||
this.supervisor.core.version,
|
||||
"core_version_needed",
|
||||
addonStoreInfo!.homeassistant
|
||||
)}
|
||||
</ha-alert>
|
||||
`
|
||||
@@ -801,11 +800,12 @@ class HassioAddonInfo extends LitElement {
|
||||
id === "stage"
|
||||
? this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`,
|
||||
{
|
||||
icon_stable: `<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
|
||||
icon_experimental: `<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
|
||||
icon_deprecated: `<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`,
|
||||
}
|
||||
"icon_stable",
|
||||
`<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
|
||||
"icon_experimental",
|
||||
`<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
|
||||
"icon_deprecated",
|
||||
`<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
`addon.dashboard.capability.${id}.description`
|
||||
@@ -867,9 +867,11 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,9 +889,11 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,9 +911,11 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,9 +933,11 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,9 +955,11 @@ class HassioAddonInfo extends LitElement {
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.failed_to_save", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.failed_to_save",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -18,9 +18,7 @@ class HassioAddonLogDashboard extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.addon) {
|
||||
return html`
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>
|
||||
`;
|
||||
return html` <ha-circular-progress active></ha-circular-progress> `;
|
||||
}
|
||||
return html`
|
||||
<div class="content">
|
||||
|
@@ -72,9 +72,11 @@ class HassioAddonLogs extends LitElement {
|
||||
try {
|
||||
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("addon.logs.get_logs", {
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"addon.logs.get_logs",
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
|
||||
const options: IFuseOptions<StoreAddon> = {
|
||||
keys: ["name", "description", "slug"],
|
||||
isCaseSensitive: false,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
minMatchCharLength: 2,
|
||||
threshold: 0.2,
|
||||
};
|
||||
const fuse = new Fuse(addons, options);
|
||||
|
@@ -17,11 +17,8 @@ class SupervisorFormfieldLabel extends LitElement {
|
||||
${this.imageUrl
|
||||
? html`<img loading="lazy" alt="" src=${this.imageUrl} class="icon" />`
|
||||
: this.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${this.iconPath}
|
||||
class="icon"
|
||||
></ha-svg-icon>`
|
||||
: ""}
|
||||
? html`<ha-svg-icon .path=${this.iconPath} class="icon"></ha-svg-icon>`
|
||||
: ""}
|
||||
<span class="label">${this.label}</span>
|
||||
${this.version
|
||||
? html`<span class="version">(${this.version})</span>`
|
||||
|
@@ -68,19 +68,17 @@ class HassioAddons extends LitElement {
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? this.supervisor.localize("dashboard.addon_stopped")
|
||||
: addon.update_available!
|
||||
? this.supervisor.localize(
|
||||
"dashboard.addon_new_version"
|
||||
)
|
||||
: this.supervisor.localize(
|
||||
"dashboard.addon_running"
|
||||
)}
|
||||
? this.supervisor.localize(
|
||||
"dashboard.addon_new_version"
|
||||
)
|
||||
: this.supervisor.localize("dashboard.addon_running")}
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
: "update stopped"
|
||||
: addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
this.hass.config.version,
|
||||
0,
|
||||
|
@@ -46,9 +46,11 @@ export class HassioUpdate extends LitElement {
|
||||
return html`
|
||||
<div class="content">
|
||||
<h1>
|
||||
${this.supervisor.localize("common.update_available", {
|
||||
count: updatesAvailable,
|
||||
})}
|
||||
${this.supervisor.localize(
|
||||
"common.update_available",
|
||||
"count",
|
||||
updatesAvailable
|
||||
)}
|
||||
🎉
|
||||
</h1>
|
||||
<div class="card-group">
|
||||
|
@@ -95,7 +95,7 @@ class HassioBackupDialog
|
||||
</ha-header-bar>
|
||||
</div>
|
||||
${this._restoringBackup
|
||||
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`
|
||||
<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
|
@@ -57,7 +57,7 @@ class HassioCreateBackupDialog extends LitElement {
|
||||
)}
|
||||
>
|
||||
${this._creatingBackup
|
||||
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
|
||||
? html`<ha-circular-progress active></ha-circular-progress>`
|
||||
: html`<supervisor-backup-content
|
||||
.hass=${this.hass}
|
||||
.supervisor=${this._dialogParams.supervisor}
|
||||
|
@@ -71,11 +71,7 @@ class HassioDatadiskDialog extends LitElement {
|
||||
?hideActions=${this.moving}
|
||||
>
|
||||
${this.moving
|
||||
? html` <ha-circular-progress
|
||||
aria-label="Moving"
|
||||
size="large"
|
||||
indeterminate
|
||||
>
|
||||
? html` <ha-circular-progress alt="Moving" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this.dialogParams.supervisor.localize(
|
||||
@@ -109,12 +105,12 @@ class HassioDatadiskDialog extends LitElement {
|
||||
</ha-select>
|
||||
`
|
||||
: this.devices === undefined
|
||||
? this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.loading_devices"
|
||||
)
|
||||
: this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.no_devices"
|
||||
)}
|
||||
? this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.loading_devices"
|
||||
)
|
||||
: this.dialogParams.supervisor.localize(
|
||||
"dialog.datadisk_move.no_devices"
|
||||
)}
|
||||
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
|
@@ -145,7 +145,8 @@ export class DialogHassioNetwork
|
||||
? html`<p>
|
||||
${this.supervisor.localize(
|
||||
"dialog.network.connected_to",
|
||||
{ ssid: this._interface?.wifi?.ssid }
|
||||
"ssid",
|
||||
this._interface?.wifi?.ssid
|
||||
)}
|
||||
</p>`
|
||||
: ""}
|
||||
@@ -155,11 +156,7 @@ export class DialogHassioNetwork
|
||||
.disabled=${this._scanning}
|
||||
>
|
||||
${this._scanning
|
||||
? html`<ha-circular-progress
|
||||
aria-label="Scanning"
|
||||
indeterminate
|
||||
size="small"
|
||||
>
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: this.supervisor.localize("dialog.network.scan_ap")}
|
||||
</mwc-button>
|
||||
@@ -278,7 +275,7 @@ export class DialogHassioNetwork
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress indeterminate size="small">
|
||||
? html`<ha-circular-progress active size="small">
|
||||
</ha-circular-progress>`
|
||||
: this.supervisor.localize("common.save")}
|
||||
</mwc-button>
|
||||
|
@@ -158,7 +158,7 @@ class HassioRepositoriesDialog extends LitElement {
|
||||
<mwc-button @click=${this._addRepository}>
|
||||
${this._processing
|
||||
? html`<ha-circular-progress
|
||||
indeterminate
|
||||
active
|
||||
size="small"
|
||||
></ha-circular-progress>`
|
||||
: this._dialogParams!.supervisor.localize(
|
||||
|
@@ -76,15 +76,17 @@ class HassioMyRedirect extends LitElement {
|
||||
const redirect = REDIRECTS[path];
|
||||
|
||||
if (!redirect) {
|
||||
this._error = this.supervisor.localize("my.not_supported", {
|
||||
link: html`<a
|
||||
this._error = this.supervisor.localize(
|
||||
"my.not_supported",
|
||||
"link",
|
||||
html`<a
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href="https://my.home-assistant.io/faq.html#supported-pages"
|
||||
>
|
||||
${this.supervisor.localize("my.faq_link")}
|
||||
</a>`,
|
||||
});
|
||||
</a>`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -67,8 +67,8 @@ class HassioRouter extends HassRouterPage {
|
||||
const route = hassioPanel
|
||||
? this.route
|
||||
: ingressPanel && this.panel.config?.ingress
|
||||
? this._ingressRoute(this.panel.config?.ingress)
|
||||
: this.routeTail;
|
||||
? this._ingressRoute(this.panel.config?.ingress)
|
||||
: this.routeTail;
|
||||
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
|
@@ -96,11 +96,13 @@ class HassioCoreInfo extends LitElement {
|
||||
slot="primaryAction"
|
||||
class="warning"
|
||||
@click=${this._coreRestart}
|
||||
.title=${this.supervisor.localize("common.restart_name", {
|
||||
name: "Core",
|
||||
})}
|
||||
.title=${this.supervisor.localize(
|
||||
"common.restart_name",
|
||||
"name",
|
||||
"Core"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize("common.restart_name", { name: "Core" })}
|
||||
${this.supervisor.localize("common.restart_name", "name", "Core")}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -120,12 +122,16 @@ class HassioCoreInfo extends LitElement {
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("confirm.restart.title", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
text: this.supervisor.localize("confirm.restart.text", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
title: this.supervisor.localize(
|
||||
"confirm.restart.title",
|
||||
"name",
|
||||
"Home Assistant Core"
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"confirm.restart.text",
|
||||
"name",
|
||||
"Home Assistant Core"
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.restart"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
@@ -140,9 +146,11 @@ class HassioCoreInfo extends LitElement {
|
||||
} catch (err: any) {
|
||||
if (this.hass.connection.connected) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.failed_to_restart_name", {
|
||||
name: "Home Assistant Core",
|
||||
}),
|
||||
title: this.supervisor.localize(
|
||||
"common.failed_to_restart_name",
|
||||
"name",
|
||||
"Home AssistantCore"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
@@ -109,19 +109,19 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</ha-progress-button>
|
||||
`
|
||||
: this.supervisor.supervisor.channel === "stable"
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_description"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
? html`
|
||||
<ha-progress-button
|
||||
@click=${this._toggleBeta}
|
||||
.title=${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_description"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize(
|
||||
"system.supervisor.join_beta_action"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: ""}
|
||||
</ha-settings-row>
|
||||
|
||||
${this.supervisor.supervisor.supported
|
||||
@@ -200,13 +200,17 @@ class HassioSupervisorInfo extends LitElement {
|
||||
<ha-progress-button
|
||||
class="warning"
|
||||
@click=${this._supervisorRestart}
|
||||
.title=${this.supervisor.localize("common.restart_name", {
|
||||
name: "Supervisor",
|
||||
})}
|
||||
.title=${this.supervisor.localize(
|
||||
"common.restart_name",
|
||||
"name",
|
||||
"Supervisor"
|
||||
)}
|
||||
>
|
||||
${this.supervisor.localize("common.restart_name", {
|
||||
name: "Supervisor",
|
||||
})}
|
||||
${this.supervisor.localize(
|
||||
"common.restart_name",
|
||||
"name",
|
||||
"Supervisor"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -288,12 +292,16 @@ class HassioSupervisorInfo extends LitElement {
|
||||
button.progress = true;
|
||||
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
title: this.supervisor.localize("confirm.restart.title", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
text: this.supervisor.localize("confirm.restart.text", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
title: this.supervisor.localize(
|
||||
"confirm.restart.title",
|
||||
"name",
|
||||
"Supervisor"
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"confirm.restart.text",
|
||||
"name",
|
||||
"Supervisor"
|
||||
),
|
||||
confirmText: this.supervisor.localize("common.restart"),
|
||||
dismissText: this.supervisor.localize("common.cancel"),
|
||||
});
|
||||
@@ -307,9 +315,11 @@ class HassioSupervisorInfo extends LitElement {
|
||||
await restartSupervisor(this.hass);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.supervisor.localize("common.failed_to_restart_name", {
|
||||
name: "Supervisor",
|
||||
}),
|
||||
title: this.supervisor.localize(
|
||||
"common.failed_to_restart_name",
|
||||
"name",
|
||||
"Supervisor"
|
||||
),
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
} finally {
|
||||
@@ -324,7 +334,8 @@ class HassioSupervisorInfo extends LitElement {
|
||||
),
|
||||
text: this.supervisor.localize(
|
||||
"system.supervisor.share_diagonstics_description",
|
||||
{ line_break: html`<br /><br />` }
|
||||
"line_break",
|
||||
html`<br /><br />`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@@ -124,10 +124,13 @@ class HassioSupervisorLog extends LitElement {
|
||||
this._selectedLogProvider
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = this.supervisor.localize("system.log.get_logs", {
|
||||
provider: this._selectedLogProvider,
|
||||
error: extractApiErrorMessage(err),
|
||||
});
|
||||
this._error = this.supervisor.localize(
|
||||
"system.log.get_logs",
|
||||
"provider",
|
||||
this._selectedLogProvider,
|
||||
"error",
|
||||
extractApiErrorMessage(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -70,8 +70,8 @@ const changelogUrl = (
|
||||
return version.includes("dev")
|
||||
? "https://github.com/home-assistant/core/commits/dev"
|
||||
: version.includes("b")
|
||||
? "https://next.home-assistant.io/latest-release-notes/"
|
||||
: "https://www.home-assistant.io/latest-release-notes/";
|
||||
? "https://next.home-assistant.io/latest-release-notes/"
|
||||
: "https://www.home-assistant.io/latest-release-notes/";
|
||||
}
|
||||
if (entry === "os") {
|
||||
return version.includes("dev")
|
||||
@@ -141,51 +141,44 @@ class UpdateAvailableCard extends LitElement {
|
||||
})}
|
||||
</p>`
|
||||
: !this._updating
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-faded>
|
||||
`
|
||||
: ""}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize(
|
||||
"update_available.description",
|
||||
{
|
||||
name: this._name,
|
||||
version: this._version,
|
||||
newest_version: this._version_latest,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
${["core", "addon"].includes(this._updateType)
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox checked></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`<ha-circular-progress
|
||||
aria-label="Updating"
|
||||
size="large"
|
||||
indeterminate
|
||||
>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this.supervisor.localize("update_available.updating", {
|
||||
? html`
|
||||
${this._changelogContent
|
||||
? html`
|
||||
<ha-faded>
|
||||
<ha-markdown .content=${this._changelogContent}>
|
||||
</ha-markdown>
|
||||
</ha-faded>
|
||||
`
|
||||
: ""}
|
||||
<div class="versions">
|
||||
<p>
|
||||
${this.supervisor.localize("update_available.description", {
|
||||
name: this._name,
|
||||
version: this._version_latest,
|
||||
version: this._version,
|
||||
newest_version: this._version_latest,
|
||||
})}
|
||||
</p>`}
|
||||
</p>
|
||||
</div>
|
||||
${["core", "addon"].includes(this._updateType)
|
||||
? html`
|
||||
<ha-formfield
|
||||
.label=${this.supervisor.localize(
|
||||
"update_available.create_backup"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox checked></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`
|
||||
: html`<ha-circular-progress alt="Updating" size="large" active>
|
||||
</ha-circular-progress>
|
||||
<p class="progress-text">
|
||||
${this.supervisor.localize("update_available.updating", {
|
||||
name: this._name,
|
||||
version: this._version_latest,
|
||||
})}
|
||||
</p>`}
|
||||
</div>
|
||||
${this._version !== this._version_latest && !this._updating
|
||||
? html`
|
||||
|
119
package.json
119
package.json
@@ -25,41 +25,42 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.23.5",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@codemirror/autocomplete": "6.11.1",
|
||||
"@codemirror/commands": "6.3.2",
|
||||
"@codemirror/language": "6.9.3",
|
||||
"@babel/runtime": "7.23.2",
|
||||
"@braintree/sanitize-url": "6.0.4",
|
||||
"@codemirror/autocomplete": "6.11.0",
|
||||
"@codemirror/commands": "6.3.0",
|
||||
"@codemirror/language": "6.9.2",
|
||||
"@codemirror/legacy-modes": "6.3.3",
|
||||
"@codemirror/search": "6.5.5",
|
||||
"@codemirror/state": "6.3.3",
|
||||
"@codemirror/view": "6.22.1",
|
||||
"@codemirror/search": "6.5.4",
|
||||
"@codemirror/state": "6.3.1",
|
||||
"@codemirror/view": "6.22.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.0",
|
||||
"@formatjs/intl-displaynames": "6.6.4",
|
||||
"@formatjs/intl-datetimeformat": "6.11.2",
|
||||
"@formatjs/intl-displaynames": "6.6.2",
|
||||
"@formatjs/intl-getcanonicallocales": "2.3.0",
|
||||
"@formatjs/intl-listformat": "7.5.3",
|
||||
"@formatjs/intl-locale": "3.4.3",
|
||||
"@formatjs/intl-numberformat": "8.9.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.10",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.10",
|
||||
"@fullcalendar/core": "6.1.10",
|
||||
"@fullcalendar/daygrid": "6.1.10",
|
||||
"@fullcalendar/interaction": "6.1.10",
|
||||
"@fullcalendar/list": "6.1.10",
|
||||
"@fullcalendar/luxon3": "6.1.10",
|
||||
"@fullcalendar/timegrid": "6.1.10",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@formatjs/intl-listformat": "7.5.1",
|
||||
"@formatjs/intl-locale": "3.4.1",
|
||||
"@formatjs/intl-numberformat": "8.8.1",
|
||||
"@formatjs/intl-pluralrules": "5.2.8",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.8",
|
||||
"@fullcalendar/core": "6.1.9",
|
||||
"@fullcalendar/daygrid": "6.1.9",
|
||||
"@fullcalendar/interaction": "6.1.9",
|
||||
"@fullcalendar/list": "6.1.9",
|
||||
"@fullcalendar/luxon3": "6.1.9",
|
||||
"@fullcalendar/timegrid": "6.1.9",
|
||||
"@lezer/highlight": "1.1.6",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.6",
|
||||
"@lit-labs/observers": "2.0.2",
|
||||
"@lit-labs/virtualizer": "2.0.11",
|
||||
"@lit-labs/virtualizer": "2.0.10",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.18",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
"@material/mwc-checkbox": "0.27.0",
|
||||
"@material/mwc-circular-progress": "0.27.0",
|
||||
"@material/mwc-dialog": "0.27.0",
|
||||
"@material/mwc-drawer": "0.27.0",
|
||||
"@material/mwc-fab": "0.27.0",
|
||||
@@ -83,6 +84,9 @@
|
||||
"@material/web": "=1.0.1",
|
||||
"@mdi/js": "7.3.67",
|
||||
"@mdi/svg": "7.3.67",
|
||||
"@polymer/iron-flex-layout": "3.0.1",
|
||||
"@polymer/iron-input": "3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "3.0.1",
|
||||
"@polymer/paper-input": "3.2.1",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
"@polymer/paper-listbox": "3.0.1",
|
||||
@@ -90,8 +94,8 @@
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.2.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.2.5",
|
||||
"@vaadin/combo-box": "24.2.2",
|
||||
"@vaadin/vaadin-themable-mixin": "24.2.2",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@@ -99,36 +103,36 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.4.1",
|
||||
"chart.js": "4.4.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.33.3",
|
||||
"core-js": "3.33.2",
|
||||
"cropperjs": "1.6.1",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"element-internals-polyfill": "1.3.9",
|
||||
"fuse.js": "7.0.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"hls.js": "1.4.13",
|
||||
"hls.js": "1.4.12",
|
||||
"home-assistant-js-websocket": "9.1.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.5.8",
|
||||
"intl-messageformat": "10.5.5",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.4.4",
|
||||
"marked": "11.0.1",
|
||||
"luxon": "3.4.3",
|
||||
"marked": "9.1.6",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
"punycode": "2.3.1",
|
||||
"qr-scanner": "1.4.2",
|
||||
"qrcode": "1.5.3",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"roboto-fontface": "0.10.0",
|
||||
"rrule": "2.8.1",
|
||||
"sortablejs": "1.15.1",
|
||||
"rrule": "2.7.2",
|
||||
"sortablejs": "1.15.0",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "1.0.3",
|
||||
"tinykeys": "2.1.0",
|
||||
@@ -136,7 +140,7 @@
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.37",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.9",
|
||||
"vis-data": "7.1.8",
|
||||
"vis-network": "9.1.9",
|
||||
"vue": "2.7.15",
|
||||
"vue2-daterange-picker": "0.6.8",
|
||||
@@ -150,15 +154,14 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.23.5",
|
||||
"@babel/helper-define-polyfill-provider": "0.4.3",
|
||||
"@babel/plugin-proposal-decorators": "7.23.5",
|
||||
"@babel/plugin-transform-runtime": "7.23.4",
|
||||
"@babel/preset-env": "7.23.5",
|
||||
"@babel/core": "7.23.3",
|
||||
"@babel/plugin-proposal-decorators": "7.23.3",
|
||||
"@babel/plugin-transform-runtime": "7.23.3",
|
||||
"@babel/preset-env": "7.23.3",
|
||||
"@babel/preset-typescript": "7.23.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.8.3",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.8.0",
|
||||
"@koa/cors": "4.0.0",
|
||||
"@lokalise/node-api": "12.1.0",
|
||||
"@lokalise/node-api": "12.0.0",
|
||||
"@octokit/auth-oauth-device": "6.0.1",
|
||||
"@octokit/plugin-retry": "6.0.1",
|
||||
"@octokit/rest": "20.0.2",
|
||||
@@ -175,27 +178,27 @@
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.8",
|
||||
"@types/leaflet-draw": "1.0.11",
|
||||
"@types/luxon": "3.3.7",
|
||||
"@types/mocha": "10.0.6",
|
||||
"@types/leaflet-draw": "1.0.10",
|
||||
"@types/luxon": "3.3.4",
|
||||
"@types/mocha": "10.0.4",
|
||||
"@types/qrcode": "1.5.5",
|
||||
"@types/serve-handler": "6.1.4",
|
||||
"@types/sortablejs": "1.15.7",
|
||||
"@types/tar": "6.1.10",
|
||||
"@types/sortablejs": "1.15.5",
|
||||
"@types/tar": "6.1.9",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "6.13.2",
|
||||
"@typescript-eslint/parser": "6.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "6.10.0",
|
||||
"@typescript-eslint/parser": "6.10.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "4.3.10",
|
||||
"del": "7.1.0",
|
||||
"eslint": "8.55.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.1.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-import-resolver-webpack": "0.13.8",
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
@@ -204,7 +207,7 @@
|
||||
"eslint-plugin-unused-imports": "3.0.0",
|
||||
"eslint-plugin-wc": "2.0.4",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.3.10",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
@@ -216,7 +219,7 @@
|
||||
"husky": "8.0.3",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.0",
|
||||
"lint-staged": "15.1.0",
|
||||
"lit-analyzer": "2.0.1",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.5",
|
||||
@@ -225,11 +228,11 @@
|
||||
"object-hash": "3.0.0",
|
||||
"open": "9.1.0",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.1.0",
|
||||
"prettier": "3.0.3",
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.10.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "17.0.1",
|
||||
"source-map-url": "0.4.1",
|
||||
@@ -237,7 +240,7 @@
|
||||
"tar": "6.2.0",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-lit-plugin": "2.0.1",
|
||||
"typescript": "5.3.3",
|
||||
"typescript": "5.2.2",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "5.89.0",
|
||||
@@ -253,10 +256,10 @@
|
||||
"@polymer/polymer": "patch:@polymer/polymer@3.5.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "2.8.0",
|
||||
"clean-css": "5.3.3",
|
||||
"clean-css": "5.3.2",
|
||||
"@lit/reactive-element": "1.6.3",
|
||||
"sortablejs@1.15.0": "patch:sortablejs@npm%3A1.15.0#./.yarn/patches/sortablejs-npm-1.15.0-f3a393abcc.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.0.2"
|
||||
"packageManager": "yarn@4.0.1"
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20231208.2"
|
||||
version = "20231030.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@@ -40,5 +40,4 @@ docker run \
|
||||
--file /opt/src/${LOCAL_FILE} \
|
||||
--lang-iso ${LANG_ISO} \
|
||||
--convert-placeholders=false \
|
||||
--replace-modified=true \
|
||||
# --cleanup-mode=true
|
||||
--replace-modified=true
|
||||
|
@@ -8,20 +8,12 @@ import "../components/ha-alert";
|
||||
import "../components/ha-checkbox";
|
||||
import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
|
||||
import "../components/ha-formfield";
|
||||
import {
|
||||
AuthProvider,
|
||||
autocompleteLoginFields,
|
||||
createLoginFlow,
|
||||
deleteLoginFlow,
|
||||
redirectWithAuthCode,
|
||||
submitLoginFlow,
|
||||
} from "../data/auth";
|
||||
import { AuthProvider, autocompleteLoginFields } from "../data/auth";
|
||||
import {
|
||||
DataEntryFlowStep,
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import "./ha-auth-form";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -37,18 +29,18 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public step?: DataEntryFlowStep;
|
||||
|
||||
@property({ type: Boolean }) private storeToken = false;
|
||||
|
||||
@state() private _state: State = "loading";
|
||||
|
||||
@state() private _stepData?: Record<string, any>;
|
||||
|
||||
@state() private _step?: DataEntryFlowStep;
|
||||
|
||||
@state() private _errorMessage?: string;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _storeToken = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -56,29 +48,27 @@ export class HaAuthFlow extends LitElement {
|
||||
willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("step")) {
|
||||
if (!changedProps.has("_step")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.step) {
|
||||
if (!this._step) {
|
||||
this._stepData = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this._state = "step";
|
||||
|
||||
const oldStep = changedProps.get("step") as HaAuthFlow["step"];
|
||||
const oldStep = changedProps.get("_step") as HaAuthFlow["_step"];
|
||||
|
||||
if (
|
||||
!oldStep ||
|
||||
this.step.flow_id !== oldStep.flow_id ||
|
||||
(this.step.type === "form" &&
|
||||
this._step.flow_id !== oldStep.flow_id ||
|
||||
(this._step.type === "form" &&
|
||||
oldStep.type === "form" &&
|
||||
this.step.step_id !== oldStep.step_id)
|
||||
this._step.step_id !== oldStep.step_id)
|
||||
) {
|
||||
this._stepData =
|
||||
this.step.type === "form"
|
||||
? computeInitialHaFormData(this.step.data_schema)
|
||||
this._step.type === "form"
|
||||
? computeInitialHaFormData(this._step.data_schema)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
@@ -86,27 +76,13 @@ export class HaAuthFlow extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<style>
|
||||
ha-auth-flow .store-token {
|
||||
margin-left: -16px;
|
||||
}
|
||||
a.forgot-password {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.space-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
form {
|
||||
ha-auth-flow .action {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
max-width: 336px;
|
||||
width: 100%;
|
||||
}
|
||||
ha-auth-form {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
ha-auth-flow .store-token {
|
||||
margin-top: 10px;
|
||||
margin-left: -16px;
|
||||
}
|
||||
</style>
|
||||
<form>${this._renderForm()}</form>
|
||||
@@ -141,7 +117,7 @@ export class HaAuthFlow extends LitElement {
|
||||
this._providerChanged(this.authProvider);
|
||||
}
|
||||
|
||||
if (!changedProps.has("step") || this.step?.type !== "form") {
|
||||
if (!changedProps.has("_step") || this._step?.type !== "form") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,31 +131,20 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderForm() {
|
||||
const showBack =
|
||||
this.step?.type === "form" &&
|
||||
this.authProvider?.users &&
|
||||
!["select_mfa_module", "mfa"].includes(this.step.step_id);
|
||||
|
||||
switch (this._state) {
|
||||
case "step":
|
||||
if (this.step == null) {
|
||||
if (this._step == null) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
${this._renderStep(this.step)}
|
||||
<div class="action ${showBack ? "space-between" : ""}">
|
||||
${showBack
|
||||
? html`<mwc-button @click=${this._localFlow}>
|
||||
${this.localize("ui.panel.page-authorize.form.previous")}
|
||||
</mwc-button>`
|
||||
: nothing}
|
||||
${this._renderStep(this._step)}
|
||||
<div class="action">
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._handleSubmit}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.step.type === "form"
|
||||
${this._step.type === "form"
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
: this.localize("ui.panel.page-authorize.form.start_over")}
|
||||
</mwc-button>
|
||||
@@ -188,9 +153,11 @@ export class HaAuthFlow extends LitElement {
|
||||
case "error":
|
||||
return html`
|
||||
<ha-alert alert-type="error">
|
||||
${this.localize("ui.panel.page-authorize.form.error", {
|
||||
error: this._errorMessage,
|
||||
})}
|
||||
${this.localize(
|
||||
"ui.panel.page-authorize.form.error",
|
||||
"error",
|
||||
this._errorMessage
|
||||
)}
|
||||
</ha-alert>
|
||||
<div class="action">
|
||||
<mwc-button raised @click=${this._startOver}>
|
||||
@@ -220,11 +187,6 @@ export class HaAuthFlow extends LitElement {
|
||||
`;
|
||||
case "form":
|
||||
return html`
|
||||
<h1>
|
||||
${!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? this.localize("ui.panel.page-authorize.welcome_home")
|
||||
: this.localize("ui.panel.page-authorize.just_checking")}
|
||||
</h1>
|
||||
${this._computeStepDescription(step)}
|
||||
<ha-auth-form
|
||||
.data=${this._stepData}
|
||||
@@ -238,28 +200,15 @@ export class HaAuthFlow extends LitElement {
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<div class="space-between">
|
||||
<ha-formfield
|
||||
class="store-token"
|
||||
.label=${this.localize(
|
||||
"ui.panel.page-authorize.store_token"
|
||||
)}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this.storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize(
|
||||
"ui.panel.page-authorize.forgot_password"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<ha-formfield
|
||||
class="store-token"
|
||||
.label=${this.localize("ui.panel.page-authorize.store_token")}
|
||||
>
|
||||
<ha-checkbox
|
||||
.checked=${this._storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
></ha-checkbox>
|
||||
</ha-formfield>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
@@ -269,12 +218,15 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
|
||||
private _storeTokenChanged(e: CustomEvent<HTMLInputElement>) {
|
||||
this.storeToken = (e.currentTarget as HTMLInputElement).checked;
|
||||
this._storeToken = (e.currentTarget as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
private async _providerChanged(newProvider?: AuthProvider) {
|
||||
if (this.step && this.step.type === "form") {
|
||||
deleteLoginFlow(this.step.flow_id).catch((err) => {
|
||||
if (this._step && this._step.type === "form") {
|
||||
fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "same-origin",
|
||||
}).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error delete obsoleted auth flow", err);
|
||||
});
|
||||
@@ -289,26 +241,26 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await createLoginFlow(this.clientId, this.redirectUri, [
|
||||
newProvider.type,
|
||||
newProvider.id,
|
||||
]);
|
||||
const response = await fetch("/auth/login_flow", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify({
|
||||
client_id: this.clientId,
|
||||
handler: [newProvider.type, newProvider.id],
|
||||
redirect_uri: this.redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// allow auth provider bypass the login form
|
||||
if (data.type === "create_entry") {
|
||||
redirectWithAuthCode(
|
||||
this.redirectUri!,
|
||||
data.result,
|
||||
this.oauth2State,
|
||||
this.storeToken
|
||||
);
|
||||
this._redirect(data.result);
|
||||
return;
|
||||
}
|
||||
|
||||
this.step = data;
|
||||
this._step = data;
|
||||
this._state = "step";
|
||||
} else {
|
||||
this._state = "error";
|
||||
@@ -322,6 +274,27 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _redirect(authCode: string) {
|
||||
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
|
||||
let url = this.redirectUri!;
|
||||
if (!url.includes("?")) {
|
||||
url += "?";
|
||||
} else if (!url.endsWith("&")) {
|
||||
url += "&";
|
||||
}
|
||||
|
||||
url += `code=${encodeURIComponent(authCode)}`;
|
||||
|
||||
if (this.oauth2State) {
|
||||
url += `&state=${encodeURIComponent(this.oauth2State)}`;
|
||||
}
|
||||
if (this._storeToken) {
|
||||
url += `&storeToken=true`;
|
||||
}
|
||||
|
||||
document.location.assign(url);
|
||||
}
|
||||
|
||||
private _stepDataChanged(ev: CustomEvent) {
|
||||
this._stepData = ev.detail.value;
|
||||
}
|
||||
@@ -358,10 +331,10 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
private async _handleSubmit(ev: Event) {
|
||||
ev.preventDefault();
|
||||
if (this.step == null) {
|
||||
if (this._step == null) {
|
||||
return;
|
||||
}
|
||||
if (this.step.type !== "form") {
|
||||
if (this._step.type !== "form") {
|
||||
this._providerChanged(this.authProvider);
|
||||
return;
|
||||
}
|
||||
@@ -370,7 +343,11 @@ export class HaAuthFlow extends LitElement {
|
||||
const postData = { ...this._stepData, client_id: this.clientId };
|
||||
|
||||
try {
|
||||
const response = await submitLoginFlow(this.step.flow_id, postData);
|
||||
const response = await fetch(`/auth/login_flow/${this._step.flow_id}`, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
body: JSON.stringify(postData),
|
||||
});
|
||||
|
||||
const newStep = await response.json();
|
||||
|
||||
@@ -381,15 +358,10 @@ export class HaAuthFlow extends LitElement {
|
||||
}
|
||||
|
||||
if (newStep.type === "create_entry") {
|
||||
redirectWithAuthCode(
|
||||
this.redirectUri!,
|
||||
newStep.result,
|
||||
this.oauth2State,
|
||||
this.storeToken
|
||||
);
|
||||
this._redirect(newStep.result);
|
||||
return;
|
||||
}
|
||||
this.step = newStep;
|
||||
this._step = newStep;
|
||||
this._state = "step";
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
@@ -400,10 +372,6 @@ export class HaAuthFlow extends LitElement {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _localFlow() {
|
||||
fireEvent(this, "default-login-flow", { value: false });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -41,8 +41,8 @@ export class HaAuthFormString extends HaFormString {
|
||||
!this.isPassword
|
||||
? this.stringType
|
||||
: this.unmaskedPassword
|
||||
? "text"
|
||||
: "password"
|
||||
? "text"
|
||||
: "password"
|
||||
}
|
||||
.label=${this.label}
|
||||
.value=${this.data || ""}
|
||||
|
@@ -13,7 +13,6 @@ import {
|
||||
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
|
||||
import { registerServiceWorker } from "../util/register-service-worker";
|
||||
import "./ha-auth-flow";
|
||||
import "./ha-local-auth-flow";
|
||||
|
||||
import("./ha-pick-auth-provider");
|
||||
|
||||
@@ -40,8 +39,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _forceDefaultLogin = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const query = extractSearchParamsObject() as AuthUrlSearchParams;
|
||||
@@ -63,7 +60,6 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
ha-authorize ha-alert {
|
||||
display: block;
|
||||
margin: 16px 0;
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
}
|
||||
</style>
|
||||
<ha-alert alert-type="error"
|
||||
@@ -72,7 +68,19 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
`;
|
||||
}
|
||||
|
||||
const inactiveProviders = this._authProviders?.filter(
|
||||
if (!this._authProviders) {
|
||||
return html`
|
||||
<style>
|
||||
ha-authorize p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
<p>${this.localize("ui.panel.page-authorize.initializing")}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
const inactiveProviders = this._authProviders.filter(
|
||||
(prv) => prv !== this._authProvider
|
||||
);
|
||||
|
||||
@@ -82,82 +90,20 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
<style>
|
||||
ha-pick-auth-provider {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
ha-auth-flow,
|
||||
ha-local-auth-flow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
ha-auth-flow {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
}
|
||||
ha-alert {
|
||||
display: block;
|
||||
margin: 16px 0;
|
||||
background-color: var(--primary-background-color, #fafafa);
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.card-content {
|
||||
background: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
box-shadow: var(--ha-card-box-shadow, none);
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
border-width: var(--ha-card-border-width, 1px);
|
||||
border-style: solid;
|
||||
border-color: var(
|
||||
--ha-card-border-color,
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
color: var(--primary-text-color);
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
}
|
||||
.action {
|
||||
margin: 16px 0 8px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 336px;
|
||||
justify-content: center;
|
||||
}
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.footer {
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
ha-language-picker {
|
||||
width: 200px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
--ha-select-height: 40px;
|
||||
--mdc-select-fill-color: none;
|
||||
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-ink-color: var(--primary-text-color, #212121);
|
||||
--mdc-select-idle-line-color: transparent;
|
||||
--mdc-select-hover-line-color: transparent;
|
||||
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
|
||||
--mdc-shape-small: 0;
|
||||
}
|
||||
.footer a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-text-color);
|
||||
margin-right: 16px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
${!this._ownInstance
|
||||
@@ -174,61 +120,33 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>`
|
||||
: html`<p>${this.localize("ui.panel.page-authorize.authorizing")}</p>`}
|
||||
${inactiveProviders.length > 0
|
||||
? html`<p>
|
||||
${this.localize("ui.panel.page-authorize.logging_in_with", {
|
||||
authProviderName: html`<b>${this._authProvider!.name}</b>`,
|
||||
})}
|
||||
</p>`
|
||||
: nothing}
|
||||
|
||||
<div
|
||||
class="card-content"
|
||||
@default-login-flow=${this._handleDefaultLoginFlow}
|
||||
>
|
||||
${!this._authProvider
|
||||
? html`<p>
|
||||
${this.localize("ui.panel.page-authorize.initializing")}
|
||||
</p> `
|
||||
: !this._forceDefaultLogin &&
|
||||
this._authProvider!.users &&
|
||||
this.clientId != null &&
|
||||
this.redirectUri != null
|
||||
? html`<ha-local-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.authProviders=${this._authProviders}
|
||||
.localize=${this.localize}
|
||||
.ownInstance=${this._ownInstance}
|
||||
></ha-local-auth-flow>`
|
||||
: html`<ha-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.localize=${this.localize}
|
||||
></ha-auth-flow>
|
||||
${inactiveProviders!.length > 0
|
||||
? html`
|
||||
<ha-pick-auth-provider
|
||||
.localize=${this.localize}
|
||||
.clientId=${this.clientId}
|
||||
.authProviders=${inactiveProviders}
|
||||
@pick-auth-provider=${this._handleAuthProviderPick}
|
||||
></ha-pick-auth-provider>
|
||||
`
|
||||
: ""}`}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-language-picker
|
||||
.value=${this.language}
|
||||
.label=${""}
|
||||
nativeName
|
||||
@value-changed=${this._languageChanged}
|
||||
></ha-language-picker>
|
||||
<a
|
||||
href="https://www.home-assistant.io/docs/authentication/"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize("ui.panel.page-authorize.help")}</a
|
||||
>
|
||||
</div>
|
||||
<ha-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.localize=${this.localize}
|
||||
></ha-auth-flow>
|
||||
|
||||
${inactiveProviders.length > 0
|
||||
? html`
|
||||
<ha-pick-auth-provider
|
||||
.localize=${this.localize}
|
||||
.clientId=${this.clientId}
|
||||
.authProviders=${inactiveProviders}
|
||||
@pick-auth-provider=${this._handleAuthProviderPick}
|
||||
></ha-pick-auth-provider>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -281,18 +199,12 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
if (window.innerWidth > 450) {
|
||||
import("../resources/particles");
|
||||
}
|
||||
|
||||
// If we are logging into the instance that is hosting this auth form
|
||||
// we will register the service worker to start preloading.
|
||||
if (url.host === location.host) {
|
||||
this._ownInstance = true;
|
||||
registerServiceWorker(this, false);
|
||||
}
|
||||
|
||||
import("../components/ha-language-picker");
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -333,22 +245,7 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDefaultLoginFlow(ev) {
|
||||
this._forceDefaultLogin = ev.detail.value;
|
||||
}
|
||||
|
||||
private async _handleAuthProviderPick(ev) {
|
||||
this._authProvider = ev.detail;
|
||||
}
|
||||
|
||||
private _languageChanged(ev: CustomEvent) {
|
||||
const language = ev.detail.value;
|
||||
this.language = language;
|
||||
|
||||
try {
|
||||
localStorage.setItem("selectedLanguage", JSON.stringify(language));
|
||||
} catch (err: any) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,485 +0,0 @@
|
||||
/* eslint-disable lit/prefer-static-styles */
|
||||
import "@material/mwc-button";
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/user/ha-person-badge";
|
||||
import {
|
||||
AuthProvider,
|
||||
createLoginFlow,
|
||||
deleteLoginFlow,
|
||||
redirectWithAuthCode,
|
||||
submitLoginFlow,
|
||||
} from "../data/auth";
|
||||
import { DataEntryFlowStep } from "../data/data_entry_flow";
|
||||
import { BasePerson, listUserPersons } from "../data/person";
|
||||
import "./ha-auth-textfield";
|
||||
import type { HaAuthTextField } from "./ha-auth-textfield";
|
||||
|
||||
@customElement("ha-local-auth-flow")
|
||||
export class HaLocalAuthFlow extends LitElement {
|
||||
@property({ attribute: false }) public authProvider?: AuthProvider;
|
||||
|
||||
@property({ attribute: false }) public authProviders?: AuthProvider[];
|
||||
|
||||
@property() public clientId?: string;
|
||||
|
||||
@property() public redirectUri?: string;
|
||||
|
||||
@property() public oauth2State?: string;
|
||||
|
||||
@property({ type: Boolean }) public ownInstance = false;
|
||||
|
||||
@property() public localize!: LocalizeFunc;
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
@state() private _step?: DataEntryFlowStep;
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _persons?: Record<string, BasePerson>;
|
||||
|
||||
@state() private _selectedUser?: string;
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._load();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.authProvider?.users || !this._persons) {
|
||||
return nothing;
|
||||
}
|
||||
const userIds = Object.keys(this.authProvider.users).filter(
|
||||
(userId) => userId in this._persons!
|
||||
);
|
||||
return html`
|
||||
<style>
|
||||
.content {
|
||||
max-width: 560px;
|
||||
}
|
||||
.persons {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
}
|
||||
.persons.force-small {
|
||||
max-width: 350px;
|
||||
}
|
||||
.person {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
width: 80px;
|
||||
}
|
||||
.person[role="button"] {
|
||||
outline: none;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.person[role="button"]:focus-visible {
|
||||
background: rgba(var(--rgb-primary-color), 0.1);
|
||||
}
|
||||
.person p {
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
ha-person-badge {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
--person-badge-font-size: 2em;
|
||||
}
|
||||
form {
|
||||
width: 100%;
|
||||
}
|
||||
ha-auth-textfield {
|
||||
display: block !important;
|
||||
position: relative;
|
||||
}
|
||||
ha-auth-textfield ha-icon-button {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 9;
|
||||
}
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 336px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.login-form .person {
|
||||
cursor: default;
|
||||
width: auto;
|
||||
}
|
||||
.login-form .person p {
|
||||
font-size: 28px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 32px;
|
||||
line-height: normal;
|
||||
}
|
||||
.login-form ha-person-badge {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
--person-badge-font-size: 3em;
|
||||
}
|
||||
ha-list-item {
|
||||
margin-top: 16px;
|
||||
}
|
||||
ha-button {
|
||||
--mdc-typography-button-text-transform: none;
|
||||
}
|
||||
.forgot-password-container {
|
||||
text-align: right;
|
||||
padding: 8px 0 16px 0;
|
||||
}
|
||||
a.forgot-password {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
button {
|
||||
color: var(--primary-color);
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button:focus-visible {
|
||||
background: rgba(var(--rgb-primary-color), 0.1);
|
||||
}
|
||||
</style>
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: nothing}
|
||||
${this._step
|
||||
? html`<ha-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.step=${this._step}
|
||||
storeToken
|
||||
.localize=${this.localize}
|
||||
></ha-auth-flow>`
|
||||
: this._selectedUser
|
||||
? html`<div class="login-form">
|
||||
<div class="person">
|
||||
<ha-person-badge
|
||||
.person=${this._persons[this._selectedUser]}
|
||||
></ha-person-badge>
|
||||
<p>${this._persons[this._selectedUser].name}</p>
|
||||
</div>
|
||||
<form>
|
||||
<input
|
||||
type="hidden"
|
||||
name="username"
|
||||
autocomplete="username"
|
||||
readonly
|
||||
.value=${this.authProvider.users[this._selectedUser]}
|
||||
/>
|
||||
<ha-auth-textfield
|
||||
.type=${this._unmaskedPassword ? "text" : "password"}
|
||||
autocomplete="current-password"
|
||||
id="password"
|
||||
name="password"
|
||||
.label=${this.localize(
|
||||
"ui.panel.page-authorize.form.providers.homeassistant.step.init.data.password"
|
||||
)}
|
||||
required
|
||||
autoValidate
|
||||
iconTrailing
|
||||
validationMessage="Required"
|
||||
>
|
||||
<ha-icon-button
|
||||
toggles
|
||||
.label=${this.localize(
|
||||
this._unmaskedPassword
|
||||
? "ui.panel.page-authorize.form.hide_password"
|
||||
: "ui.panel.page-authorize.form.show_password"
|
||||
) ||
|
||||
(this._unmaskedPassword
|
||||
? "Hide password"
|
||||
: "Show password")}
|
||||
@click=${this._toggleUnmaskedPassword}
|
||||
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
|
||||
></ha-icon-button>
|
||||
</ha-auth-textfield>
|
||||
<div class="forgot-password-container">
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>${this.localize(
|
||||
"ui.panel.page-authorize.forgot_password"
|
||||
)}</a
|
||||
>
|
||||
</div>
|
||||
<div class="action space-between">
|
||||
<mwc-button
|
||||
@click=${this._restart}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.localize("ui.panel.page-authorize.form.previous")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
raised
|
||||
@click=${this._handleSubmit}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.localize("ui.panel.page-authorize.form.next")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>`
|
||||
: html`<h1>
|
||||
${this.localize("ui.panel.page-authorize.welcome_home")}
|
||||
</h1>
|
||||
${this.localize("ui.panel.page-authorize.who_is_logging_in")}
|
||||
<div
|
||||
class="persons ${userIds.length < 10 && userIds.length % 4 === 1
|
||||
? "force-small"
|
||||
: ""}"
|
||||
>
|
||||
${userIds.map((userId) => {
|
||||
const person = this._persons![userId];
|
||||
|
||||
return html`<div
|
||||
class="person"
|
||||
.userId=${userId}
|
||||
@click=${this._personSelected}
|
||||
@keyup=${this._handleKeyUp}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<ha-person-badge .person=${person}></ha-person-badge>
|
||||
<p>${person.name}</p>
|
||||
</div>`;
|
||||
})}
|
||||
</div>
|
||||
<div class="action">
|
||||
<button @click=${this._otherLogin} tabindex="0">
|
||||
${this.localize("ui.panel.page-authorize.other_options")}
|
||||
</button>
|
||||
</div>`}
|
||||
`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
this.addEventListener("keypress", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
this._handleSubmit(ev);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("_selectedUser") && this._selectedUser) {
|
||||
const passwordElement = this.renderRoot.querySelector(
|
||||
"#password"
|
||||
) as HaAuthTextField;
|
||||
passwordElement.updateComplete.then(() => {
|
||||
passwordElement.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _load() {
|
||||
try {
|
||||
this._persons = await listUserPersons();
|
||||
} catch {
|
||||
this._persons = {};
|
||||
this._error = "Failed to fetch persons";
|
||||
}
|
||||
}
|
||||
|
||||
private _restart() {
|
||||
this._selectedUser = undefined;
|
||||
this._error = undefined;
|
||||
}
|
||||
|
||||
private _toggleUnmaskedPassword() {
|
||||
this._unmaskedPassword = !this._unmaskedPassword;
|
||||
}
|
||||
|
||||
private _handleKeyUp(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
this._personSelected(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private async _personSelected(ev) {
|
||||
const userId = ev.currentTarget.userId;
|
||||
if (
|
||||
this.ownInstance &&
|
||||
this.authProviders?.find((prv) => prv.type === "trusted_networks")
|
||||
) {
|
||||
try {
|
||||
const flowResponse = await createLoginFlow(
|
||||
this.clientId,
|
||||
this.redirectUri,
|
||||
["trusted_networks", null]
|
||||
);
|
||||
|
||||
const data = await flowResponse.json();
|
||||
|
||||
if (data.type === "create_entry") {
|
||||
redirectWithAuthCode(
|
||||
this.redirectUri!,
|
||||
data.result,
|
||||
this.oauth2State,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!data.data_schema[0].options.find((opt) => opt[0] === userId)) {
|
||||
throw new Error("User not available");
|
||||
}
|
||||
|
||||
const postData = { user: userId, client_id: this.clientId };
|
||||
|
||||
const response = await submitLoginFlow(data.flow_id, postData);
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.type === "create_entry") {
|
||||
redirectWithAuthCode(
|
||||
this.redirectUri!,
|
||||
result.result,
|
||||
this.oauth2State,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Invalid response");
|
||||
}
|
||||
} catch {
|
||||
deleteLoginFlow(data.flow_id).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error delete obsoleted auth flow", err);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
this._selectedUser = userId;
|
||||
}
|
||||
|
||||
private async _handleSubmit(ev: Event) {
|
||||
ev.preventDefault();
|
||||
|
||||
if (!this.authProvider?.users || !this._selectedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._error = undefined;
|
||||
this._submitting = true;
|
||||
|
||||
const flowResponse = await createLoginFlow(
|
||||
this.clientId,
|
||||
this.redirectUri,
|
||||
["homeassistant", null]
|
||||
);
|
||||
|
||||
const data = await flowResponse.json();
|
||||
|
||||
const postData = {
|
||||
username: this.authProvider.users[this._selectedUser],
|
||||
password: (this.renderRoot.querySelector("#password") as HaAuthTextField)
|
||||
.value,
|
||||
client_id: this.clientId,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await submitLoginFlow(data.flow_id, postData);
|
||||
|
||||
const newStep = await response.json();
|
||||
|
||||
if (response.status === 403) {
|
||||
this._error = newStep.message;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newStep.type === "create_entry") {
|
||||
redirectWithAuthCode(
|
||||
this.redirectUri!,
|
||||
newStep.result,
|
||||
this.oauth2State,
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (newStep.errors.base) {
|
||||
this._error = this.localize(
|
||||
`ui.panel.page-authorize.form.providers.homeassistant.error.${newStep.errors.base}`
|
||||
);
|
||||
throw new Error(this._error);
|
||||
}
|
||||
|
||||
this._step = newStep;
|
||||
} catch {
|
||||
deleteLoginFlow(data.flow_id).catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error delete obsoleted auth flow", err);
|
||||
});
|
||||
if (!this._error) {
|
||||
this._error = this.localize(
|
||||
"ui.panel.page-authorize.form.unknown_error"
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this._submitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _otherLogin() {
|
||||
fireEvent(this, "default-login-flow", { value: true });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-local-auth-flow": HaLocalAuthFlow;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"default-login-flow": { value: boolean };
|
||||
}
|
||||
}
|
@@ -21,11 +21,7 @@ export class HaPickAuthProvider extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<h3>
|
||||
<span
|
||||
>${this.localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>
|
||||
</h3>
|
||||
<p>${this.localize("ui.panel.page-authorize.pick_auth_provider")}:</p>
|
||||
<mwc-list>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
@@ -39,8 +35,8 @@ export class HaPickAuthProvider extends LitElement {
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
)}</mwc-list
|
||||
>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -49,34 +45,12 @@ export class HaPickAuthProvider extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
h3 {
|
||||
margin: 0 -16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
}
|
||||
h3:before {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
content: "";
|
||||
margin: 0 auto;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
h3 span {
|
||||
background: var(--card-background-color);
|
||||
padding: 0 15px;
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
mwc-list {
|
||||
margin: 16px -16px 0;
|
||||
--mdc-list-side-padding: 24px;
|
||||
margin: 0 -16px;
|
||||
--mdc-list-side-padding: 16px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -10,10 +10,10 @@ const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) =>
|
||||
page.component
|
||||
? isComponentLoaded(hass, page.component)
|
||||
: page.components
|
||||
? page.components.some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
)
|
||||
: true;
|
||||
? page.components.some((integration) =>
|
||||
isComponentLoaded(hass, integration)
|
||||
)
|
||||
: true;
|
||||
const isCore = (page: PageNavigation) => page.core;
|
||||
const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
|
||||
const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced;
|
||||
|
@@ -203,7 +203,6 @@ export const DOMAINS_WITH_CARD = [
|
||||
"select",
|
||||
"timer",
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
"water_heater",
|
||||
];
|
||||
|
33
src/common/dom/dynamic_content_updater.ts
Normal file
33
src/common/dom/dynamic_content_updater.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Update root's child element to be newElementTag replacing another existing child if any.
|
||||
* Copy attributes into the child element.
|
||||
*/
|
||||
export default function dynamicContentUpdater(root, newElementTag, attributes) {
|
||||
const rootEl = root;
|
||||
let customEl;
|
||||
|
||||
if (rootEl.lastChild && rootEl.lastChild.tagName === newElementTag) {
|
||||
customEl = rootEl.lastChild;
|
||||
} else {
|
||||
if (rootEl.lastChild) {
|
||||
rootEl.removeChild(rootEl.lastChild);
|
||||
}
|
||||
// Creating an element with upper case works fine in Chrome, but in FF it doesn't immediately
|
||||
// become a defined Custom Element. Polymer does that in some later pass.
|
||||
customEl = document.createElement(newElementTag.toLowerCase());
|
||||
}
|
||||
|
||||
if (customEl.setProperties) {
|
||||
customEl.setProperties(attributes);
|
||||
} else {
|
||||
// If custom element definition wasn't loaded yet - setProperties would be
|
||||
// missing, but no harm in setting attributes one-by-one then.
|
||||
Object.keys(attributes).forEach((key) => {
|
||||
customEl[key] = attributes[key];
|
||||
});
|
||||
}
|
||||
|
||||
if (customEl.parentNode === null) {
|
||||
rootEl.appendChild(customEl);
|
||||
}
|
||||
}
|
@@ -4,5 +4,5 @@ export const mainWindow =
|
||||
window.name === MAIN_WINDOW_NAME
|
||||
? window
|
||||
: parent.name === MAIN_WINDOW_NAME
|
||||
? parent
|
||||
: top!;
|
||||
? parent
|
||||
: top!;
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
DOMAIN_ATTRIBUTES_FORMATERS,
|
||||
DOMAIN_ATTRIBUTES_UNITS,
|
||||
TEMPERATURE_ATTRIBUTES,
|
||||
} from "../../data/entity_attributes";
|
||||
@@ -15,10 +14,11 @@ import { formatNumber } from "../number/format_number";
|
||||
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
||||
import { isDate } from "../string/is_date";
|
||||
import { isTimestamp } from "../string/is_timestamp";
|
||||
import { blankBeforeUnit } from "../translations/blank_before_unit";
|
||||
import { blankBeforePercent } from "../translations/blank_before_percent";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { blankBeforeUnit } from "../translations/blank_before_unit";
|
||||
|
||||
export const computeAttributeValueDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -39,18 +39,19 @@ export const computeAttributeValueDisplay = (
|
||||
|
||||
// Number value, return formatted number
|
||||
if (typeof attributeValue === "number") {
|
||||
const formattedValue = formatNumber(attributeValue, locale);
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
const formatter = DOMAIN_ATTRIBUTES_FORMATERS[domain]?.[attribute];
|
||||
|
||||
const formattedValue = formatter
|
||||
? formatter(attributeValue, locale)
|
||||
: formatNumber(attributeValue, locale);
|
||||
|
||||
let unit = DOMAIN_ATTRIBUTES_UNITS[domain]?.[attribute] as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
if (domain === "light" && attribute === "brightness") {
|
||||
const percentage = Math.round((attributeValue / 255) * 100);
|
||||
return `${percentage}${blankBeforePercent(locale)}%`;
|
||||
}
|
||||
|
||||
if (domain === "weather") {
|
||||
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
|
||||
}
|
||||
|
@@ -2,6 +2,10 @@ import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||
import {
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
updateIsInstallingFromAttributes,
|
||||
} from "../../data/update";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import {
|
||||
UNIT_TO_MILLISECOND_CONVERT,
|
||||
@@ -15,9 +19,10 @@ import {
|
||||
getNumberFormatOptions,
|
||||
isNumericFromAttributes,
|
||||
} from "../number/format_number";
|
||||
import { blankBeforeUnit } from "../translations/blank_before_unit";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeDomain } from "./compute_domain";
|
||||
import { supportsFeatureFromAttributes } from "./supports-feature";
|
||||
import { blankBeforeUnit } from "../translations/blank_before_unit";
|
||||
|
||||
export const computeStateDisplaySingleEntity = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -203,6 +208,27 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "update") {
|
||||
// When updating, and entity does not support % show "Installing"
|
||||
// When updating, and entity does support % show "Installing (xx%)"
|
||||
// When update available, show the version
|
||||
// When the latest version is skipped, show the latest version
|
||||
// When update is not available, show "Up-to-date"
|
||||
// When update is not available and there is no latest_version show "Unavailable"
|
||||
return state === "on"
|
||||
? updateIsInstallingFromAttributes(attributes)
|
||||
? supportsFeatureFromAttributes(attributes, UPDATE_SUPPORT_PROGRESS) &&
|
||||
typeof attributes.in_progress === "number"
|
||||
? localize("ui.card.update.installing_with_progress", {
|
||||
progress: attributes.in_progress,
|
||||
})
|
||||
: localize("ui.card.update.installing")
|
||||
: attributes.latest_version
|
||||
: attributes.skipped_version === attributes.latest_version
|
||||
? attributes.latest_version ?? localize("state.default.unavailable")
|
||||
: localize("ui.card.update.up_to_date");
|
||||
}
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
|
@@ -100,8 +100,8 @@ export const domainIconWithoutDefault = (
|
||||
return compareState === "unavailable"
|
||||
? mdiRobotConfused
|
||||
: compareState === "off"
|
||||
? mdiRobotOff
|
||||
: mdiRobot;
|
||||
? mdiRobotOff
|
||||
: mdiRobot;
|
||||
|
||||
case "binary_sensor":
|
||||
return binarySensorIcon(compareState, stateObj);
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import type { HTMLTemplateResult } from "lit";
|
||||
import { polyfillLocaleData } from "../../resources/locale-data-polyfill";
|
||||
import { Resources, TranslationDict } from "../../types";
|
||||
|
||||
@@ -41,13 +40,9 @@ export type FlattenObjectKeys<
|
||||
: `${Key}`
|
||||
: never;
|
||||
|
||||
// Later, don't return string when HTML is passed, and don't allow undefined
|
||||
export type LocalizeFunc<Keys extends string = LocalizeKeys> = (
|
||||
key: Keys,
|
||||
values?: Record<
|
||||
string,
|
||||
string | number | HTMLTemplateResult | null | undefined
|
||||
>
|
||||
...args: any[]
|
||||
) => string;
|
||||
|
||||
interface FormatType {
|
||||
@@ -129,7 +124,6 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
argObject = args[0];
|
||||
} else {
|
||||
for (let i = 0; i < args.length; i += 2) {
|
||||
// @ts-expect-error in some places the old format (key, value, key, value) is used
|
||||
argObject[args[i]] = args[i + 1];
|
||||
}
|
||||
}
|
||||
|
@@ -40,15 +40,15 @@ export class HaProgressButton extends LitElement {
|
||||
${this._result === "success"
|
||||
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
|
||||
: this._result === "error"
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
|
||||
: this.progress
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
indeterminate
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
|
||||
: this.progress
|
||||
? html`
|
||||
<ha-circular-progress
|
||||
size="small"
|
||||
active
|
||||
></ha-circular-progress>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
|
@@ -469,7 +469,6 @@ export class HaChartBase extends LitElement {
|
||||
.chartTooltip li {
|
||||
display: flex;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
align-items: center;
|
||||
line-height: 16px;
|
||||
padding: 4px 0;
|
||||
@@ -477,7 +476,6 @@ export class HaChartBase extends LitElement {
|
||||
.chartTooltip .title {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
direction: ltr;
|
||||
}
|
||||
.chartTooltip .footer {
|
||||
|
@@ -45,14 +45,10 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ type: Number }) public chartIndex?;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
|
||||
@state() private _chartData?: ChartData<"line">;
|
||||
|
||||
@state() private _entityIds: string[] = [];
|
||||
|
||||
private _datasetToDataIndex: number[] = [];
|
||||
|
||||
@state() private _chartOptions?: ChartOptions;
|
||||
|
||||
@state() private _yWidth = 0;
|
||||
@@ -82,9 +78,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
changedProps.has("unit") ||
|
||||
changedProps.has("logarithmicScale")
|
||||
changedProps.has("endTime")
|
||||
) {
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
@@ -138,38 +132,20 @@ export class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
},
|
||||
position: computeRTL(this.hass) ? "right" : "left",
|
||||
type: this.logarithmicScale ? "logarithmic" : "linear",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
let label = `${context.dataset.label}: ${formatNumber(
|
||||
label: (context) =>
|
||||
`${context.dataset.label}: ${formatNumber(
|
||||
context.parsed.y,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(
|
||||
undefined,
|
||||
this.hass.entities[this._entityIds[context.datasetIndex]]
|
||||
)
|
||||
)} ${this.unit}`;
|
||||
const dataIndex =
|
||||
this._datasetToDataIndex[context.datasetIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
context.parsed.x < data.states[0].last_changed
|
||||
? `\n${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `\n${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
label += source;
|
||||
}
|
||||
return label;
|
||||
},
|
||||
)} ${this.unit}`,
|
||||
},
|
||||
},
|
||||
filler: {
|
||||
@@ -191,19 +167,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
hitRadius: 50,
|
||||
},
|
||||
},
|
||||
segment: {
|
||||
borderColor: (context) => {
|
||||
// render stat data with a slightly transparent line
|
||||
const dataIndex = this._datasetToDataIndex[context.datasetIndex];
|
||||
const data = this.data[dataIndex];
|
||||
return data.statistics &&
|
||||
data.statistics.length > 0 &&
|
||||
(data.states.length === 0 ||
|
||||
context.p0.parsed.x < data.states[0].last_changed)
|
||||
? this._chartData!.datasets[dataIndex].borderColor + "7F"
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
@@ -249,7 +212,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
const entityStates = this.data;
|
||||
const datasets: ChartDataset<"line">[] = [];
|
||||
const entityIds: string[] = [];
|
||||
const datasetToDataIndex: number[] = [];
|
||||
if (entityStates.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -257,7 +219,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
this._chartTime = new Date();
|
||||
const endTime = this.endTime;
|
||||
const names = this.names || {};
|
||||
entityStates.forEach((states, dataIdx) => {
|
||||
entityStates.forEach((states) => {
|
||||
const domain = states.domain;
|
||||
const name = names[states.entity_id] || states.name;
|
||||
// array containing [value1, value2, etc]
|
||||
@@ -302,7 +264,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
data: [],
|
||||
});
|
||||
entityIds.push(states.entity_id);
|
||||
datasetToDataIndex.push(dataIdx);
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -509,7 +470,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
// Process chart data.
|
||||
// When state is `unknown`, calculate the value and break the line.
|
||||
const processData = (entityState: LineChartState) => {
|
||||
states.states.forEach((entityState) => {
|
||||
const value = safeParseFloat(entityState.state);
|
||||
const date = new Date(entityState.last_changed);
|
||||
if (value !== null && lastNullDate) {
|
||||
@@ -538,22 +499,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
) {
|
||||
lastNullDate = date;
|
||||
}
|
||||
};
|
||||
|
||||
if (states.statistics) {
|
||||
const stopTime =
|
||||
!states.states || states.states.length === 0
|
||||
? 0
|
||||
: states.states[0].last_changed;
|
||||
for (let i = 0; i < states.statistics.length; i++) {
|
||||
if (stopTime && states.statistics[i].last_changed >= stopTime) {
|
||||
break;
|
||||
}
|
||||
processData(states.statistics[i]);
|
||||
}
|
||||
}
|
||||
states.states.forEach((entityState) => {
|
||||
processData(entityState);
|
||||
});
|
||||
if (lastNullDate !== null) {
|
||||
pushData(lastNullDate, [null]);
|
||||
@@ -571,7 +516,6 @@ export class StateHistoryChartLine extends LitElement {
|
||||
datasets,
|
||||
};
|
||||
this._entityIds = entityIds;
|
||||
this._datasetToDataIndex = datasetToDataIndex;
|
||||
}
|
||||
}
|
||||
customElements.define("state-history-chart-line", StateHistoryChartLine);
|
||||
|
@@ -161,8 +161,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
const yWidth = this.showNames
|
||||
? y.width ?? 0
|
||||
: computeRTL(this.hass)
|
||||
? 0
|
||||
: y.left ?? 0;
|
||||
? 0
|
||||
: y.left ?? 0;
|
||||
if (
|
||||
this._yWidth !== Math.floor(yWidth) &&
|
||||
y.ticks.length === this.data.length
|
||||
|
@@ -73,8 +73,6 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
|
||||
private _computedStartTime!: Date;
|
||||
|
||||
private _computedEndTime!: Date;
|
||||
@@ -161,7 +159,6 @@ export class StateHistoryCharts extends LitElement {
|
||||
.names=${this.names}
|
||||
.chartIndex=${index}
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
.logarithmicScale=${this.logarithmicScale}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
@@ -303,11 +300,6 @@ export class StateHistoryCharts extends LitElement {
|
||||
padding-right: 1px;
|
||||
}
|
||||
|
||||
.entry-container:not(:first-child) {
|
||||
border-top: 2px solid var(--divider-color);
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.container,
|
||||
lit-virtualizer {
|
||||
height: 100%;
|
||||
|
@@ -71,8 +71,6 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public hideLegend = false;
|
||||
|
||||
@property({ type: Boolean }) public logarithmicScale = false;
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
|
||||
@property() public period?: string;
|
||||
@@ -100,8 +98,7 @@ export class StatisticsChart extends LitElement {
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("unit") ||
|
||||
changedProps.has("period") ||
|
||||
changedProps.has("chartType") ||
|
||||
changedProps.has("logarithmicScale")
|
||||
changedProps.has("chartType")
|
||||
) {
|
||||
this._createOptions();
|
||||
}
|
||||
@@ -201,7 +198,6 @@ export class StatisticsChart extends LitElement {
|
||||
display: unit || this.unit,
|
||||
text: unit || this.unit,
|
||||
},
|
||||
type: this.logarithmicScale ? "logarithmic" : "linear",
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
@@ -400,8 +396,8 @@ export class StatisticsChart extends LitElement {
|
||||
? type === "min" && hasMean
|
||||
? "+1"
|
||||
: type === "max"
|
||||
? "-1"
|
||||
: false
|
||||
? "-1"
|
||||
: false
|
||||
: false,
|
||||
borderColor:
|
||||
band && hasMean ? color + (this.hideLegend ? "00" : "7F") : color,
|
||||
|
@@ -40,8 +40,8 @@ export class TextBarElement extends BarElement {
|
||||
(options?.backgroundColor === "transparent"
|
||||
? "transparent"
|
||||
: luminosity(hex2rgb(options.backgroundColor)) > 0.5
|
||||
? "#000"
|
||||
: "#fff");
|
||||
? "#000"
|
||||
: "#fff");
|
||||
|
||||
// ctx.font = "12px arial";
|
||||
ctx.fillStyle = textColor;
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "element-internals-polyfill";
|
||||
import { MdAssistChip } from "@material/web/chips/assist-chip";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "element-internals-polyfill";
|
||||
import { MdChipSet } from "@material/web/chips/chip-set";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "element-internals-polyfill";
|
||||
import { MdFilterChip } from "@material/web/chips/filter-chip";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
@@ -1,4 +1,3 @@
|
||||
import "element-internals-polyfill";
|
||||
import { MdInputChip } from "@material/web/chips/input-chip";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
@@ -1,4 +1,6 @@
|
||||
// To use comlink under ES5
|
||||
import { expose } from "comlink";
|
||||
import "proxy-polyfill";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import type {
|
||||
ClonedDataTableColumnData,
|
||||
|
325
src/components/device/ha-area-devices-picker.ts
Normal file
325
src/components/device/ha-area-devices-picker.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import {
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-devices-picker";
|
||||
|
||||
interface DevicesByArea {
|
||||
[areaId: string]: AreaDevices;
|
||||
}
|
||||
|
||||
interface AreaDevices {
|
||||
id?: string;
|
||||
name: string;
|
||||
devices: string[];
|
||||
}
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaDevices> = (item) =>
|
||||
html`<mwc-list-item twoline>
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary">${item.devices.length} devices</span>
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-area-devices-picker")
|
||||
export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@property() public area?: string;
|
||||
|
||||
@property() public devices?: string[];
|
||||
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
* @type {Array}
|
||||
* @attr include-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-domains" })
|
||||
public includeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show no devices with entities of these domains.
|
||||
* @type {Array}
|
||||
* @attr exclude-domains
|
||||
*/
|
||||
@property({ type: Array, attribute: "exclude-domains" })
|
||||
public excludeDomains?: string[];
|
||||
|
||||
/**
|
||||
* Show only deviced with entities of these device classes.
|
||||
* @type {Array}
|
||||
* @attr include-device-classes
|
||||
*/
|
||||
@property({ type: Array, attribute: "include-device-classes" })
|
||||
public includeDeviceClasses?: string[];
|
||||
|
||||
@state() private _areaPicker = true;
|
||||
|
||||
@state() private _devices?: DeviceRegistryEntry[];
|
||||
|
||||
@state() private _areas?: AreaRegistryEntry[];
|
||||
|
||||
@state() private _entities?: EntityRegistryEntry[];
|
||||
|
||||
private _selectedDevices: string[] = [];
|
||||
|
||||
private _filteredDevices: DeviceRegistryEntry[] = [];
|
||||
|
||||
private _getAreasWithDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"]
|
||||
): AreaDevices[] => {
|
||||
if (!devices.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
deviceEntityLookup[entity.device_id] = [];
|
||||
}
|
||||
deviceEntityLookup[entity.device_id].push(entity);
|
||||
}
|
||||
|
||||
let inputDevices = [...devices];
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._filteredDevices = inputDevices;
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
const devicesByArea: DevicesByArea = {};
|
||||
|
||||
for (const device of inputDevices) {
|
||||
const areaId = device.area_id;
|
||||
if (areaId) {
|
||||
if (!(areaId in devicesByArea)) {
|
||||
devicesByArea[areaId] = {
|
||||
id: areaId,
|
||||
name: areaLookup[areaId].name,
|
||||
devices: [],
|
||||
};
|
||||
}
|
||||
devicesByArea[areaId].devices.push(device.id);
|
||||
}
|
||||
}
|
||||
|
||||
const sorted = Object.keys(devicesByArea)
|
||||
.sort((a, b) =>
|
||||
stringCompare(
|
||||
devicesByArea[a].name || "",
|
||||
devicesByArea[b].name || "",
|
||||
this.hass.locale.language
|
||||
)
|
||||
)
|
||||
.map((key) => devicesByArea[key]);
|
||||
|
||||
return sorted;
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this._devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this._areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this._entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("area") && this.area) {
|
||||
this._areaPicker = true;
|
||||
this.value = this.area;
|
||||
} else if (changedProps.has("devices") && this.devices) {
|
||||
this._areaPicker = false;
|
||||
const filteredDeviceIds = this._filteredDevices.map(
|
||||
(device) => device.id
|
||||
);
|
||||
const selectedDevices = this.devices.filter((device) =>
|
||||
filteredDeviceIds.includes(device)
|
||||
);
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._devices || !this._areas || !this._entities) {
|
||||
return nothing;
|
||||
}
|
||||
const areas = this._getAreasWithDevices(
|
||||
this._devices,
|
||||
this._areas,
|
||||
this._entities,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
if (!this._areaPicker || areas.length === 0) {
|
||||
return html`
|
||||
<ha-devices-picker
|
||||
@value-changed=${this._devicesPicked}
|
||||
.hass=${this.hass}
|
||||
.includeDomains=${this.includeDomains}
|
||||
.includeDeviceClasses=${this.includeDeviceClasses}
|
||||
.value=${this._selectedDevices}
|
||||
.pickDeviceLabel=${`Add ${this.label} device`}
|
||||
.pickedDeviceLabel=${`${this.label} device`}
|
||||
></ha-devices-picker>
|
||||
${areas.length > 0
|
||||
? html`
|
||||
<mwc-button @click=${this._switchPicker}
|
||||
>Choose an area</mwc-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
item-value-path="id"
|
||||
item-id-path="id"
|
||||
item-label-path="name"
|
||||
.items=${areas}
|
||||
.value=${this._value}
|
||||
.renderer=${rowRenderer}
|
||||
.label=${this.label === undefined && this.hass
|
||||
? this.hass.localize("ui.components.device-picker.device")
|
||||
: `${this.label} in area`}
|
||||
@value-changed=${this._areaPicked}
|
||||
>
|
||||
</ha-combo-box>
|
||||
<mwc-button @click=${this._switchPicker}>
|
||||
Choose individual devices
|
||||
</mwc-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _switchPicker() {
|
||||
this._areaPicker = !this._areaPicker;
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: ValueChangedEvent<string>) {
|
||||
const value = ev.detail.value;
|
||||
let selectedDevices = [];
|
||||
const target = ev.target as any;
|
||||
if (target.selectedItem) {
|
||||
selectedDevices = target.selectedItem.devices;
|
||||
}
|
||||
|
||||
if (value !== this._value || this._selectedDevices !== selectedDevices) {
|
||||
this._setValue(selectedDevices, value);
|
||||
}
|
||||
}
|
||||
|
||||
private _devicesPicked(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const selectedDevices = ev.detail.value;
|
||||
this._setValue(selectedDevices);
|
||||
}
|
||||
|
||||
private _setValue(selectedDevices: string[], value = "") {
|
||||
this.value = value;
|
||||
this._selectedDevices = selectedDevices;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: selectedDevices });
|
||||
fireEvent(this, "change");
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-devices-picker": HaAreaDevicesPicker;
|
||||
}
|
||||
}
|
@@ -1,26 +1,35 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
ScorableTextItem,
|
||||
fuzzyFilterSort,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
AreaRegistryEntry,
|
||||
subscribeAreaRegistry,
|
||||
} from "../../data/area_registry";
|
||||
import {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
computeDeviceName,
|
||||
getDeviceEntityDisplayLookup,
|
||||
DeviceEntityLookup,
|
||||
DeviceRegistryEntry,
|
||||
getDeviceEntityLookup,
|
||||
subscribeDeviceRegistry,
|
||||
} from "../../data/device_registry";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import {
|
||||
EntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../data/entity_registry";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-list-item";
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
@@ -37,13 +46,13 @@ export type HaDevicePickerDeviceFilterFunc = (
|
||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) =>
|
||||
html`<ha-list-item .twoline=${!!item.area}>
|
||||
html`<mwc-list-item .twoline=${!!item.area}>
|
||||
<span>${item.name}</span>
|
||||
<span slot="secondary">${item.area}</span>
|
||||
</ha-list-item>`;
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-device-picker")
|
||||
export class HaDevicePicker extends LitElement {
|
||||
export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -52,6 +61,12 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property() public devices?: DeviceRegistryEntry[];
|
||||
|
||||
@property() public areas?: AreaRegistryEntry[];
|
||||
|
||||
@property() public entities?: EntityRegistryEntry[];
|
||||
|
||||
/**
|
||||
* Show only devices with entities from specific domains.
|
||||
* @type {Array}
|
||||
@@ -101,8 +116,8 @@ export class HaDevicePicker extends LitElement {
|
||||
private _getDevices = memoizeOne(
|
||||
(
|
||||
devices: DeviceRegistryEntry[],
|
||||
areas: HomeAssistant["areas"],
|
||||
entities: EntityRegistryDisplayEntry[],
|
||||
areas: AreaRegistryEntry[],
|
||||
entities: EntityRegistryEntry[],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
@@ -121,7 +136,7 @@ export class HaDevicePicker extends LitElement {
|
||||
];
|
||||
}
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let deviceEntityLookup: DeviceEntityLookup = {};
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
@@ -129,7 +144,12 @@ export class HaDevicePicker extends LitElement {
|
||||
includeDeviceClasses ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
deviceEntityLookup = getDeviceEntityLookup(entities);
|
||||
}
|
||||
|
||||
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
|
||||
for (const area of areas) {
|
||||
areaLookup[area.area_id] = area;
|
||||
}
|
||||
|
||||
let inputDevices = devices.filter(
|
||||
@@ -221,8 +241,8 @@ export class HaDevicePicker extends LitElement {
|
||||
id: device.id,
|
||||
name: name,
|
||||
area:
|
||||
device.area_id && areas[device.area_id]
|
||||
? areas[device.area_id].name
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
strings: [name || ""],
|
||||
};
|
||||
@@ -256,16 +276,30 @@ export class HaDevicePicker extends LitElement {
|
||||
await this.comboBox?.focus();
|
||||
}
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeDeviceRegistry(this.hass.connection!, (devices) => {
|
||||
this.devices = devices;
|
||||
}),
|
||||
subscribeAreaRegistry(this.hass.connection!, (areas) => {
|
||||
this.areas = areas;
|
||||
}),
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
this.entities = entities;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (
|
||||
(!this._init && this.hass) ||
|
||||
(!this._init && this.devices && this.areas && this.entities) ||
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
const devices = this._getDevices(
|
||||
Object.values(this.hass.devices),
|
||||
this.hass.areas,
|
||||
Object.values(this.hass.entities),
|
||||
this.devices!,
|
||||
this.areas!,
|
||||
this.entities!,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
|
@@ -112,9 +112,9 @@ export class HaStateLabelBadge extends LitElement {
|
||||
const image = this.icon
|
||||
? ""
|
||||
: this.image
|
||||
? this.image
|
||||
: entityState.attributes.entity_picture_local ||
|
||||
entityState.attributes.entity_picture;
|
||||
? this.image
|
||||
: entityState.attributes.entity_picture_local ||
|
||||
entityState.attributes.entity_picture;
|
||||
const value =
|
||||
!image && !showIcon
|
||||
? this._computeValue(domain, entityState, entry)
|
||||
@@ -186,12 +186,12 @@ export class HaStateLabelBadge extends LitElement {
|
||||
entityState.state === UNAVAILABLE
|
||||
? "—"
|
||||
: isNumericState(entityState)
|
||||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
? formatNumber(
|
||||
entityState.state,
|
||||
this.hass!.locale,
|
||||
getNumberFormatOptions(entityState, entry)
|
||||
)
|
||||
: this.hass!.formatEntityState(entityState);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -125,7 +125,7 @@ class StateInfo extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.name[inDialog],
|
||||
.name[in-dialog],
|
||||
:host([secondary-line]) .name {
|
||||
line-height: 20px;
|
||||
}
|
||||
|
@@ -1,8 +1,12 @@
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-multi-textfield";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-aliases-editor")
|
||||
class AliasesEditor extends LitElement {
|
||||
@@ -18,23 +22,107 @@ class AliasesEditor extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-multi-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${this.aliases}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass!.localize("ui.dialogs.aliases.label")}
|
||||
.removeLabel=${this.hass!.localize("ui.dialogs.aliases.remove")}
|
||||
.addLabel=${this.hass!.localize("ui.dialogs.aliases.add")}
|
||||
item-index
|
||||
@value-changed=${this._aliasesChanged}
|
||||
>
|
||||
</ha-multi-textfield>
|
||||
${this.aliases.map(
|
||||
(alias, index) => html`
|
||||
<div class="layout horizontal center-center row">
|
||||
<ha-textfield
|
||||
.disabled=${this.disabled}
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
.label=${this.hass!.localize("ui.dialogs.aliases.input_label", {
|
||||
number: index + 1,
|
||||
})}
|
||||
.value=${alias}
|
||||
?data-last=${index === this.aliases.length - 1}
|
||||
@input=${this._editAlias}
|
||||
@keydown=${this._keyDownAlias}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
|
||||
number: index + 1,
|
||||
})}
|
||||
@click=${this._removeAlias}
|
||||
.path=${mdiDeleteOutline}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div class="layout horizontal center-center">
|
||||
<mwc-button @click=${this._addAlias} .disabled=${this.disabled}>
|
||||
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _aliasesChanged(value) {
|
||||
private async _addAlias() {
|
||||
this.aliases = [...this.aliases, ""];
|
||||
this._fireChanged(this.aliases);
|
||||
await this.updateComplete;
|
||||
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
|
||||
| HaTextField
|
||||
| undefined;
|
||||
field?.focus();
|
||||
}
|
||||
|
||||
private async _editAlias(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
const aliases = [...this.aliases];
|
||||
aliases[index] = (ev.target as any).value;
|
||||
this._fireChanged(aliases);
|
||||
}
|
||||
|
||||
private async _keyDownAlias(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
ev.stopPropagation();
|
||||
this._addAlias();
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeAlias(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
const aliases = [...this.aliases];
|
||||
aliases.splice(index, 1);
|
||||
this._fireChanged(aliases);
|
||||
}
|
||||
|
||||
private _fireChanged(value) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-icon-button {
|
||||
display: block;
|
||||
}
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
#alias_input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.alias {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
--mdc-icon-button-size: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@@ -1,96 +0,0 @@
|
||||
import { mdiChevronRight, mdiSofa } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { showAreaFilterDialog } from "../dialogs/area-filter/show-area-filter-dialog";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export type AreaFilterValue = {
|
||||
hidden?: string[];
|
||||
order?: string[];
|
||||
};
|
||||
|
||||
@customElement("ha-area-filter")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: AreaFilterValue;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const allAreasCount = Object.keys(this.hass.areas).length;
|
||||
const hiddenAreasCount = this.value?.hidden?.length ?? 0;
|
||||
|
||||
const description =
|
||||
hiddenAreasCount === 0
|
||||
? this.hass.localize("ui.components.area-filter.all_areas")
|
||||
: allAreasCount === hiddenAreasCount
|
||||
? this.hass.localize("ui.components.area-filter.no_areas")
|
||||
: this.hass.localize("ui.components.area-filter.area_count", {
|
||||
count: allAreasCount - hiddenAreasCount,
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-list-item
|
||||
tabindex="0"
|
||||
role="button"
|
||||
hasMeta
|
||||
twoline
|
||||
graphic="icon"
|
||||
@click=${this._edit}
|
||||
@keydown=${this._edit}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiSofa}></ha-svg-icon>
|
||||
<span>${this.label}</span>
|
||||
<span slot="secondary">${description}</span>
|
||||
<ha-svg-icon
|
||||
slot="meta"
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.path=${mdiChevronRight}
|
||||
></ha-svg-icon>
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _edit(ev) {
|
||||
if (ev.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") {
|
||||
return;
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const value = await showAreaFilterDialog(this, {
|
||||
title: this.label,
|
||||
initialValue: this.value,
|
||||
});
|
||||
if (!value) return;
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-list-item {
|
||||
--mdc-list-side-padding-left: 8px;
|
||||
--mdc-list-side-padding-right: 8px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-area-filter": HaAreaPicker;
|
||||
}
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -24,22 +25,21 @@ import {
|
||||
showAlertDialog,
|
||||
showPromptDialog,
|
||||
} from "../dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-list-item";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) =>
|
||||
html`<ha-list-item
|
||||
html`<mwc-list-item
|
||||
class=${classMap({ "add-new": item.area_id === "add_new" })}
|
||||
>
|
||||
${item.name}
|
||||
</ha-list-item>`;
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@@ -328,7 +328,7 @@ export class HaAreaPicker extends LitElement {
|
||||
item-value-path="area_id"
|
||||
item-id-path="area_id"
|
||||
item-label-path="name"
|
||||
.value=${this._value}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.label=${this.label === undefined && this.hass
|
||||
@@ -347,19 +347,18 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value;
|
||||
if (!filterString) {
|
||||
const filter = ev.detail.value;
|
||||
if (!filter) {
|
||||
this.comboBox.filteredItems = this.comboBox.items;
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
||||
filterString,
|
||||
target.items || []
|
||||
filter,
|
||||
this.comboBox?.items || []
|
||||
);
|
||||
if (!this.noAdd && filteredItems?.length === 0) {
|
||||
this._suggestion = filterString;
|
||||
this._suggestion = filter;
|
||||
this.comboBox.filteredItems = [
|
||||
{
|
||||
area_id: "add_new_suggestion",
|
||||
|
@@ -102,7 +102,6 @@ export class HaAreasPicker extends SubscribeMixin(LitElement) {
|
||||
.placeholder=${this.placeholder}
|
||||
.required=${this.required && !currentAreas.length}
|
||||
@value-changed=${this._addArea}
|
||||
.excludeAreas=${currentAreas}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
`;
|
||||
|
@@ -62,11 +62,8 @@ class HaAttributeValue extends LitElement {
|
||||
|
||||
static styles = css`
|
||||
pre {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -125,6 +125,13 @@ class HaAttributes extends LitElement {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
pre {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
hr {
|
||||
border-color: var(--divider-color);
|
||||
border-bottom: none;
|
||||
|
@@ -1,114 +0,0 @@
|
||||
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-big-number")
|
||||
export class HaBigNumber extends LitElement {
|
||||
@property() public value!: number;
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: "unit-position" })
|
||||
public unitPosition: "top" | "bottom" = "top";
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
protected render() {
|
||||
const formatted = formatNumber(
|
||||
this.value,
|
||||
this.hass?.locale,
|
||||
this.formatOptions
|
||||
);
|
||||
const [integer] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
|
||||
const temperatureDecimal = formatted.replace(integer, "");
|
||||
|
||||
const formattedValue = `${this.value}${
|
||||
this.unit ? `${blankBeforeUnit(this.unit)}${this.unit}` : ""
|
||||
}`;
|
||||
|
||||
const unitBottom = this.unitPosition === "bottom";
|
||||
|
||||
return html`
|
||||
<p class="value">
|
||||
<span aria-hidden="true" class="displayed-value">
|
||||
<span>${integer}</span>
|
||||
<span class="addon ${classMap({ bottom: unitBottom })}">
|
||||
<span class="decimal">${temperatureDecimal}</span>
|
||||
<span class="unit">${this.unit}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="visually-hidden">${formattedValue}</span>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
font-size: 57px;
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
.value {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
direction: ltr;
|
||||
}
|
||||
.displayed-value {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.addon {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.addon.bottom {
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
.addon.bottom .unit {
|
||||
margin-bottom: 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.value .decimal {
|
||||
font-size: 0.42em;
|
||||
line-height: 1.33;
|
||||
}
|
||||
.value .unit {
|
||||
font-size: 0.33em;
|
||||
line-height: 1.26;
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-big-number": HaBigNumber;
|
||||
}
|
||||
}
|
@@ -3,7 +3,6 @@ import "@material/mwc-menu";
|
||||
import type { Corner, Menu, MenuCorner } from "@material/mwc-menu";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
|
||||
import type { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
@@ -69,7 +68,7 @@ export class HaButtonMenu extends LitElement {
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (mainWindow.document.dir === "rtl") {
|
||||
if (document.dir === "rtl") {
|
||||
this.updateComplete.then(() => {
|
||||
this.querySelectorAll("mwc-list-item").forEach((item) => {
|
||||
const style = document.createElement("style");
|
||||
|
@@ -126,11 +126,8 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_entity",
|
||||
{
|
||||
entity_name: computeStateName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem
|
||||
),
|
||||
}
|
||||
"entity_name",
|
||||
computeStateName((ev.currentTarget as any).comboBox.selectedItem)
|
||||
);
|
||||
const items = await findRelated(this.hass, "entity", entityId);
|
||||
fireEvent(this, "related-changed", {
|
||||
@@ -149,12 +146,11 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_device",
|
||||
{
|
||||
device_name: computeDeviceName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem,
|
||||
this.hass
|
||||
),
|
||||
}
|
||||
"device_name",
|
||||
computeDeviceName(
|
||||
(ev.currentTarget as any).comboBox.selectedItem,
|
||||
this.hass
|
||||
)
|
||||
);
|
||||
const items = await findRelated(this.hass, "device", deviceId);
|
||||
|
||||
@@ -174,7 +170,8 @@ export class HaRelatedFilterButtonMenu extends LitElement {
|
||||
}
|
||||
const filter = this.hass.localize(
|
||||
"ui.components.related-filter-menu.filtered_by_area",
|
||||
{ area_name: (ev.currentTarget as any).comboBox.selectedItem.name }
|
||||
"area_name",
|
||||
(ev.currentTarget as any).comboBox.selectedItem.name
|
||||
);
|
||||
const items = await findRelated(this.hass, "area", areaId);
|
||||
fireEvent(this, "related-changed", {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user