mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-02 05:01:55 +00:00
Compare commits
76 Commits
20260624.3
..
tools
| Author | SHA1 | Date | |
|---|---|---|---|
| 932e74fdaa | |||
| df65ba0a3d | |||
| 61cdc8ba82 | |||
| 2936614b54 | |||
| 627e7822cc | |||
| 3b4a17cf26 | |||
| 5422614fbe | |||
| e1e4b29227 | |||
| 49e74bc374 | |||
| cd539efc98 | |||
| 4cf70c40c9 | |||
| 1bc24d9dfa | |||
| e84cb85c8e | |||
| 23335fffdb | |||
| 0a93a681e3 | |||
| 7bc2cad83e | |||
| 39ee60a8ef | |||
| e06d46e87f | |||
| 45704587f3 | |||
| f7c9033b22 | |||
| 0edb5c5241 | |||
| 3ca9b8b9aa | |||
| ba10fd0447 | |||
| 3171e00929 | |||
| 032790750c | |||
| 9807b9bb33 | |||
| 29853ab37d | |||
| d9e996d901 | |||
| 3d1630e497 | |||
| 301c08dc86 | |||
| bc9af7fc2f | |||
| 86c1a81aca | |||
| 05afa19a76 | |||
| 66775f03dd | |||
| 53e47e58f1 | |||
| 7b2569346f | |||
| 72fe6e1cbb | |||
| 26f270720a | |||
| e01bef53dc | |||
| 04226dda32 | |||
| b8fc05d5c4 | |||
| d602e77fc3 | |||
| a19842bd4d | |||
| 13872baa8c | |||
| a1aaf3fe33 | |||
| 84840dc922 | |||
| 8e43688ed8 | |||
| d9037b84c8 | |||
| c070765f54 | |||
| 8e5d976f7b | |||
| 2dbb052200 | |||
| 3b04f29755 | |||
| 865b5b1b80 | |||
| b44c69b1b0 | |||
| 27787e51f8 | |||
| dc7daf3156 | |||
| b898468193 | |||
| 781aa116b8 | |||
| 60c86899f3 | |||
| f8d870d6bb | |||
| 4d82b352a9 | |||
| 179b4cf77c | |||
| 542f07606a | |||
| cf2c440e7b | |||
| 27fbabb71b | |||
| 389af6e00c | |||
| 7ff4cf58e8 | |||
| f849302876 | |||
| 1db707937b | |||
| 70f0d12e43 | |||
| 12bb09dad2 | |||
| f08ffefe28 | |||
| 9de89278cd | |||
| 207d997a3a | |||
| 0bb32aa1b4 | |||
| ba0310ee58 |
@@ -67,7 +67,7 @@ DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed w
|
||||
<!--
|
||||
If your issue is about how an entity is shown in the UI, please add the state
|
||||
and attributes for all situations with a screenshot of the UI.
|
||||
You can find this information at `/config/developer-tools/state`
|
||||
You can find this information at `/config/tools/state`
|
||||
-->
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -94,8 +94,8 @@ body:
|
||||
label: State of relevant entities
|
||||
description: >
|
||||
If your issue is about how an entity is shown in the UI, please add the
|
||||
state and attributes for all situations. You can find this information
|
||||
at Developer Tools -> States.
|
||||
state and attributes for all situations. You can find this
|
||||
information in the Details view of the More info dialog.
|
||||
render: txt
|
||||
- type: textarea
|
||||
attributes:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
// Fails the check when a pull request carries a label that blocks merging, and
|
||||
// writes the outcome to the job summary. Invoked from the `check` job in
|
||||
// .github/workflows/blocking-labels.yaml via actions/github-script:
|
||||
//
|
||||
// const { default: checkBlockingLabels } =
|
||||
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mjs`);
|
||||
// await checkBlockingLabels({ github, context, core });
|
||||
|
||||
export default async function checkBlockingLabels({ context, core }) {
|
||||
const blockingLabels = [
|
||||
"wait for backend",
|
||||
"Needs UX",
|
||||
"Do Not Review",
|
||||
"Blocked",
|
||||
"has-parent",
|
||||
];
|
||||
const prLabels = context.payload.pull_request.labels.map((l) => l.name);
|
||||
const found = blockingLabels.filter((bl) => prLabels.includes(bl));
|
||||
if (found.length > 0) {
|
||||
const message = `This Pull Request is blocked by label${found.length > 1 ? "s" : ""}: ${found.join(", ")}`;
|
||||
await core.summary
|
||||
.addHeading(":no_entry_sign: Pull Request is blocked", 2)
|
||||
.addRaw(message)
|
||||
.write();
|
||||
core.setFailed(message);
|
||||
} else {
|
||||
await core.summary
|
||||
.addHeading(
|
||||
":white_check_mark: Pull Request is clear to merge after review",
|
||||
2
|
||||
)
|
||||
.addRaw(
|
||||
"This Pull Request is not blocked by any labels which prevent it from being merged."
|
||||
)
|
||||
.write();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env node
|
||||
// Checks that a pull request follows the contribution standards: it must use the
|
||||
// PR template, tick exactly one "Type of change" option, and describe the change.
|
||||
// Labels and comments the PR when it does not, and fails the check so it blocks
|
||||
// merging. Invoked from the `check` job in .github/workflows/pull-request-standards.yaml
|
||||
// via actions/github-script:
|
||||
//
|
||||
// const { default: checkStandards } =
|
||||
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`);
|
||||
// await checkStandards({ github, context, core });
|
||||
|
||||
export default async function checkPullRequestStandards({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
}) {
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
||||
if (pr.user.type === "Bot") {
|
||||
core.info(`Skipping bot author: ${pr.user.login}`);
|
||||
return;
|
||||
}
|
||||
if (pr.draft) {
|
||||
core.info("Skipping draft pull request");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: "home-assistant",
|
||||
username: pr.user.login,
|
||||
});
|
||||
core.info(`Skipping organization member: ${pr.user.login}`);
|
||||
return;
|
||||
} catch (_error) {
|
||||
core.info(
|
||||
`${pr.user.login} is not an organization member, checking standards`
|
||||
);
|
||||
}
|
||||
|
||||
const label = "Needs Template";
|
||||
const marker = "<!-- pr-standards-check -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = pr.number;
|
||||
|
||||
let body = pr.body || "";
|
||||
let previous;
|
||||
do {
|
||||
previous = body;
|
||||
body = body.replace(/<!--[\s\S]*?-->/g, "");
|
||||
} while (body !== previous);
|
||||
const normalized = body.toLowerCase();
|
||||
|
||||
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
||||
const ignoreMissing = async (fn) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info("Target already removed, nothing to do");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
||||
const setMinimized = async (subjectId, minimized) => {
|
||||
const mutation = minimized
|
||||
? `mutation($id: ID!) {
|
||||
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`
|
||||
: `mutation($id: ID!) {
|
||||
unminimizeComment(input: { subjectId: $id }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
await github.graphql(mutation, { id: subjectId });
|
||||
} catch (error) {
|
||||
core.info(
|
||||
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Content of a "## <name>" section, or null when the heading is absent.
|
||||
const section = (name) => {
|
||||
const match = body.match(
|
||||
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
||||
);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const problems = [];
|
||||
|
||||
const requiredHeadings = [
|
||||
"## proposed change",
|
||||
"## type of change",
|
||||
"## checklist",
|
||||
];
|
||||
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
||||
problems.push(
|
||||
"Use the pull request template without removing its sections."
|
||||
);
|
||||
}
|
||||
|
||||
const typeOfChange = section("type of change");
|
||||
if (typeOfChange !== null) {
|
||||
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
||||
if (ticked !== 1) {
|
||||
problems.push('Select exactly one option under "Type of change".');
|
||||
}
|
||||
}
|
||||
|
||||
const proposedChange = section("proposed change");
|
||||
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
||||
problems.push('Describe your changes under "Proposed change".');
|
||||
}
|
||||
|
||||
const isValid = problems.length === 0;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
const existing = comments.find((c) => c.body.includes(marker));
|
||||
const hasLabel = pr.labels.some((l) => l.name === label);
|
||||
|
||||
if (isValid) {
|
||||
core.info("Pull request standards met");
|
||||
|
||||
if (hasLabel) {
|
||||
await ignoreMissing(() =>
|
||||
github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
name: label,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (existing) {
|
||||
await setMinimized(existing.node_id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
labels: [label],
|
||||
});
|
||||
}
|
||||
|
||||
const message =
|
||||
`${marker}\n` +
|
||||
`Hey @${pr.user.login}!\n\n` +
|
||||
`Thank you for your contribution! To help reviewers, please update ` +
|
||||
`this pull request to follow our pull request standards:\n\n` +
|
||||
problems.map((p) => `- ${p}`).join("\n") +
|
||||
`\n\n` +
|
||||
`Please complete the ` +
|
||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
||||
`for more on creating a great pull request (see point 6).`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: existing.id,
|
||||
body: message,
|
||||
});
|
||||
await setMinimized(existing.node_id, false);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body: message,
|
||||
});
|
||||
}
|
||||
|
||||
// Fail this check so it can block the PR from being merged
|
||||
core.setFailed(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
// Restricts Task issues to organization members: closes and labels the issue with
|
||||
// an explanatory comment when the author is not an org member. Invoked from the
|
||||
// `check-authorization` job in .github/workflows/restrict-task-creation.yml via
|
||||
// actions/github-script:
|
||||
//
|
||||
// const { default: checkTaskAuthorization } =
|
||||
// await import(`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mjs`);
|
||||
// await checkTaskAuthorization({ github, context, core });
|
||||
|
||||
export default async function checkTaskAuthorization({
|
||||
github,
|
||||
context,
|
||||
core,
|
||||
}) {
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
// Check if user is an organization member
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: "home-assistant",
|
||||
username: issueAuthor,
|
||||
});
|
||||
core.info(`✅ ${issueAuthor} is an organization member`);
|
||||
return; // Authorized
|
||||
} catch (_error) {
|
||||
core.info(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
||||
}
|
||||
|
||||
// Close the issue with a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body:
|
||||
`Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
||||
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
|
||||
`If you would like to:\n` +
|
||||
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
|
||||
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
||||
`If you believe you should have access to create Task issues, please contact the maintainers.`,
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: "closed",
|
||||
});
|
||||
|
||||
// Add a label to indicate this was auto-closed
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ["auto-closed"],
|
||||
});
|
||||
}
|
||||
@@ -20,31 +20,16 @@ jobs:
|
||||
name: Check for labels which block the Pull Request from being merged
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out workflow scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/scripts
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = [
|
||||
"wait for backend",
|
||||
"Needs UX",
|
||||
"Do Not Review",
|
||||
"Blocked",
|
||||
"has-parent",
|
||||
];
|
||||
const prLabels = context.payload.pull_request.labels.map(
|
||||
(l) => l.name
|
||||
const { default: checkBlockingLabels } = await import(
|
||||
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-blocking-labels.mjs`
|
||||
);
|
||||
const found = blockingLabels.filter((bl) => prLabels.includes(bl));
|
||||
if (found.length > 0) {
|
||||
const message = `This Pull Request is blocked by label${found.length > 1 ? "s" : ""}: ${found.join(", ")}`;
|
||||
await core.summary
|
||||
.addHeading(":no_entry_sign: Pull Request is blocked", 2)
|
||||
.addRaw(message)
|
||||
.write();
|
||||
core.setFailed(message);
|
||||
} else {
|
||||
await core.summary
|
||||
.addHeading(":white_check_mark: Pull Request is clear to merge after review", 2)
|
||||
.addRaw("This Pull Request is not blocked by any labels which prevent it from being merged.")
|
||||
.write();
|
||||
}
|
||||
await checkBlockingLabels({ github, context, core });
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
@@ -105,6 +105,8 @@ jobs:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Check entrypoint bundle size budget
|
||||
run: yarn run check-bundlesize
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: dev
|
||||
persist-credentials: false
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: master
|
||||
persist-credentials: false
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+15
-19
@@ -28,7 +28,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -155,19 +155,19 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Download demo build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: demo-dist
|
||||
path: demo/dist/
|
||||
|
||||
- name: Download e2e test app build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: e2e-test-app-dist
|
||||
path: test/e2e/app/dist/
|
||||
|
||||
- name: Download gallery build
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: gallery-dist
|
||||
path: gallery/dist/
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
run: yarn install --immutable
|
||||
|
||||
- name: Download blob report (local)
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: blob-report-local
|
||||
@@ -229,16 +229,12 @@ jobs:
|
||||
path: test/e2e/reports/combined/
|
||||
retention-days: 14
|
||||
|
||||
- name: Post report link to PR
|
||||
- name: Post report to PR
|
||||
if: github.event_name == 'pull_request' && needs.e2e-local.result == 'failure'
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||
const body = `## Playwright E2E tests failed\n\nThe combined HTML report is available as a workflow artifact.\n\n[View workflow run](${runUrl})`;
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body,
|
||||
});
|
||||
const { default: postReportComment } = await import(
|
||||
`${process.env.GITHUB_WORKSPACE}/test/e2e/post-report-comment.mjs`
|
||||
);
|
||||
await postReportComment({ github, context, core });
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Pull request standards
|
||||
|
||||
on:
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, no PR code checkout
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] -- safe: reads PR metadata from event payload only, checks out base repo scripts only, never PR head code
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
@@ -23,168 +23,16 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write # To label and comment on pull requests
|
||||
steps:
|
||||
- name: Check out workflow scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/scripts
|
||||
- name: Check pull request standards
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Exempt bots (Copilot agent, dependabot), drafts, and maintainers.
|
||||
if (pr.user.type === "Bot") {
|
||||
core.info(`Skipping bot author: ${pr.user.login}`);
|
||||
return;
|
||||
}
|
||||
if (pr.draft) {
|
||||
core.info("Skipping draft pull request");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: "home-assistant",
|
||||
username: pr.user.login,
|
||||
});
|
||||
core.info(`Skipping organization member: ${pr.user.login}`);
|
||||
return;
|
||||
} catch (error) {
|
||||
core.info(`${pr.user.login} is not an organization member, checking standards`);
|
||||
}
|
||||
|
||||
const label = "Needs Template";
|
||||
const marker = "<!-- pr-standards-check -->";
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = pr.number;
|
||||
|
||||
const body = (pr.body || "").replace(/<!--[\s\S]*?-->/g, "");
|
||||
const normalized = body.toLowerCase();
|
||||
|
||||
// Ignore 404s from mutations that race manual edits or cancelled runs.
|
||||
const ignoreMissing = async (fn) => {
|
||||
try {
|
||||
await fn();
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.info("Target already removed, nothing to do");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Hide/restore our comment via GraphQL (REST cannot minimize).
|
||||
const setMinimized = async (subjectId, minimized) => {
|
||||
const mutation = minimized
|
||||
? `mutation($id: ID!) {
|
||||
minimizeComment(input: { subjectId: $id, classifier: RESOLVED }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`
|
||||
: `mutation($id: ID!) {
|
||||
unminimizeComment(input: { subjectId: $id }) {
|
||||
clientMutationId
|
||||
}
|
||||
}`;
|
||||
try {
|
||||
await github.graphql(mutation, { id: subjectId });
|
||||
} catch (error) {
|
||||
core.info(
|
||||
`Could not ${minimized ? "minimize" : "restore"} comment: ${error.message}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Content of a "## <name>" section, or null when the heading is absent.
|
||||
const section = (name) => {
|
||||
const match = body.match(
|
||||
new RegExp(`##\\s${name}([\\s\\S]*?)(?=\\n##\\s|$)`, "i")
|
||||
);
|
||||
return match ? match[1] : null;
|
||||
};
|
||||
|
||||
const problems = [];
|
||||
|
||||
const requiredHeadings = [
|
||||
"## proposed change",
|
||||
"## type of change",
|
||||
"## checklist",
|
||||
];
|
||||
if (requiredHeadings.some((h) => !normalized.includes(h))) {
|
||||
problems.push(
|
||||
"Use the pull request template without removing its sections."
|
||||
);
|
||||
}
|
||||
|
||||
const typeOfChange = section("type of change");
|
||||
if (typeOfChange !== null) {
|
||||
const ticked = (typeOfChange.match(/-\s*\[[xX]\]/g) || []).length;
|
||||
if (ticked !== 1) {
|
||||
problems.push(
|
||||
'Select exactly one option under "Type of change".'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const proposedChange = section("proposed change");
|
||||
if (proposedChange !== null && proposedChange.trim().length === 0) {
|
||||
problems.push('Describe your changes under "Proposed change".');
|
||||
}
|
||||
|
||||
const isValid = problems.length === 0;
|
||||
|
||||
const comments = await github.paginate(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number, per_page: 100 }
|
||||
);
|
||||
const existing = comments.find((c) => c.body.includes(marker));
|
||||
const hasLabel = pr.labels.some((l) => l.name === label);
|
||||
|
||||
if (isValid) {
|
||||
core.info("Pull request standards met");
|
||||
|
||||
if (hasLabel) {
|
||||
await ignoreMissing(() =>
|
||||
github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number, name: label,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (existing) {
|
||||
await setMinimized(existing.node_id, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Pull request standards not met:\n- ${problems.join("\n- ")}`);
|
||||
|
||||
if (!hasLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number, labels: [label],
|
||||
});
|
||||
}
|
||||
|
||||
const message =
|
||||
`${marker}\n` +
|
||||
`Hey @${pr.user.login}!\n\n` +
|
||||
`Thank you for your contribution! To help reviewers, please update ` +
|
||||
`this pull request to follow our pull request standards:\n\n` +
|
||||
problems.map((p) => `- ${p}`).join("\n") +
|
||||
`\n\n` +
|
||||
`Please complete the ` +
|
||||
`[PR template](https://github.com/home-assistant/frontend/blob/dev/.github/PULL_REQUEST_TEMPLATE.md?plain=1) ` +
|
||||
`and see the [developer docs](https://developers.home-assistant.io/docs/review-process) ` +
|
||||
`for more on creating a great pull request (see point 6).`;
|
||||
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({
|
||||
owner, repo, comment_id: existing.id, body: message,
|
||||
});
|
||||
await setMinimized(existing.node_id, false);
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner, repo, issue_number, body: message,
|
||||
});
|
||||
}
|
||||
|
||||
// Fail this check so it can block the PR from being merged
|
||||
core.setFailed(
|
||||
`Pull request standards not met:\n- ${problems.join("\n- ")}`
|
||||
const { default: checkStandards } = await import(
|
||||
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-pull-request-standards.mjs`
|
||||
);
|
||||
await checkStandards({ github, context, core });
|
||||
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@693d20e7c1ce1a81d3a41962f85914253b518449 # v7.3.1
|
||||
- uses: release-drafter/release-drafter@ed4bc48ec97379be2258e7b7ac2624a3e26ab809 # v7.4.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@e91ad1948e57189485b9c1ad608af0c303946f89 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@f4ca6f671bd429efb108c0f2fa0ae8af0215986c # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
|
||||
@@ -36,52 +36,21 @@ jobs:
|
||||
name: Check authorization
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out workflow scripts
|
||||
issues: write # To comment on, label, and close issues
|
||||
# Only run if this is a Task issue type (from the issue form)
|
||||
if: github.event.issue.type.name == 'Task'
|
||||
steps:
|
||||
- name: Check out workflow scripts
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: .github/scripts
|
||||
- name: Check if user is authorized
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const issueAuthor = context.payload.issue.user.login;
|
||||
|
||||
// Check if user is an organization member
|
||||
try {
|
||||
await github.rest.orgs.checkMembershipForUser({
|
||||
org: 'home-assistant',
|
||||
username: issueAuthor
|
||||
});
|
||||
console.log(`✅ ${issueAuthor} is an organization member`);
|
||||
return; // Authorized
|
||||
} catch (error) {
|
||||
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
||||
}
|
||||
|
||||
// Close the issue with a comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
||||
`Task issues are restricted to Open Home Foundation staff and authorized contributors.\n\n` +
|
||||
`If you would like to:\n` +
|
||||
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/frontend/issues/new?template=bug_report.yml)\n` +
|
||||
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
||||
`If you believe you should have access to create Task issues, please contact the maintainers.`
|
||||
});
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Add a label to indicate this was auto-closed
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['auto-closed']
|
||||
});
|
||||
const { default: checkTaskAuthorization } = await import(
|
||||
`${process.env.GITHUB_WORKSPACE}/.github/scripts/check-task-authorization.mjs`
|
||||
);
|
||||
await checkTaskAuthorization({ github, context, core });
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"_comment": "Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. Enforced by build-scripts/check-bundle-size.cjs in CI. Re-seed after an intentional change with `--update --headroom=<percent>`.",
|
||||
"frontend-modern": {
|
||||
"app": 561513,
|
||||
"core": 54473,
|
||||
"authorize": 544272,
|
||||
"onboarding": 647136
|
||||
},
|
||||
"frontend-legacy": {
|
||||
"app": 790323,
|
||||
"core": 237208,
|
||||
"authorize": 765464,
|
||||
"onboarding": 918679
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/* global require, process, __dirname */
|
||||
// Enforce a strict size budget on the initial JS of the most critical
|
||||
// entrypoints (`app` and `core`). These two are downloaded on every cold load
|
||||
// before anything interactive can happen, so unintended growth here hurts
|
||||
// first-load performance directly.
|
||||
//
|
||||
// In production rspack does not split initial chunks (splitChunks only operates
|
||||
// on `!chunk.canBeInitial()`), so each entrypoint resolves to a single initial
|
||||
// JS asset. We read the per-build stats written by StatsWriterPlugin and compare
|
||||
// the entrypoint's initial JS size against a committed budget.
|
||||
//
|
||||
// Usage:
|
||||
// node build-scripts/check-bundle-size.cjs # enforce, exit 1 on regression
|
||||
// node build-scripts/check-bundle-size.cjs --update # rewrite budgets from current sizes
|
||||
// node build-scripts/check-bundle-size.cjs --update --headroom=3 # current + 3% headroom
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const paths = require("./paths.cjs");
|
||||
|
||||
// Entrypoints whose initial JS we hold to a strict budget. These are all
|
||||
// downloaded on a user-facing cold load before anything interactive can happen:
|
||||
// `app`/`core` for the main app, plus the standalone `authorize` and
|
||||
// `onboarding` pages. `custom-panel` is intentionally excluded (only loaded
|
||||
// when a custom panel is opened).
|
||||
const TRACKED_ENTRYPOINTS = ["app", "core", "authorize", "onboarding"];
|
||||
|
||||
// App build stats files, as written by StatsWriterPlugin (`${name}.json`).
|
||||
const BUILDS = ["frontend-modern", "frontend-legacy"];
|
||||
|
||||
const BUDGET_FILE = path.join(__dirname, "bundle-budget.json");
|
||||
const STATS_DIR = path.join(paths.build_dir, "stats");
|
||||
|
||||
const readStats = (build) => {
|
||||
const file = path.join(STATS_DIR, `${build}.json`);
|
||||
if (!fs.existsSync(file)) {
|
||||
throw new Error(
|
||||
`Missing stats file: ${path.relative(process.cwd(), file)}.\n` +
|
||||
`Run a production build first (e.g. \`gulp build-app\`), then re-run this check.`
|
||||
);
|
||||
}
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
};
|
||||
|
||||
// Initial JS bytes for an entrypoint = sum of the .js asset sizes of its initial
|
||||
// entry chunk(s). Sizes are raw (uncompressed) bytes, matching the stats output.
|
||||
const entrypointInitialJS = (stats, entrypoint) => {
|
||||
const assetSize = new Map(stats.assets.map((a) => [a.name, a.size]));
|
||||
let total = 0;
|
||||
let found = false;
|
||||
for (const chunk of stats.chunks) {
|
||||
if (!chunk.entry || !chunk.initial) {
|
||||
continue;
|
||||
}
|
||||
if (!(chunk.names || []).includes(entrypoint)) {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
for (const file of chunk.files || []) {
|
||||
if (file.endsWith(".js") && assetSize.has(file)) {
|
||||
total += assetSize.get(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new Error(`Entrypoint "${entrypoint}" not found in bundle stats.`);
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
const kib = (bytes) => `${(bytes / 1024).toFixed(1)} KiB`;
|
||||
|
||||
const main = () => {
|
||||
const update = process.argv.includes("--update");
|
||||
const headroomArg = process.argv.find((a) => a.startsWith("--headroom="));
|
||||
const headroom = headroomArg ? Number(headroomArg.split("=")[1]) : 0;
|
||||
|
||||
const current = {};
|
||||
for (const build of BUILDS) {
|
||||
const stats = readStats(build);
|
||||
current[build] = {};
|
||||
for (const entrypoint of TRACKED_ENTRYPOINTS) {
|
||||
current[build][entrypoint] = entrypointInitialJS(stats, entrypoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (update) {
|
||||
const budget = { _comment: BUDGET_COMMENT };
|
||||
for (const build of BUILDS) {
|
||||
budget[build] = {};
|
||||
for (const entrypoint of TRACKED_ENTRYPOINTS) {
|
||||
budget[build][entrypoint] = Math.ceil(
|
||||
current[build][entrypoint] * (1 + headroom / 100)
|
||||
);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(BUDGET_FILE, `${JSON.stringify(budget, null, 2)}\n`);
|
||||
console.log(
|
||||
`Updated ${path.relative(process.cwd(), BUDGET_FILE)} from current sizes` +
|
||||
(headroom ? ` (+${headroom}% headroom).` : ".")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(BUDGET_FILE)) {
|
||||
throw new Error(
|
||||
`Missing budget file ${path.relative(process.cwd(), BUDGET_FILE)}.\n` +
|
||||
`Seed it from a production build with: node build-scripts/check-bundle-size.cjs --update --headroom=3`
|
||||
);
|
||||
}
|
||||
const budget = JSON.parse(fs.readFileSync(BUDGET_FILE, "utf8"));
|
||||
|
||||
let failed = false;
|
||||
console.log("Initial JS budget (entry chunks, raw bytes):\n");
|
||||
for (const build of BUILDS) {
|
||||
for (const entrypoint of TRACKED_ENTRYPOINTS) {
|
||||
const actual = current[build][entrypoint];
|
||||
const limit = budget[build] && budget[build][entrypoint];
|
||||
if (typeof limit !== "number") {
|
||||
failed = true;
|
||||
console.log(
|
||||
` ✗ ${build} / ${entrypoint}: no budget set (current ${kib(actual)})`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const ok = actual <= limit;
|
||||
const delta = (((actual - limit) / limit) * 100).toFixed(1);
|
||||
console.log(
|
||||
` ${ok ? "✓" : "✗"} ${build} / ${entrypoint}: ` +
|
||||
`${kib(actual)} / ${kib(limit)}${ok ? "" : ` (+${delta}% over budget)`}`
|
||||
);
|
||||
if (!ok) {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(
|
||||
"\nInitial JS budget exceeded for a critical entrypoint.\n" +
|
||||
"Investigate what was pulled into the entry chunk (a static import that should be lazy?).\n" +
|
||||
"If the growth is intentional, re-seed the budget:\n" +
|
||||
" node build-scripts/check-bundle-size.cjs --update --headroom=3"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\nAll tracked entrypoints within budget.");
|
||||
};
|
||||
|
||||
const BUDGET_COMMENT =
|
||||
"Initial JS budget (raw/uncompressed bytes) for the cold-load critical entrypoints. " +
|
||||
"Enforced by build-scripts/check-bundle-size.cjs in CI. " +
|
||||
"Re-seed after an intentional change with `--update --headroom=<percent>`.";
|
||||
|
||||
main();
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
import { glob } from "glob";
|
||||
import gulp from "gulp";
|
||||
import yaml from "js-yaml";
|
||||
import { load as loadYaml } from "js-yaml";
|
||||
import { marked } from "marked";
|
||||
import path from "path";
|
||||
import paths from "../paths.cjs";
|
||||
@@ -47,7 +47,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
|
||||
if (descriptionContent.startsWith("---")) {
|
||||
const metadataEnd = descriptionContent.indexOf("---", 3);
|
||||
metadata = yaml.load(descriptionContent.substring(3, metadataEnd));
|
||||
metadata = loadYaml(descriptionContent.substring(3, metadataEnd));
|
||||
descriptionContent = descriptionContent
|
||||
.substring(metadataEnd + 3)
|
||||
.trim();
|
||||
|
||||
@@ -240,6 +240,7 @@ gulp.task("rspack-dev-server-e2e-test-app", () =>
|
||||
),
|
||||
contentBase: paths.e2eTestApp_output_root,
|
||||
port: 8095,
|
||||
open: false,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -56,88 +56,100 @@ class HcCast extends LitElement {
|
||||
|
||||
return html`
|
||||
<hc-layout .auth=${this.auth} .connection=${this.connection}>
|
||||
${this.askWrite
|
||||
? html`
|
||||
<p class="question action-item">
|
||||
Stay logged in?
|
||||
<span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleSaveTokens}
|
||||
>
|
||||
YES
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleSkipSaveTokens}
|
||||
>
|
||||
NO
|
||||
</ha-button>
|
||||
</span>
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
${error
|
||||
? html` <div class="card-content">${error}</div> `
|
||||
: !this.castManager.status
|
||||
${
|
||||
this.askWrite
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<ha-button @click=${this._handleLaunch}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon>
|
||||
Start Casting
|
||||
</ha-button>
|
||||
<p class="question action-item">
|
||||
Stay logged in?
|
||||
<span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleSaveTokens}
|
||||
>
|
||||
YES
|
||||
</ha-button>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
@click=${this._handleSkipSaveTokens}
|
||||
>
|
||||
NO
|
||||
</ha-button>
|
||||
</span>
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<ha-list @action=${this._handlePickView} activatable>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
{
|
||||
title: "Home",
|
||||
},
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
.activated=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
.selected=${this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)}
|
||||
>
|
||||
${view.title || view.path || "Unnamed view"}
|
||||
${view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="graphic"
|
||||
></ha-icon>
|
||||
`
|
||||
: html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiViewDashboard}
|
||||
></ha-svg-icon>`}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>
|
||||
`}
|
||||
: ""
|
||||
}
|
||||
${
|
||||
error
|
||||
? html` <div class="card-content">${error}</div> `
|
||||
: !this.castManager.status
|
||||
? html`
|
||||
<p class="center-item">
|
||||
<ha-button @click=${this._handleLaunch}>
|
||||
<ha-svg-icon slot="start" .path=${mdiCast}></ha-svg-icon>
|
||||
Start Casting
|
||||
</ha-button>
|
||||
</p>
|
||||
`
|
||||
: html`
|
||||
<div class="section-header">PICK A VIEW</div>
|
||||
<ha-list @action=${this._handlePickView} activatable>
|
||||
${(
|
||||
this.lovelaceViews ?? [
|
||||
{
|
||||
title: "Home",
|
||||
},
|
||||
]
|
||||
).map(
|
||||
(view, idx) => html`
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
.activated=${
|
||||
this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)
|
||||
}
|
||||
.selected=${
|
||||
this.castManager.status?.lovelacePath ===
|
||||
(view.path ?? idx)
|
||||
}
|
||||
>
|
||||
${view.title || view.path || "Unnamed view"}
|
||||
${
|
||||
view.icon
|
||||
? html`
|
||||
<ha-icon
|
||||
.icon=${view.icon}
|
||||
slot="graphic"
|
||||
></ha-icon>
|
||||
`
|
||||
: html`<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiViewDashboard}
|
||||
></ha-svg-icon>`
|
||||
}
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>
|
||||
`
|
||||
}
|
||||
|
||||
<div class="card-actions">
|
||||
${this.castManager.status
|
||||
? html`
|
||||
<ha-button appearance="plain" @click=${this._handleLaunch}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiCastConnected}
|
||||
></ha-svg-icon>
|
||||
Manage
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
${
|
||||
this.castManager.status
|
||||
? html`
|
||||
<ha-button appearance="plain" @click=${this._handleLaunch}>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiCastConnected}
|
||||
></ha-svg-icon>
|
||||
Manage
|
||||
</ha-button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<div class="spacer"></div>
|
||||
<ha-button
|
||||
variant="danger"
|
||||
|
||||
@@ -135,9 +135,11 @@ export class HcConnect extends LitElement {
|
||||
Show Demo
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
.path=${this.castManager.castState === "CONNECTED"
|
||||
? mdiCastConnected
|
||||
: mdiCast}
|
||||
.path=${
|
||||
this.castManager.castState === "CONNECTED"
|
||||
? mdiCastConnected
|
||||
: mdiCast
|
||||
}
|
||||
></ha-svg-icon>
|
||||
</ha-button>
|
||||
<div class="spacer"></div>
|
||||
|
||||
@@ -26,18 +26,20 @@ class HcLayout extends LitElement {
|
||||
/>
|
||||
<h1 class="card-header">
|
||||
Home Assistant Cast${this.subtitle ? ` – ${this.subtitle}` : ""}
|
||||
${this.auth
|
||||
? html`
|
||||
<div class="subtitle">
|
||||
<a href=${this.auth.data.hassUrl} target="_blank"
|
||||
>${this.auth.data.hassUrl.substr(
|
||||
this.auth.data.hassUrl.indexOf("//") + 2
|
||||
)}</a
|
||||
>
|
||||
${this.user ? html` – ${this.user.name} ` : ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${
|
||||
this.auth
|
||||
? html`
|
||||
<div class="subtitle">
|
||||
<a href=${this.auth.data.hassUrl} target="_blank"
|
||||
>${this.auth.data.hassUrl.substr(
|
||||
this.auth.data.hassUrl.indexOf("//") + 2
|
||||
)}</a
|
||||
>
|
||||
${this.user ? html` – ${this.user.name} ` : ""}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</h1>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,7 @@ export interface DemoConfig {
|
||||
authorName: string;
|
||||
authorUrl: string;
|
||||
description?:
|
||||
| string
|
||||
| ((localize: LocalizeFunc) => string | TemplateResult<1>);
|
||||
string | ((localize: LocalizeFunc) => string | TemplateResult<1>);
|
||||
lovelace: (localize: LocalizeFunc) => LovelaceConfig;
|
||||
entities: (localize: LocalizeFunc) => EntityInput[];
|
||||
theme: () => Record<string, string> | null;
|
||||
|
||||
@@ -43,28 +43,30 @@ export class HADemoCard extends LitElement implements LovelaceCard {
|
||||
<ha-card>
|
||||
<div class="picker">
|
||||
<div class="label">
|
||||
${this._switching
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: until(
|
||||
selectedDemoConfig.then(
|
||||
(conf) => html`
|
||||
${conf.name}
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.page-demo.cards.demo.demo_by",
|
||||
{
|
||||
name: html`
|
||||
<a target="_blank" href=${conf.authorUrl}>
|
||||
${conf.authorName}
|
||||
</a>
|
||||
`,
|
||||
}
|
||||
)}
|
||||
</small>
|
||||
`
|
||||
),
|
||||
""
|
||||
)}
|
||||
${
|
||||
this._switching
|
||||
? html`<ha-spinner></ha-spinner>`
|
||||
: until(
|
||||
selectedDemoConfig.then(
|
||||
(conf) => html`
|
||||
${conf.name}
|
||||
<small>
|
||||
${this.hass.localize(
|
||||
"ui.panel.page-demo.cards.demo.demo_by",
|
||||
{
|
||||
name: html`
|
||||
<a target="_blank" href=${conf.authorUrl}>
|
||||
${conf.authorName}
|
||||
</a>
|
||||
`,
|
||||
}
|
||||
)}
|
||||
</small>
|
||||
`
|
||||
),
|
||||
""
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<ha-button @click=${this._nextConfig} .disabled=${this._switching}>
|
||||
|
||||
+3
-1
@@ -66,7 +66,9 @@ export class HaDemo extends HomeAssistantAppEl {
|
||||
this._updateHass(hassUpdate),
|
||||
};
|
||||
|
||||
const hass = provideHass(this, initial, true);
|
||||
// `false` for contexts: HomeAssistantAppEl already provides them via
|
||||
// `contextMixin`, so let provideHass skip them to avoid duplicate providers.
|
||||
const hass = provideHass(this, initial, true, false);
|
||||
const localizePromise =
|
||||
// @ts-ignore
|
||||
this._loadFragmentTranslations(hass.language, "page-demo").then(
|
||||
|
||||
+137
-143
@@ -8,103 +8,100 @@ import type {
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS(
|
||||
"energy/get_prefs",
|
||||
(): EnergyPreferences => ({
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "solar",
|
||||
stat_energy_from: "sensor.solar_production",
|
||||
stat_rate: "sensor.power_solar",
|
||||
config_entry_solar_forecast: ["solar_forecast"],
|
||||
},
|
||||
{
|
||||
type: "battery",
|
||||
stat_energy_from: "sensor.battery_output",
|
||||
stat_energy_to: "sensor.battery_input",
|
||||
stat_rate: "sensor.power_battery",
|
||||
},
|
||||
{
|
||||
type: "gas",
|
||||
stat_energy_from: "sensor.energy_gas",
|
||||
stat_cost: "sensor.energy_gas_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
type: "water",
|
||||
stat_energy_from: "sensor.energy_water",
|
||||
stat_cost: "sensor.energy_water_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
device_consumption: [
|
||||
{
|
||||
stat_consumption: "sensor.energy_car",
|
||||
stat_rate: "sensor.power_car",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_ac",
|
||||
stat_rate: "sensor.power_ac",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_washing_machine",
|
||||
stat_rate: "sensor.power_washing_machine",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_dryer",
|
||||
stat_rate: "sensor.power_dryer",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_heat_pump",
|
||||
stat_rate: "sensor.power_heat_pump",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_boiler",
|
||||
stat_rate: "sensor.power_boiler",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [
|
||||
{
|
||||
stat_consumption: "sensor.water_kitchen",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.water_garden",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
hass.mockWS(
|
||||
"energy/info",
|
||||
(): EnergyInfo => ({ cost_sensors: {}, solar_forecast_domains: [] })
|
||||
);
|
||||
hass.mockWS("energy/get_prefs", (): EnergyPreferences => ({
|
||||
energy_sources: [
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_1",
|
||||
stat_energy_to: "sensor.energy_production_tarif_1",
|
||||
stat_cost: "sensor.energy_consumption_tarif_1_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_1_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
stat_energy_from: "sensor.energy_consumption_tarif_2",
|
||||
stat_energy_to: "sensor.energy_production_tarif_2",
|
||||
stat_cost: "sensor.energy_consumption_tarif_2_cost",
|
||||
stat_compensation: "sensor.energy_production_tarif_2_compensation",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
entity_energy_price_export: null,
|
||||
number_energy_price_export: null,
|
||||
stat_rate: "sensor.power_grid_return",
|
||||
cost_adjustment_day: 0,
|
||||
},
|
||||
{
|
||||
type: "solar",
|
||||
stat_energy_from: "sensor.solar_production",
|
||||
stat_rate: "sensor.power_solar",
|
||||
config_entry_solar_forecast: ["solar_forecast"],
|
||||
},
|
||||
{
|
||||
type: "battery",
|
||||
stat_energy_from: "sensor.battery_output",
|
||||
stat_energy_to: "sensor.battery_input",
|
||||
stat_rate: "sensor.power_battery",
|
||||
},
|
||||
{
|
||||
type: "gas",
|
||||
stat_energy_from: "sensor.energy_gas",
|
||||
stat_cost: "sensor.energy_gas_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
{
|
||||
type: "water",
|
||||
stat_energy_from: "sensor.energy_water",
|
||||
stat_cost: "sensor.energy_water_cost",
|
||||
entity_energy_price: null,
|
||||
number_energy_price: null,
|
||||
},
|
||||
],
|
||||
device_consumption: [
|
||||
{
|
||||
stat_consumption: "sensor.energy_car",
|
||||
stat_rate: "sensor.power_car",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_ac",
|
||||
stat_rate: "sensor.power_ac",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_washing_machine",
|
||||
stat_rate: "sensor.power_washing_machine",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_dryer",
|
||||
stat_rate: "sensor.power_dryer",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_heat_pump",
|
||||
stat_rate: "sensor.power_heat_pump",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.energy_boiler",
|
||||
stat_rate: "sensor.power_boiler",
|
||||
},
|
||||
],
|
||||
device_consumption_water: [
|
||||
{
|
||||
stat_consumption: "sensor.water_kitchen",
|
||||
},
|
||||
{
|
||||
stat_consumption: "sensor.water_garden",
|
||||
},
|
||||
],
|
||||
}));
|
||||
hass.mockWS("energy/info", (): EnergyInfo => ({
|
||||
cost_sensors: {},
|
||||
solar_forecast_domains: [],
|
||||
}));
|
||||
hass.mockWS(
|
||||
"energy/fossil_energy_consumption",
|
||||
({ period }): FossilEnergyConsumption => ({
|
||||
@@ -113,51 +110,48 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
||||
);
|
||||
const todayString = format(startOfToday(), "yyyy-MM-dd");
|
||||
const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd");
|
||||
hass.mockWS(
|
||||
"energy/solar_forecast",
|
||||
(): EnergySolarForecasts => ({
|
||||
solar_forecast: {
|
||||
wh_hours: {
|
||||
[`${todayString}T06:00:00`]: 0,
|
||||
[`${todayString}T06:23:00`]: 6,
|
||||
[`${todayString}T06:45:00`]: 39,
|
||||
[`${todayString}T07:00:00`]: 28,
|
||||
[`${todayString}T08:00:00`]: 208,
|
||||
[`${todayString}T09:00:00`]: 352,
|
||||
[`${todayString}T10:00:00`]: 544,
|
||||
[`${todayString}T11:00:00`]: 748,
|
||||
[`${todayString}T12:00:00`]: 1259,
|
||||
[`${todayString}T13:00:00`]: 1361,
|
||||
[`${todayString}T14:00:00`]: 1373,
|
||||
[`${todayString}T15:00:00`]: 1370,
|
||||
[`${todayString}T16:00:00`]: 1186,
|
||||
[`${todayString}T17:00:00`]: 937,
|
||||
[`${todayString}T18:00:00`]: 652,
|
||||
[`${todayString}T19:00:00`]: 370,
|
||||
[`${todayString}T20:00:00`]: 155,
|
||||
[`${todayString}T21:48:00`]: 24,
|
||||
[`${todayString}T22:36:00`]: 0,
|
||||
[`${tomorrowString}T06:01:00`]: 0,
|
||||
[`${tomorrowString}T06:23:00`]: 9,
|
||||
[`${tomorrowString}T06:45:00`]: 47,
|
||||
[`${tomorrowString}T07:00:00`]: 48,
|
||||
[`${tomorrowString}T08:00:00`]: 473,
|
||||
[`${tomorrowString}T09:00:00`]: 827,
|
||||
[`${tomorrowString}T10:00:00`]: 1153,
|
||||
[`${tomorrowString}T11:00:00`]: 1413,
|
||||
[`${tomorrowString}T12:00:00`]: 1590,
|
||||
[`${tomorrowString}T13:00:00`]: 1652,
|
||||
[`${tomorrowString}T14:00:00`]: 1612,
|
||||
[`${tomorrowString}T15:00:00`]: 1438,
|
||||
[`${tomorrowString}T16:00:00`]: 1149,
|
||||
[`${tomorrowString}T17:00:00`]: 830,
|
||||
[`${tomorrowString}T18:00:00`]: 542,
|
||||
[`${tomorrowString}T19:00:00`]: 311,
|
||||
[`${tomorrowString}T20:00:00`]: 140,
|
||||
[`${tomorrowString}T21:47:00`]: 22,
|
||||
[`${tomorrowString}T22:34:00`]: 0,
|
||||
},
|
||||
hass.mockWS("energy/solar_forecast", (): EnergySolarForecasts => ({
|
||||
solar_forecast: {
|
||||
wh_hours: {
|
||||
[`${todayString}T06:00:00`]: 0,
|
||||
[`${todayString}T06:23:00`]: 6,
|
||||
[`${todayString}T06:45:00`]: 39,
|
||||
[`${todayString}T07:00:00`]: 28,
|
||||
[`${todayString}T08:00:00`]: 208,
|
||||
[`${todayString}T09:00:00`]: 352,
|
||||
[`${todayString}T10:00:00`]: 544,
|
||||
[`${todayString}T11:00:00`]: 748,
|
||||
[`${todayString}T12:00:00`]: 1259,
|
||||
[`${todayString}T13:00:00`]: 1361,
|
||||
[`${todayString}T14:00:00`]: 1373,
|
||||
[`${todayString}T15:00:00`]: 1370,
|
||||
[`${todayString}T16:00:00`]: 1186,
|
||||
[`${todayString}T17:00:00`]: 937,
|
||||
[`${todayString}T18:00:00`]: 652,
|
||||
[`${todayString}T19:00:00`]: 370,
|
||||
[`${todayString}T20:00:00`]: 155,
|
||||
[`${todayString}T21:48:00`]: 24,
|
||||
[`${todayString}T22:36:00`]: 0,
|
||||
[`${tomorrowString}T06:01:00`]: 0,
|
||||
[`${tomorrowString}T06:23:00`]: 9,
|
||||
[`${tomorrowString}T06:45:00`]: 47,
|
||||
[`${tomorrowString}T07:00:00`]: 48,
|
||||
[`${tomorrowString}T08:00:00`]: 473,
|
||||
[`${tomorrowString}T09:00:00`]: 827,
|
||||
[`${tomorrowString}T10:00:00`]: 1153,
|
||||
[`${tomorrowString}T11:00:00`]: 1413,
|
||||
[`${tomorrowString}T12:00:00`]: 1590,
|
||||
[`${tomorrowString}T13:00:00`]: 1652,
|
||||
[`${tomorrowString}T14:00:00`]: 1612,
|
||||
[`${tomorrowString}T15:00:00`]: 1438,
|
||||
[`${tomorrowString}T16:00:00`]: 1149,
|
||||
[`${tomorrowString}T17:00:00`]: 830,
|
||||
[`${tomorrowString}T18:00:00`]: 542,
|
||||
[`${tomorrowString}T19:00:00`]: 311,
|
||||
[`${tomorrowString}T20:00:00`]: 140,
|
||||
[`${tomorrowString}T21:47:00`]: 22,
|
||||
[`${tomorrowString}T22:34:00`]: 0,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -2,11 +2,8 @@ import type { EntitySources } from "../../../src/data/entity/entity_sources";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockEntitySources = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS(
|
||||
"entity/source",
|
||||
(): EntitySources => ({
|
||||
"sensor.co2_intensity": { domain: "co2signal" },
|
||||
"sensor.grid_fossil_fuel_percentage": { domain: "co2signal" },
|
||||
})
|
||||
);
|
||||
hass.mockWS("entity/source", (): EntitySources => ({
|
||||
"sensor.co2_intensity": { domain: "co2signal" },
|
||||
"sensor.grid_fossil_fuel_percentage": { domain: "co2signal" },
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -2,12 +2,9 @@ import type { NetworkUrls } from "../../../src/data/network";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockNetwork = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS(
|
||||
"network/url",
|
||||
(): NetworkUrls => ({
|
||||
internal: "http://homeassistant.local:8123",
|
||||
external: "https://demo-instance.ui.nabu.casa",
|
||||
cloud: "https://demo-instance.ui.nabu.casa",
|
||||
})
|
||||
);
|
||||
hass.mockWS("network/url", (): NetworkUrls => ({
|
||||
internal: "http://homeassistant.local:8123",
|
||||
external: "https://demo-instance.ui.nabu.casa",
|
||||
cloud: "https://demo-instance.ui.nabu.casa",
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockTranslations = (hass: MockHomeAssistant) => {
|
||||
hass.mockWS(
|
||||
"frontend/get_translations",
|
||||
(/* msg: {language: string, category: string} */) => ({ resources: {} })
|
||||
);
|
||||
hass.mockWS("frontend/get_translations", (
|
||||
/* msg: {language: string, category: string} */
|
||||
) => ({ resources: {} }));
|
||||
};
|
||||
|
||||
@@ -240,6 +240,12 @@ export default tseslint.config(
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [".github/scripts/*.mjs"],
|
||||
languageOptions: {
|
||||
globals: globals.node,
|
||||
},
|
||||
},
|
||||
{
|
||||
plugins: {
|
||||
html,
|
||||
|
||||
@@ -101,9 +101,11 @@ class DemoBlackWhiteRow extends LitElement {
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
${this.value
|
||||
? html`<pre>${JSON.stringify(this.value, undefined, 2)}</pre>`
|
||||
: nothing}
|
||||
${
|
||||
this.value
|
||||
? html`<pre>${JSON.stringify(this.value, undefined, 2)}</pre>`
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -34,9 +34,11 @@ class DemoCard extends LitElement {
|
||||
return html`
|
||||
<h2>
|
||||
${this.config.heading}
|
||||
${this._size !== undefined
|
||||
? html`<small>(size ${this._size})</small>`
|
||||
: ""}
|
||||
${
|
||||
this._size !== undefined
|
||||
? html`<small>(size ${this._size})</small>`
|
||||
: ""
|
||||
}
|
||||
</h2>
|
||||
<div class="root">
|
||||
<hui-card
|
||||
@@ -44,9 +46,11 @@ class DemoCard extends LitElement {
|
||||
.hass=${this.hass}
|
||||
@card-updated=${this._cardUpdated}
|
||||
></hui-card>
|
||||
${this.showConfig
|
||||
? html`<pre>${this.config.config.trim()}</pre>`
|
||||
: nothing}
|
||||
${
|
||||
this.showConfig
|
||||
? html`<pre>${this.config.config.trim()}</pre>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ class DemoMoreInfo extends LitElement {
|
||||
<div class="root">
|
||||
<div id="card">
|
||||
<ha-card>
|
||||
${!computeShowNewMoreInfo(state)
|
||||
? html`<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
in-dialog
|
||||
></state-card-content>`
|
||||
: nothing}
|
||||
${
|
||||
!computeShowNewMoreInfo(state)
|
||||
? html`<state-card-content
|
||||
.stateObj=${state}
|
||||
.hass=${this.hass}
|
||||
in-dialog
|
||||
></state-card-content>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<more-info-content
|
||||
.hass=${this.hass}
|
||||
|
||||
+165
-55
@@ -1,3 +1,4 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { mdiCog, mdiMenu } from "@mdi/js";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -19,6 +20,22 @@ import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/ha-top-app-bar-fixed";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import {
|
||||
apiContext,
|
||||
areasContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
devicesContext,
|
||||
entitiesContext,
|
||||
floorsContext,
|
||||
formattersContext,
|
||||
internationalizationContext,
|
||||
registriesContext,
|
||||
servicesContext,
|
||||
statesContext,
|
||||
uiContext,
|
||||
} from "../../src/data/context";
|
||||
import { updateHassGroups } from "../../src/data/context/updateContext";
|
||||
import type { HomeAssistant, ThemeSettings } from "../../src/types";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
import {
|
||||
@@ -113,6 +130,65 @@ class HaGallery extends LitElement {
|
||||
|
||||
@state() private _drawerOpen = !this._narrow;
|
||||
|
||||
// Fallback Lit context providers for the whole gallery. The real app's root
|
||||
// element provides these via `contextMixin`; here we mirror that so demos
|
||||
// which render context-consuming components without setting up their own hass
|
||||
// (e.g. bare component demos) still resolve `localize`, formatters, config,
|
||||
// etc. instead of throwing during init. Demos that call `provideHass`
|
||||
// register their own providers closer in the tree, which take precedence.
|
||||
private _contextProviders = {
|
||||
registries: new ContextProvider(this, { context: registriesContext }),
|
||||
internationalization: new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
}),
|
||||
api: new ContextProvider(this, { context: apiContext }),
|
||||
connection: new ContextProvider(this, { context: connectionContext }),
|
||||
ui: new ContextProvider(this, { context: uiContext }),
|
||||
config: new ContextProvider(this, { context: configContext }),
|
||||
formatters: new ContextProvider(this, { context: formattersContext }),
|
||||
};
|
||||
|
||||
// The individual (non-grouped) contexts contextMixin also provides. Components
|
||||
// such as ha-area-picker / ha-entity-picker consume these directly, so the
|
||||
// fallback must cover them too.
|
||||
private _singleContextProviders = {
|
||||
states: new ContextProvider(this, { context: statesContext }),
|
||||
services: new ContextProvider(this, { context: servicesContext }),
|
||||
entities: new ContextProvider(this, { context: entitiesContext }),
|
||||
devices: new ContextProvider(this, { context: devicesContext }),
|
||||
areas: new ContextProvider(this, { context: areasContext }),
|
||||
floors: new ContextProvider(this, { context: floorsContext }),
|
||||
};
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Refresh the fallback contexts before each render so theme/page changes in
|
||||
// the gallery hass propagate to consuming components.
|
||||
const hass = this._galleryHass;
|
||||
(
|
||||
Object.keys(
|
||||
this._contextProviders
|
||||
) as (keyof typeof this._contextProviders)[]
|
||||
).forEach((group) => {
|
||||
const provider = this._contextProviders[group];
|
||||
provider.setValue(
|
||||
(updateHassGroups[group] as (h: HomeAssistant, v?: any) => any)(
|
||||
hass,
|
||||
provider.value
|
||||
)
|
||||
);
|
||||
});
|
||||
(
|
||||
Object.keys(
|
||||
this._singleContextProviders
|
||||
) as (keyof typeof this._singleContextProviders)[]
|
||||
).forEach((key) => {
|
||||
(this._singleContextProviders[key] as ContextProvider<any>).setValue(
|
||||
hass[key]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const isSettingsPage = this._page === SETTINGS_PAGE;
|
||||
const page = isSettingsPage ? undefined : PAGES[this._page];
|
||||
@@ -135,38 +211,46 @@ class HaGallery extends LitElement {
|
||||
</ha-sidebar>
|
||||
<div slot="appContent" class="app-content">
|
||||
<ha-top-app-bar-fixed .narrow=${this._narrow}>
|
||||
${this._narrow || !this._drawerOpen
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._toggleDrawer}
|
||||
.path=${mdiMenu}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${
|
||||
this._narrow || !this._drawerOpen
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._toggleDrawer}
|
||||
.path=${mdiMenu}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div slot="title">
|
||||
${isSettingsPage
|
||||
? "Settings"
|
||||
: page?.metadata.title || this._page.split("/")[1]}
|
||||
${
|
||||
isSettingsPage
|
||||
? "Settings"
|
||||
: page?.metadata.title || this._page.split("/")[1]
|
||||
}
|
||||
</div>
|
||||
<div class="content">
|
||||
${isSettingsPage
|
||||
? html`<gallery-settings
|
||||
.hass=${this._galleryHass}
|
||||
.themeSettings=${this._themeSettings}
|
||||
.narrow=${this._narrow}
|
||||
.rtl=${this._rtl}
|
||||
@theme-settings-changed=${this._themeSettingsChanged}
|
||||
@gallery-rtl-changed=${this._rtlChanged}
|
||||
></gallery-settings>`
|
||||
: html`
|
||||
${page?.description
|
||||
? html`
|
||||
<page-description .page=${this._page}>
|
||||
</page-description>
|
||||
`
|
||||
: nothing}
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
`}
|
||||
${
|
||||
isSettingsPage
|
||||
? html`<gallery-settings
|
||||
.hass=${this._galleryHass}
|
||||
.themeSettings=${this._themeSettings}
|
||||
.narrow=${this._narrow}
|
||||
.rtl=${this._rtl}
|
||||
@theme-settings-changed=${this._themeSettingsChanged}
|
||||
@gallery-rtl-changed=${this._rtlChanged}
|
||||
></gallery-settings>`
|
||||
: html`
|
||||
${
|
||||
page?.description
|
||||
? html`
|
||||
<page-description .page=${this._page}>
|
||||
</page-description>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${dynamicElement(`demo-${this._page.replace("/", "-")}`)}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
${isSettingsPage || !page ? nothing : this._renderPageFooter(page)}
|
||||
</ha-top-app-bar-fixed>
|
||||
@@ -314,13 +398,15 @@ class HaGallery extends LitElement {
|
||||
.header=${group.header}
|
||||
?expanded=${expanded}
|
||||
>
|
||||
${group.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="gallery-sidebar-icon"
|
||||
.path=${group.icon}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
group.icon
|
||||
? html`<ha-svg-icon
|
||||
slot="leading-icon"
|
||||
class="gallery-sidebar-icon"
|
||||
.path=${group.icon}
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
${content}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
@@ -378,9 +464,11 @@ class HaGallery extends LitElement {
|
||||
?selected=${this._page === page}
|
||||
href=${`#${page}`}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline">${title}</span>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
@@ -411,23 +499,30 @@ class HaGallery extends LitElement {
|
||||
Suggest an edit to this page, or provide/view feedback for this page.
|
||||
</div>
|
||||
<div>
|
||||
${page.description || Object.keys(page.metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
${page.demo
|
||||
? html`
|
||||
<a href=${`${GITHUB_DEMO_URL}${this._page}.ts`} target="_blank">
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
page.description || Object.keys(page.metadata).length > 0
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.markdown`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit text
|
||||
</a>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
page.demo
|
||||
? html`
|
||||
<a
|
||||
href=${`${GITHUB_DEMO_URL}${this._page}.ts`}
|
||||
target="_blank"
|
||||
>
|
||||
Edit demo
|
||||
</a>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -576,6 +671,21 @@ class HaGallery extends LitElement {
|
||||
callWS: async () => undefined,
|
||||
fetchWithAuth: async () => new Response(),
|
||||
sendWS: () => undefined,
|
||||
formatEntityState: (stateObj, stateValue) =>
|
||||
(stateValue != null ? stateValue : stateObj.state) ?? "",
|
||||
formatEntityStateToParts: (stateObj, stateValue) => [
|
||||
{
|
||||
type: "value",
|
||||
value: (stateValue != null ? stateValue : stateObj.state) ?? "",
|
||||
},
|
||||
],
|
||||
formatEntityAttributeName: (_stateObj, attribute) => attribute,
|
||||
formatEntityAttributeValue: (stateObj, attribute, value) =>
|
||||
value != null ? value : (stateObj.attributes[attribute] ?? ""),
|
||||
formatEntityName: (stateObj, type) =>
|
||||
typeof type === "string"
|
||||
? type
|
||||
: (stateObj.attributes.friendly_name ?? stateObj.entity_id),
|
||||
} as unknown as HomeAssistant;
|
||||
}
|
||||
|
||||
|
||||
@@ -149,9 +149,11 @@ export class DemoAutomationDescribeAction extends LitElement {
|
||||
<ha-card header="Actions">
|
||||
<div class="action">
|
||||
<span>
|
||||
${this._action
|
||||
? describeAction(this.hass, [], this._action)
|
||||
: "<invalid YAML>"}
|
||||
${
|
||||
this._action
|
||||
? describeAction(this.hass, [], this._action)
|
||||
: "<invalid YAML>"
|
||||
}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
label="Action Config"
|
||||
|
||||
@@ -74,9 +74,11 @@ export class DemoAutomationDescribeCondition extends LitElement {
|
||||
<ha-card header="Conditions">
|
||||
<div class="condition">
|
||||
<span>
|
||||
${this._condition
|
||||
? describeCondition(this._condition, this.hass, [])
|
||||
: "<invalid YAML>"}
|
||||
${
|
||||
this._condition
|
||||
? describeCondition(this._condition, this.hass, [])
|
||||
: "<invalid YAML>"
|
||||
}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
label="Condition Config"
|
||||
|
||||
@@ -98,9 +98,11 @@ export class DemoAutomationDescribeTrigger extends LitElement {
|
||||
<ha-card header="Triggers">
|
||||
<div class="trigger">
|
||||
<span>
|
||||
${this._trigger
|
||||
? describeTrigger(this._trigger, this.hass, [])
|
||||
: "<invalid YAML>"}
|
||||
${
|
||||
this._trigger
|
||||
? describeTrigger(this._trigger, this.hass, [])
|
||||
: "<invalid YAML>"
|
||||
}
|
||||
</span>
|
||||
<ha-yaml-editor
|
||||
label="Trigger Config"
|
||||
|
||||
@@ -54,16 +54,18 @@ export class DemoAutomationTrace extends LitElement {
|
||||
@value-changed=${this._handleTimelineValueChanged}
|
||||
.sampleIdx=${idx}
|
||||
></hat-trace-timeline>
|
||||
${selectedNode && graph
|
||||
? html`<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.selected=${selectedNode}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.trackedNodes=${graph.trackedNodes}
|
||||
.renderedNodes=${graph.renderedNodes}
|
||||
></ha-trace-path-details>`
|
||||
: nothing}
|
||||
${
|
||||
selectedNode && graph
|
||||
? html`<ha-trace-path-details
|
||||
.hass=${this.hass}
|
||||
.trace=${trace.trace}
|
||||
.selected=${selectedNode}
|
||||
.logbookEntries=${trace.logbookEntries}
|
||||
.trackedNodes=${graph.trackedNodes}
|
||||
.renderedNodes=${graph.renderedNodes}
|
||||
></ha-trace-path-details>`
|
||||
: nothing
|
||||
}
|
||||
<button @click=${() => console.log(trace)}>Log trace</button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
@@ -33,20 +33,24 @@ export class DemoHaChips extends LitElement {
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-assist-chip .label=${chip.content}>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
</ha-assist-chip>
|
||||
`
|
||||
)}
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-assist-chip .label=${chip.content} selected>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
</ha-assist-chip>
|
||||
`
|
||||
)}
|
||||
@@ -56,20 +60,24 @@ export class DemoHaChips extends LitElement {
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-filter-chip .label=${chip.content}>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
</ha-filter-chip>
|
||||
`
|
||||
)}
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-filter-chip .label=${chip.content} selected>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
</ha-filter-chip>
|
||||
`
|
||||
)}
|
||||
@@ -79,10 +87,12 @@ export class DemoHaChips extends LitElement {
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-input-chip .label=${chip.content}>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
${chip.content}
|
||||
</ha-input-chip>
|
||||
`
|
||||
@@ -90,10 +100,12 @@ export class DemoHaChips extends LitElement {
|
||||
${chips.map(
|
||||
(chip) => html`
|
||||
<ha-input-chip .label=${chip.content} selected>
|
||||
${chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
chip.icon
|
||||
? html`<ha-svg-icon slot="icon" .path=${chip.icon}>
|
||||
</ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
</ha-input-chip>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -92,14 +92,16 @@ export class DemoHaControlSelectMenu extends LitElement {
|
||||
.value=${option.value}
|
||||
.graphic=${option.icon ? "icon" : undefined}
|
||||
>
|
||||
${option.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${option.icon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
option.icon
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${option.icon}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${option.label ?? option.value}
|
||||
</ha-list-item>
|
||||
`
|
||||
|
||||
@@ -60,9 +60,9 @@ export class DemoHaLabelBadge extends LitElement {
|
||||
${badges.map(
|
||||
(badge) => html`
|
||||
<ha-label-badge
|
||||
style="--ha-label-badge-color: ${colors[
|
||||
Math.floor(Math.random() * colors.length)
|
||||
]}"
|
||||
style="--ha-label-badge-color: ${
|
||||
colors[Math.floor(Math.random() * colors.length)]
|
||||
}"
|
||||
.label=${badge.label}
|
||||
.description=${badge.description}
|
||||
.image=${badge.image}
|
||||
@@ -78,9 +78,9 @@ export class DemoHaLabelBadge extends LitElement {
|
||||
(badge) => html`
|
||||
<div class="badge">
|
||||
<ha-label-badge
|
||||
style="--ha-label-badge-color: ${colors[
|
||||
Math.floor(Math.random() * colors.length)
|
||||
]}"
|
||||
style="--ha-label-badge-color: ${
|
||||
colors[Math.floor(Math.random() * colors.length)]
|
||||
}"
|
||||
.label=${badge.label}
|
||||
.description=${badge.description}
|
||||
.image=${badge.image}
|
||||
|
||||
@@ -244,8 +244,7 @@ export class DemoHaList extends LitElement {
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
|
||||
>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card
|
||||
@@ -272,8 +271,7 @@ selected: ${JSON.stringify(this._toJson(this._multiCheckStart))}</pre
|
||||
)}
|
||||
</ha-list-selectable>
|
||||
<pre>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre
|
||||
>
|
||||
selected: ${JSON.stringify(this._toJson(this._multiCheckEnd))}</pre>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Option: all combinations">
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
|
||||
@@ -15,11 +14,6 @@ import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import {
|
||||
configContext,
|
||||
internationalizationContext,
|
||||
} from "../../../../src/data/context";
|
||||
import { updateHassGroups } from "../../../../src/data/context/updateContext";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
import type { LabelRegistryEntry } from "../../../../src/data/label/label_registry";
|
||||
@@ -528,17 +522,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
|
||||
private data = SCHEMAS.map(() => ({}));
|
||||
|
||||
// The date/datetime selectors and the date-picker dialog consume these
|
||||
// contexts (provided by the root element in the real app). Provide them here
|
||||
// so they work in the gallery.
|
||||
private _i18nProvider = new ContextProvider(this, {
|
||||
context: internationalizationContext,
|
||||
});
|
||||
|
||||
private _configProvider = new ContextProvider(this, {
|
||||
context: configContext,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const hass = provideHass(this);
|
||||
@@ -560,16 +543,6 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("hass") && this.hass) {
|
||||
this._i18nProvider.setValue(
|
||||
updateHassGroups.internationalization(this.hass)
|
||||
);
|
||||
this._configProvider.setValue(updateHassGroups.config(this.hass));
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.addEventListener("show-dialog", this._dialogManager);
|
||||
@@ -723,11 +696,13 @@ class DemoHaSelector extends LitElement implements ProvideHassElement {
|
||||
([key, value]) => html`
|
||||
<ha-settings-row narrow slot=${slot}>
|
||||
<span slot="heading">${value?.name || key}</span>
|
||||
${value?.description
|
||||
? html`<span slot="description"
|
||||
>${value?.description}</span
|
||||
>`
|
||||
: nothing}
|
||||
${
|
||||
value?.description
|
||||
? html`<span slot="description"
|
||||
>${value?.description}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${value!.selector}
|
||||
|
||||
@@ -248,7 +248,7 @@ class DemoThermostatEntity extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.updateTranslations("lovelace", "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
|
||||
@@ -151,7 +151,7 @@ class DemoMoreInfoClimate extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ class DemoMoreInfoHumidifier extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
const hass = provideHass(this._demoRoot, {}, false, true);
|
||||
const hass = provideHass(this._demoRoot);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
@@ -65,30 +65,34 @@ class LandingPageLogs extends LitElement {
|
||||
<ha-button appearance="plain" @click=${this._toggleLogDetails}>
|
||||
${this.localize(this._show ? "hide_details" : "show_details")}
|
||||
</ha-button>
|
||||
${this._show
|
||||
? html`<ha-icon-button
|
||||
.label=${this.localize("logs.download_logs")}
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${
|
||||
this._show
|
||||
? html`<ha-icon-button
|
||||
.label=${this.localize("logs.download_logs")}
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${this._error
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.localize("logs.fetch_error")}
|
||||
>
|
||||
<ha-button
|
||||
size="small"
|
||||
variant="danger"
|
||||
@click=${this._startLogStream}
|
||||
${
|
||||
this._error
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.localize("logs.fetch_error")}
|
||||
>
|
||||
${this.localize("logs.retry")}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
<ha-button
|
||||
size="small"
|
||||
variant="danger"
|
||||
@click=${this._startLogStream}
|
||||
>
|
||||
${this.localize("logs.retry")}
|
||||
</ha-button>
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div
|
||||
class=${classMap({
|
||||
logs: true,
|
||||
|
||||
@@ -55,13 +55,15 @@ class LandingPageNetwork extends LitElement {
|
||||
})}
|
||||
</p>
|
||||
<p>${this.localize("network_issue.resolve_different")}</p>
|
||||
${!dnsPrimaryInterfaceNameservers
|
||||
? html`
|
||||
<p>
|
||||
<b>${this.localize("network_issue.no_primary_interface")} </b>
|
||||
</p>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
!dnsPrimaryInterfaceNameservers
|
||||
? html`
|
||||
<p>
|
||||
<b>${this.localize("network_issue.no_primary_interface")} </b>
|
||||
</p>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="actions">
|
||||
${ALTERNATIVE_DNS_SERVERS.map(
|
||||
({ translationKey }, key) =>
|
||||
|
||||
@@ -61,39 +61,47 @@ class HaLandingPage extends LandingPageBaseElement {
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<h1>${this.localize("header")}</h1>
|
||||
${!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<ha-progress-bar
|
||||
.indeterminate=${this._progress <= 0}
|
||||
.value=${this._progress > 0 ? this._progress : undefined}
|
||||
.loading=${this._progress >= 0}
|
||||
>${this._progress > 0
|
||||
? `${Math.round(this._progress)}%`
|
||||
: nothing}</ha-progress-bar
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
${networkIssue || this._networkInfoError
|
||||
? html`
|
||||
<landing-page-network
|
||||
.localize=${this.localize}
|
||||
.networkInfo=${this._networkInfo}
|
||||
.error=${this._networkInfoError}
|
||||
@dns-set=${this._fetchSupervisorInfo}
|
||||
></landing-page-network>
|
||||
`
|
||||
: nothing}
|
||||
${this._supervisorError
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.localize("error_title")}
|
||||
>
|
||||
${this.localize("error_description")}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
!networkIssue && !this._supervisorError
|
||||
? html`
|
||||
<p>${this.localize("subheader")}</p>
|
||||
<ha-progress-bar
|
||||
.indeterminate=${this._progress <= 0}
|
||||
.value=${this._progress > 0 ? this._progress : undefined}
|
||||
.loading=${this._progress >= 0}
|
||||
>${
|
||||
this._progress > 0
|
||||
? `${Math.round(this._progress)}%`
|
||||
: nothing
|
||||
}</ha-progress-bar
|
||||
>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
networkIssue || this._networkInfoError
|
||||
? html`
|
||||
<landing-page-network
|
||||
.localize=${this.localize}
|
||||
.networkInfo=${this._networkInfo}
|
||||
.error=${this._networkInfoError}
|
||||
@dns-set=${this._fetchSupervisorInfo}
|
||||
></landing-page-network>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._supervisorError
|
||||
? html`
|
||||
<ha-alert
|
||||
alert-type="error"
|
||||
.title=${this.localize("error_title")}
|
||||
>
|
||||
${this.localize("error_description")}
|
||||
</ha-alert>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<landing-page-logs
|
||||
.localize=${this.localize}
|
||||
@landing-page-error=${this._showError}
|
||||
|
||||
+23
-22
@@ -23,10 +23,12 @@
|
||||
"test": "vitest run --config test/vitest.config.ts",
|
||||
"test:bench": "vitest bench --run --config test/vitest.bench.config.ts",
|
||||
"test:coverage": "vitest run --config test/vitest.config.ts --coverage",
|
||||
"check-bundlesize": "node build-scripts/check-bundle-size.cjs",
|
||||
"test:e2e": "node test/e2e/run-suites.mjs demo app gallery",
|
||||
"test:e2e:show-report": "yarn playwright show-report test/e2e/reports/combined",
|
||||
"test:e2e:demo": "playwright test --config test/e2e/playwright.demo.config.ts",
|
||||
"test:e2e:app": "playwright test --config test/e2e/playwright.app.config.ts",
|
||||
"test:e2e:app:dev": "test/e2e/app/script/develop_app",
|
||||
"test:e2e:gallery": "playwright test --config test/e2e/playwright.gallery.config.ts"
|
||||
},
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
@@ -36,14 +38,14 @@
|
||||
"@babel/runtime": "8.0.0",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.3",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
"@codemirror/commands": "6.10.4",
|
||||
"@codemirror/lang-jinja": "6.0.1",
|
||||
"@codemirror/lang-yaml": "6.1.3",
|
||||
"@codemirror/language": "6.12.3",
|
||||
"@codemirror/language": "6.12.4",
|
||||
"@codemirror/lint": "6.9.7",
|
||||
"@codemirror/search": "6.7.1",
|
||||
"@codemirror/state": "6.6.0",
|
||||
"@codemirror/view": "6.43.1",
|
||||
"@codemirror/state": "6.7.0",
|
||||
"@codemirror/view": "6.43.4",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.9",
|
||||
@@ -77,9 +79,10 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.2.1",
|
||||
"@tsparticles/preset-links": "4.2.1",
|
||||
"@tsparticles/engine": "4.3.0",
|
||||
"@tsparticles/preset-links": "4.3.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.198.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"barcode-detector": "3.2.0",
|
||||
@@ -96,13 +99,12 @@
|
||||
"echarts": "6.1.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.4.2",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.16",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.5",
|
||||
"intl-messageformat": "11.2.8",
|
||||
"js-yaml": "4.2.0",
|
||||
"intl-messageformat": "11.2.9",
|
||||
"js-yaml": "5.2.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -139,22 +141,21 @@
|
||||
"@babel/preset-env": "8.0.2",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.62.0",
|
||||
"@html-eslint/eslint-plugin": "0.63.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.60.0",
|
||||
"@rsdoctor/rspack-plugin": "1.5.15",
|
||||
"@rspack/core": "2.0.8",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.16",
|
||||
"@rspack/core": "2.1.1",
|
||||
"@rspack/dev-server": "2.1.0",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
"@types/culori": "4.0.1",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet-draw": "1.0.13",
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
@@ -168,10 +169,10 @@
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.5.0",
|
||||
"eslint": "10.6.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
"eslint-plugin-lit": "2.3.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
@@ -180,7 +181,7 @@
|
||||
"fs-extra": "11.3.5",
|
||||
"generate-license-file": "4.2.1",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.6.0",
|
||||
"globals": "17.7.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-brotli": "3.0.0",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -197,15 +198,15 @@
|
||||
"map-stream": "0.0.7",
|
||||
"minify-literals": "2.0.2",
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.8.4",
|
||||
"prettier": "3.9.1",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.16",
|
||||
"tar": "7.5.19",
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.61.1",
|
||||
"typescript-eslint": "8.62.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.9",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -218,7 +219,7 @@
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.21",
|
||||
"globals": "17.6.0",
|
||||
"globals": "17.7.0",
|
||||
"tslib": "2.8.1",
|
||||
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"glob@^10.2.2": "^10.5.0"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260624.3"
|
||||
version = "20260624.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
+23
-17
@@ -182,9 +182,11 @@ export class HaAuthFlow extends LitElement {
|
||||
@click=${this._handleSubmit}
|
||||
.loading=${this._submitting}
|
||||
>
|
||||
${this.step.type === "form"
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
: this.localize("ui.panel.page-authorize.form.start_over")}
|
||||
${
|
||||
this.step.type === "form"
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
: this.localize("ui.panel.page-authorize.form.start_over")
|
||||
}
|
||||
</ha-button>
|
||||
</div>
|
||||
`;
|
||||
@@ -224,9 +226,11 @@ 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")}
|
||||
${
|
||||
!["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)}
|
||||
${keyed(
|
||||
@@ -244,17 +248,19 @@ export class HaAuthFlow extends LitElement {
|
||||
)}
|
||||
|
||||
<div class="space-between">
|
||||
${this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<ha-checkbox
|
||||
.checked=${this._storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
>
|
||||
${this.localize("ui.panel.page-authorize.store_token")}
|
||||
</ha-checkbox>
|
||||
`
|
||||
: ""}
|
||||
${
|
||||
this.clientId === genClientId() &&
|
||||
!["select_mfa_module", "mfa"].includes(step.step_id)
|
||||
? html`
|
||||
<ha-checkbox
|
||||
.checked=${this._storeToken}
|
||||
@change=${this._storeTokenChanged}
|
||||
>
|
||||
${this.localize("ui.panel.page-authorize.store_token")}
|
||||
</ha-checkbox>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<a
|
||||
class="forgot-password"
|
||||
href="https://www.home-assistant.io/docs/locked_out/#forgot-password"
|
||||
|
||||
+50
-37
@@ -147,45 +147,58 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
|
||||
}
|
||||
</style>
|
||||
|
||||
${!this._ownInstance
|
||||
? html`<ha-alert .alertType=${app ? "info" : "warning"}>
|
||||
${app
|
||||
? this.localize("ui.panel.page-authorize.authorizing_app", {
|
||||
app: appNames[this.clientId!],
|
||||
})
|
||||
: this.localize("ui.panel.page-authorize.authorizing_client", {
|
||||
clientId: html`<b
|
||||
>${this.clientId
|
||||
? punycode.toASCII(this.clientId)
|
||||
: this.clientId}</b
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${
|
||||
!this._ownInstance
|
||||
? html`<ha-alert .alertType=${app ? "info" : "warning"}>
|
||||
${
|
||||
app
|
||||
? this.localize("ui.panel.page-authorize.authorizing_app", {
|
||||
app: appNames[this.clientId!],
|
||||
})
|
||||
: this.localize(
|
||||
"ui.panel.page-authorize.authorizing_client",
|
||||
{
|
||||
clientId: html`<b
|
||||
>${
|
||||
this.clientId
|
||||
? punycode.toASCII(this.clientId)
|
||||
: this.clientId
|
||||
}</b
|
||||
>`,
|
||||
}
|
||||
)
|
||||
}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="card-content">
|
||||
${!this._authProvider
|
||||
? html`<p>
|
||||
${this.localize("ui.panel.page-authorize.initializing")}
|
||||
</p> `
|
||||
: html`<ha-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.localize=${this.localize}
|
||||
.initStoreToken=${this._preselectStoreToken}
|
||||
></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>
|
||||
`
|
||||
: ""}`}
|
||||
${
|
||||
!this._authProvider
|
||||
? html`<p>
|
||||
${this.localize("ui.panel.page-authorize.initializing")}
|
||||
</p> `
|
||||
: html`<ha-auth-flow
|
||||
.clientId=${this.clientId}
|
||||
.redirectUri=${this.redirectUri}
|
||||
.oauth2State=${this.oauth2State}
|
||||
.authProvider=${this._authProvider}
|
||||
.localize=${this.localize}
|
||||
.initStoreToken=${this._preselectStoreToken}
|
||||
></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
|
||||
|
||||
@@ -33,10 +33,7 @@ export interface ShowDemoMessage extends BaseCastMessage {
|
||||
}
|
||||
|
||||
export type HassMessage =
|
||||
| ShowDemoMessage
|
||||
| GetStatusMessage
|
||||
| ConnectMessage
|
||||
| ShowLovelaceViewMessage;
|
||||
ShowDemoMessage | GetStatusMessage | ConnectMessage | ShowLovelaceViewMessage;
|
||||
|
||||
export const castSendAuth = (cast: CastManager, auth: Auth) =>
|
||||
cast.sendMessage({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import timezones from "google-timezones-json";
|
||||
import { timeZonesNames } from "@vvo/tzdb";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
|
||||
const RESOLVED_RAW = Intl.DateTimeFormat?.().resolvedOptions?.().timeZone;
|
||||
@@ -10,7 +10,7 @@ const RESOLVED_TIME_ZONE =
|
||||
RESOLVED_RAW &&
|
||||
(RESOLVED_RAW === "UTC" ||
|
||||
RESOLVED_RAW === "Etc/UTC" ||
|
||||
RESOLVED_RAW in timezones)
|
||||
timeZonesNames.includes(RESOLVED_RAW))
|
||||
? RESOLVED_RAW
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
export type WeekdayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
export type WeekdayShort =
|
||||
| "sun"
|
||||
| "mon"
|
||||
| "tue"
|
||||
| "wed"
|
||||
| "thu"
|
||||
| "fri"
|
||||
| "sat";
|
||||
"sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
|
||||
|
||||
export type WeekdayLong =
|
||||
| "sunday"
|
||||
|
||||
@@ -13,8 +13,7 @@ export const isNavigationClick = (e: MouseEvent, preventDefault = true) => {
|
||||
const anchor = e
|
||||
.composedPath()
|
||||
.find((n) => (n as HTMLElement).tagName === "A") as
|
||||
| HTMLAnchorElement
|
||||
| undefined;
|
||||
HTMLAnchorElement | undefined;
|
||||
if (
|
||||
!anchor ||
|
||||
anchor.target ||
|
||||
|
||||
@@ -82,8 +82,7 @@ export const computeAttributeValueToParts = (
|
||||
: formatNumber(attributeValue, locale);
|
||||
|
||||
let unit = DOMAIN_ATTRIBUTES_UNITS[domain]?.[attribute] as
|
||||
| string
|
||||
| undefined;
|
||||
string | undefined;
|
||||
|
||||
if (domain === "weather") {
|
||||
unit = getWeatherUnit(config, stateObj as WeatherEntity, attribute);
|
||||
@@ -156,8 +155,7 @@ export const computeAttributeValueToParts = (
|
||||
const domain = computeDomain(entityId);
|
||||
const deviceClass = stateObj.attributes.device_class;
|
||||
const registryEntry = entities[entityId] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
const translationKey = registryEntry?.translation_key;
|
||||
|
||||
const formattedValue =
|
||||
|
||||
@@ -14,8 +14,7 @@ export const computeEntityName = (
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
|
||||
if (!entry) {
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
|
||||
@@ -49,8 +49,7 @@ export const computeStateDisplay = (
|
||||
state?: string
|
||||
): string => {
|
||||
const entity = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
return computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
@@ -302,8 +301,7 @@ export const computeStateToParts = (
|
||||
state?: string
|
||||
): ValuePart[] => {
|
||||
const entity = entities?.[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
return computeStateToPartsFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
|
||||
@@ -24,8 +24,7 @@ export const getEntityContext = (
|
||||
floors: HomeAssistant["floors"]
|
||||
): EntityContext => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
| undefined;
|
||||
EntityRegistryDisplayEntry | undefined;
|
||||
|
||||
if (!entry) {
|
||||
return {
|
||||
@@ -52,9 +51,7 @@ export const getEntityAreaId = (
|
||||
|
||||
export const getEntityEntryContext = (
|
||||
entry:
|
||||
| EntityRegistryDisplayEntry
|
||||
| EntityRegistryEntry
|
||||
| ExtEntityRegistryEntry,
|
||||
EntityRegistryDisplayEntry | EntityRegistryEntry | ExtEntityRegistryEntry,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
|
||||
@@ -114,8 +114,7 @@ export const computeLocalize = async <Keys extends string = LocalizeKeys>(
|
||||
|
||||
const messageKey = key + translatedValue;
|
||||
let translatedMessage = cache._localizationCache![messageKey] as
|
||||
| IntlMessageFormat
|
||||
| undefined;
|
||||
IntlMessageFormat | undefined;
|
||||
|
||||
if (!translatedMessage) {
|
||||
try {
|
||||
|
||||
@@ -10,11 +10,7 @@ import {
|
||||
} from "./query-params";
|
||||
|
||||
export type HistoryLogbookTargetParamKey =
|
||||
| "entity_id"
|
||||
| "label_id"
|
||||
| "floor_id"
|
||||
| "area_id"
|
||||
| "device_id";
|
||||
"entity_id" | "label_id" | "floor_id" | "area_id" | "device_id";
|
||||
|
||||
export const historyLogbookTargetParamKeys: readonly HistoryLogbookTargetParamKey[] =
|
||||
["entity_id", "label_id", "floor_id", "area_id", "device_id"];
|
||||
|
||||
@@ -2,9 +2,7 @@ import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
export type SearchParamsSource =
|
||||
| URLSearchParams
|
||||
| Record<string, string>
|
||||
| string;
|
||||
URLSearchParams | Record<string, string> | string;
|
||||
|
||||
export interface QueryParamConfig {
|
||||
list?: readonly string[];
|
||||
|
||||
@@ -27,7 +27,7 @@ export const deepEqual = (
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (!deepEqual(a[i], b[i], options)) {
|
||||
return false;
|
||||
}
|
||||
@@ -71,7 +71,7 @@ export const deepEqual = (
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
@@ -94,13 +94,13 @@ export const deepEqual = (
|
||||
if (length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
const key = keys[i];
|
||||
|
||||
if (!deepEqual(a[key], b[key], options)) {
|
||||
|
||||
@@ -3,14 +3,7 @@ import type { FrontendLocaleData } from "../../data/translation";
|
||||
import { firstWeekdayIndex } from "../datetime/first_weekday";
|
||||
|
||||
export type Unit =
|
||||
| "second"
|
||||
| "minute"
|
||||
| "hour"
|
||||
| "day"
|
||||
| "week"
|
||||
| "month"
|
||||
| "quarter"
|
||||
| "year";
|
||||
"second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year";
|
||||
|
||||
const MS_PER_SECOND = 1e3;
|
||||
const SECS_PER_MIN = 60;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const shallowEqual = (a: any, b: any): boolean => {
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export const shallowEqual = (a: any, b: any): boolean => {
|
||||
if (length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false;
|
||||
}
|
||||
@@ -85,13 +85,13 @@ export const shallowEqual = (a: any, b: any): boolean => {
|
||||
if (length !== Object.keys(b).length) {
|
||||
return false;
|
||||
}
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, keys[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = length; i-- !== 0; ) {
|
||||
for (i = length; i-- !== 0;) {
|
||||
const key = keys[i];
|
||||
|
||||
if (a[key] !== b[key]) {
|
||||
|
||||
@@ -56,11 +56,13 @@ export class HaAutomationConditionLiveTest extends LitElement {
|
||||
)}
|
||||
></ha-automation-row-live-test>
|
||||
</div>
|
||||
${this._liveTestResult.message
|
||||
? html`<ha-tooltip for="indicator"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing}
|
||||
${
|
||||
this._liveTestResult.message
|
||||
? html`<ha-tooltip for="indicator"
|
||||
>${this._liveTestResult.message}</ha-tooltip
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,16 +41,18 @@ export class HaAutomationRow extends LitElement {
|
||||
role="button"
|
||||
@keydown=${this._handleKeydown}
|
||||
>
|
||||
${this.leftChevron
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${mdiChevronUp}
|
||||
@click=${this._handleExpand}
|
||||
@keydown=${this._handleExpand}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this.leftChevron
|
||||
? html`
|
||||
<ha-icon-button
|
||||
class="expand-button"
|
||||
.path=${mdiChevronUp}
|
||||
@click=${this._handleExpand}
|
||||
@keydown=${this._handleExpand}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="leading-icon-wrapper">
|
||||
<slot name="leading-icon"></slot>
|
||||
</div>
|
||||
|
||||
@@ -34,37 +34,47 @@ export class HaProgressButton extends LitElement {
|
||||
.appearance=${appearance}
|
||||
.disabled=${this.disabled}
|
||||
.loading=${this.progress}
|
||||
.variant=${this._result === "success"
|
||||
? "success"
|
||||
: this._result === "error"
|
||||
? "danger"
|
||||
: this.variant}
|
||||
.variant=${
|
||||
this._result === "success"
|
||||
? "success"
|
||||
: this._result === "error"
|
||||
? "danger"
|
||||
: this.variant
|
||||
}
|
||||
class=${classMap({
|
||||
result: !!this._result,
|
||||
success: this._result === "success",
|
||||
error: this._result === "error",
|
||||
})}
|
||||
>
|
||||
${this.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${this.iconPath}
|
||||
slot="start"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
this.iconPath
|
||||
? html`<ha-svg-icon
|
||||
.path=${this.iconPath}
|
||||
slot="start"
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<slot>${this.label}</slot>
|
||||
</ha-button>
|
||||
${!this._result
|
||||
? nothing
|
||||
: html`
|
||||
<div class="progress">
|
||||
${this._result === "success"
|
||||
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
|
||||
: this._result === "error"
|
||||
? html`<ha-svg-icon .path=${mdiAlertOctagram}></ha-svg-icon>`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
${
|
||||
!this._result
|
||||
? nothing
|
||||
: html`
|
||||
<div class="progress">
|
||||
${
|
||||
this._result === "success"
|
||||
? html`<ha-svg-icon .path=${mdiCheckBold}></ha-svg-icon>`
|
||||
: this._result === "error"
|
||||
? html`<ha-svg-icon
|
||||
.path=${mdiAlertOctagram}
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -352,16 +352,18 @@ export class HaChartBase extends LitElement {
|
||||
<div
|
||||
class="chart-controls ${classMap({ small: this.smallControls })}"
|
||||
>
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${
|
||||
this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -465,9 +467,11 @@ export class HaChartBase extends LitElement {
|
||||
@click=${this._toggleDataset}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._hiddenDatasets.has(id)
|
||||
? mdiCircleOutline
|
||||
: mdiCheckCircle}
|
||||
.path=${
|
||||
this._hiddenDatasets.has(id)
|
||||
? mdiCircleOutline
|
||||
: mdiCheckCircle
|
||||
}
|
||||
style=${styleMap({
|
||||
color: this._hiddenDatasets.has(id) ? undefined : color,
|
||||
})}
|
||||
@@ -485,26 +489,30 @@ export class HaChartBase extends LitElement {
|
||||
${value ? html`<div class="value">${value}</div>` : nothing}
|
||||
</li>`;
|
||||
})}
|
||||
${items.length > overflowLimit
|
||||
? html`<li>
|
||||
<ha-assist-chip
|
||||
@click=${this._toggleExpandedLegend}
|
||||
filled
|
||||
label=${this.expandLegend
|
||||
? this.hass.localize(
|
||||
"ui.components.history_charts.collapse_legend"
|
||||
)
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.expand_legend"
|
||||
)} (${items.length - overflowLimit})`}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${this.expandLegend ? mdiChevronUp : mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
</li>`
|
||||
: nothing}
|
||||
${
|
||||
items.length > overflowLimit
|
||||
? html`<li>
|
||||
<ha-assist-chip
|
||||
@click=${this._toggleExpandedLegend}
|
||||
filled
|
||||
label=${
|
||||
this.expandLegend
|
||||
? this.hass.localize(
|
||||
"ui.components.history_charts.collapse_legend"
|
||||
)
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.expand_legend"
|
||||
)} (${items.length - overflowLimit})`
|
||||
}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="trailing-icon"
|
||||
.path=${this.expandLegend ? mdiChevronUp : mdiChevronDown}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
</li>`
|
||||
: nothing
|
||||
}
|
||||
</ul>
|
||||
</div>`;
|
||||
}
|
||||
@@ -667,8 +675,7 @@ export class HaChartBase extends LitElement {
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
LegendComponentOption | undefined;
|
||||
|
||||
let customLegendItem;
|
||||
if (legend?.type === "custom") {
|
||||
@@ -685,8 +692,7 @@ export class HaChartBase extends LitElement {
|
||||
private _updateHiddenStatsFromOptions(options: HaECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
| undefined;
|
||||
LegendComponentOption | undefined;
|
||||
Object.entries(legend?.selected || {}).forEach(([stat, selected]) => {
|
||||
if (selected === false) {
|
||||
this._getAllIdsFromLegend(options, stat).forEach((id) =>
|
||||
@@ -699,11 +705,9 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _getDataZoomConfig(): DataZoomComponentOption | undefined {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
| undefined;
|
||||
XAXisOption | undefined;
|
||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
YAXisOption | undefined;
|
||||
if (xAxis?.type === "value" && yAxis?.type === "category") {
|
||||
// vertical data zoom doesn't work well in this case and horizontal is pointless
|
||||
return undefined;
|
||||
@@ -1014,8 +1018,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
private _getSeries() {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
| undefined;
|
||||
XAXisOption | undefined;
|
||||
const series = ensureArray(this.data).map((s) => {
|
||||
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
|
||||
? undefined
|
||||
|
||||
@@ -205,9 +205,9 @@ export class StateHistoryChartLine extends LitElement {
|
||||
return html`<br /><ha-chart-tooltip-marker
|
||||
.color=${String(param.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${param.seriesName
|
||||
? html`${param.seriesName}: `
|
||||
: nothing}${value}${statSuffix}`;
|
||||
${
|
||||
param.seriesName ? html`${param.seriesName}: ` : nothing
|
||||
}${value}${statSuffix}`;
|
||||
})}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -145,9 +145,11 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
return html`${seriesName
|
||||
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
return html`${
|
||||
seriesName
|
||||
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: nothing
|
||||
}<ha-chart-tooltip-marker
|
||||
.color=${String(color ?? "")}
|
||||
.rtl=${rtl}
|
||||
></ha-chart-tooltip-marker
|
||||
|
||||
@@ -149,32 +149,36 @@ export class StateHistoryCharts extends LitElement {
|
||||
this._chartCount = combinedItems.length;
|
||||
|
||||
return html`
|
||||
${this.virtualize
|
||||
? html`<div
|
||||
class="container ha-scrollbar"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${combinedItems}
|
||||
.renderItem=${this._renderHistoryItem}
|
||||
${
|
||||
this.virtualize
|
||||
? html`<div
|
||||
class="container ha-scrollbar"
|
||||
@scroll=${this._saveScrollPos}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`}
|
||||
${this.syncCharts && this._hasZoomedCharts
|
||||
? html`<ha-button
|
||||
size="l"
|
||||
class="reset-button"
|
||||
@click=${this._handleGlobalZoomReset}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.history_charts.zoom_reset")}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="ha-scrollbar"
|
||||
.items=${combinedItems}
|
||||
.renderItem=${this._renderHistoryItem}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</div>`
|
||||
: html`${combinedItems.map((item, index) =>
|
||||
this._renderHistoryItem(item, index)
|
||||
)}`
|
||||
}
|
||||
${
|
||||
this.syncCharts && this._hasZoomedCharts
|
||||
? html`<ha-button
|
||||
size="l"
|
||||
class="reset-button"
|
||||
@click=${this._handleGlobalZoomReset}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
${this.hass.localize("ui.components.history_charts.zoom_reset")}
|
||||
</ha-button>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,10 +79,7 @@ export class StatisticsChart extends LitElement {
|
||||
public statTypes: StatisticType[] = ["sum", "min", "mean", "max"];
|
||||
|
||||
@property({ attribute: false }) public chartType:
|
||||
| "line"
|
||||
| "line-stack"
|
||||
| "bar"
|
||||
| "bar-stack" = "line";
|
||||
"line" | "line-stack" | "bar" | "bar-stack" = "line";
|
||||
|
||||
@property({ attribute: false }) public minYAxis?: number;
|
||||
|
||||
@@ -197,8 +194,10 @@ export class StatisticsChart extends LitElement {
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo &&
|
||||
!this._statisticIds.every(isExternalStatistic)}
|
||||
.clickLabelForMoreInfo=${
|
||||
this.clickForMoreInfo &&
|
||||
!this._statisticIds.every(isExternalStatistic)
|
||||
}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
@@ -330,9 +329,9 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
return html`${rows.map(
|
||||
(row, i) =>
|
||||
html`${row.time
|
||||
? html`${row.time}<br />`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
html`${
|
||||
row.time ? html`${row.time}<br />` : nothing
|
||||
}<ha-chart-tooltip-marker
|
||||
.color=${row.color}
|
||||
></ha-chart-tooltip-marker>
|
||||
${row.seriesName}:
|
||||
|
||||
@@ -161,13 +161,15 @@ export class DialogDataTableSettings extends LitElement {
|
||||
graphic="icon"
|
||||
noninteractive
|
||||
>${col.title || col.label || col.key}
|
||||
${canMove && isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${
|
||||
canMove && isVisible
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
slot="graphic"
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
<ha-icon-button
|
||||
tabindex="0"
|
||||
class="action"
|
||||
|
||||
@@ -61,29 +61,34 @@ class HaDataTableLabels extends LitElement {
|
||||
(label) => label.label_id,
|
||||
(label) => this._renderLabel(label, true)
|
||||
)}
|
||||
${hidden > 0
|
||||
? html`
|
||||
<ha-dropdown
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click=${stopPropagation}
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<ha-label slot="trigger" class="plus" dense>
|
||||
+${hidden}
|
||||
</ha-label>
|
||||
${repeat(
|
||||
labels.slice(this._visibleCount),
|
||||
(label) => label.label_id,
|
||||
(label) => html`
|
||||
<ha-dropdown-item .value=${label.label_id} .item=${label}>
|
||||
${this._renderLabel(label, false)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
hidden > 0
|
||||
? html`
|
||||
<ha-dropdown
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click=${stopPropagation}
|
||||
@wa-select=${this._handleDropdownSelect}
|
||||
>
|
||||
<ha-label slot="trigger" class="plus" dense>
|
||||
+${hidden}
|
||||
</ha-label>
|
||||
${repeat(
|
||||
labels.slice(this._visibleCount),
|
||||
(label) => label.label_id,
|
||||
(label) => html`
|
||||
<ha-dropdown-item
|
||||
.value=${label.label_id}
|
||||
.item=${label}
|
||||
>
|
||||
${this._renderLabel(label, false)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-chip-set>
|
||||
</div>
|
||||
|
||||
@@ -187,9 +192,11 @@ class HaDataTableLabels extends LitElement {
|
||||
@keydown=${clickAction ? this._labelClicked : undefined}
|
||||
.description=${label.description}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
${
|
||||
label?.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing
|
||||
}
|
||||
${label.name}
|
||||
</ha-label>
|
||||
`;
|
||||
|
||||
@@ -423,17 +423,19 @@ export class HaDataTable extends LitElement {
|
||||
return html`
|
||||
<div class="mdc-data-table">
|
||||
<slot name="header" @slotchange=${this._calcTableHeight}>
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.searchLabel}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${
|
||||
this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.searchLabel}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</slot>
|
||||
<div
|
||||
class="mdc-data-table__table ${classMap({
|
||||
@@ -454,24 +456,32 @@ export class HaDataTable extends LitElement {
|
||||
@scroll=${this._scrollContent}
|
||||
>
|
||||
<slot name="header-row">
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${!!this._checkedRows.length &&
|
||||
this._checkedRows.length !== this._checkableRowsCount}
|
||||
.checked=${!!this._checkedRows.length &&
|
||||
this._checkedRows.length === this._checkableRowsCount}
|
||||
${
|
||||
this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
|
||||
role="columnheader"
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleHeaderRowCheckboxClick}
|
||||
.indeterminate=${
|
||||
!!this._checkedRows.length &&
|
||||
this._checkedRows.length !==
|
||||
this._checkableRowsCount
|
||||
}
|
||||
.checked=${
|
||||
!!this._checkedRows.length &&
|
||||
this._checkedRows.length ===
|
||||
this._checkableRowsCount
|
||||
}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${Object.entries(columns).map(([key, column]) => {
|
||||
if (
|
||||
column.hidden ||
|
||||
@@ -517,63 +527,74 @@ export class HaDataTable extends LitElement {
|
||||
.columnId=${key}
|
||||
title=${ifDefined(column.title)}
|
||||
>
|
||||
${column.sortable
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${sorted && this.sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
${
|
||||
column.sortable
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
.path=${
|
||||
sorted && this.sortDirection === "desc"
|
||||
? mdiArrowDown
|
||||
: mdiArrowUp
|
||||
}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
<span>${column.title}</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</slot>
|
||||
</div>
|
||||
${!this._filteredData?.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div class="mdc-data-table__cell grows center" role="cell">
|
||||
${!this._filteredData
|
||||
? this._i18n?.localize?.("ui.common.loading") ||
|
||||
"Loading"
|
||||
: this.data.length
|
||||
? this._i18n?.localize?.(
|
||||
"ui.components.data-table.no_match_filter"
|
||||
) || "No rows matching current filters"
|
||||
: this.noDataText ||
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"}
|
||||
${
|
||||
!this._filteredData?.length
|
||||
? html`
|
||||
<div class="mdc-data-table__content">
|
||||
<div class="mdc-data-table__row" role="row">
|
||||
<div
|
||||
class="mdc-data-table__cell grows center"
|
||||
role="cell"
|
||||
>
|
||||
${
|
||||
!this._filteredData
|
||||
? this._i18n?.localize?.("ui.common.loading") ||
|
||||
"Loading"
|
||||
: this.data.length
|
||||
? this._i18n?.localize?.(
|
||||
"ui.components.data-table.no_match_filter"
|
||||
) || "No rows matching current filters"
|
||||
: this.noDataText ||
|
||||
this._i18n?.localize?.(
|
||||
"ui.components.data-table.no-data"
|
||||
) ||
|
||||
"No data"
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._groupData(
|
||||
this._filteredData,
|
||||
this._i18n?.localize,
|
||||
this._i18n?.locale,
|
||||
this.appendRow,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups,
|
||||
this.sortColumn,
|
||||
this.sortDirection
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
`
|
||||
: html`
|
||||
<lit-virtualizer
|
||||
scroller
|
||||
class="mdc-data-table__content scroller ha-scrollbar"
|
||||
tabindex=${ifDefined(!this.autoHeight ? "0" : undefined)}
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._groupData(
|
||||
this._filteredData,
|
||||
this._i18n?.localize,
|
||||
this._i18n?.locale,
|
||||
this.appendRow,
|
||||
this.groupColumn,
|
||||
this.groupOrder,
|
||||
this._collapsedGroups,
|
||||
this.sortColumn,
|
||||
this.sortDirection
|
||||
)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${renderRow}
|
||||
></lit-virtualizer>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -614,23 +635,25 @@ export class HaDataTable extends LitElement {
|
||||
)}
|
||||
.selectable=${row.selectable !== false}
|
||||
>
|
||||
${this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
role="cell"
|
||||
>
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@click=${this._handleRowCheckboxClicked}
|
||||
.rowId=${row[this.id]}
|
||||
.disabled=${row.selectable === false}
|
||||
.checked=${this._checkedRows.includes(String(row[this.id]))}
|
||||
${
|
||||
this.selectable
|
||||
? html`
|
||||
<div
|
||||
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
|
||||
role="cell"
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@click=${this._handleRowCheckboxClicked}
|
||||
.rowId=${row[this.id]}
|
||||
.disabled=${row.selectable === false}
|
||||
.checked=${this._checkedRows.includes(String(row[this.id]))}
|
||||
>
|
||||
</ha-checkbox>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${Object.entries(columns).map(([key, column]) => {
|
||||
if (
|
||||
(narrow && !column.main && !column.showNarrow) ||
|
||||
@@ -663,38 +686,46 @@ export class HaDataTable extends LitElement {
|
||||
flex: column.flex || 1,
|
||||
})}
|
||||
>
|
||||
${column.template
|
||||
? column.template(row)
|
||||
: narrow && column.main
|
||||
? html`<div class="primary">${row[key]}</div>
|
||||
<div class="secondary">
|
||||
${Object.entries(columns)
|
||||
.filter(
|
||||
([key2, column2]) =>
|
||||
!column2.hidden &&
|
||||
!column2.main &&
|
||||
!column2.showNarrow &&
|
||||
!(this.columnOrder &&
|
||||
this.columnOrder.includes(key2)
|
||||
? (this.hiddenColumns?.includes(key2) ??
|
||||
column2.defaultHidden)
|
||||
: column2.defaultHidden)
|
||||
)
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${i !== 0
|
||||
? STRINGS_SEPARATOR_DOT
|
||||
: nothing}${column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]}`
|
||||
)}
|
||||
</div>
|
||||
${column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing}`
|
||||
: html`${row[key]}${column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing}`}
|
||||
${
|
||||
column.template
|
||||
? column.template(row)
|
||||
: narrow && column.main
|
||||
? html`<div class="primary">${row[key]}</div>
|
||||
<div class="secondary">
|
||||
${Object.entries(columns)
|
||||
.filter(
|
||||
([key2, column2]) =>
|
||||
!column2.hidden &&
|
||||
!column2.main &&
|
||||
!column2.showNarrow &&
|
||||
!(this.columnOrder &&
|
||||
this.columnOrder.includes(key2)
|
||||
? (this.hiddenColumns?.includes(key2) ??
|
||||
column2.defaultHidden)
|
||||
: column2.defaultHidden)
|
||||
)
|
||||
.map(
|
||||
([key2, column2], i) =>
|
||||
html`${
|
||||
i !== 0 ? STRINGS_SEPARATOR_DOT : nothing
|
||||
}${
|
||||
column2.template
|
||||
? column2.template(row)
|
||||
: row[key2]
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing
|
||||
}`
|
||||
: html`${row[key]}${
|
||||
column.extraTemplate
|
||||
? column.extraTemplate(row)
|
||||
: nothing
|
||||
}`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@@ -831,16 +862,20 @@ export class HaDataTable extends LitElement {
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiChevronUp}
|
||||
.label=${localize?.(
|
||||
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
||||
) || (collapsed ? "Expand" : "Collapse")}
|
||||
.label=${
|
||||
localize?.(
|
||||
`ui.components.data-table.${collapsed ? "expand" : "collapse"}`
|
||||
) || (collapsed ? "Expand" : "Collapse")
|
||||
}
|
||||
class=${collapsed ? "collapsed" : ""}
|
||||
>
|
||||
</ha-icon-button>
|
||||
${groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") ||
|
||||
"Ungrouped"
|
||||
: groupName || ""}
|
||||
${
|
||||
groupName === UNDEFINED_GROUP_KEY
|
||||
? localize?.("ui.components.data-table.ungrouped") ||
|
||||
"Ungrouped"
|
||||
: groupName || ""
|
||||
}
|
||||
</div>`,
|
||||
});
|
||||
if (!collapsedGroups.includes(groupName)) {
|
||||
|
||||
@@ -143,9 +143,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
|
||||
render() {
|
||||
return html`<div class="picker">
|
||||
${this.ranges !== false && this.ranges
|
||||
? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
|
||||
: nothing}
|
||||
${
|
||||
this.ranges !== false && this.ranges
|
||||
? html`<div class="date-range-ranges">${this._renderRanges()}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="range">
|
||||
<calendar-range
|
||||
.value=${this._dateValue}
|
||||
@@ -176,34 +178,36 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
></ha-icon-button-next>
|
||||
<calendar-month></calendar-month>
|
||||
</calendar-range>
|
||||
${this.timePicker
|
||||
? html`
|
||||
<div class="times">
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.from.hours}:${this._timeValue.from.minutes}`}
|
||||
.locale=${this._i18n.locale}
|
||||
@value-changed=${this._handleChangeTime}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.time_from"
|
||||
)}
|
||||
id="from"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
|
||||
.locale=${this._i18n.locale}
|
||||
@value-changed=${this._handleChangeTime}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.time_to"
|
||||
)}
|
||||
id="to"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this.timePicker
|
||||
? html`
|
||||
<div class="times">
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.from.hours}:${this._timeValue.from.minutes}`}
|
||||
.locale=${this._i18n.locale}
|
||||
@value-changed=${this._handleChangeTime}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.time_from"
|
||||
)}
|
||||
id="from"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
|
||||
.locale=${this._i18n.locale}
|
||||
@value-changed=${this._handleChangeTime}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.time_to"
|
||||
)}
|
||||
id="to"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
|
||||
@@ -131,98 +131,106 @@ export class HaDateRangePicker extends LitElement {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="date-range-inputs">
|
||||
${!this.minimal
|
||||
? html`<ha-textarea
|
||||
id="field"
|
||||
rows="1"
|
||||
resize="auto"
|
||||
${
|
||||
!this.minimal
|
||||
? html`<ha-textarea
|
||||
id="field"
|
||||
rows="1"
|
||||
resize="auto"
|
||||
@click=${this._openPicker}
|
||||
@keydown=${this._handleKeydown}
|
||||
.value=${
|
||||
(isThisYear(this.startDate)
|
||||
? formatShortDateTime(
|
||||
this.startDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: formatShortDateTimeWithYear(
|
||||
this.startDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)) +
|
||||
(window.innerWidth >= 459 ? " - " : " - \n") +
|
||||
(isThisYear(this.endDate)
|
||||
? formatShortDateTime(
|
||||
this.endDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: formatShortDateTimeWithYear(
|
||||
this.endDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
))
|
||||
}
|
||||
.label=${
|
||||
this._i18n.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
) +
|
||||
" - " +
|
||||
this._i18n.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)
|
||||
}
|
||||
.disabled=${this.disabled}
|
||||
readonly
|
||||
></ha-textarea>
|
||||
<ha-icon-button-prev
|
||||
.label=${this._i18n.localize("ui.common.previous")}
|
||||
@click=${this._handlePrev}
|
||||
>
|
||||
</ha-icon-button-prev>
|
||||
<ha-icon-button-next
|
||||
.label=${this._i18n.localize("ui.common.next")}
|
||||
@click=${this._handleNext}
|
||||
>
|
||||
</ha-icon-button-next>`
|
||||
: html`<ha-icon-button
|
||||
@click=${this._openPicker}
|
||||
@keydown=${this._handleKeydown}
|
||||
.value=${(isThisYear(this.startDate)
|
||||
? formatShortDateTime(
|
||||
this.startDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: formatShortDateTimeWithYear(
|
||||
this.startDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)) +
|
||||
(window.innerWidth >= 459 ? " - " : " - \n") +
|
||||
(isThisYear(this.endDate)
|
||||
? formatShortDateTime(
|
||||
this.endDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
)
|
||||
: formatShortDateTimeWithYear(
|
||||
this.endDate,
|
||||
this._i18n.locale,
|
||||
this._hassConfig
|
||||
))}
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
) +
|
||||
" - " +
|
||||
this._i18n.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
readonly
|
||||
></ha-textarea>
|
||||
<ha-icon-button-prev
|
||||
.label=${this._i18n.localize("ui.common.previous")}
|
||||
@click=${this._handlePrev}
|
||||
>
|
||||
</ha-icon-button-prev>
|
||||
<ha-icon-button-next
|
||||
.label=${this._i18n.localize("ui.common.next")}
|
||||
@click=${this._handleNext}
|
||||
>
|
||||
</ha-icon-button-next>`
|
||||
: html`<ha-icon-button
|
||||
@click=${this._openPicker}
|
||||
.disabled=${this.disabled}
|
||||
id="field"
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.select_date_range"
|
||||
)}
|
||||
.path=${mdiCalendar}
|
||||
></ha-icon-button>`}
|
||||
id="field"
|
||||
.label=${this._i18n.localize(
|
||||
"ui.components.date-range-picker.select_date_range"
|
||||
)}
|
||||
.path=${mdiCalendar}
|
||||
></ha-icon-button>`
|
||||
}
|
||||
</div>
|
||||
${this._pickerWrapperOpen || this._opened
|
||||
? this._openedNarrow
|
||||
? html`
|
||||
<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
>
|
||||
${this._renderPicker()}
|
||||
</ha-bottom-sheet>
|
||||
`
|
||||
: html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
class=${this._opened ? "open" : ""}
|
||||
without-arrow
|
||||
distance="0"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="field"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-hide=${this._handlePopoverHide}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
>
|
||||
${this._renderPicker()}
|
||||
</wa-popover>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this._pickerWrapperOpen || this._opened
|
||||
? this._openedNarrow
|
||||
? html`
|
||||
<ha-bottom-sheet
|
||||
flexcontent
|
||||
.open=${this._pickerWrapperOpen}
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@closed=${this._hidePicker}
|
||||
>
|
||||
${this._renderPicker()}
|
||||
</ha-bottom-sheet>
|
||||
`
|
||||
: html`
|
||||
<wa-popover
|
||||
.open=${this._pickerWrapperOpen}
|
||||
style="--body-width: ${this._popoverWidth}px;"
|
||||
class=${this._opened ? "open" : ""}
|
||||
without-arrow
|
||||
distance="0"
|
||||
.placement=${this.popoverPlacement}
|
||||
for="field"
|
||||
auto-size="vertical"
|
||||
auto-size-padding="16"
|
||||
@wa-after-show=${this._dialogOpened}
|
||||
@wa-hide=${this._handlePopoverHide}
|
||||
@wa-after-hide=${this._hidePicker}
|
||||
trap-focus
|
||||
>
|
||||
${this._renderPicker()}
|
||||
</wa-popover>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -105,21 +105,25 @@ export class HaDialogDatePicker extends DialogMixin<DatePickerDialogParams>(
|
||||
.dialogAnchor=${this.dialogAnchor}
|
||||
open
|
||||
width="small"
|
||||
.headerTitle=${this._value?.title ||
|
||||
this._i18n.localize("ui.dialogs.date-picker.title")}
|
||||
.headerTitle=${
|
||||
this._value?.title ||
|
||||
this._i18n.localize("ui.dialogs.date-picker.title")
|
||||
}
|
||||
.headerSubtitle=${this._value?.year}
|
||||
header-subtitle-position="above"
|
||||
>
|
||||
${this.params.canClear
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiBackspace}
|
||||
.label=${this._i18n.localize("ui.dialogs.date-picker.clear")}
|
||||
slot="headerActionItems"
|
||||
@click=${this._clear}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this.params.canClear
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiBackspace}
|
||||
.label=${this._i18n.localize("ui.dialogs.date-picker.clear")}
|
||||
slot="headerActionItems"
|
||||
@click=${this._clear}
|
||||
></ha-icon-button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<wa-divider></wa-divider>
|
||||
<calendar-date
|
||||
.value=${this._value?.dateString}
|
||||
|
||||
@@ -165,22 +165,24 @@ export class HaDevicePicker extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${configEntry
|
||||
? html`<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: configEntry.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>`
|
||||
: nothing}
|
||||
${
|
||||
configEntry
|
||||
? html`<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: configEntry.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline">${primary}</span>
|
||||
<span slot="supporting-text">${secondary}</span>
|
||||
`;
|
||||
@@ -189,36 +191,42 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
private _rowRenderer: RenderItemFunction<DevicePickerItem> = (item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
${item.domain
|
||||
? html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: item.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
item.domain
|
||||
? html`
|
||||
<img
|
||||
slot="start"
|
||||
alt=""
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
src=${brandsUrl(
|
||||
{
|
||||
domain: item.domain,
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
/>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.domain_name
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${item.domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
item.domain_name
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${item.domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
|
||||
@@ -111,14 +111,16 @@ class HaEntitiesPicker extends LitElement {
|
||||
.createDomains=${this.createDomains}
|
||||
@value-changed=${this._entityChanged}
|
||||
></ha-entity-picker>
|
||||
${this.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="entity-handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this.reorder
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
class="entity-handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
|
||||
@@ -86,10 +86,12 @@ class HaEntityAttributePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label ??
|
||||
this.hass.localize(
|
||||
"ui.components.entity.entity-attribute-picker.attribute"
|
||||
)}
|
||||
.label=${
|
||||
this.label ??
|
||||
this.hass.localize(
|
||||
"ui.components.entity.entity-attribute-picker.attribute"
|
||||
)
|
||||
}
|
||||
.disabled=${this.disabled || !this.entityId}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
|
||||
@@ -27,9 +27,11 @@ import "../input/ha-input";
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${
|
||||
item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing
|
||||
}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
@@ -61,9 +63,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
@property({ attribute: false }) public entityId?: string;
|
||||
|
||||
@property({ attribute: false }) public value?:
|
||||
| string
|
||||
| EntityNameItem
|
||||
| EntityNameItem[];
|
||||
string | EntityNameItem | EntityNameItem[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@@ -125,18 +125,22 @@ export class HaEntityNamePicker extends LitElement {
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "custom"
|
||||
? this._renderTextInput()
|
||||
: this._renderPicker()}
|
||||
${
|
||||
this._mode === "custom"
|
||||
? this._renderTextInput()
|
||||
: this._renderPicker()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -210,20 +214,22 @@ export class HaEntityNamePicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
${
|
||||
this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
|
||||
@@ -226,9 +226,11 @@ export class HaEntityPicker extends LitElement {
|
||||
return html`
|
||||
${this._renderExtraOptionStart(extraOption)}
|
||||
<span slot="headline">${extraOption.primary}</span>
|
||||
${extraOption.secondary
|
||||
? html`<span slot="supporting-text">${extraOption.secondary}</span>`
|
||||
: nothing}
|
||||
${
|
||||
extraOption.secondary
|
||||
? html`<span slot="supporting-text">${extraOption.secondary}</span>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -273,38 +275,46 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${item.icon_path
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
style="margin: 0 4px"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
></state-badge>
|
||||
`}
|
||||
${
|
||||
item.icon_path
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
style="margin: 0 4px"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
></state-badge>
|
||||
`
|
||||
}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.stateObj && showEntityId
|
||||
? html`
|
||||
<span slot="supporting-text" class="code">
|
||||
${item.stateObj.entity_id}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
${item.domain_name && !showEntityId
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${item.domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
item.stateObj && showEntityId
|
||||
? html`
|
||||
<span slot="supporting-text" class="code">
|
||||
${item.stateObj.entity_id}
|
||||
</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
item.domain_name && !showEntityId
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${item.domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
@@ -452,10 +462,12 @@ export class HaEntityPicker extends LitElement {
|
||||
.searchKeys=${entityComboBoxKeys}
|
||||
.noSort=${this._hasRelatedContext}
|
||||
use-top-label
|
||||
.addButtonLabel=${this.addButton
|
||||
? (this.addButtonLabel ??
|
||||
this._i18n.localize("ui.components.entity.entity-picker.add"))
|
||||
: undefined}
|
||||
.addButtonLabel=${
|
||||
this.addButton
|
||||
? (this.addButtonLabel ??
|
||||
this._i18n.localize("ui.components.entity.entity-picker.add"))
|
||||
: undefined
|
||||
}
|
||||
.unknownItemText=${this._i18n.localize(
|
||||
"ui.components.entity.entity-picker.unknown"
|
||||
)}
|
||||
|
||||
@@ -293,20 +293,22 @@ export class HaStateContentPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
${
|
||||
this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`
|
||||
}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
|
||||
@@ -77,33 +77,37 @@ export class HaEntityStatesPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.value=${state}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.disabled && index === value.length - 1
|
||||
? this.helper
|
||||
: undefined}
|
||||
.helper=${
|
||||
this.disabled && index === value.length - 1
|
||||
? this.helper
|
||||
: undefined
|
||||
}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-entity-state-picker>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
<div>
|
||||
${(this.disabled && value.length) || hideValue
|
||||
? nothing
|
||||
: keyed(
|
||||
value.length,
|
||||
html`<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
.hideStates=${hide}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
@value-changed=${this._addValue}
|
||||
></ha-entity-state-picker>`
|
||||
)}
|
||||
${
|
||||
(this.disabled && value.length) || hideValue
|
||||
? nothing
|
||||
: keyed(
|
||||
value.length,
|
||||
html`<ha-entity-state-picker
|
||||
.hass=${this.hass}
|
||||
.entityId=${this.entityId}
|
||||
.attribute=${this.attribute}
|
||||
.extraOptions=${this.extraOptions}
|
||||
.hideStates=${hide}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
@value-changed=${this._addValue}
|
||||
></ha-entity-state-picker>`
|
||||
)
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ export class HaEntityToggle extends LitElement {
|
||||
.path=${mdiFlashOff}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
@click=${this._turnOff}
|
||||
class=${!this._isOn && this.stateObj.state !== UNKNOWN
|
||||
? "state-active"
|
||||
: ""}
|
||||
class=${
|
||||
!this._isOn && this.stateObj.state !== UNKNOWN ? "state-active" : ""
|
||||
}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.label=${`Turn ${computeStateName(this.stateObj)} on`}
|
||||
|
||||
@@ -136,21 +136,27 @@ export class HaStateLabelBadge extends LitElement {
|
||||
entityState,
|
||||
this._timerTimeRemaining
|
||||
)}
|
||||
.description=${this.showName
|
||||
? (this.name ?? computeStateName(entityState))
|
||||
: undefined}
|
||||
.description=${
|
||||
this.showName
|
||||
? (this.name ?? computeStateName(entityState))
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
${!image && showIcon
|
||||
? html`<ha-state-icon
|
||||
.icon=${this.icon}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>`
|
||||
: ""}
|
||||
${value && !image && !showIcon
|
||||
? html`<span class=${value && value.length > 4 ? "big" : ""}
|
||||
>${value}</span
|
||||
>`
|
||||
: ""}
|
||||
${
|
||||
!image && showIcon
|
||||
? html`<ha-state-icon
|
||||
.icon=${this.icon}
|
||||
.stateObj=${entityState}
|
||||
></ha-state-icon>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
value && !image && !showIcon
|
||||
? html`<span class=${value && value.length > 4 ? "big" : ""}
|
||||
>${value}</span
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
</ha-label-badge>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -341,28 +341,37 @@ export class HaStatisticPicker extends LitElement {
|
||||
const item = this._computeItem(statisticId);
|
||||
|
||||
return html`
|
||||
${item.stateObj
|
||||
? html`
|
||||
<state-badge .stateObj=${item.stateObj} slot="start"></state-badge>
|
||||
`
|
||||
: item.icon_path
|
||||
${
|
||||
item.stateObj
|
||||
? html`
|
||||
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
|
||||
<state-badge
|
||||
.stateObj=${item.stateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
`
|
||||
: nothing}
|
||||
: item.icon_path
|
||||
? html`
|
||||
<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${this.canEdit
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
.value=${statisticId}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${
|
||||
item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this.canEdit
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
.value=${statisticId}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -471,31 +480,37 @@ export class HaStatisticPicker extends LitElement {
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
return html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
${item.icon_path
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
style="margin: 0 4px"
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: item.stateObj
|
||||
${
|
||||
item.icon_path
|
||||
? html`
|
||||
<state-badge
|
||||
<ha-svg-icon
|
||||
style="margin: 0 4px"
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
></state-badge>
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
: item.stateObj
|
||||
? html`
|
||||
<state-badge
|
||||
slot="start"
|
||||
.stateObj=${item.stateObj}
|
||||
></state-badge>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline">${item.primary} </span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${item.statistic_id && showEntityId
|
||||
? html`<span slot="supporting-text" class="code">
|
||||
${item.statistic_id}
|
||||
</span>`
|
||||
: nothing}
|
||||
${
|
||||
item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
item.statistic_id && showEntityId
|
||||
? html`<span slot="supporting-text" class="code">
|
||||
${item.statistic_id}
|
||||
</span>`
|
||||
: nothing
|
||||
}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -112,8 +112,9 @@ class HaStatisticsPicker extends LitElement {
|
||||
<div>
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.includeStatisticsUnitOfMeasurement=${this
|
||||
.includeStatisticsUnitOfMeasurement}
|
||||
.includeStatisticsUnitOfMeasurement=${
|
||||
this.includeStatisticsUnitOfMeasurement
|
||||
}
|
||||
.includeUnitClass=${this.includeUnitClass}
|
||||
.includeDeviceClass=${this.includeDeviceClass}
|
||||
.statisticTypes=${this.statisticTypes}
|
||||
|
||||
@@ -32,39 +32,41 @@ class StateInfo extends LitElement {
|
||||
<div class="name ${this.inDialog ? "in-dialog" : ""}" .title=${name}>
|
||||
${name}
|
||||
</div>
|
||||
${this.inDialog
|
||||
? html`<div class="time-ago">
|
||||
<ha-tooltip for="relative-time">
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_changed"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_updated"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="relative-time"
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>`
|
||||
: html`<div class="extra-info"><slot></slot></div>`}
|
||||
${
|
||||
this.inDialog
|
||||
? html`<div class="time-ago">
|
||||
<ha-tooltip for="relative-time">
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_changed"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.last_updated"
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="relative-time"
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
</div>`
|
||||
: html`<div class="extra-info"><slot></slot></div>`
|
||||
}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,14 +172,18 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
return html`
|
||||
<ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
|
||||
${this._renderCloseButton("navigationIcon")}
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
${
|
||||
this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`
|
||||
}
|
||||
${
|
||||
this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`
|
||||
}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
`;
|
||||
@@ -202,13 +206,15 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.open=${this.open}
|
||||
.preventScrimClose=${this.preventScrimClose}
|
||||
>
|
||||
${!this.withoutHeader
|
||||
? html`
|
||||
<slot name="header" slot="header"
|
||||
>${this._renderHeaderContent()}</slot
|
||||
>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
!this.withoutHeader
|
||||
? html`
|
||||
<slot name="header" slot="header"
|
||||
>${this._renderHeaderContent()}</slot
|
||||
>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-bottom-sheet>
|
||||
|
||||
@@ -89,9 +89,11 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
|
||||
@wa-after-hide=${this._handlePopoverAfterHide}
|
||||
>
|
||||
<div class="popover-surface" @click=${this._handlePopoverClick}>
|
||||
${this.withoutHeader
|
||||
? nothing
|
||||
: html`<slot name="header">${this._renderHeaderContent()}</slot>`}
|
||||
${
|
||||
this.withoutHeader
|
||||
? nothing
|
||||
: html`<slot name="header">${this._renderHeaderContent()}</slot>`
|
||||
}
|
||||
<div class="content-wrapper">
|
||||
<div class="body"><slot></slot></div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user