Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten c1eabeb29f Remove light card for generated mode 2020-01-17 21:53:38 +01:00
500 changed files with 9647 additions and 22271 deletions
-85
View File
@@ -1,85 +0,0 @@
---
name: Report a bug with the UI, Frontend or Lovelace
about: Report an issue related to the Home Assistant frontend.
labels: bug
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Do not report issues for custom Lovelace cards.
- Provide as many details as possible. Paste logs, configuration samples and code into the backticks.
DO NOT DELETE ANY TEXT from this template! Otherwise, your issue may be closed without comment.
-->
## Checklist
- [ ] I have updated to the latest available Home Assistant version.
- [ ] I have cleared the cache of my browser.
- [ ] I have tried a different browser to see if it is related to my browser.
## The problem
<!--
Describe the issue you are experiencing here to communicate to the
maintainers. Tell us about the current behavior.
If possible provide a screenshot with a description.
-->
## Expected behavior
<!--
Describe what you expected to happen or it should look/behave.
If possible provide a screenshot with a description.
-->
## Steps to reproduce
<!--
Provide steps for us, that helps reproducing your issue.
For example:
1. Add a climate integration
2. Navigate to Lovelace
3. Click more info of the climate entity
4. Set the HVAC action to heat
5. Set the temperature higher than the current temperature
6. Set the HVAC action to cool
-->
## Environment
<!--
Provide details about the versions you are using, which helps us reproducing
and finding the issue quicker. Version information is found in the
Home Assistant frontend: Developer tools -> Info.
Browser version and operating system is important! Please try to replicate
your issue in a different browser and be sure to include your findings.
-->
- Home Assistant release with the issue:
- Last working Home Assistant release (if known):
- UI Type (States or Lovelace):
- Browser and browser version:
- Operating system:
## Problem-relevant configuration
<!--
An example configuration that caused the problem for you. Fill this out even
if it seems unimportant to you. Please be sure to remove personal information
like passwords, private URLs and other credentials.
-->
```yaml
```
## Javascript errors shown in your browser console/inspector
<!--
If you come across any javascript or other error logs, e.g., in your browser
console/inspector please provide them.
-->
```txt
```
## Additional information
-25
View File
@@ -1,25 +0,0 @@
---
name: Request a feature for the UI, Frontend or Lovelace
about: Request an new feature for the Home Assistant frontend.
labels: feature request
---
<!--
DO NOT DELETE ANY TEXT from this template!
Otherwise, your request may be closed without comment.
-->
## The request
<!--
Describe to our maintainers, the feature you would like to be added.
Please be clear and concise and, if possible, provide a screenshot or mockup.
-->
## The alternatives
<!--
Are you currently using, or have you considered alternatives?
If so, could you please describe those?
-->
## Additional information
+78
View File
@@ -0,0 +1,78 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
<!-- READ THIS FIRST:
- If you need additional help with this template please refer to https://www.home-assistant.io/help/reporting_issues/
- Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases
- Provide as many details as possible. Do not delete any text from this template!
-->
**Checklist:**
- [ ] I updated to the latest version available
- [ ] I cleared the cache of my browser
**Home Assistant release with the issue:**
<!--
- Frontend -> Developer tools -> Info
- Or use this command: hass --version
-->
**Last working Home Assistant release (if known):**
**UI (States or Lovelace UI?):**
<!--
- Frontend -> Developer tools -> Info
-->
**Browser and Operating System:**
<!--
Provide details about what browser (and version) you are seeing the issue in. And also which operating system this is on. If possible try to replicate the issue in other browsers and include your findings here.
-->
**Description of problem:**
<!--
Explain what the issue is, and what is the current behaviour. If possible provide a screenshot with a description.
-->
**Expected behaviour:**
<!--
Explain how things should look/behave. If possible provide a screenshot with a description.
-->
**Relevant config:**
<!--
Give the config of both the integration that is used, the Lovelace config, scene, automation or otherwise relevant configuration.
-->
**Steps to reproduce this problem:**
<!--
Sum up all steps that are necessary to reproduce this bug.
For example:
1. Add a climate integration
2. Navigate to Lovelace
3. Click more info of the climate entity
4. Set the hvac action to heat
5. Set the temperature higher than the current temperature
6. Set the hvac action to cool
-->
**Javascript errors shown in the web inspector (if applicable):**
```
```
**Additional information:**
-14
View File
@@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
url: https://github.com/home-assistant/home-assistant/issues
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
- name: Report incorrect or missing information on our website
url: https://github.com/home-assistant/home-assistant.io/issues
about: Our documentation has its own issue tracker. Please report issues with the website there.
- name: I have a question or need support
url: https://www.home-assistant.io/help
about: We use GitHub for tracking bugs, check our website for resources on getting help.
- name: I'm unsure where to go
url: https://www.home-assistant.io/join-chat
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: feature request
assignees: ""
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
-77
View File
@@ -1,77 +0,0 @@
<!--
You are amazing! Thanks for contributing to our project!
Please, DO NOT DELETE ANY TEXT from this template! (unless instructed).
-->
## Breaking change
<!--
If your PR contains a breaking change for existing users, it is important
to tell them what breaks, how to make it work again and why we did this.
This piece of text is published with the release notes, so it helps if you
write it towards our users, not us.
Note: Remove this section if this PR is NOT a breaking change.
-->
## Proposed change
<!--
Describe the big picture of your changes here to communicate to the
maintainers why we should accept this pull request. If it fixes a bug
or resolves a feature request, be sure to link to that issue in the
additional information section.
-->
## Type of change
<!--
What type of change does your PR introduce to the Home Assistant frontend?
NOTE: Please, check only 1! box!
If your PR requires multiple boxes to be checked, you'll most likely need to
split it into multiple PRs. This makes things easier and faster to code review.
-->
- [ ] Dependency upgrade
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (thank you!)
- [ ] Breaking change (fix/feature causing existing functionality to break)
- [ ] Code quality improvements to existing code or addition of tests
## Example configuration
<!--
Supplying a configuration snippet, makes it easier for a maintainer to test
your PR.
-->
```yaml
```
## Additional information
<!--
Details are important, and help maintainers processing your PR.
Please be sure to fill out additional details, if applicable.
-->
- This PR fixes or closes issue: fixes #
- This PR is related to issue:
- Link to documentation pull request:
## Checklist
<!--
Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If you're unsure about any of them, don't hesitate to ask.
We're here to help! This is simply a reminder of what we are going to look
for before merging your code.
-->
- [ ] The code change is tested and works locally.
- [ ] There is no commented out code in this PR.
- [ ] Tests have been added to verify that the new code works.
If user exposed functionality or configuration variables are added/changed:
- [ ] Documentation added/updated for [www.home-assistant.io][docs-repository]
<!--
Thank you for contributing <3
-->
[docs-repository]: https://github.com/home-assistant/home-assistant.io
-56
View File
@@ -1,56 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 90
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- feature request
- Help wanted
- to do
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: stale
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
There hasn't been any activity on this issue recently. Due to the high number
of incoming GitHub notifications, we have to clean some of the old issues,
as many of them have already been resolved with the latest updates.
Please make sure to update to the latest Home Assistant version and check
if that solves the issue. Let us know if that works for you by adding a
comment 👍
This issue now has been marked as stale and will be closed if no further
activity occurs. Thank you for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
-27
View File
@@ -1,27 +0,0 @@
# https://dev.azure.com/home-assistant
trigger: none
pr: none
schedules:
- cron: "0 0 * * *"
displayName: "build preview"
branches:
include:
- dev
always: false
variables:
- group: netlify
jobs:
- job: 'Netlify_preview'
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
# Cast
curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_CAST}
# Demo
curl -X POST -d {} https://api.netlify.com/build_hooks/${NETLIFY_DEMO}
displayName: 'Trigger netlify build preview'
-1
View File
@@ -34,7 +34,6 @@ module.exports.babelLoaderConfig = ({ latestBuild }) => {
},
],
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-nullish-coalescing-operator",
[
require("@babel/plugin-proposal-decorators").default,
{ decoratorsBeforeExport: true },
+1 -1
View File
@@ -57,7 +57,7 @@ const handler = (done) => (err, stats) => {
gulp.task("webpack-watch-app", () => {
// we are not calling done, so this command will run forever
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
webpack(bothBuilds(createAppConfig, { isProdBuild: false })).watch(
{},
handler()
);
+3 -6
View File
@@ -22,11 +22,7 @@ const createWebpackConfig = ({
isProdBuild,
latestBuild,
isStatsBuild,
dontHash,
}) => {
if (!dontHash) {
dontHash = new Set();
}
return {
mode: isProdBuild ? "production" : "development",
devtool: isProdBuild ? "source-map" : "inline-cheap-module-source-map",
@@ -107,6 +103,8 @@ const createWebpackConfig = ({
},
output: {
filename: ({ chunk }) => {
const dontHash = new Set();
if (!isProdBuild || dontHash.has(chunk.name)) {
return `${chunk.name}.js`;
}
@@ -224,12 +222,11 @@ const createHassioConfig = ({ isProdBuild, latestBuild }) => {
}
const config = createWebpackConfig({
entry: {
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.ts"),
entrypoint: path.resolve(paths.hassio_dir, "src/entrypoint.js"),
},
outputRoot: "",
isProdBuild,
latestBuild,
dontHash: new Set(["entrypoint"]),
});
config.output.path = paths.hassio_root;
+1 -1
View File
@@ -39,7 +39,7 @@ class HcCast extends LitElement {
@property() private askWrite = false;
@property() private lovelaceConfig?: LovelaceConfig | null;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (this.lovelaceConfig === undefined) {
return html`
<loading-screen></loading-screen>>
+1 -1
View File
@@ -70,7 +70,7 @@ export class HcConnect extends LitElement {
@property() private castManager?: CastManager | null;
private openDemo = false;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (this.cannotConnect) {
const tokens = loadTokens();
return html`
+5 -4
View File
@@ -22,7 +22,7 @@ class HcLayout extends LitElement {
@property() public connection?: Connection;
@property() public user?: HassUser;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
<ha-card>
<div class="layout">
@@ -50,12 +50,13 @@ class HcLayout extends LitElement {
</div>
</ha-card>
<div class="footer">
<a href="./faq.html">Frequently Asked Questions</a> Found a bug?
<a
<a href="./faq.html">Frequently Asked Questions</a> Found a bug? Let
@balloob know
<!-- <a
href="https://github.com/home-assistant/home-assistant-polymer/issues"
target="_blank"
>Let us know!</a
>
> -->
</div>
`;
}
+1 -1
View File
@@ -16,7 +16,7 @@ class HcDemo extends HassElement {
@property() public lovelacePath!: string;
@property() private _lovelaceConfig?: LovelaceConfig;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this._lovelaceConfig) {
return html``;
}
+1 -1
View File
@@ -14,7 +14,7 @@ class HcLaunchScreen extends LitElement {
@property() public hass?: HomeAssistant;
@property() public error?: string;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
<div class="container">
<img
+1 -1
View File
@@ -22,7 +22,7 @@ class HcLovelace extends LitElement {
@property() public viewPath?: string | number;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const index = this._viewIndex;
if (index === undefined) {
return html`
+1 -1
View File
@@ -50,7 +50,7 @@ export class HcMain extends HassElement {
}
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (this._showDemo) {
return html`
<hc-demo .lovelacePath=${this._lovelacePath}></hc-demo>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 68 KiB

+10 -10
View File
@@ -395,7 +395,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
cards: [
{
entity: "script.air_cleaner_quiet",
type: "button",
type: "entity-button",
name: "AC bed",
tap_action: {
action: "call-service",
@@ -408,7 +408,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.air_cleaner_auto",
type: "button",
type: "entity-button",
name: "AC bed",
tap_action: {
action: "call-service",
@@ -421,7 +421,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.air_cleaner_turbo",
type: "button",
type: "entity-button",
name: "AC bed",
tap_action: {
action: "call-service",
@@ -434,7 +434,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.ac_off",
type: "button",
type: "entity-button",
name: "AC",
tap_action: {
action: "call-service",
@@ -447,7 +447,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
{
entity: "script.ac_on",
type: "button",
type: "entity-button",
name: "AC",
tap_action: {
action: "call-service",
@@ -658,7 +658,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
action: "call-service",
service: "script.goodnight",
},
type: "button",
type: "entity-button",
icon: "mdi:weather-night",
},
{
@@ -670,7 +670,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "scene.turn_on",
},
type: "button",
type: "entity-button",
icon: "mdi:coffee-outline",
},
{
@@ -682,7 +682,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "scene.turn_on",
},
type: "button",
type: "entity-button",
icon: "mdi:television-classic",
},
],
@@ -743,7 +743,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "light.toggle",
},
type: "button",
type: "entity-button",
icon: "mdi:page-layout-footer",
},
{
@@ -755,7 +755,7 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
},
service: "light.toggle",
},
type: "button",
type: "entity-button",
icon: "mdi:page-layout-header",
},
],
+3 -3
View File
@@ -10,7 +10,7 @@ import {
import "../../../src/components/ha-icon";
import {
LovelaceRow,
EntityRow,
CastConfig,
} from "../../../src/panels/lovelace/entity-rows/types";
import { HomeAssistant } from "../../../src/types";
@@ -18,7 +18,7 @@ import { CastManager } from "../../../src/cast/cast_manager";
import { castSendShowDemo } from "../../../src/cast/receiver_messages";
@customElement("cast-demo-row")
class CastDemoRow extends LitElement implements LovelaceRow {
class CastDemoRow extends LitElement implements EntityRow {
public hass!: HomeAssistant;
@property() private _castManager?: CastManager | null;
@@ -27,7 +27,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
// No config possible.
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (
!this._castManager ||
this._castManager.castState === "NO_DEVICES_AVAILABLE"
+1 -1
View File
@@ -2,7 +2,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { safeLoad } from "js-yaml";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
import { createCardElement } from "../../../src/panels/lovelace/common/create-card-element";
class DemoCard extends PolymerElement {
static get template() {
@@ -15,14 +15,14 @@ const CONFIGS = [
{
heading: "Basic example",
config: `
- type: button
- type: entity-button
entity: light.bed_light
`,
},
{
heading: "With Name",
config: `
- type: button
- type: entity-button
name: Bedroom
entity: light.bed_light
`,
@@ -30,7 +30,7 @@ const CONFIGS = [
{
heading: "With Icon",
config: `
- type: button
- type: entity-button
entity: light.bed_light
icon: mdi:hotel
`,
@@ -38,7 +38,7 @@ const CONFIGS = [
{
heading: "Without State",
config: `
- type: button
- type: entity-button
entity: light.bed_light
show_state: false
`,
@@ -46,7 +46,7 @@ const CONFIGS = [
{
heading: "Custom Tap Action (toggle)",
config: `
- type: button
- type: entity-button
entity: light.bed_light
tap_action:
action: toggle
@@ -55,7 +55,7 @@ const CONFIGS = [
{
heading: "Running Service",
config: `
- type: button
- type: entity-button
entity: light.bed_light
service: light.toggle
`,
@@ -63,13 +63,13 @@ const CONFIGS = [
{
heading: "Invalid Entity",
config: `
- type: button
- type: entity-button
entity: sensor.invalid_entity
`,
},
];
class DemoButtonEntity extends PolymerElement {
class DemoEntityButtonEntity extends PolymerElement {
static get template() {
return html`
<demo-cards
@@ -97,4 +97,4 @@ class DemoButtonEntity extends PolymerElement {
}
}
customElements.define("demo-hui-button-card", DemoButtonEntity);
customElements.define("demo-hui-entity-button-card", DemoEntityButtonEntity);
+1 -1
View File
@@ -6,7 +6,7 @@ import { actionHandler } from "../../../src/panels/lovelace/common/directives/ac
import { ActionHandlerEvent } from "../../../src/data/lovelace";
export class DemoUtilLongPress extends LitElement {
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
${this.renderStyle()}
${[1, 2, 3].map(
@@ -15,7 +15,7 @@ import { HomeAssistant } from "../../../src/types";
import {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio/addon";
} from "../../../src/data/hassio";
import { navigate } from "../../../src/common/navigate";
import { filterAndSort } from "../components/hassio-filter-addons";
@@ -36,88 +36,82 @@ class HassioAddonRepositoryEl extends LitElement {
}
);
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const repo = this.repo;
const addons = this._getAddons(this.addons, this.filter);
const ha105pluss = this._computeHA105plus;
if (this.filter && addons.length < 1) {
return html`
<div class="content">
<p class="description">
No results found in "${repo.name}"
</p>
<div class="card-group">
<div class="title">
<div class="description">
No results found in "${repo.name}"
</div>
</div>
</div>
`;
}
return html`
<div class="content">
<h1>
<div class="card-group">
<div class="title">
${repo.name}
</h1>
<p class="description">
Maintained by ${repo.maintainer}<br />
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
</p>
<div class="card-group">
${addons.map(
(addon) => html`
<paper-card
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${addon.installed && addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle"}
.iconTitle=${addon.installed
? addon.installed !== addon.version
? "New version available"
: "Add-on is installed"
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
.iconClass=${addon.installed
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
? "not_available"
: ""}
.iconImage=${ha105pluss && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
.showTopbar=${addon.installed || !addon.available}
.topbarClass=${addon.installed
? addon.installed !== addon.version
? "update"
: "installed"
: !addon.available
? "unavailable"
: ""}
></hassio-card-content>
</div>
</paper-card>
`
)}
<div class="description">
Maintained by ${repo.maintainer}<br />
<a class="repo" href=${repo.url} target="_blank">${repo.url}</a>
</div>
</div>
${addons.map(
(addon) => html`
<paper-card
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this.addonTapped}
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
.available=${addon.available}
.icon=${this.computeIcon(addon)}
.iconTitle=${this.computeIconTitle(addon)}
.iconClass=${this.computeIconClass(addon)}
></hassio-card-content>
</div>
</paper-card>
`
)}
</div>
`;
}
private _addonTapped(ev) {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
private computeIcon(addon) {
return addon.installed && addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle";
}
private get _computeHA105plus(): boolean {
const [major, minor] = this.hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 105);
private computeIconTitle(addon) {
if (addon.installed) {
return addon.installed !== addon.version
? "New version available"
: "Add-on is installed";
}
return addon.available
? "Add-on is not installed"
: "Add-on is not available on your system";
}
private computeIconClass(addon) {
if (addon.installed) {
return addon.installed !== addon.version ? "update" : "installed";
}
return !addon.available ? "not_available" : "";
}
private addonTapped(ev) {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
}
static get styles(): CSSResultArray {
+2 -2
View File
@@ -14,7 +14,7 @@ import {
HassioAddonInfo,
fetchHassioAddonsInfo,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
} from "../../../src/data/hassio";
import "../../../src/layouts/loading-screen";
import "../components/hassio-search-input";
@@ -48,7 +48,7 @@ class HassioAddonStore extends LitElement {
await this._loadData();
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this._addons || !this._repos) {
return html`
<loading-screen></loading-screen>
@@ -17,7 +17,7 @@ import "../../../src/components/buttons/ha-call-api-button";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { HomeAssistant } from "../../../src/types";
import { HassioAddonRepository } from "../../../src/data/hassio/addon";
import { HassioAddonRepository } from "../../../src/data/hassio";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { repeat } from "lit-html/directives/repeat";
@@ -33,66 +33,64 @@ class HassioRepositoriesEditor extends LitElement {
.sort((a, b) => (a.name < b.name ? -1 : 1))
);
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const repos = this._sortedRepos(this.repos);
return html`
<div class="content">
<h1>
<div class="card-group">
<div class="title">
Repositories
</h1>
<p class="description">
Configure which add-on repositories to fetch data from:
</p>
<div class="card-group">
${// Use repeat so that the fade-out from call-service-api-button
// stays with the correct repo after we add/delete one.
repeat(
repos,
(repo) => repo.slug,
(repo) => html`
<paper-card>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${repo.name}
.description=${repo.url}
icon="hassio:github-circle"
></hassio-card-content>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeRemoveRepoData(repos, repo.url)}
class="warning"
>
Remove
</ha-call-api-button>
</div>
</paper-card>
`
)}
<paper-card>
<div class="card-content add">
<iron-icon icon="hassio:github-circle"></iron-icon>
<paper-input
label="Add new repository by URL"
.value=${this._repoUrl}
@value-changed=${this._urlChanged}
></paper-input>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeAddRepoData(repos, this._repoUrl)}
>
Add
</ha-call-api-button>
</div>
</paper-card>
<div class="description">
Configure which add-on repositories to fetch data from:
</div>
</div>
${// Use repeat so that the fade-out from call-service-api-button
// stays with the correct repo after we add/delete one.
repeat(
repos,
(repo) => repo.slug,
(repo) => html`
<paper-card>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${repo.name}
.description=${repo.url}
icon="hassio:github-circle"
></hassio-card-content>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeRemoveRepoData(repos, repo.url)}
class="warning"
>
Remove
</ha-call-api-button>
</div>
</paper-card>
`
)}
<paper-card>
<div class="card-content add">
<iron-icon icon="hassio:github-circle"></iron-icon>
<paper-input
label="Add new repository by URL"
.value=${this._repoUrl}
@value-changed=${this._urlChanged}
></paper-input>
</div>
<div class="card-actions">
<ha-call-api-button
path="hassio/supervisor/options"
.hass=${this.hass}
.data=${this.computeAddRepoData(repos, this._repoUrl)}
>
Add
</ha-call-api-button>
</div>
</paper-card>
</div>
`;
}
+138
View File
@@ -0,0 +1,138 @@
import "web-animations-js/web-animations-next-lite.min";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/resources/ha-style";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonAudio extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host,
paper-card,
paper-dropdown-menu {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
paper-item {
width: 450px;
}
.card-actions {
text-align: right;
}
</style>
<paper-card heading="Audio">
<div class="card-content">
<template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div>
</template>
<paper-dropdown-menu label="Input">
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
selected="{{selectedInput}}"
>
<template is="dom-repeat" items="[[inputDevices]]">
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu label="Output">
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
selected="{{selectedOutput}}"
>
<template is="dom-repeat" items="[[outputDevices]]">
<paper-item device$="[[item.device]]">[[item.name]]</paper-item>
</template>
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="card-actions">
<mwc-button on-click="_saveSettings">Save</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addon: {
type: Object,
observer: "addonChanged",
},
inputDevices: Array,
outputDevices: Array,
selectedInput: String,
selectedOutput: String,
error: String,
};
}
addonChanged(addon) {
this.setProperties({
selectedInput: addon.audio_input || "null",
selectedOutput: addon.audio_output || "null",
});
if (this.outputDevices) return;
const noDevice = [{ device: "null", name: "-" }];
this.hass.callApi("get", "hassio/hardware/audio").then(
(resp) => {
const dev = resp.data.audio;
const input = Object.keys(dev.input).map((key) => ({
device: key,
name: dev.input[key],
}));
const output = Object.keys(dev.output).map((key) => ({
device: key,
name: dev.output[key],
}));
this.setProperties({
inputDevices: noDevice.concat(input),
outputDevices: noDevice.concat(output),
});
},
() => {
this.setProperties({
inputDevices: noDevice,
outputDevices: noDevice,
});
}
);
}
_saveSettings() {
this.error = null;
const path = `hassio/addons/${this.addon.slug}/options`;
this.hass
.callApi("post", path, {
audio_input: this.selectedInput === "null" ? null : this.selectedInput,
audio_output:
this.selectedOutput === "null" ? null : this.selectedOutput,
})
.then(
() => {
this.fire("hass-api-called", { success: true, path: path });
},
(resp) => {
this.error = resp.body.message;
}
);
}
}
customElements.define("hassio-addon-audio", HassioAddonAudio);
-186
View File
@@ -1,186 +0,0 @@
import "web-animations-js/web-animations-next-lite.min";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
setHassioAddonOption,
HassioAddonSetOptionParams,
} from "../../../src/data/hassio/addon";
import {
HassioHardwareAudioDevice,
fetchHassioHardwareAudio,
} from "../../../src/data/hassio/hardware";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
@customElement("hassio-addon-audio")
class HassioAddonAudio extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property() private _inputDevices?: HassioHardwareAudioDevice[];
@property() private _outputDevices?: HassioHardwareAudioDevice[];
@property() private _selectedInput!: null | string;
@property() private _selectedOutput!: null | string;
protected render(): TemplateResult {
return html`
<paper-card heading="Audio">
<div class="card-content">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<paper-dropdown-menu
label="Input"
@iron-select=${this._setInputDevice}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedInput}
>
${this._inputDevices &&
this._inputDevices.map((item) => {
return html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
label="Output"
@iron-select=${this._setOutputDevice}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="device"
.selected=${this._selectedOutput}
>
${this._outputDevices &&
this._outputDevices.map((item) => {
return html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
</div>
<div class="card-actions">
<mwc-button @click=${this._saveSettings}>Save</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host,
paper-card,
paper-dropdown-menu {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
paper-item {
width: 450px;
}
.card-actions {
text-align: right;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
this._addonChanged();
}
}
private _setInputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
this._selectedInput = device || null;
}
private _setOutputDevice(ev): void {
const device = ev.detail.item.getAttribute("device");
this._selectedOutput = device || null;
}
private async _addonChanged(): Promise<void> {
this._selectedInput = this.addon.audio_input;
this._selectedOutput = this.addon.audio_output;
if (this._outputDevices) {
return;
}
const noDevice: HassioHardwareAudioDevice = { device: null, name: "-" };
try {
const { audio } = await fetchHassioHardwareAudio(this.hass);
const input = Object.keys(audio.input).map((key) => ({
device: key,
name: audio.input[key],
}));
const output = Object.keys(audio.output).map((key) => ({
device: key,
name: audio.output[key],
}));
this._inputDevices = [noDevice, ...input];
this._outputDevices = [noDevice, ...output];
} catch {
this._error = "Failed to fetch audio hardware";
this._inputDevices = [noDevice];
this._outputDevices = [noDevice];
}
}
private async _saveSettings(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
audio_input: this._selectedInput || null,
audio_output: this._selectedOutput || null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
} catch {
this._error = "Failed to set addon audio device";
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-audio": HassioAddonAudio;
}
}
@@ -0,0 +1,111 @@
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
class HassioAddonConfig extends PolymerElement {
static get template() {
return html`
<style include="ha-style">
:host {
display: block;
}
paper-card {
display: block;
}
.card-actions {
@apply --layout;
@apply --layout-justified;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
iron-autogrow-textarea {
width: 100%;
font-family: monospace;
}
.syntaxerror {
color: var(--google-red-500);
}
</style>
<paper-card heading="Config">
<div class="card-content">
<template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div>
</template>
<iron-autogrow-textarea
id="config"
value="{{config}}"
></iron-autogrow-textarea>
</div>
<div class="card-actions">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/options"
data="[[resetData]]"
>Reset to defaults</ha-call-api-button
>
<mwc-button on-click="saveTapped" disabled="[[!configParsed]]"
>Save</mwc-button
>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addon: {
type: Object,
observer: "addonChanged",
},
addonSlug: String,
config: {
type: String,
observer: "configChanged",
},
configParsed: Object,
error: String,
resetData: {
type: Object,
value: {
options: null,
},
},
};
}
addonChanged(addon) {
this.config = addon ? JSON.stringify(addon.options, null, 2) : "";
}
configChanged(config) {
try {
this.$.config.classList.remove("syntaxerror");
this.configParsed = JSON.parse(config);
} catch (err) {
this.$.config.classList.add("syntaxerror");
this.configParsed = null;
}
}
saveTapped() {
this.error = null;
this.hass
.callApi("post", `hassio/addons/${this.addonSlug}/options`, {
options: this.configParsed,
})
.catch((resp) => {
this.error = resp.body.message;
});
}
}
customElements.define("hassio-addon-config", HassioAddonConfig);
@@ -1,179 +0,0 @@
import "@polymer/iron-autogrow-textarea/iron-autogrow-textarea";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
query,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
setHassioAddonOption,
HassioAddonSetOptionParams,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-yaml-editor";
// tslint:disable-next-line: no-duplicate-imports
import { HaYamlEditor } from "../../../src/components/ha-yaml-editor";
import { showConfirmationDialog } from "../../../src/dialogs/generic/show-dialog-box";
@customElement("hassio-addon-config")
class HassioAddonConfig extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property({ type: Boolean }) private _configHasChanged = false;
@query("ha-yaml-editor") private _editor!: HaYamlEditor;
protected render(): TemplateResult {
const editor = this._editor;
// If editor not rendered, don't show the error.
const valid = editor ? editor.isValid : true;
return html`
<paper-card heading="Config">
<div class="card-content">
<ha-yaml-editor
@value-changed=${this._configChanged}
></ha-yaml-editor>
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
${valid
? ""
: html`
<div class="errors">Invalid YAML</div>
`}
</div>
<div class="card-actions">
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</mwc-button>
<mwc-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !valid}
>
Save
</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
paper-card {
display: block;
}
.card-actions {
display: flex;
justify-content: space-between;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
iron-autogrow-textarea {
width: 100%;
font-family: monospace;
}
.syntaxerror {
color: var(--google-red-500);
}
`,
];
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (changedProperties.has("addon")) {
this._editor.setValue(this.addon.options);
}
}
private _configChanged(): void {
this._configHasChanged = true;
this.requestUpdate();
}
private async _resetTapped(): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
});
if (!confirmed) {
return;
}
this._error = undefined;
const data: HassioAddonSetOptionParams = {
options: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${err.body?.message ||
err}`;
}
}
private async _saveTapped(): Promise<void> {
let data: HassioAddonSetOptionParams;
this._error = undefined;
try {
data = {
options: this._editor.value,
};
} catch (err) {
this._error = err;
return;
}
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
response: undefined,
path: "options",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to save addon configuration, ${err.body?.message ||
err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-config": HassioAddonConfig;
}
}
+624
View File
@@ -0,0 +1,624 @@
import "@polymer/iron-icon/iron-icon";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/ha-switch";
import "../../../src/resources/ha-style";
import "../components/hassio-card-content";
import { EventsMixin } from "../../../src/mixins/events-mixin";
import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
const PERMIS_DESC = {
rating: {
title: "Add-on Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Hass.io API Access",
description:
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
class HassioAddonInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host {
display: block;
}
paper-card {
display: block;
margin-bottom: 16px;
}
paper-card.warning {
background-color: var(--google-red-500);
color: white;
--paper-card-header-color: white;
}
paper-card.warning mwc-button {
color: white !important;
}
.warning {
color: var(--google-red-500);
}
.addon-header {
@apply --paper-font-headline;
}
.light-color {
color: var(--secondary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.description {
margin-bottom: 16px;
}
.logo img {
max-height: 60px;
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 8px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state iron-icon {
width: 16px;
color: var(--secondary-text-color);
}
ha-switch {
display: inline;
}
iron-icon.running {
color: var(--paper-green-400);
}
iron-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
ha-markdown img {
max-width: 100%;
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--iron-icon-height: 45px;
}
.protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a, ha-markdown a {
color: var(--primary-color);
}
</style>
<template is="dom-if" if="[[computeUpdateAvailable(addon)]]">
<paper-card heading="Update available! 🎉">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[addon.name]] [[addon.last_version]] is available"
description="You are currently running version [[addon.version]]"
icon="hassio:arrow-up-bold-circle"
icon-class="update"
></hassio-card-content>
<template is="dom-if" if="[[!addon.available]]">
<p>This update is no longer compatible with your system.</p>
</template>
</div>
<div class="card-actions">
<ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/update"
disabled="[[!addon.available]]"
>
Update
</ha-call-api-button
>
<template is="dom-if" if="[[addon.changelog]]">
<mwc-button on-click="openChangelog">Changelog</mwc-button>
</template>
</div>
</paper-card>
</template>
<template is="dom-if" if="[[!addon.protected]]">
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
<div class="card-actions protection-enable">
<mwc-button on-click="protectionToggled">Enable Protection mode</mwc-button>
</div>
</div>
</paper-card>
</template>
<paper-card>
<div class="card-content">
<div class="addon-header">
[[addon.name]]
<div class="addon-version light-color">
<template is="dom-if" if="[[addon.version]]">
[[addon.version]]
<template is="dom-if" if="[[isRunning]]">
<iron-icon
title="Add-on is running"
class="running"
icon="hassio:circle"
></iron-icon>
</template>
<template is="dom-if" if="[[!isRunning]]">
<iron-icon
title="Add-on is stopped"
class="stopped"
icon="hassio:circle"
></iron-icon>
</template>
</template>
<template is="dom-if" if="[[!addon.version]]">
[[addon.last_version]]
</template>
</div>
</div>
<div class="description light-color">
[[addon.description]].<br />
Visit
<a href="[[addon.url]]" target="_blank">[[addon.name]] page</a> for
details.
</div>
<template is="dom-if" if="[[addon.logo]]">
<a href="[[addon.url]]" target="_blank" class="logo">
<img src="/api/hassio/addons/[[addonSlug]]/logo" />
</a>
</template>
<div class="security">
<ha-label-badge
class$="[[computeSecurityClassName(addon.rating)]]"
on-click="showMoreInfo"
id="rating"
value="[[addon.rating]]"
label="rating"
description=""
></ha-label-badge>
<template is="dom-if" if="[[addon.host_network]]">
<ha-label-badge
on-click="showMoreInfo"
id="host_network"
icon="hassio:network"
label="host"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.full_access]]">
<ha-label-badge
on-click="showMoreInfo"
id="full_access"
icon="hassio:chip"
label="hardware"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.homeassistant_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="homeassistant_api"
icon="hassio:home-assistant"
label="hass"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[computeHassioApi(addon)]]">
<ha-label-badge
on-click="showMoreInfo"
id="hassio_api"
icon="hassio:home-assistant"
label="hassio"
description="[[addon.hassio_role]]"
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.docker_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="docker_api"
icon="hassio:docker"
label="docker"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.host_pid]]">
<ha-label-badge
on-click="showMoreInfo"
id="host_pid"
icon="hassio:pound"
label="host pid"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.apparmor]]">
<ha-label-badge
on-click="showMoreInfo"
class$="[[computeApparmorClassName(addon.apparmor)]]"
id="apparmor"
icon="hassio:shield"
label="apparmor"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.auth_api]]">
<ha-label-badge
on-click="showMoreInfo"
id="auth_api"
icon="hassio:key"
label="auth"
description=""
></ha-label-badge>
</template>
<template is="dom-if" if="[[addon.ingress]]">
<ha-label-badge
on-click="showMoreInfo"
id="ingress"
icon="hassio:cursor-default-click-outline"
label="ingress"
description=""
></ha-label-badge>
</template>
</div>
<template is="dom-if" if="[[addon.version]]">
<div class="state">
<div>Start on boot</div>
<ha-switch
on-change="startOnBootToggled"
checked="[[computeStartOnBoot(addon.boot)]]"
></ha-switch>
</div>
<div class="state">
<div>Auto update</div>
<ha-switch
on-change="autoUpdateToggled"
checked="[[addon.auto_update]]"
></ha-switch>
</div>
<template is="dom-if" if="[[addon.ingress]]">
<div class="state">
<div>Show in sidebar</div>
<ha-switch
on-change="panelToggled"
checked="[[addon.ingress_panel]]"
disabled="[[_computeCannotIngressSidebar(hass, addon)]]"
></ha-switch>
<template is="dom-if" if="[[_computeCannotIngressSidebar(hass, addon)]]">
<span>This option requires Home Assistant 0.92 or later.</span>
</template>
</div>
</template>
<template is="dom-if" if="[[_computeUsesProtectedOptions(addon)]]">
<div class="state">
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>Grant the add-on elevated system access.</paper-tooltip>
</span>
</div>
<ha-switch
on-change="protectionToggled"
checked="[[addon.protected]]"
></ha-switch>
</div>
</template>
</template>
</div>
<div class="card-actions">
<template is="dom-if" if="[[addon.version]]">
<mwc-button class="warning" on-click="_unistallClicked"
>Uninstall</mwc-button
>
<template is="dom-if" if="[[addon.build]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/rebuild"
>Rebuild</ha-call-api-button
>
</template>
<template is="dom-if" if="[[isRunning]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/restart"
>Restart</ha-call-api-button
>
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/stop"
>Stop</ha-call-api-button
>
</template>
<template is="dom-if" if="[[!isRunning]]">
<ha-call-api-button
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/start"
>Start</ha-call-api-button
>
</template>
<template
is="dom-if"
if="[[computeShowWebUI(addon.ingress, addon.webui, isRunning)]]"
>
<a
href="[[pathWebui(addon.webui)]]"
tabindex="-1"
target="_blank"
class="right"
><mwc-button>Open web UI</mwc-button></a
>
</template>
<template
is="dom-if"
if="[[computeShowIngressUI(addon.ingress, isRunning)]]"
>
<mwc-button
tabindex="-1"
class="right"
on-click="openIngress"
>Open web UI</mwc-button>
</template>
</template>
<template is="dom-if" if="[[!addon.version]]">
<template is="dom-if" if="[[!addon.available]]">
<p class="warning">This add-on is not available on your system.</p>
</template>
<ha-call-api-button
disabled="[[!addon.available]]"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/install"
>Install</ha-call-api-button
>
</template>
</div>
</paper-card>
<template is="dom-if" if="[[addon.long_description]]">
<paper-card>
<div class="card-content">
<ha-markdown content="[[addon.long_description]]"></ha-markdown>
</div>
</paper-card>
</template>
`;
}
static get properties() {
return {
hass: Object,
addon: Object,
addonSlug: String,
isRunning: { type: Boolean, computed: "computeIsRunning(addon)" },
};
}
computeIsRunning(addon) {
return addon && addon.state === "started";
}
computeUpdateAvailable(addon) {
return (
addon &&
!addon.detached &&
addon.version &&
addon.version !== addon.last_version
);
}
computeHassioApi(addon) {
return (
addon.hassio_api &&
(addon.hassio_role === "manager" || addon.hassio_role === "admin")
);
}
computeApparmorClassName(apparmor) {
if (apparmor === "profile") {
return "green";
}
if (apparmor === "disable") {
return "red";
}
return "";
}
pathWebui(webui) {
return webui && webui.replace("[HOST]", document.location.hostname);
}
computeShowWebUI(ingress, webui, isRunning) {
return !ingress && webui && isRunning;
}
openIngress() {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
computeShowIngressUI(ingress, isRunning) {
return ingress && isRunning;
}
computeStartOnBoot(state) {
return state === "auto";
}
computeSecurityClassName(rating) {
if (rating > 4) {
return "green";
}
if (rating > 2) {
return "yellow";
}
return "red";
}
startOnBootToggled() {
const data = { boot: this.addon.boot === "auto" ? "manual" : "auto" };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
}
autoUpdateToggled() {
const data = { auto_update: !this.addon.auto_update };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
}
protectionToggled() {
const data = { protected: !this.addon.protected };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/security`, data);
this.set("addon.protected", !this.addon.protected);
}
panelToggled() {
const data = { ingress_panel: !this.addon.ingress_panel };
this.hass.callApi("POST", `hassio/addons/${this.addonSlug}/options`, data);
}
showMoreInfo(e) {
const id = e.target.getAttribute("id");
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
}
openChangelog() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
.then(
(resp) => resp,
() => "Error getting changelog"
)
.then((content) => {
showHassioMarkdownDialog(this, {
title: "Changelog",
content: content,
});
});
}
_unistallClicked() {
if (!confirm("Are you sure you want to uninstall this add-on?")) {
return;
}
const path = `hassio/addons/${this.addonSlug}/uninstall`;
const eventData = {
path: path,
};
this.hass
.callApi("post", path)
.then(
(resp) => {
eventData.success = true;
eventData.response = resp;
},
(resp) => {
eventData.success = false;
eventData.response = resp;
}
)
.then(() => {
this.fire("hass-api-called", eventData);
});
}
_computeCannotIngressSidebar(hass, addon) {
return !addon.ingress || !this._computeHA92plus(hass);
}
_computeUsesProtectedOptions(addon) {
return addon.docker_api || addon.full_access || addon.host_pid;
}
_computeHA92plus(hass) {
const [major, minor] = hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
}
}
customElements.define("hassio-addon-info", HassioAddonInfo);
-799
View File
@@ -1,799 +0,0 @@
import "@material/mwc-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-card/paper-card";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-label-badge";
import "../../../src/components/ha-markdown";
import "../../../src/components/ha-switch";
import "../components/hassio-card-content";
import { fireEvent } from "../../../src/common/dom/fire_event";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
setHassioAddonOption,
setHassioAddonSecurity,
uninstallHassioAddon,
installHassioAddon,
fetchHassioAddonChangelog,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { navigate } from "../../../src/common/navigate";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
const PERMIS_DESC = {
rating: {
title: "Add-on Security Rating",
description:
"Hass.io provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Hass.io API Access",
description:
"The add-on was given access to the Hass.io API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Hass.io system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Hass.io system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property({ type: Boolean }) private _installing = false;
protected render(): TemplateResult {
return html`
${this._computeUpdateAvailable
? html`
<paper-card heading="Update available! 🎉">
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title="${this.addon.name} ${this.addon
.last_version} is available"
.description="You are currently running version ${this.addon
.version}"
icon="hassio:arrow-up-bold-circle"
iconClass="update"
></hassio-card-content>
${!this.addon.available
? html`
<p>
This update is no longer compatible with your system.
</p>
`
: ""}
</div>
<div class="card-actions">
<ha-call-api-button
.hass=${this.hass}
.disabled=${!this.addon.available}
path="hassio/addons/${this.addon.slug}/update"
>
Update
</ha-call-api-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
Changelog
</mwc-button>
`
: ""}
</div>
</paper-card>
`
: ""}
${!this.addon.protected
? html`
<paper-card heading="Warning: Protection mode is disabled!" class="warning">
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>Enable Protection mode</mwc-button>
</div>
</div>
</paper-card>
`
: ""}
<paper-card>
<div class="card-content">
<div class="addon-header">
${this.addon.name}
<div class="addon-version light-color">
${this.addon.version
? html`
${this.addon.version}
${this._computeIsRunning
? html`
<iron-icon
title="Add-on is running"
class="running"
icon="hassio:circle"
></iron-icon>
`
: html`
<iron-icon
title="Add-on is stopped"
class="stopped"
icon="hassio:circle"
></iron-icon>
`}
`
: html`
${this.addon.last_version}
`}
</div>
</div>
<div class="description light-color">
${this.addon.description}.<br />
Visit
<a href="${this.addon.url}" target="_blank">
${this.addon.name} page</a
>
for details.
</div>
${this.addon.logo
? html`
<a href="${this.addon.url}" target="_blank" class="logo">
<img src="/api/hassio/addons/${this.addon.slug}/logo" />
</a>
`
: ""}
<div class="security">
<ha-label-badge
class=${classMap({
green: [5, 6].includes(Number(this.addon.rating)),
yellow: [3, 4].includes(Number(this.addon.rating)),
red: [1, 2].includes(Number(this.addon.rating)),
})}
@click=${this._showMoreInfo}
id="rating"
.value=${this.addon.rating}
label="rating"
description=""
></ha-label-badge>
${this.addon.host_network
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
icon="hassio:network"
label="host"
description=""
></ha-label-badge>
`
: ""}
${this.addon.full_access
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
icon="hassio:chip"
label="hardware"
description=""
></ha-label-badge>
`
: ""}
${this.addon.homeassistant_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
icon="hassio:home-assistant"
label="hass"
description=""
></ha-label-badge>
`
: ""}
${this._computeHassioApi
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
icon="hassio:home-assistant"
label="hassio"
.description=${this.addon.hassio_role}
></ha-label-badge>
`
: ""}
${this.addon.docker_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
icon="hassio:docker"
label="docker"
description=""
></ha-label-badge>
`
: ""}
${this.addon.host_pid
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
icon="hassio:pound"
label="host pid"
description=""
></ha-label-badge>
`
: ""}
${this.addon.apparmor
? html`
<ha-label-badge
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
icon="hassio:shield"
label="apparmor"
description=""
></ha-label-badge>
`
: ""}
${this.addon.auth_api
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
icon="hassio:key"
label="auth"
description=""
></ha-label-badge>
`
: ""}
${this.addon.ingress
? html`
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
icon="hassio:cursor-default-click-outline"
label="ingress"
description=""
></ha-label-badge>
`
: ""}
</div>
${this.addon.version
? html`
<div class="state">
<div>Start on boot</div>
<ha-switch
@change=${this._startOnBootToggled}
.checked=${this.addon.boot === "auto"}
haptic
></ha-switch>
</div>
<div class="state">
<div>Auto update</div>
<ha-switch
@change=${this._autoUpdateToggled}
.checked=${this.addon.auto_update}
haptic
></ha-switch>
</div>
${this.addon.ingress
? html`
<div class="state">
<div>Show in sidebar</div>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
${this._computeCannotIngressSidebar
? html`
<span>
This option requires Home Assistant 0.92 or
later.
</span>
`
: ""}
</div>
`
: ""}
${this._computeUsesProtectedOptions
? html`
<div class="state">
<div>
Protection mode
<span>
<iron-icon icon="hassio:information"></iron-icon>
<paper-tooltip>
Grant the add-on elevated system access.
</paper-tooltip>
</span>
</div>
<ha-switch
@change=${this._protectionToggled}
.checked=${this.addon.protected}
haptic
></ha-switch>
</div>
`
: ""}
`
: ""}
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
</div>
<div class="card-actions">
${this.addon.version
? html`
<mwc-button class="warning" @click=${this._uninstallClicked}>
Uninstall
</mwc-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
</ha-call-api-button>
`
: ""}
${this._computeIsRunning
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/restart"
>
Restart
</ha-call-api-button>
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/stop"
>
Stop
</ha-call-api-button>
`
: html`
<ha-call-api-button
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/start"
>
Start
</ha-call-api-button>
`}
${this._computeShowWebUI
? html`
<a
.href=${this._pathWebui}
tabindex="-1"
target="_blank"
class="right"
>
<mwc-button>
Open web UI
</mwc-button>
</a>
`
: ""}
${this._computeShowIngressUI
? html`
<mwc-button class="right" @click=${this._openIngress}>
Open web UI
</mwc-button>
`
: ""}
`
: html`
${!this.addon.available
? html`
<p class="warning">
This add-on is not available on your system.
</p>
`
: ""}
<ha-progress-button
.disabled=${!this.addon.available}
.progress=${this._installing}
@click=${this._installClicked}
>
Install
</ha-progress-button>
`}
</div>
</paper-card>
${this.addon.long_description
? html`
<paper-card>
<div class="card-content">
<ha-markdown
.content=${this.addon.long_description}
></ha-markdown>
</div>
</paper-card>
`
: ""}
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
paper-card {
display: block;
margin-bottom: 16px;
}
paper-card.warning {
background-color: var(--google-red-500);
color: white;
--paper-card-header-color: white;
}
paper-card.warning mwc-button {
--mdc-theme-primary: white !important;
}
.warning {
color: var(--google-red-500);
--mdc-theme-primary: var(--google-red-500);
}
.light-color {
color: var(--secondary-text-color);
}
.addon-header {
font-size: 24px;
color: var(--paper-card-header-color, --primary-text-color);
}
.addon-version {
float: right;
font-size: 15px;
vertical-align: middle;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.description {
margin-bottom: 16px;
}
.logo img {
max-height: 60px;
margin: 16px 0;
display: block;
}
.state {
display: flex;
margin: 33px 0;
}
.state div {
width: 180px;
display: inline-block;
}
.state iron-icon {
width: 16px;
height: 16px;
color: var(--secondary-text-color);
}
ha-switch {
display: flex;
}
iron-icon.running {
color: var(--paper-green-400);
}
iron-icon.stopped {
color: var(--google-red-300);
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
ha-markdown img {
max-width: 100%;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
.description a,
ha-markdown a {
color: var(--primary-color);
}
.red {
--ha-label-badge-color: var(--label-badge-red, #df4c1e);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue, #039be5);
}
.green {
--ha-label-badge-color: var(--label-badge-green, #0da035);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow, #f4b400);
}
.security {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
}
.security h3 {
margin-bottom: 8px;
font-weight: normal;
}
.security ha-label-badge {
cursor: pointer;
margin-right: 4px;
--iron-icon-height: 45px;
}
`,
];
}
private get _computeHassioApi(): boolean {
return (
this.addon.hassio_api &&
(this.addon.hassio_role === "manager" ||
this.addon.hassio_role === "admin")
);
}
private get _computeApparmorClassName(): string {
if (this.addon.apparmor === "profile") {
return "green";
}
if (this.addon.apparmor === "disable") {
return "red";
}
return "";
}
private _showMoreInfo(ev): void {
const id = ev.target.getAttribute("id");
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
});
}
private get _computeIsRunning(): boolean {
return this.addon?.state === "started";
}
private get _computeUpdateAvailable(): boolean | "" {
return (
this.addon &&
!this.addon.detached &&
this.addon.version &&
this.addon.version !== this.addon.last_version
);
}
private get _pathWebui(): string | null {
return (
this.addon.webui &&
this.addon.webui.replace("[HOST]", document.location.hostname)
);
}
private get _computeShowWebUI(): boolean | "" | null {
return !this.addon.ingress && this.addon.webui && this._computeIsRunning;
}
private _openIngress(): void {
navigate(this, `/hassio/ingress/${this.addon.slug}`);
}
private get _computeShowIngressUI(): boolean {
return this.addon.ingress && this._computeIsRunning;
}
private get _computeCannotIngressSidebar(): boolean {
return !this.addon.ingress || !this._computeHA92plus;
}
private get _computeUsesProtectedOptions(): boolean {
return (
this.addon.docker_api || this.addon.full_access || this.addon.host_pid
);
}
private get _computeHA92plus(): boolean {
const [major, minor] = this.hass.config.version.split(".", 2);
return Number(major) > 0 || (major === "0" && Number(minor) >= 92);
}
private async _startOnBootToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
boot: this.addon.boot === "auto" ? "manual" : "auto",
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _autoUpdateToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
auto_update: !this.addon.auto_update,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _protectionToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetSecurityParams = {
protected: !this.addon.protected,
};
try {
await setHassioAddonSecurity(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "security",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon security option, ${err.body?.message ||
err}`;
}
}
private async _panelToggled(): Promise<void> {
this._error = undefined;
const data: HassioAddonSetOptionParams = {
ingress_panel: !this.addon.ingress_panel,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${err.body?.message || err}`;
}
}
private async _openChangelog(): Promise<void> {
this._error = undefined;
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addon.slug
);
showHassioMarkdownDialog(this, {
title: "Changelog",
content,
});
} catch (err) {
this._error = `Failed to get addon changelog, ${err.body?.message ||
err}`;
}
}
private async _installClicked(): Promise<void> {
this._error = undefined;
this._installing = true;
try {
await installHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "install",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to install addon, ${err.body?.message || err}`;
}
this._installing = false;
}
private async _uninstallClicked(): Promise<void> {
if (!confirm("Are you sure you want to uninstall this add-on?")) {
return;
}
this._error = undefined;
try {
await uninstallHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "uninstall",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to uninstall addon, ${err.body?.message || err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-info": HassioAddonInfo;
}
}
@@ -0,0 +1,66 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import "../../../src/resources/ha-style";
class HassioAddonLogs extends PolymerElement {
static get template() {
return html`
<style include="ha-style">
:host,
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
</style>
${ANSI_HTML_STYLE}
<paper-card heading="Log">
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button on-click="refresh">Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addonSlug: {
type: String,
observer: "addonSlugChanged",
},
};
}
addonSlugChanged(slug) {
if (!this.hass) {
setTimeout(() => {
this.addonChanged(slug);
}, 0);
return;
}
this.refresh();
}
refresh() {
this.hass
.callApi("get", `hassio/addons/${this.addonSlug}/logs`)
.then((text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
});
}
}
customElements.define("hassio-addon-logs", HassioAddonLogs);
@@ -1,95 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
fetchHassioAddonLogs,
} from "../../../src/data/hassio/addon";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
@customElement("hassio-addon-logs")
class HassioAddonLogs extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@query("#content") private _logContent!: any;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult {
return html`
<paper-card heading="Log">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
ANSI_HTML_STYLE,
css`
:host,
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
const content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
while (this._logContent.lastChild) {
this._logContent.removeChild(this._logContent.lastChild as Node);
}
this._logContent.appendChild(parseTextToColoredPre(content));
} catch (err) {
this._error = `Failed to get addon logs, ${err.body?.message || err}`;
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-logs": HassioAddonLogs;
}
}
@@ -0,0 +1,129 @@
import "@polymer/paper-card/paper-card";
import "@polymer/paper-input/paper-input";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import "../../../src/resources/ha-style";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioAddonNetwork extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style">
:host {
display: block;
}
paper-card {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-actions {
@apply --layout;
@apply --layout-justified;
}
</style>
<paper-card heading="Network">
<div class="card-content">
<template is="dom-if" if="[[error]]">
<div class="errors">[[error]]</div>
</template>
<table>
<tbody>
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
</tr>
<template is="dom-repeat" items="[[config]]">
<tr>
<td>[[item.container]]</td>
<td>
<paper-input
placeholder="disabled"
value="{{item.host}}"
no-label-float=""
></paper-input>
</td>
<td>[[item.description]]</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="card-actions">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/addons/[[addonSlug]]/options"
data="[[resetData]]"
>Reset to defaults</ha-call-api-button
>
<mwc-button on-click="saveTapped">Save</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
addonSlug: String,
config: Object,
addon: {
type: Object,
observer: "addonChanged",
},
error: String,
resetData: {
type: Object,
value: {
network: null,
},
},
};
}
addonChanged(addon) {
if (!addon) return;
const network = addon.network || {};
const description = addon.network_description || {};
const items = Object.keys(network).map((key) => ({
container: key,
host: network[key],
description: description[key],
}));
this.config = items.sort(function(el1, el2) {
return el1.host - el2.host;
});
}
saveTapped() {
this.error = null;
const data = {};
this.config.forEach(function(item) {
data[item.container] = parseInt(item.host);
});
const path = `hassio/addons/${this.addonSlug}/options`;
this.hass
.callApi("post", path, {
network: data,
})
.then(
() => {
this.fire("hass-api-called", { success: true, path: path });
},
(resp) => {
this.error = resp.body.message;
}
);
}
}
customElements.define("hassio-addon-network", HassioAddonNetwork);
@@ -1,202 +0,0 @@
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
import { HomeAssistant } from "../../../src/types";
import {
HassioAddonDetails,
HassioAddonSetOptionParams,
setHassioAddonOption,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { fireEvent } from "../../../src/common/dom/fire_event";
interface NetworkItem {
description: string;
container: string;
host: number | null;
}
interface NetworkItemInput extends PaperInputElement {
container: string;
}
@customElement("hassio-addon-network")
class HassioAddonNetwork extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addon!: HassioAddonDetails;
@property() private _error?: string;
@property() private _config?: NetworkItem[];
public connectedCallback(): void {
super.connectedCallback();
this._setNetworkConfig();
}
protected render(): TemplateResult {
if (!this._config) {
return html``;
}
return html`
<paper-card heading="Network">
<div class="card-content">
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<table>
<tbody>
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
</tr>
${this._config!.map((item) => {
return html`
<tr>
<td>${item.container}</td>
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder="disabled"
.value=${item.host}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${item.description}</td>
</tr>
`;
})}
</tbody>
</table>
</div>
<div class="card-actions">
<mwc-button class="warning" @click=${this._resetTapped}>
Reset to defaults
</mwc-button>
<mwc-button @click=${this._saveTapped}>Save</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
display: block;
}
paper-card {
display: block;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
.card-actions {
display: flex;
justify-content: space-between;
}
`,
];
}
protected update(changedProperties: PropertyValues): void {
super.update(changedProperties);
if (changedProperties.has("addon")) {
this._setNetworkConfig();
}
}
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
const items: NetworkItem[] = Object.keys(network).map((key) => {
return {
container: key,
host: network[key],
description: description[key],
};
});
this._config = items.sort((a, b) => (a.container > b.container ? 1 : -1));
}
private async _configChanged(ev: Event): Promise<void> {
const target = ev.target as NetworkItemInput;
this._config!.forEach((item) => {
if (
item.container === target.container &&
item.host !== parseInt(String(target.value), 10)
) {
item.host = target.value ? parseInt(String(target.value), 10) : null;
}
});
}
private async _resetTapped(): Promise<void> {
const data: HassioAddonSetOptionParams = {
network: null,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon network configuration, ${err.body
?.message || err}`;
}
}
private async _saveTapped(): Promise<void> {
this._error = undefined;
const networkconfiguration = {};
this._config!.forEach((item) => {
networkconfiguration[item.container] = parseInt(String(item.host), 10);
});
const data: HassioAddonSetOptionParams = {
network: networkconfiguration,
};
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon network configuration, ${err.body
?.message || err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-network": HassioAddonNetwork;
}
}
+139
View File
@@ -0,0 +1,139 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-info";
import "./hassio-addon-logs";
import "./hassio-addon-network";
class HassioAddonView extends PolymerElement {
static get template() {
return html`
<style>
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}
}
</style>
<hass-subpage header="Hass.io: add-on details" hassio>
<div class="content">
<hassio-addon-info
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[addonSlug]]"
></hassio-addon-info>
<template is="dom-if" if="[[addon.version]]">
<hassio-addon-config
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[addonSlug]]"
></hassio-addon-config>
<template is="dom-if" if="[[addon.audio]]">
<hassio-addon-audio
hass="[[hass]]"
addon="[[addon]]"
></hassio-addon-audio>
</template>
<template is="dom-if" if="[[addon.network]]">
<hassio-addon-network
hass="[[hass]]"
addon="[[addon]]"
addon-slug="[[addonSlug]]"
></hassio-addon-network>
</template>
<hassio-addon-logs
hass="[[hass]]"
addon-slug="[[addonSlug]]"
></hassio-addon-logs>
</template>
</div>
</hass-subpage>
`;
}
static get properties() {
return {
hass: Object,
route: {
type: Object,
observer: "routeDataChanged",
},
addonSlug: {
type: String,
computed: "_computeSlug(route)",
},
addon: Object,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
const path = ev.detail.path;
if (!path) return;
if (path.substr(path.lastIndexOf("/") + 1) === "uninstall") {
history.back();
} else {
this.routeDataChanged(this.route);
}
}
routeDataChanged(routeData) {
const addon = routeData.path.substr(1);
this.hass.callApi("get", `hassio/addons/${addon}/info`).then(
(info) => {
this.addon = info.data;
},
() => {
this.addon = null;
}
);
}
_computeSlug(route) {
return route.path.substr(1);
}
}
customElements.define("hassio-addon-view", HassioAddonView);
-159
View File
@@ -1,159 +0,0 @@
import "@polymer/app-layout/app-header-layout/app-header-layout";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/paper-spinner/paper-spinner-lite";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant, Route } from "../../../src/types";
import {
HassioAddonDetails,
fetchHassioAddonInfo,
} from "../../../src/data/hassio/addon";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "./hassio-addon-audio";
import "./hassio-addon-config";
import "./hassio-addon-info";
import "./hassio-addon-logs";
import "./hassio-addon-network";
@customElement("hassio-addon-view")
class HassioAddonView extends LitElement {
@property() public hass!: HomeAssistant;
@property() public route!: Route;
@property() public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html`
<paper-spinner-lite active></paper-spinner-lite>
`;
}
return html`
<hass-subpage header="Hass.io: add-on details" hassio>
<div class="content">
<hassio-addon-info
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-info>
${this.addon && this.addon.version
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-config>
${this.addon.audio
? html`
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-audio>
`
: ""}
${this.addon.network
? html`
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-network>
`
: ""}
<hassio-addon-logs
.hass=${this.hass}
.addon=${this.addon}
></hassio-addon-logs>
`
: ""}
</div>
</hass-subpage>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
color: var(--primary-text-color);
--paper-card-header-color: var(--primary-text-color);
}
.content {
padding: 24px 0 32px;
display: flex;
flex-direction: column;
align-items: center;
}
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}
}
`,
];
}
protected async firstUpdated(): Promise<void> {
await this._routeDataChanged(this.route);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
const path: string = ev.detail.path;
if (!path) {
return;
}
if (path === "uninstall") {
history.back();
} else {
await this._routeDataChanged(this.route);
}
}
private async _routeDataChanged(routeData: Route): Promise<void> {
const addon = routeData.path.substr(1);
try {
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch {
this.addon = undefined;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-view": HassioAddonView;
}
}
@@ -1,75 +1,68 @@
import { css } from "lit-element";
import { html } from "@polymer/polymer/lib/utils/html-tag";
interface State {
bold: boolean;
italic: boolean;
underline: boolean;
strikethrough: boolean;
foregroundColor: null | string;
backgroundColor: null | string;
}
export const ANSI_HTML_STYLE = css`
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
export const ANSI_HTML_STYLE = html`
<style>
.bold {
font-weight: bold;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.strikethrough {
text-decoration: line-through;
}
.underline.strikethrough {
text-decoration: underline line-through;
}
.fg-red {
color: rgb(222, 56, 43);
}
.fg-green {
color: rgb(57, 181, 74);
}
.fg-yellow {
color: rgb(255, 199, 6);
}
.fg-blue {
color: rgb(0, 111, 184);
}
.fg-magenta {
color: rgb(118, 38, 113);
}
.fg-cyan {
color: rgb(44, 181, 233);
}
.fg-white {
color: rgb(204, 204, 204);
}
.bg-black {
background-color: rgb(0, 0, 0);
}
.bg-red {
background-color: rgb(222, 56, 43);
}
.bg-green {
background-color: rgb(57, 181, 74);
}
.bg-yellow {
background-color: rgb(255, 199, 6);
}
.bg-blue {
background-color: rgb(0, 111, 184);
}
.bg-magenta {
background-color: rgb(118, 38, 113);
}
.bg-cyan {
background-color: rgb(44, 181, 233);
}
.bg-white {
background-color: rgb(204, 204, 204);
}
</style>
`;
export function parseTextToColoredPre(text) {
@@ -77,7 +70,7 @@ export function parseTextToColoredPre(text) {
const re = /\033(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g;
let i = 0;
const state: State = {
const state = {
bold: false,
italic: false,
underline: false,
@@ -88,42 +81,29 @@ export function parseTextToColoredPre(text) {
const addSpan = (content) => {
const span = document.createElement("span");
if (state.bold) {
span.classList.add("bold");
}
if (state.italic) {
span.classList.add("italic");
}
if (state.underline) {
span.classList.add("underline");
}
if (state.strikethrough) {
span.classList.add("strikethrough");
}
if (state.foregroundColor !== null) {
if (state.bold) span.classList.add("bold");
if (state.italic) span.classList.add("italic");
if (state.underline) span.classList.add("underline");
if (state.strikethrough) span.classList.add("strikethrough");
if (state.foregroundColor !== null)
span.classList.add(`fg-${state.foregroundColor}`);
}
if (state.backgroundColor !== null) {
if (state.backgroundColor !== null)
span.classList.add(`bg-${state.backgroundColor}`);
}
span.appendChild(document.createTextNode(content));
pre.appendChild(span);
};
/* eslint-disable no-cond-assign */
let match;
// tslint:disable-next-line
while ((match = re.exec(text)) !== null) {
const j = match!.index;
const j = match.index;
addSpan(text.substring(i, j));
i = j + match[0].length;
if (match[1] === undefined) {
continue;
}
if (match[1] === undefined) continue;
match[1].split(";").forEach((colorCode: string) => {
switch (parseInt(colorCode, 10)) {
match[1].split(";").forEach((colorCode) => {
switch (parseInt(colorCode)) {
case 0:
// reset
state.bold = false;
+10 -68
View File
@@ -17,40 +17,21 @@ class HassioCardContent extends LitElement {
@property() public hass!: HomeAssistant;
@property() public title!: string;
@property() public description?: string;
@property({ type: Boolean }) public available: boolean = true;
@property({ type: Boolean }) public showTopbar: boolean = false;
@property() public topbarClass?: string;
@property({ type: Boolean }) public available?: boolean;
@property() public datetime?: string;
@property() public iconTitle?: string;
@property() public iconClass?: string;
@property() public icon = "hass:help-circle";
@property() public iconImage?: string;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
${this.showTopbar
? html`
<div class="topbar ${this.topbarClass}"></div>
`
: ""}
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img src="${this.iconImage}" title="${this.iconTitle}" />
<div></div>
</div>
`
: html`
<iron-icon
class=${this.iconClass}
.icon=${this.icon}
.title=${this.iconTitle}
></iron-icon>
`}
<iron-icon
class=${this.iconClass}
.icon=${this.icon}
.title=${this.iconTitle}
></iron-icon>
<div>
<div class="title">
${this.title}
</div>
<div class="title">${this.title}</div>
<div class="addition">
${this.description}
${/* treat as available when undefined */
@@ -72,9 +53,8 @@ class HassioCardContent extends LitElement {
static get styles(): CSSResult {
return css`
iron-icon {
margin-right: 24px;
margin-left: 8px;
margin-top: 12px;
margin-right: 16px;
margin-top: 16px;
float: left;
color: var(--secondary-text-color);
}
@@ -108,44 +88,6 @@ class HassioCardContent extends LitElement {
ha-relative-time {
display: block;
}
.icon_image img {
max-height: 40px;
max-width: 40px;
margin-top: 4px;
margin-right: 16px;
float: left;
}
.icon_image.stopped,
.icon_image.not_available {
filter: grayscale(1);
}
.dot {
position: absolute;
background-color: var(--paper-orange-400);
width: 12px;
height: 12px;
top: 8px;
right: 8px;
border-radius: 50%;
}
.topbar {
position: absolute;
width: 100%;
height: 2px;
top: 0;
left: 0;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.topbar.installed {
background-color: var(--primary-color);
}
.topbar.update {
background-color: var(--accent-color);
}
.topbar.unavailable {
background-color: var(--error-color);
}
`;
}
}
@@ -1,4 +1,4 @@
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
import { HassioAddonInfo } from "../../../src/data/hassio";
import * as Fuse from "fuse.js";
export function filterAndSort(addons: HassioAddonInfo[], filter: string) {
+1 -1
View File
@@ -16,7 +16,7 @@ import "@material/mwc-button";
class HassioSearchInput extends LitElement {
@property() private filter?: string;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
<div class="search-container">
<paper-input
+92
View File
@@ -0,0 +1,92 @@
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../components/hassio-card-content";
import "../resources/hassio-style";
import NavigateMixin from "../../../src/mixins/navigate-mixin";
class HassioAddons extends NavigateMixin(PolymerElement) {
static get template() {
return html`
<style include="ha-style hassio-style">
paper-card {
cursor: pointer;
}
</style>
<div class="content card-group">
<div class="title">Add-ons</div>
<template is="dom-if" if="[[!addons.length]]">
<paper-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<a href="#" on-click="openStore">the add-on store</a> to get
started!
</div>
</paper-card>
</template>
<template
is="dom-repeat"
items="[[addons]]"
as="addon"
sort="sortAddons"
>
<paper-card on-click="addonTapped">
<div class="card-content">
<hassio-card-content
hass="[[hass]]"
title="[[addon.name]]"
description="[[addon.description]]"
available="[[addon.available]]"
icon="[[computeIcon(addon)]]"
icon-title="[[computeIconTitle(addon)]]"
icon-class="[[computeIconClass(addon)]]"
></hassio-card-content>
</div>
</paper-card>
</template>
</div>
`;
}
static get properties() {
return {
hass: Object,
addons: Array,
};
}
sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
computeIcon(addon) {
return addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle";
}
computeIconTitle(addon) {
if (addon.installed !== addon.version) return "New version available";
return addon.state === "started"
? "Add-on is running"
: "Add-on is stopped";
}
computeIconClass(addon) {
if (addon.installed !== addon.version) return "update";
return addon.state === "started" ? "running" : "";
}
addonTapped(ev) {
this.navigate("/hassio/addon/" + ev.model.addon.slug);
ev.target.blur();
}
openStore(ev) {
this.navigate("/hassio/store");
ev.target.blur();
}
}
customElements.define("hassio-addons", HassioAddons);
-109
View File
@@ -1,109 +0,0 @@
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { HomeAssistant } from "../../../src/types";
import { HassioAddonInfo } from "../../../src/data/hassio/addon";
import { navigate } from "../../../src/common/navigate";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "../components/hassio-card-content";
@customElement("hassio-addons")
class HassioAddons extends LitElement {
@property() public hass!: HomeAssistant;
@property() public addons?: HassioAddonInfo[];
protected render(): TemplateResult {
const [major, minor] = this.hass.config.version.split(".", 2);
const ha105pluss =
Number(major) > 0 || (major === "0" && Number(minor) >= 105);
return html`
<div class="content">
<h1>Add-ons</h1>
<div class="card-group">
${!this.addons
? html`
<paper-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<a href="#" @click=${this._openStore}>the add-on store</a>
to get started!
</div>
</paper-card>
`
: this.addons
.sort((a, b) => (a.name > b.name ? 1 : -1))
.map(
(addon) => html`
<paper-card .addon=${addon} @click=${this._addonTapped}>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title=${addon.name}
.description=${addon.description}
available
.showTopbar=${addon.installed !== addon.version}
topbarClass="update"
.icon=${addon.installed !== addon.version
? "hassio:arrow-up-bold-circle"
: "hassio:puzzle"}
.iconTitle=${addon.state !== "started"
? "Add-on is stopped"
: addon.installed !== addon.version
? "New version available"
: "Add-on is running"}
.iconClass=${addon.installed &&
addon.installed !== addon.version
? addon.state === "started"
? "update"
: "update stopped"
: addon.installed && addon.state === "started"
? "running"
: "stopped"}
.iconImage=${ha105pluss && addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined}
></hassio-card-content>
</div>
</paper-card>
`
)}
</div>
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
cursor: pointer;
}
`,
];
}
private _addonTapped(ev: any): void {
navigate(this, `/hassio/addon/${ev.currentTarget.addon.slug}`);
}
private _openStore(): void {
navigate(this, "/hassio/store");
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addons": HassioAddons;
}
}
+10 -13
View File
@@ -9,22 +9,22 @@ import {
} from "lit-element";
import "./hassio-addons";
import "./hassio-update";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioHomeAssistantInfo,
} from "../../../src/data/hassio/supervisor";
HassioHassOSInfo,
} from "../../../src/data/hassio";
@customElement("hassio-dashboard")
class HassioDashboard extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hassInfo!: HassioHomeAssistantInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
<div class="content">
<hassio-update
@@ -41,15 +41,12 @@ class HassioDashboard extends LitElement {
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
css`
.content {
margin: 0 auto;
}
`,
];
static get styles(): CSSResult {
return css`
.content {
margin: 0 auto;
}
`;
}
}
+27 -29
View File
@@ -10,14 +10,13 @@ import {
import "@polymer/iron-icon/iron-icon";
import { HomeAssistant } from "../../../src/types";
import { HassioHassOSInfo } from "../../../src/data/hassio/host";
import {
HassioHomeAssistantInfo,
HassioHassOSInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
} from "../../../src/data/hassio";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
@@ -27,23 +26,20 @@ import "../components/hassio-card-content";
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property() public hass!: HomeAssistant;
@property() public hassInfo: HassioHomeAssistantInfo;
@property() public hassOsInfo?: HassioHassOSInfo;
@property() public supervisorInfo: HassioSupervisorInfo;
@property() private _error?: string;
protected render(): TemplateResult {
@property() public error?: string;
protected render(): TemplateResult | void {
const updatesAvailable: number = [
this.hassInfo,
this.supervisorInfo,
this.hassOsInfo,
].filter((value) => {
return (
!!value &&
(value.last_version
? value.version !== value.last_version
: value.version !== value.version_latest)
);
return !!value && value.version !== value.last_version;
}).length;
if (!updatesAvailable) {
@@ -52,19 +48,19 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
${this._error
${this.error
? html`
<div class="error">Error: ${this._error}</div>
<div class="error">Error: ${this.error}</div>
`
: ""}
<h1>
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
</h1>
<div class="card-group">
<div class="title">
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
</div>
${this._renderUpdateCard(
"Home Assistant Core",
"Home Assistant",
this.hassInfo.version,
this.hassInfo.last_version,
"hassio/homeassistant/update",
@@ -74,7 +70,7 @@ export class HassioUpdate extends LitElement {
"hassio:home-assistant"
)}
${this._renderUpdateCard(
"Supervisor",
"Hass.io Supervisor",
this.supervisorInfo.version,
this.supervisorInfo.last_version,
"hassio/supervisor/update",
@@ -82,7 +78,7 @@ export class HassioUpdate extends LitElement {
)}
${this.hassOsInfo
? this._renderUpdateCard(
"Operating System",
"HassOS",
this.hassOsInfo.version,
this.hassOsInfo.version_latest,
"hassio/hassos/update",
@@ -138,22 +134,28 @@ export class HassioUpdate extends LitElement {
private _apiCalled(ev) {
if (ev.detail.success) {
this._error = "";
this.error = "";
return;
}
const response = ev.detail.response;
typeof response.body === "object"
? (this._error = response.body.message || "Unknown error")
: (this._error = response.body);
? (this.error = response.body.message || "Unknown error")
: (this.error = response.body);
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
:host {
width: 33%;
}
paper-card {
display: inline-block;
margin-bottom: 32px;
}
.icon {
--iron-icon-height: 48px;
--iron-icon-width: 48px;
@@ -168,10 +170,6 @@ export class HassioUpdate extends LitElement {
.warning {
color: var(--secondary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
}
.card-actions {
text-align: right;
}
@@ -1,59 +1,20 @@
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import { PaperDialogElement } from "@polymer/paper-dialog";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { hassioStyle } from "../../resources/hassio-style";
import { haStyleDialog } from "../../../../src/resources/styles";
import { HassioMarkdownDialogParams } from "./show-dialog-hassio-markdown";
import "../../../../src/components/dialog/ha-paper-dialog";
import "../../../../src/components/ha-markdown";
import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog";
import { customElement } from "lit-element";
import { PaperDialogElement } from "@polymer/paper-dialog";
@customElement("dialog-hassio-markdown")
class HassioMarkdownDialog extends LitElement {
@property() public title!: string;
@property() public content!: string;
@query("#dialog") private _dialog!: PaperDialogElement;
public showDialog(params: HassioMarkdownDialogParams) {
this.title = params.title;
this.content = params.content;
this._dialog.open();
}
protected render(): TemplateResult {
class HassioMarkdownDialog extends PolymerElement {
static get template() {
return html`
<ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">${this.title}</div>
</app-toolbar>
<paper-dialog-scrollable>
<ha-markdown .content=${this.content || ""}></ha-markdown>
</paper-dialog-scrollable>
</ha-paper-dialog>
`;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
hassioStyle,
css`
<style include="ha-style-dialog">
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
@@ -91,8 +52,32 @@ class HassioMarkdownDialog extends LitElement {
background-color: var(--primary-color);
}
}
`,
];
</style>
<ha-paper-dialog id="dialog" with-backdrop="">
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">[[title]]</div>
</app-toolbar>
<paper-dialog-scrollable>
<ha-markdown content="[[content]]"></ha-markdown>
</paper-dialog-scrollable>
</ha-paper-dialog>
`;
}
static get properties() {
return {
title: String,
content: String,
};
}
public showDialog(params) {
this.setProperties(params);
(this.$.dialog as PaperDialogElement).open();
}
}
@@ -1,33 +1,20 @@
import "@material/mwc-button";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/iron-icon/iron-icon";
import "@material/mwc-button";
import "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
import "@polymer/paper-icon-button/paper-icon-button";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import { PaperDialogElement } from "@polymer/paper-dialog";
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import {
fetchHassioSnapshotInfo,
HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { getSignedPath } from "../../../../src/data/auth";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
import { haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { PolymerChangedEvent } from "../../../../src/polymer-types";
import "../../../../src/resources/ha-style";
import "../../../../src/components/dialog/ha-paper-dialog";
import { customElement } from "lit-element";
import { PaperDialogElement } from "@polymer/paper-dialog";
import { HassioSnapshotDialogParams } from "./show-dialog-hassio-snapshot";
import { fetchHassioSnapshotInfo } from "../../../../src/data/hassio";
const _computeFolders = (folders) => {
const list: Array<{ slug: string; name: string; checked: boolean }> = [];
@@ -59,179 +46,21 @@ const _computeAddons = (addons) => {
}));
};
interface AddonItem {
slug: string;
name: string;
version: string;
checked: boolean | null | undefined;
}
interface FolderItem {
slug: string;
name: string;
checked: boolean | null | undefined;
}
@customElement("dialog-hassio-snapshot")
class HassioSnapshotDialog extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _error?: string;
@property() private snapshot?: HassioSnapshotDetail;
@property() private _folders!: FolderItem[];
@property() private _addons!: AddonItem[];
@property() private _dialogParams?: HassioSnapshotDialogParams;
@property() private _snapshotPassword!: string;
@property() private _restoreHass: boolean | null | undefined = true;
@query("#dialog") private _dialog!: PaperDialogElement;
class HassioSnapshotDialog extends PolymerElement {
// Commented out because it breaks Polymer! Kept around for when we migrate
// to Lit. Now just putting ts-ignore everywhere because we need this out.
// Sorry future developer.
// public hass!: HomeAssistant;
// protected error?: string;
// private snapshot?: any;
// private dialogParams?: HassioSnapshotDialogParams;
// private restoreHass!: boolean;
// private snapshotPassword!: string;
public async showDialog(params: HassioSnapshotDialogParams) {
this.snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
this._folders = _computeFolders(
this.snapshot.folders
).sort((a: FolderItem, b: FolderItem) => (a.name > b.name ? 1 : -1));
this._addons = _computeAddons(
this.snapshot.addons
).sort((a: AddonItem, b: AddonItem) => (a.name > b.name ? 1 : -1));
this._dialogParams = params;
try {
this._dialog.open();
} catch {
await this.showDialog(params);
}
}
protected render(): TemplateResult {
if (!this.snapshot) {
return html``;
}
static get template() {
return html`
<ha-paper-dialog
id="dialog"
with-backdrop=""
.on-iron-overlay-closed=${this._dialogClosed}
>
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">${this._computeName}</div>
</app-toolbar>
<div class="details">
${this.snapshot.type === "full"
? "Full snapshot"
: "Partial snapshot"}
(${this._computeSize})<br />
${this._formatDatetime(this.snapshot.date)}
</div>
<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) =>
(this._restoreHass = (ev.target as PaperCheckboxElement).checked)}"
>
Home Assistant ${this.snapshot.homeassistant}
</paper-checkbox>
${this._folders.length
? html`
<div>Folders:</div>
<paper-dialog-scrollable class="no-margin-top">
${this._folders.map((item) => {
return html`
<paper-checkbox
.checked=${item.checked}
@change="${(ev: Event) =>
this._updateFolders(
item,
(ev.target as PaperCheckboxElement).checked
)}"
>
${item.name}
</paper-checkbox>
`;
})}
</paper-dialog-scrollable>
`
: ""}
${this._addons.length
? html`
<div>Add-on:</div>
<paper-dialog-scrollable class="no-margin-top">
${this._addons.map((item) => {
return html`
<paper-checkbox
.checked=${item.checked}
@change="${(ev: Event) =>
this._updateAddons(
item,
(ev.target as PaperCheckboxElement).checked
)}"
>
${item.name}
</paper-checkbox>
`;
})}
</paper-dialog-scrollable>
`
: ""}
${this.snapshot.protected
? html`
<paper-input
autofocus=""
label="Password"
type="password"
@value-changed=${this._passwordInput}
.value=${this._snapshotPassword}
></paper-input>
`
: ""}
${this._error
? html`
<p class="error">Error: ${this._error}</p>
`
: ""}
<div>Actions:</div>
<ul class="buttons">
<li>
<mwc-button @click=${this._downloadClicked}>
<iron-icon icon="hassio:download" class="icon"></iron-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button @click=${this._partialRestoreClicked}>
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Restore Selected
</mwc-button>
</li>
${this.snapshot.type === "full"
? html`
<li>
<mwc-button @click=${this._fullRestoreClicked}>
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
`
: ""}
<li>
<mwc-button @click=${this._deleteClicked}>
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog>
`;
}
static get styles(): CSSResult[] {
return [
haStyleDialog,
css`
<style include="ha-style-dialog">
ha-paper-dialog {
min-width: 350px;
font-size: 14px;
@@ -283,155 +112,259 @@ class HassioSnapshotDialog extends LitElement {
.no-margin-top {
margin-top: 0;
}
`,
];
</style>
<ha-paper-dialog
id="dialog"
with-backdrop=""
on-iron-overlay-closed="_dialogClosed"
>
<app-toolbar>
<paper-icon-button
icon="hassio:close"
dialog-dismiss=""
></paper-icon-button>
<div main-title="">[[_computeName(snapshot)]]</div>
</app-toolbar>
<div class="details">
[[_computeType(snapshot.type)]] ([[_computeSize(snapshot.size)]])<br />
[[_formatDatetime(snapshot.date)]]
</div>
<div>Home Assistant:</div>
<paper-checkbox checked="{{restoreHass}}">
Home Assistant [[snapshot.homeassistant]]
</paper-checkbox>
<template is="dom-if" if="[[_folders.length]]">
<div>Folders:</div>
<template is="dom-repeat" items="[[_folders]]">
<paper-checkbox checked="{{item.checked}}">
[[item.name]]
</paper-checkbox>
</template>
</template>
<template is="dom-if" if="[[_addons.length]]">
<div>Add-ons:</div>
<paper-dialog-scrollable class="no-margin-top">
<template is="dom-repeat" items="[[_addons]]" sort="_sortAddons">
<paper-checkbox checked="{{item.checked}}">
[[item.name]] <span class="details">([[item.version]])</span>
</paper-checkbox>
</template>
</paper-dialog-scrollable>
</template>
<template is="dom-if" if="[[snapshot.protected]]">
<paper-input
autofocus=""
label="Password"
type="password"
value="{{snapshotPassword}}"
></paper-input>
</template>
<template is="dom-if" if="[[error]]">
<p class="error">Error: [[error]]</p>
</template>
<div>Actions:</div>
<ul class="buttons">
<li>
<mwc-button on-click="_downloadClicked">
<iron-icon icon="hassio:download" class="icon"></iron-icon>
Download Snapshot
</mwc-button>
</li>
<li>
<mwc-button on-click="_partialRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Restore Selected
</mwc-button>
</li>
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
<li>
<mwc-button on-click="_fullRestoreClicked">
<iron-icon icon="hassio:history" class="icon"> </iron-icon>
Wipe &amp; restore
</mwc-button>
</li>
</template>
<li>
<mwc-button on-click="_deleteClicked">
<iron-icon icon="hassio:delete" class="icon warning"> </iron-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>
</li>
</ul>
</ha-paper-dialog>
`;
}
private _updateFolders(item: FolderItem, value: boolean | null | undefined) {
this._folders = this._folders.map((folder) => {
if (folder.slug === item.slug) {
folder.checked = value;
}
return folder;
static get properties() {
return {
hass: Object,
dialogParams: Object,
snapshot: Object,
_folders: Object,
_addons: Object,
restoreHass: {
type: Boolean,
value: true,
},
snapshotPassword: String,
error: String,
};
}
public async showDialog(params: HassioSnapshotDialogParams) {
// @ts-ignore
const snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
this.setProperties({
dialogParams: params,
snapshot,
_folders: _computeFolders(snapshot.folders),
_addons: _computeAddons(snapshot.addons),
});
(this.$.dialog as PaperDialogElement).open();
}
private _updateAddons(item: AddonItem, value: boolean | null | undefined) {
this._addons = this._addons.map((addon) => {
if (addon.slug === item.slug) {
addon.checked = value;
}
return addon;
});
protected _isFullSnapshot(type) {
return type === "full";
}
private _passwordInput(ev: PolymerChangedEvent<string>) {
this._snapshotPassword = ev.detail.value;
}
private _partialRestoreClicked() {
protected _partialRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
// @ts-ignore
const addons = this._addons
.filter((addon) => addon.checked)
.map((addon) => addon.slug);
// @ts-ignore
const folders = this._folders
.filter((folder) => folder.checked)
.map((folder) => folder.slug);
const data: {
homeassistant: boolean | null | undefined;
addons: any;
folders: any;
password?: string;
} = {
homeassistant: this._restoreHass,
const data = {
// @ts-ignore
homeassistant: this.restoreHass,
addons,
folders,
};
if (this.snapshot!.protected) {
data.password = this._snapshotPassword;
// @ts-ignore
if (this.snapshot.protected) {
// @ts-ignore
data.password = this.snapshotPassword;
}
// @ts-ignore
this.hass
.callApi(
"POST",
`hassio/snapshots/${this.snapshot!.slug}/restore/partial`,
// @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/partial`,
data
)
.then(
() => {
alert("Snapshot restored!");
this._dialog.close();
(this.$.dialog as PaperDialogElement).close();
},
(error) => {
this._error = error.body.message;
// @ts-ignore
this.error = error.body.message;
}
);
}
private _fullRestoreClicked() {
protected _fullRestoreClicked() {
if (!confirm("Are you sure you want to restore this snapshot?")) {
return;
}
const data = this.snapshot!.protected
? { password: this._snapshotPassword }
// @ts-ignore
const data = this.snapshot.protected
? {
password:
// @ts-ignore
this.snapshotPassword,
}
: undefined;
// @ts-ignore
this.hass
.callApi(
"POST",
`hassio/snapshots/${this.snapshot!.slug}/restore/full`,
// @ts-ignore
`hassio/snapshots/${this.dialogParams!.slug}/restore/full`,
data
)
.then(
() => {
alert("Snapshot restored!");
this._dialog.close();
(this.$.dialog as PaperDialogElement).close();
},
(error) => {
this._error = error.body.message;
// @ts-ignore
this.error = error.body.message;
}
);
}
private _deleteClicked() {
protected _deleteClicked() {
if (!confirm("Are you sure you want to delete this snapshot?")) {
return;
}
// @ts-ignore
this.hass
.callApi("POST", `hassio/snapshots/${this.snapshot!.slug}/remove`)
// @ts-ignore
.callApi("POST", `hassio/snapshots/${this.dialogParams!.slug}/remove`)
.then(
() => {
this._dialog.close();
this._dialogParams!.onDelete();
(this.$.dialog as PaperDialogElement).close();
// @ts-ignore
this.dialogParams!.onDelete();
},
(error) => {
this._error = error.body.message;
// @ts-ignore
this.error = error.body.message;
}
);
}
private async _downloadClicked() {
let signedPath: { path: string };
protected async _downloadClicked() {
let signedPath;
try {
signedPath = await getSignedPath(
// @ts-ignore
this.hass,
`/api/hassio/snapshots/${this.snapshot!.slug}/download`
// @ts-ignore
`/api/hassio/snapshots/${this.dialogParams!.slug}/download`
);
} catch (err) {
alert(`Error: ${err.message}`);
return;
}
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
// @ts-ignore
const name = this._computeName(this.snapshot).replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a");
a.href = signedPath.path;
a.download = `Hass_io_${name}.tar`;
this._dialog.appendChild(a);
this.$.dialog.appendChild(a);
a.click();
this._dialog.removeChild(a);
this.$.dialog.removeChild(a);
}
private get _computeName() {
return this.snapshot
? this.snapshot.name || this.snapshot.slug
: "Unnamed snapshot";
protected _computeName(snapshot) {
return snapshot ? snapshot.name || snapshot.slug : "Unnamed snapshot";
}
private get _computeSize() {
return Math.ceil(this.snapshot!.size * 10) / 10 + " MB";
protected _computeType(type) {
return type === "full" ? "Full snapshot" : "Partial snapshot";
}
private _formatDatetime(datetime) {
protected _computeSize(size) {
return Math.ceil(size * 10) / 10 + " MB";
}
protected _sortAddons(a, b) {
return a.name < b.name ? -1 : 1;
}
protected _formatDatetime(datetime) {
return new Date(datetime).toLocaleDateString(navigator.language, {
weekday: "long",
year: "numeric",
@@ -442,12 +375,13 @@ class HassioSnapshotDialog extends LitElement {
});
}
private _dialogClosed() {
this._dialogParams = undefined;
this.snapshot = undefined;
this._snapshotPassword = "";
this._folders = [];
this._addons = [];
protected _dialogClosed() {
this.setProperties({
dialogParams: undefined,
snapshot: undefined,
_addons: [],
_folders: [],
});
}
}
@@ -1,12 +1,9 @@
window.loadES5Adapter().then(() => {
// eslint-disable-next-line
import(/* webpackChunkName: "roboto" */ "../../src/resources/roboto");
// eslint-disable-next-line
import(/* webpackChunkName: "hassio-icons" */ "./resources/hassio-icons");
// eslint-disable-next-line
import(/* webpackChunkName: "hassio-main" */ "./hassio-main");
});
const styleEl = document.createElement("style");
styleEl.innerHTML = `
body {
+31 -99
View File
@@ -12,28 +12,22 @@ import {
import { HomeAssistant } from "../../src/types";
import {
fetchHassioSupervisorInfo,
fetchHassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioHomeAssistantInfo,
createHassioSession,
HassioPanelInfo,
} from "../../src/data/hassio/supervisor";
import {
fetchHassioHostInfo,
fetchHassioHassOsInfo,
fetchHassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioHostInfo,
HassioHassOSInfo,
} from "../../src/data/hassio/host";
import { fetchHassioAddonInfo } from "../../src/data/hassio/addon";
HassioHomeAssistantInfo,
fetchHassioAddonInfo,
createHassioSession,
HassioPanelInfo,
} from "../../src/data/hassio";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
// Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-pages-with-tabs";
import { navigate } from "../../src/common/navigate";
import {
showAlertDialog,
AlertDialogParams,
} from "../../src/dialogs/generic/show-dialog-box";
// The register callback of the IronA11yKeysBehavior inside paper-icon-button
// is not called, causing _keyBindings to be uninitiliazed for paper-icon-button,
@@ -76,6 +70,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
},
},
};
@property() private _supervisorInfo: HassioSupervisorInfo;
@property() private _hostInfo: HassioHostInfo;
@property() private _hassOsInfo?: HassioHassOSInfo;
@@ -84,12 +79,7 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.parentElement,
this.hass.themes,
this.hass.selectedTheme,
true
);
applyThemesOnElement(this, this.hass.themes, this.hass.selectedTheme, true);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
// Paulus - March 17, 2019
// We went to a single hass-toggle-menu event in HA 0.90. However, the
@@ -115,14 +105,6 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
})
);
// Forward haptic events to parent window.
window.addEventListener("haptic", (ev) => {
// @ts-ignore
fireEvent(window.parent, ev.type, ev.detail, {
bubbles: false,
});
});
makeDialogManager(this, document.body);
}
@@ -174,81 +156,31 @@ class HassioMain extends ProvideHassLitMixin(HassRouterPage) {
}
private async _redirectIngress(addonSlug: string) {
// When we trigger a navigation, we sleep to make sure we don't
// show the hassio dashboard before navigating away.
const awaitAlert = async (
alertParams: AlertDialogParams,
action: () => void
) => {
await new Promise((resolve) => {
alertParams.confirm = resolve;
showAlertDialog(this, alertParams);
});
action();
await new Promise((resolve) => setTimeout(resolve, 1000));
};
const createSessionPromise = createHassioSession(this.hass).then(
() => true,
() => false
);
let addon;
try {
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
const [addon] = await Promise.all([
fetchHassioAddonInfo(this.hass, addonSlug).catch(() => {
throw new Error("Failed to fetch add-on info");
}),
createHassioSession(this.hass).catch(() => {
throw new Error("Failed to create an ingress session");
}),
]);
if (!addon.ingress_url) {
alert("Add-on does not support Ingress");
return;
}
if (addon.state !== "started") {
alert("Add-on is not running. Please start it first");
navigate(this, `/hassio/addon/${addon.slug}`, true);
return;
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
} catch (err) {
await awaitAlert(
{
text: "Unable to fetch add-on info to start Ingress",
title: "Hass.io",
},
() => history.back()
);
return;
alert("Unable to open ingress connection");
}
if (!addon.ingress_url) {
await awaitAlert(
{
text: "Add-on does not support Ingress",
title: addon.name,
},
() => history.back()
);
return;
}
if (addon.state !== "started") {
await awaitAlert(
{
text: "Add-on is not running. Please start it first",
title: addon.name,
},
() => navigate(this, `/hassio/addon/${addon.slug}`, true)
);
return;
}
if (!(await createSessionPromise)) {
await awaitAlert(
{
text: "Unable to create an Ingress session",
title: addon.name,
},
() => history.back()
);
return;
}
location.assign(addon.ingress_url);
// await a promise that doesn't resolve, so we show the loading screen
// while we load the next page.
await new Promise(() => undefined);
}
private _apiCalled(ev) {
+6 -5
View File
@@ -23,11 +23,12 @@ import scrollToTarget from "../../src/common/dom/scroll-to-target";
import { haStyle } from "../../src/resources/styles";
import { HomeAssistant, Route } from "../../src/types";
import { navigate } from "../../src/common/navigate";
import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
} from "../../src/data/hassio/supervisor";
HassioHassOSInfo,
} from "../../src/data/hassio";
const HAS_REFRESH_BUTTON = ["store", "snapshots"];
@@ -41,7 +42,7 @@ class HassioPagesWithTabs extends LitElement {
@property() public hassInfo!: HassioHomeAssistantInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const page = this._page;
return html`
<app-header-layout has-scrolling-region>
@@ -52,7 +53,7 @@ class HassioPagesWithTabs extends LitElement {
.narrow=${this.narrow}
hassio
></ha-menu-button>
<div main-title>Supervisor</div>
<div main-title>Hass.io</div>
${HAS_REFRESH_BUTTON.includes(page)
? html`
<paper-icon-button
@@ -123,7 +124,7 @@ class HassioPagesWithTabs extends LitElement {
}
paper-tabs {
margin-left: 12px;
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
--paper-tabs-selection-bar-color: #fff;
text-transform: uppercase;
}
`,
+3 -2
View File
@@ -11,11 +11,12 @@ import "./dashboard/hassio-dashboard";
import "./snapshots/hassio-snapshots";
import "./addon-store/hassio-addon-store";
import "./system/hassio-system";
import { HassioHostInfo, HassioHassOSInfo } from "../../src/data/hassio/host";
import {
HassioSupervisorInfo,
HassioHostInfo,
HassioHomeAssistantInfo,
} from "../../src/data/hassio/supervisor";
HassioHassOSInfo,
} from "../../src/data/hassio";
@customElement("hassio-tabs-router")
class HassioTabsRouter extends HassRouterPage {
@@ -9,11 +9,11 @@ import {
css,
} from "lit-element";
import { HomeAssistant, Route } from "../../../src/types";
import { createHassioSession } from "../../../src/data/hassio/supervisor";
import {
createHassioSession,
HassioAddonDetails,
fetchHassioAddonInfo,
} from "../../../src/data/hassio/addon";
} from "../../../src/data/hassio";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
@@ -23,7 +23,7 @@ class HassioIngressView extends LitElement {
@property() public route!: Route;
@property() private _addon?: HassioAddonDetails;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this._addon) {
return html`
<hass-loading-screen></hass-loading-screen>
+66
View File
@@ -0,0 +1,66 @@
import { css } from "lit-element";
const documentContainer = document.createElement("template");
documentContainer.setAttribute("style", "display: none;");
export const hassioStyle = css`
.card-group {
margin-top: 24px;
}
.card-group .title {
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
.card-group .description {
font-size: 0.5em;
font-weight: 500;
margin-top: 4px;
}
.card-group paper-card {
--card-group-columns: 4;
width: calc(
(100% - 12px * var(--card-group-columns)) / var(--card-group-columns)
);
margin: 4px;
vertical-align: top;
}
@media screen and (max-width: 1200px) and (min-width: 901px) {
.card-group paper-card {
--card-group-columns: 3;
}
}
@media screen and (max-width: 900px) and (min-width: 601px) {
.card-group paper-card {
--card-group-columns: 2;
}
}
@media screen and (max-width: 600px) and (min-width: 0) {
.card-group paper-card {
width: 100%;
margin: 4px 0;
}
.content {
padding: 0;
}
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.error {
color: var(--google-red-500);
margin-top: 16px;
}
`;
documentContainer.innerHTML = `<dom-module id="hassio-style">
<template>
<style>
${hassioStyle.toString()}
</style>
</template>
</dom-module>`;
document.head.appendChild(documentContainer.content);
-51
View File
@@ -1,51 +0,0 @@
import { css } from "lit-element";
export const hassioStyle = css`
.content {
margin: 8px;
}
h1 {
color: var(--primary-text-color);
font-size: 2em;
margin-bottom: 8px;
font-family: var(--paper-font-headline_-_font-family);
-webkit-font-smoothing: var(--paper-font-headline_-_-webkit-font-smoothing);
font-size: var(--paper-font-headline_-_font-size);
font-weight: var(--paper-font-headline_-_font-weight);
letter-spacing: var(--paper-font-headline_-_letter-spacing);
line-height: var(--paper-font-headline_-_line-height);
padding-left: 8px;
}
.description {
margin-top: 4px;
padding-left: 8px;
}
.card-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 8px;
}
@media screen and (min-width: 640px) {
.card-group {
grid-template-columns: repeat(auto-fit, minmax(300px, 0.5fr));
}
}
@media screen and (min-width: 1020px) {
.card-group {
grid-template-columns: repeat(auto-fit, minmax(300px, 0.333fr));
}
}
@media screen and (min-width: 1300px) {
.card-group {
grid-template-columns: repeat(auto-fit, minmax(300px, 0.25fr));
}
}
ha-call-api-button {
font-weight: 500;
color: var(--primary-color);
}
.error {
color: var(--error-color);
margin-top: 16px;
}
`;
+11 -13
View File
@@ -17,20 +17,19 @@ import "@polymer/paper-radio-group/paper-radio-group";
import "../components/hassio-card-content";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { showHassioSnapshotDialog } from "../dialogs/snapshot/show-dialog-hassio-snapshot";
import { HomeAssistant } from "../../../src/types";
import {
HassioSnapshot,
HassioSupervisorInfo,
fetchHassioSnapshots,
reloadHassioSnapshots,
HassioFullSnapshotCreateParams,
HassioPartialSnapshotCreateParams,
createHassioFullSnapshot,
createHassioPartialSnapshot,
} from "../../../src/data/hassio/snapshot";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
} from "../../../src/data/hassio";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { fireEvent } from "../../../src/common/dom/fire_event";
@@ -76,17 +75,17 @@ class HassioSnapshots extends LitElement {
await this._updateSnapshots();
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
<div class="content">
<h1>
Create snapshot
</h1>
<p class="description">
Snapshots allow you to easily backup and restore all data of your Home
Assistant instance.
</p>
<div class="card-group">
<div class="title">
Create snapshot
<div class="description">
Snapshots allow you to easily backup and restore all data of your
Hass.io instance.
</div>
</div>
<paper-card>
<div class="card-content">
<paper-input
@@ -173,8 +172,8 @@ class HassioSnapshots extends LitElement {
</paper-card>
</div>
<h1>Available snapshots</h1>
<div class="card-group">
<div class="title">Available snapshots</div>
${this._snapshots === undefined
? undefined
: this._snapshots.length === 0
@@ -335,7 +334,6 @@ class HassioSnapshots extends LitElement {
static get styles(): CSSResultArray {
return [
haStyle,
hassioStyle,
css`
paper-radio-group {
+201
View File
@@ -0,0 +1,201 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import { EventsMixin } from "../../../src/mixins/events-mixin";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
class HassioHostInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-card {
display: inline-block;
width: 400px;
margin-left: 8px;
}
.card-content {
height: 200px;
color: var(--primary-text-color);
}
@media screen and (max-width: 830px) {
paper-card {
margin-top: 8px;
margin-left: 0;
width: 100%;
}
.card-content {
height: auto;
}
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
mwc-button.info {
max-width: calc(50% - 12px);
}
table.info {
margin-bottom: 10px;
}
</style>
<paper-card>
<div class="card-content">
<h2>Host system</h2>
<table class="info">
<tbody>
<tr>
<td>Hostname</td>
<td>[[data.hostname]]</td>
</tr>
<tr>
<td>System</td>
<td>[[data.operating_system]]</td>
</tr>
<template is="dom-if" if="[[data.deployment]]">
<tr>
<td>Deployment</td>
<td>[[data.deployment]]</td>
</tr>
</template>
</tbody>
</table>
<mwc-button raised on-click="_showHardware" class="info">
Hardware
</mwc-button>
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
<mwc-button raised on-click="_changeHostnameClicked" class="info">
Change hostname
</mwc-button>
</template>
<template is="dom-if" if="[[errors]]">
<div class="errors">Error: [[errors]]</div>
</template>
</div>
<div class="card-actions">
<template is="dom-if" if="[[_featureAvailable(data, 'reboot')]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/host/reboot"
>Reboot</ha-call-api-button
>
</template>
<template is="dom-if" if="[[_featureAvailable(data, 'shutdown')]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/host/shutdown"
>Shutdown</ha-call-api-button
>
</template>
<template is="dom-if" if="[[_featureAvailable(data, 'hassos')]]">
<ha-call-api-button
class="warning"
hass="[[hass]]"
path="hassio/hassos/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
</template>
<template is="dom-if" if="[[_computeUpdateAvailable(hassOsInfo)]]">
<ha-call-api-button hass="[[hass]]" path="hassio/hassos/update"
>Update</ha-call-api-button
>
</template>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
data: Object,
hassOsInfo: Object,
errors: String,
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
if (ev.detail.success) {
this.errors = null;
return;
}
var response = ev.detail.response;
if (typeof response.body === "object") {
this.errors = response.body.message || "Unknown error";
} else {
this.errors = response.body;
}
}
_computeUpdateAvailable(data) {
return data && data.version !== data.version_latest;
}
_featureAvailable(data, feature) {
return data && data.features && data.features.includes(feature);
}
_showHardware() {
this.hass
.callApi("get", "hassio/hardware/info")
.then(
(resp) => this._objectToMarkdown(resp.data),
() => "Error getting hardware info"
)
.then((content) => {
showHassioMarkdownDialog(this, {
title: "Hardware",
content: content,
});
});
}
_objectToMarkdown(obj, indent = "") {
let data = "";
Object.keys(obj).forEach((key) => {
if (typeof obj[key] !== "object") {
data += `${indent}- ${key}: ${obj[key]}\n`;
} else {
data += `${indent}- ${key}:\n`;
if (Array.isArray(obj[key])) {
if (obj[key].length) {
data +=
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
}
} else {
data += this._objectToMarkdown(obj[key], ` ${indent}`);
}
}
});
return data;
}
_changeHostnameClicked() {
const curHostname = this.data.hostname;
const hostname = prompt("Please enter a new hostname:", curHostname);
if (hostname && hostname !== curHostname) {
this.hass.callApi("post", "hassio/host/options", { hostname });
}
}
}
customElements.define("hassio-host-info", HassioHostInfo);
-229
View File
@@ -1,229 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import {
HassioHostInfo as HassioHostInfoType,
HassioHassOSInfo,
} from "../../../src/data/hassio/host";
import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import { HomeAssistant } from "../../../src/types";
import { showHassioMarkdownDialog } from "../dialogs/markdown/show-dialog-hassio-markdown";
import "../../../src/components/buttons/ha-call-api-button";
@customElement("hassio-host-info")
class HassioHostInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public hostInfo!: HassioHostInfoType;
@property() public hassOsInfo!: HassioHassOSInfo;
@property() private _errors?: string;
public render(): TemplateResult | void {
return html`
<paper-card>
<div class="card-content">
<h2>Host system</h2>
<table class="info">
<tbody>
<tr>
<td>Hostname</td>
<td>${this.hostInfo.hostname}</td>
</tr>
<tr>
<td>System</td>
<td>${this.hostInfo.operating_system}</td>
</tr>
${this.hostInfo.deployment
? html`
<tr>
<td>Deployment</td>
<td>${this.hostInfo.deployment}</td>
</tr>
`
: ""}
</tbody>
</table>
<mwc-button raised @click=${this._showHardware} class="info">
Hardware
</mwc-button>
${this.hostInfo.features.includes("hostname")
? html`
<mwc-button
raised
@click=${this._changeHostnameClicked}
class="info"
>
Change hostname
</mwc-button>
`
: ""}
${this._errors
? html`
<div class="errors">Error: ${this._errors}</div>
`
: ""}
</div>
<div class="card-actions">
${this.hostInfo.features.includes("reboot")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/host/reboot"
>Reboot</ha-call-api-button
>
`
: ""}
${this.hostInfo.features.includes("shutdown")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/host/shutdown"
>Shutdown</ha-call-api-button
>
`
: ""}
${this.hostInfo.features.includes("hassos")
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
path="hassio/hassos/config/sync"
title="Load HassOS configs or updates from USB"
>Import from USB</ha-call-api-button
>
`
: ""}
${this.hostInfo.version !== this.hostInfo.version_latest
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/hassos/update"
>Update</ha-call-api-button
>
`
: ""}
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
height: 100%;
width: 100%;
}
.card-content {
color: var(--primary-text-color);
box-sizing: border-box;
height: calc(100% - 47px);
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
mwc-button.info {
max-width: calc(50% - 12px);
}
table.info {
margin-bottom: 10px;
}
.warning {
--mdc-theme-primary: var(--google-red-500);
}
`,
];
}
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _showHardware(): Promise<void> {
try {
const content = this._objectToMarkdown(
await fetchHassioHardwareInfo(this.hass)
);
showHassioMarkdownDialog(this, {
title: "Hardware",
content,
});
} catch (err) {
showHassioMarkdownDialog(this, {
title: "Hardware",
content: "Error getting hardware info",
});
}
}
private _objectToMarkdown(obj, indent = ""): string {
let data = "";
Object.keys(obj).forEach((key) => {
if (typeof obj[key] !== "object") {
data += `${indent}- ${key}: ${obj[key]}\n`;
} else {
data += `${indent}- ${key}:\n`;
if (Array.isArray(obj[key])) {
if (obj[key].length) {
data +=
`${indent} - ` + obj[key].join(`\n${indent} - `) + "\n";
}
} else {
data += this._objectToMarkdown(obj[key], ` ${indent}`);
}
}
});
return data;
}
private _changeHostnameClicked(): void {
const curHostname = this.hostInfo.hostname;
const hostname = prompt("Please enter a new hostname:", curHostname);
if (hostname && hostname !== curHostname) {
this.hass.callApi("POST", "hassio/host/options", { hostname });
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-host-info": HassioHostInfo;
}
}
+175
View File
@@ -0,0 +1,175 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "../../../src/components/buttons/ha-call-api-button";
import { EventsMixin } from "../../../src/mixins/events-mixin";
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-card {
display: inline-block;
width: 400px;
}
.card-content {
height: 200px;
color: var(--primary-text-color);
}
@media screen and (max-width: 830px) {
paper-card {
width: 100%;
}
.card-content {
height: auto;
}
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
</style>
<paper-card>
<div class="card-content">
<h2>Hass.io supervisor</h2>
<table class="info">
<tbody>
<tr>
<td>Version</td>
<td>[[data.version]]</td>
</tr>
<tr>
<td>Latest version</td>
<td>[[data.last_version]]</td>
</tr>
<template is="dom-if" if='[[!_equals(data.channel, "stable")]]'>
<tr>
<td>Channel</td>
<td>[[data.channel]]</td>
</tr>
</template>
</tbody>
</table>
<template is="dom-if" if="[[errors]]">
<div class="errors">Error: [[errors]]</div>
</template>
</div>
<div class="card-actions">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
<template is="dom-if" if="[[computeUpdateAvailable(data)]]">
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/update"
>Update</ha-call-api-button
>
</template>
<template is="dom-if" if='[[_equals(data.channel, "beta")]]'>
<ha-call-api-button
hass="[[hass]]"
path="hassio/supervisor/options"
data="[[leaveBeta]]"
>Leave beta channel</ha-call-api-button
>
</template>
<template is="dom-if" if='[[_equals(data.channel, "stable")]]'>
<mwc-button
on-click="_joinBeta"
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</mwc-button
>
</template>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
data: Object,
errors: String,
leaveBeta: {
type: Object,
value: { channel: "stable" },
},
};
}
ready() {
super.ready();
this.addEventListener("hass-api-called", (ev) => this.apiCalled(ev));
}
apiCalled(ev) {
if (ev.detail.success) {
this.errors = null;
return;
}
var response = ev.detail.response;
if (typeof response.body === "object") {
this.errors = response.body.message || "Unknown error";
} else {
this.errors = response.body;
}
}
computeUpdateAvailable(data) {
return data.version !== data.last_version;
}
_equals(a, b) {
return a === b;
}
_joinBeta() {
if (
!confirm(`WARNING:
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
This inludes beta releases for:
- Home Assistant (Release Candidates)
- Hass.io supervisor
- Host system`)
) {
return;
}
const method = "post";
const path = "hassio/supervisor/options";
const data = { channel: "beta" };
const eventData = {
method: method,
path: path,
data: data,
};
this.hass
.callApi(method, path, data)
.then(
(resp) => {
eventData.success = true;
eventData.response = resp;
},
(resp) => {
eventData.success = false;
eventData.response = resp;
}
)
.then(() => {
this.fire("hass-api-called", eventData);
});
}
}
customElements.define("hassio-supervisor-info", HassioSupervisorInfo);
-177
View File
@@ -1,177 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../src/common/dom/fire_event";
import {
HassioSupervisorInfo as HassioSupervisorInfoType,
setSupervisorOption,
SupervisorOptions,
} from "../../../src/data/hassio/supervisor";
import { HomeAssistant } from "../../../src/types";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import "../../../src/components/buttons/ha-call-api-button";
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfoType;
@property() private _errors?: string;
public render(): TemplateResult | void {
return html`
<paper-card>
<div class="card-content">
<h2>Supervisor</h2>
<table class="info">
<tbody>
<tr>
<td>Version</td>
<td>${this.supervisorInfo.version}</td>
</tr>
<tr>
<td>Latest version</td>
<td>${this.supervisorInfo.last_version}</td>
</tr>
${this.supervisorInfo.channel !== "stable"
? html`
<tr>
<td>Channel</td>
<td>${this.supervisorInfo.channel}</td>
</tr>
`
: ""}
</tbody>
</table>
${this._errors
? html`
<div class="errors">Error: ${this._errors}</div>
`
: ""}
</div>
<div class="card-actions">
<ha-call-api-button .hass=${this.hass} path="hassio/supervisor/reload"
>Reload</ha-call-api-button
>
${this.supervisorInfo.version !== this.supervisorInfo.last_version
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/update"
>Update</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "beta"
? html`
<ha-call-api-button
.hass=${this.hass}
path="hassio/supervisor/options"
.data=${{ channel: "stable" }}
>Leave beta channel</ha-call-api-button
>
`
: ""}
${this.supervisorInfo.channel === "stable"
? html`
<mwc-button
@click=${this._joinBeta}
class="warning"
title="Get beta updates for Home Assistant (RCs), supervisor and host"
>Join beta channel</mwc-button
>
`
: ""}
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
paper-card {
height: 100%;
width: 100%;
}
.card-content {
color: var(--primary-text-color);
box-sizing: border-box;
height: calc(100% - 47px);
}
.info {
width: 100%;
}
.info td:nth-child(2) {
text-align: right;
}
.errors {
color: var(--google-red-500);
margin-top: 16px;
}
`,
];
}
protected firstUpdated(): void {
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private _apiCalled(ev): void {
if (ev.detail.success) {
this._errors = undefined;
return;
}
const response = ev.detail.response;
this._errors =
typeof response.body === "object"
? response.body.message || "Unknown error"
: response.body;
}
private async _joinBeta() {
if (
!confirm(`WARNING:
Beta releases are for testers and early adopters and can contain unstable code changes. Make sure you have backups of your data before you activate this feature.
This inludes beta releases for:
- Home Assistant (Release Candidates)
- Hass.io supervisor
- Host system`)
) {
return;
}
try {
const data: SupervisorOptions = { channel: "beta" };
await setSupervisorOption(this.hass, data);
const eventdata = {
success: true,
response: undefined,
path: "option",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._errors = `Error joining beta channel, ${err.body?.message || err}`;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-supervisor-info": HassioSupervisorInfo;
}
}
@@ -0,0 +1,64 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
class HassioSupervisorLog extends PolymerElement {
static get template() {
return html`
<style include="ha-style">
paper-card {
display: block;
}
pre {
overflow-x: auto;
white-space: pre-wrap;
overflow-wrap: break-word;
}
.fg-green {
color: var(--primary-text-color) !important;
}
</style>
${ANSI_HTML_STYLE}
<paper-card>
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button on-click="refresh">Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get properties() {
return {
hass: Object,
};
}
ready() {
super.ready();
this.loadData();
}
loadData() {
this.hass.callApi("get", "hassio/supervisor/logs").then(
(text) => {
while (this.$.content.lastChild) {
this.$.content.removeChild(this.$.content.lastChild);
}
this.$.content.appendChild(parseTextToColoredPre(text));
},
() => {
this.$.content.innerHTML =
'<span class="fg-red bold">Error fetching logs</span>';
}
);
}
refresh() {
this.loadData();
}
}
customElements.define("hassio-supervisor-log", HassioSupervisorLog);
@@ -1,90 +0,0 @@
import "@material/mwc-button";
import "@polymer/paper-card/paper-card";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
query,
} from "lit-element";
import { ANSI_HTML_STYLE, parseTextToColoredPre } from "../ansi-to-html";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { fetchSupervisorLogs } from "../../../src/data/hassio/supervisor";
@customElement("hassio-supervisor-log")
class HassioSupervisorLog extends LitElement {
@property() public hass!: HomeAssistant;
@property() private _error?: string;
@query("#content") private _logContent!: HTMLDivElement;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
public render(): TemplateResult | void {
return html`
<paper-card>
${this._error
? html`
<div class="errors">${this._error}</div>
`
: ""}
<div class="card-content" id="content"></div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
</div>
</paper-card>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
ANSI_HTML_STYLE,
css`
paper-card {
width: 100%;
}
pre {
white-space: pre-wrap;
}
.errors {
color: var(--google-red-500);
margin-bottom: 16px;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
const content = await fetchSupervisorLogs(this.hass);
while (this._logContent.lastChild) {
this._logContent.removeChild(this._logContent.lastChild as Node);
}
this._logContent.appendChild(parseTextToColoredPre(content));
} catch (err) {
this._error = `Failed to get supervisor logs, ${err.body?.message ||
err}`;
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-supervisor-log": HassioSupervisorLog;
}
}
+51
View File
@@ -0,0 +1,51 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./hassio-host-info";
import "./hassio-supervisor-info";
import "./hassio-supervisor-log";
class HassioSystem extends PolymerElement {
static get template() {
return html`
<style>
.content {
margin: 4px;
color: var(--primary-text-color);
}
.title {
margin-top: 24px;
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
</style>
<div class="content">
<div class="title">Information</div>
<hassio-supervisor-info
hass="[[hass]]"
data="[[supervisorInfo]]"
></hassio-supervisor-info>
<hassio-host-info
hass="[[hass]]"
data="[[hostInfo]]"
hass-os-info="[[hassOsInfo]]"
></hassio-host-info>
<div class="title">System log</div>
<hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log>
</div>
`;
}
static get properties() {
return {
hass: Object,
supervisorInfo: Object,
hostInfo: Object,
hassOsInfo: Object,
};
}
}
customElements.define("hassio-system", HassioSystem);
-81
View File
@@ -1,81 +0,0 @@
import "@polymer/paper-menu-button/paper-menu-button";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { hassioStyle } from "../resources/hassio-style";
import { haStyle } from "../../../src/resources/styles";
import {
HassioHostInfo,
HassioHassOSInfo,
} from "../../../src/data/hassio/host";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import { HomeAssistant } from "../../../src/types";
import "./hassio-host-info";
import "./hassio-supervisor-info";
import "./hassio-supervisor-log";
@customElement("hassio-system")
class HassioSystem extends LitElement {
@property() public hass!: HomeAssistant;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property() public hostInfo!: HassioHostInfo;
@property() public hassOsInfo!: HassioHassOSInfo;
public render(): TemplateResult | void {
return html`
<div class="content">
<h1>Information</h1>
<div class="card-group">
<hassio-supervisor-info
.hass=${this.hass}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info>
<hassio-host-info
.hass=${this.hass}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info>
</div>
<h1>System log</h1>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
</div>
`;
}
static get styles(): CSSResult[] {
return [
haStyle,
hassioStyle,
css`
.content {
margin: 8px;
color: var(--primary-text-color);
}
.title {
margin-top: 24px;
color: var(--primary-text-color);
font-size: 2em;
padding-left: 8px;
margin-bottom: 8px;
}
hassio-supervisor-log {
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-system": HassioSystem;
}
}
+24 -37
View File
@@ -9,7 +9,6 @@
"scripts": {
"build": "script/build_frontend",
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'cast/src/**/*.ts' 'test-mocha/**/*.ts' && tsc",
"lint-hassio": "eslint hassio/src && tslint 'hassio/src/**/*.ts'",
"mocha": "node_modules/.bin/ts-mocha -p test-mocha/tsconfig.test.json --opts test-mocha/mocha.opts",
"test": "npm run lint && npm run mocha",
"docker_build": "sh ./script/docker_run.sh build $npm_package_version",
@@ -18,16 +17,16 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@material/chips": "^5.0.0",
"@material/data-table": "^5.0.0",
"@material/mwc-base": "^0.13.0",
"@material/mwc-button": "^0.13.0",
"@material/mwc-checkbox": "^0.13.0",
"@material/mwc-dialog": "^0.13.0",
"@material/mwc-fab": "^0.13.0",
"@material/mwc-ripple": "^0.13.0",
"@material/mwc-switch": "^0.13.0",
"@mdi/svg": "4.9.95",
"@material/chips": "^3.2.0",
"@material/data-table": "^3.2.0",
"@material/mwc-base": "^0.10.0",
"@material/mwc-button": "^0.10.0",
"@material/mwc-checkbox": "^0.10.0",
"@material/mwc-dialog": "^0.10.0",
"@material/mwc-fab": "^0.10.0",
"@material/mwc-ripple": "^0.10.0",
"@material/mwc-switch": "^0.10.0",
"@mdi/svg": "4.7.95",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-localize-behavior": "^3.0.1",
"@polymer/app-route": "^3.0.2",
@@ -70,8 +69,8 @@
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
"@thomasloven/round-slider": "0.3.7",
"@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7",
"@vaadin/vaadin-combo-box": "^5.0.6",
"@vaadin/vaadin-date-picker": "^4.0.3",
"@webcomponents/shadycss": "^1.9.0",
"@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0",
@@ -79,17 +78,15 @@
"codemirror": "^5.49.0",
"cpx": "^1.5.0",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
"es6-object-assign": "^1.1.0",
"fecha": "^3.0.2",
"fuse.js": "^3.4.4",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.12.4",
"home-assistant-js-websocket": "4.4.1",
"home-assistant-js-websocket": "^4.4.0",
"intl-messageformat": "^2.2.0",
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"leaflet-draw": "^1.0.4",
"lit-element": "^2.2.1",
"lit-html": "^1.1.0",
"lit-virtualizer": "^0.4.2",
@@ -109,17 +106,16 @@
"xss": "^1.0.6"
},
"devDependencies": {
"@babel/core": "^7.8.4",
"@babel/plugin-external-helpers": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-proposal-optional-chaining": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-react-jsx": "^7.8.3",
"@babel/preset-env": "^7.8.4",
"@babel/preset-typescript": "^7.8.3",
"@babel/core": "^7.7.4",
"@babel/plugin-external-helpers": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-proposal-optional-chaining": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-react-jsx": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"@babel/preset-typescript": "^7.7.4",
"@types/chai": "^4.1.7",
"@types/chromecast-caf-receiver": "^3.0.12",
"@types/chromecast-caf-sender": "^1.0.1",
@@ -127,7 +123,6 @@
"@types/hls.js": "^0.12.3",
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^5.2.6",
"@types/webspeechapi": "^0.0.29",
@@ -187,15 +182,7 @@
"resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
"lit-html": "^1.1.2",
"@material/button": "^5.0.0",
"@material/checkbox": "^5.0.0",
"@material/dialog": "^5.0.0",
"@material/fab": "^5.0.0",
"@material/switch": "^5.0.0",
"@material/ripple": "^5.0.0",
"@material/dom": "^5.0.0",
"@material/touch-target": "^5.0.0"
"lit-html": "^1.1.2"
},
"main": "src/home-assistant.js",
"husky": {
+1 -1
View File
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20200220.4",
version="20200108.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
+1
View File
@@ -91,6 +91,7 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
.redirectUri="${this.redirectUri}"
.oauth2State="${this.oauth2State}"
.authProvider="${this._authProvider}"
.step="{{step}}"
></ha-auth-flow>
${inactiveProviders.length > 0
+1 -1
View File
@@ -12,7 +12,7 @@ class HaBadgesCard extends LitElement {
@property() public hass?: HomeAssistant;
@property() public states?: HassEntity[];
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.hass || !this.states) {
return html``;
}
-1
View File
@@ -44,7 +44,6 @@ export const DOMAINS_WITH_MORE_INFO = [
"light",
"lock",
"media_player",
"person",
"script",
"sun",
"timer",
@@ -1,31 +0,0 @@
// Check for support of native locale string options
function checkToLocaleDateStringSupportsOptions() {
try {
new Date().toLocaleDateString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
function checkToLocaleTimeStringSupportsOptions() {
try {
new Date().toLocaleTimeString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
function checkToLocaleStringSupportsOptions() {
try {
new Date().toLocaleString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export const toLocaleDateStringSupportsOptions = checkToLocaleDateStringSupportsOptions();
export const toLocaleTimeStringSupportsOptions = checkToLocaleTimeStringSupportsOptions();
export const toLocaleStringSupportsOptions = checkToLocaleStringSupportsOptions();
+12 -3
View File
@@ -1,11 +1,20 @@
import fecha from "fecha";
import { toLocaleDateStringSupportsOptions } from "./check_options_support";
export const formatDate = toLocaleDateStringSupportsOptions
// Check for support of native locale string options
function toLocaleDateStringSupportsOptions() {
try {
new Date().toLocaleDateString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export default toLocaleDateStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleDateString(locales, {
year: "numeric",
month: "long",
day: "numeric",
})
: (dateObj: Date) => fecha.format(dateObj, "longDate");
: (dateObj: Date) => fecha.format(dateObj, "mediumDate");
+12 -23
View File
@@ -1,7 +1,16 @@
import fecha from "fecha";
import { toLocaleStringSupportsOptions } from "./check_options_support";
export const formatDateTime = toLocaleStringSupportsOptions
// Check for support of native locale string options
function toLocaleStringSupportsOptions() {
try {
new Date().toLocaleString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export default toLocaleStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
@@ -10,24 +19,4 @@ export const formatDateTime = toLocaleStringSupportsOptions
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) =>
fecha.format(
dateObj,
`${fecha.masks.longDate}, ${fecha.masks.shortTime}`
);
export const formatDateTimeWithSeconds = toLocaleStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleString(locales, {
year: "numeric",
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})
: (dateObj: Date) =>
fecha.format(
dateObj,
`${fecha.masks.longDate}, ${fecha.masks.mediumTime}`
);
: (dateObj: Date) => fecha.format(dateObj, "haDateTime");
+11 -11
View File
@@ -1,19 +1,19 @@
import fecha from "fecha";
import { toLocaleTimeStringSupportsOptions } from "./check_options_support";
export const formatTime = toLocaleTimeStringSupportsOptions
// Check for support of native locale string options
function toLocaleTimeStringSupportsOptions() {
try {
new Date().toLocaleTimeString("i");
} catch (e) {
return e.name === "RangeError";
}
return false;
}
export default toLocaleTimeStringSupportsOptions()
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "shortTime");
export const formatTimeWithSeconds = toLocaleTimeStringSupportsOptions
? (dateObj: Date, locales: string) =>
dateObj.toLocaleTimeString(locales, {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
})
: (dateObj: Date) => fecha.format(dateObj, "mediumTime");
+1 -3
View File
@@ -1,5 +1,3 @@
import { derivedStyles } from "../../resources/styles";
const hexToRgb = (hex: string): string | null => {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const checkHex = hex.replace(shorthandRegex, (_m, r, g, b) => {
@@ -38,7 +36,7 @@ export const applyThemesOnElement = (
}
const styles = { ...element._themes };
if (themeName !== "default") {
const theme = { ...derivedStyles, ...themes.themes[themeName] };
const theme = themes.themes[themeName];
Object.keys(theme).forEach((key) => {
const prefixedKey = `--${key}`;
element._themes[prefixedKey] = "";
+1 -7
View File
@@ -2,12 +2,10 @@ import { Map } from "leaflet";
// Sets up a Leaflet map on the provided DOM element
export type LeafletModuleType = typeof import("leaflet");
export type LeafletDrawModuleType = typeof import("leaflet-draw");
export const setupLeafletMap = async (
mapElement: HTMLElement,
darkMode = false,
draw = false
darkMode = false
): Promise<[Map, LeafletModuleType]> => {
if (!mapElement.parentNode) {
throw new Error("Cannot setup Leaflet map on disconnected element");
@@ -18,10 +16,6 @@ export const setupLeafletMap = async (
)) as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) {
await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw");
}
const map = Leaflet.map(mapElement);
const style = document.createElement("link");
style.setAttribute("href", "/static/images/leaflet/leaflet.css");
-12
View File
@@ -1,12 +0,0 @@
import { HassEntity } from "home-assistant-js-websocket";
export const computeActiveState = (stateObj: HassEntity): string => {
const domain = stateObj.entity_id.split(".")[0];
let state = stateObj.state;
if (domain === "climate") {
state = stateObj.attributes.hvac_action;
}
return state;
};
+3 -3
View File
@@ -1,8 +1,8 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
import { formatDateTime } from "../datetime/format_date_time";
import { formatDate } from "../datetime/format_date";
import { formatTime } from "../datetime/format_time";
import formatDateTime from "../datetime/format_date_time";
import formatDate from "../datetime/format_date";
import formatTime from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize";
export const computeStateDisplay = (
+4 -4
View File
@@ -8,7 +8,7 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
const fixedIcons = {
alert: "hass:alert",
alexa: "hass:amazon-alexa",
automation: "hass:robot",
automation: "hass:playlist-play",
calendar: "hass:calendar",
camera: "hass:video",
climate: "hass:thermostat",
@@ -36,8 +36,8 @@ const fixedIcons = {
plant: "hass:flower",
proximity: "hass:apple-safari",
remote: "hass:remote",
scene: "hass:palette",
script: "hass:script-text",
scene: "hass:google-pages",
script: "hass:file-document",
sensor: "hass:eye",
simple_alarm: "hass:bell",
sun: "hass:white-balance-sunny",
@@ -48,7 +48,7 @@ const fixedIcons = {
water_heater: "hass:thermometer",
weather: "hass:weather-cloudy",
weblink: "hass:open-in-new",
zone: "hass:map-marker-radius",
zone: "hass:map-marker",
};
export const domainIcon = (domain: string, state?: string): string => {
@@ -1,12 +0,0 @@
export const addDistanceToCoord = (
location: [number, number],
dx: number,
dy: number
): [number, number] => {
const rEarth = 6378000;
const newLatitude = location[0] + (dy / rEarth) * (180 / Math.PI);
const newLongitude =
location[1] +
((dx / rEarth) * (180 / Math.PI)) / Math.cos((location[0] * Math.PI) / 180);
return [newLatitude, newLongitude];
};
+8 -18
View File
@@ -7,43 +7,33 @@ import {
property,
} from "lit-element";
import { fireEvent } from "../dom/fire_event";
import "@polymer/iron-icon/iron-icon";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-icon-button/paper-icon-button";
import "../../components/ha-icon";
import { classMap } from "lit-html/directives/class-map";
import "@material/mwc-button";
@customElement("search-input")
class SearchInput extends LitElement {
@property() public filter?: string;
@property({ type: Boolean, attribute: "no-label-float" })
public noLabelFloat? = false;
@property({ type: Boolean, attribute: "no-underline" })
public noUnderline = false;
public focus() {
this.shadowRoot!.querySelector("paper-input")!.focus();
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
return html`
<style>
.no-underline {
--paper-input-container-underline: {
display: none;
height: 0;
}
}
</style>
<div class="search-container">
<paper-input
class=${classMap({ "no-underline": this.noUnderline })}
autofocus
label="Search"
.value=${this.filter}
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
<ha-icon icon="hass:magnify" slot="prefix" class="prefix"></ha-icon>
<iron-icon
icon="hass:magnify"
slot="prefix"
class="prefix"
></iron-icon>
${this.filter &&
html`
<paper-icon-button
-69
View File
@@ -1,69 +0,0 @@
import { css } from "lit-element";
export const iconColorCSS = css`
ha-icon[data-domain="alert"][data-state="on"],
ha-icon[data-domain="automation"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
ha-icon[data-domain="calendar"][data-state="on"],
ha-icon[data-domain="camera"][data-state="streaming"],
ha-icon[data-domain="cover"][data-state="open"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="input_boolean"][data-state="on"],
ha-icon[data-domain="lock"][data-state="unlocked"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="running"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],
ha-icon[data-domain="vacuum"][data-state="cleaning"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
ha-icon[data-domain="climate"][data-state="cooling"] {
color: var(--cool-color, #2b9af9);
}
ha-icon[data-domain="climate"][data-state="heating"] {
color: var(--heat-color, #ff8100);
}
ha-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red));
}
ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green));
}
ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite;
}
ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite;
}
@keyframes pulse {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
ha-icon[data-domain="plant"][data-state="problem"],
ha-icon[data-domain="zwave"][data-state="dead"] {
color: var(--error-state-color, #db4437);
}
/* Color the icon if unavailable */
ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
`;
@@ -3,7 +3,7 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
import "./ha-progress-button";
import { EventsMixin } from "../../mixins/events-mixin";
import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
import { showConfirmationDialog } from "../../dialogs/confirmation/show-dialog-confirmation";
/*
* @appliesMixin EventsMixin
+97 -133
View File
@@ -80,7 +80,6 @@ export interface DataTableColumnData extends DataTableSortColumnData {
export interface DataTableRowData {
[key: string]: any;
selectable?: boolean;
}
@customElement("ha-data-table")
@@ -102,8 +101,6 @@ export class HaDataTable extends BaseElement {
@property({ type: String }) private _sortColumn?: string;
@property({ type: String }) private _sortDirection: SortingDirection = null;
@property({ type: Array }) private _filteredData: DataTableRowData[] = [];
@query("slot[name='header']") private _header!: HTMLSlotElement;
@query(".scroller") private _scroller!: HTMLDivElement;
private _sortColumns: {
[key: string]: DataTableSortColumnData;
} = {};
@@ -173,7 +170,7 @@ export class HaDataTable extends BaseElement {
protected render() {
return html`
<div class="mdc-data-table">
<slot name="header" @slotchange=${this._calcScrollHeight}>
<slot name="header">
${this._filterable
? html`
<div class="table-header">
@@ -184,116 +181,112 @@ export class HaDataTable extends BaseElement {
`
: ""}
</slot>
<div class="scroller">
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
${this.selectable
? html`
<th
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
.checked=${this._headerChecked}
>
</ha-checkbox>
</th>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<table class="mdc-data-table__table">
<thead>
<tr class="mdc-data-table__header-row">
${this.selectable
? html`
<th
class="mdc-data-table__header-cell ${classMap(classes)}"
class="mdc-data-table__header-cell mdc-data-table__header-cell--checkbox"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
>
${column.sortable
? html`
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
<span>${column.title}</span>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleHeaderRowCheckboxChange}
.indeterminate=${this._headerIndeterminate}
.checked=${this._headerChecked}
>
</ha-checkbox>
</th>
`;
})}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
.selectable=${row.selectable !== false}
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
const sorted = key === this._sortColumn;
const classes = {
"mdc-data-table__header-cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__header-cell--icon": Boolean(
column.type && column.type === "icon"
),
sortable: Boolean(column.sortable),
"not-sorted": Boolean(column.sortable && !sorted),
};
return html`
<th
class="mdc-data-table__header-cell ${classMap(classes)}"
role="columnheader"
scope="col"
@click=${this._handleHeaderClick}
data-column-id="${key}"
>
${this.selectable
${column.sortable
? html`
<td
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
>
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.disabled=${row.selectable === false}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</td>
<ha-icon
.icon=${sorted && this._sortDirection === "desc"
? "hass:arrow-down"
: "hass:arrow-up"}
></ha-icon>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<span>${column.title}</span>
</th>
`;
})}
</tr>
</thead>
<tbody class="mdc-data-table__content">
${repeat(
this._filteredData!,
(row: DataTableRowData) => row[this.id],
(row: DataTableRowData) => html`
<tr
data-row-id="${row[this.id]}"
@click=${this._handleRowClick}
class="mdc-data-table__row"
>
${this.selectable
? html`
<td
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
class="mdc-data-table__cell mdc-data-table__cell--checkbox"
>
${column.template
? column.template(row[key], row)
: row[key]}
<ha-checkbox
class="mdc-data-table__row-checkbox"
@change=${this._handleRowCheckboxChange}
.checked=${this._checkedRows.includes(
String(row[this.id])
)}
>
</ha-checkbox>
</td>
`;
})}
</tr>
`
)}
</tbody>
</table>
</div>
`
: ""}
${Object.entries(this.columns).map((columnEntry) => {
const [key, column] = columnEntry;
return html`
<td
class="mdc-data-table__cell ${classMap({
"mdc-data-table__cell--numeric": Boolean(
column.type && column.type === "numeric"
),
"mdc-data-table__cell--icon": Boolean(
column.type && column.type === "icon"
),
})}"
>
${column.template
? column.template(row[key], row)
: row[key]}
</td>
`;
})}
</tr>
`
)}
</tbody>
</table>
</div>
`;
}
@@ -301,12 +294,9 @@ export class HaDataTable extends BaseElement {
protected createAdapter(): MDCDataTableAdapter {
return {
addClassAtRowIndex: (rowIndex: number, cssClasses: string) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this.rowElements[rowIndex].classList.add(cssClasses);
},
getRowCount: () => this.rowElements.length,
getRowCount: () => this.data.length,
getRowElements: () => this.rowElements,
getRowIdAtIndex: (rowIndex: number) => this._getRowIdAtIndex(rowIndex),
getRowIndexByChildElement: (el: Element) =>
@@ -315,7 +305,7 @@ export class HaDataTable extends BaseElement {
isCheckboxAtRowIndexChecked: (rowIndex: number) =>
this._checkedRows.includes(this._getRowIdAtIndex(rowIndex)),
isHeaderRowCheckboxChecked: () => this._headerChecked,
isRowsSelectable: () => this.selectable,
isRowsSelectable: () => true,
notifyRowSelectionChanged: () => undefined,
notifySelectedAll: () => undefined,
notifyUnselectedAll: () => undefined,
@@ -338,9 +328,6 @@ export class HaDataTable extends BaseElement {
this._headerIndeterminate = indeterminate;
},
setRowCheckboxCheckedAtIndex: (rowIndex: number, checked: boolean) => {
if (!(this.rowElements[rowIndex] as any).selectable) {
return;
}
this._setRowChecked(this._getRowIdAtIndex(rowIndex), checked);
},
};
@@ -447,11 +434,6 @@ export class HaDataTable extends BaseElement {
this._debounceSearch(ev.detail.value);
}
private async _calcScrollHeight() {
await this.updateComplete;
this._scroller.style.maxHeight = `calc(100% - ${this._header.clientHeight}px)`;
}
static get styles(): CSSResult {
return css`
/* default mdc styles, colors changed, without checkbox styles */
@@ -525,7 +507,6 @@ export class HaDataTable extends BaseElement {
padding-left: 16px;
/* @noflip */
padding-right: 0;
width: 40px;
}
[dir="rtl"] .mdc-data-table__header-cell--checkbox,
.mdc-data-table__header-cell--checkbox[dir="rtl"],
@@ -568,7 +549,6 @@ export class HaDataTable extends BaseElement {
.mdc-data-table__cell--icon {
color: var(--secondary-text-color);
text-align: center;
width: 24px;
}
.mdc-data-table__header-cell {
@@ -604,14 +584,8 @@ export class HaDataTable extends BaseElement {
/* custom from here */
:host {
display: block;
}
.mdc-data-table {
display: block;
border-width: var(--data-table-border-width, 1px);
height: 100%;
}
.mdc-data-table__header-cell {
overflow: hidden;
@@ -640,16 +614,6 @@ export class HaDataTable extends BaseElement {
.table-header {
border-bottom: 1px solid rgba(var(--rgb-primary-text-color), 0.12);
}
search-input {
position: relative;
top: 2px;
}
.scroller {
overflow: auto;
}
slot[name="header"] {
display: block;
}
`;
}
}
@@ -23,7 +23,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
DeviceEntityLookup,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@@ -31,6 +30,7 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
@@ -239,7 +239,6 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("area") && this.area) {
this._areaPicker = true;
this.value = this.area;
@@ -255,9 +254,9 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
}
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this._devices || !this._areas || !this._entities) {
return html``;
return;
}
const areas = this._getDevices(
this._devices,
@@ -83,7 +83,7 @@ export abstract class HaDeviceAutomationPicker<
return `${this._automations[idx].device_id}_${idx}`;
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (this._renderEmpty) {
return html``;
}
+3 -3
View File
@@ -22,7 +22,6 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
computeDeviceName,
DeviceEntityLookup,
} from "../../data/device_registry";
import { compare } from "../../common/string/compare";
import { PolymerChangedEvent } from "../../polymer-types";
@@ -30,6 +29,7 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { DeviceEntityLookup } from "../../panels/config/devices/ha-devices-data-table";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
@@ -203,9 +203,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
];
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.devices || !this.areas || !this.entities) {
return html``;
return;
}
const devices = this._getDevices(
this.devices,
+2 -2
View File
@@ -37,9 +37,9 @@ class HaDevicesPicker extends LitElement {
public pickedDeviceLabel?: string;
@property({ attribute: "pick-device-label" }) public pickDeviceLabel?: string;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
return;
}
const currentDevices = this._currentDevices;
+1 -1
View File
@@ -6,7 +6,7 @@ import { Debouncer } from "@polymer/polymer/lib/utils/debounce";
import { timeOut } from "@polymer/polymer/lib/utils/async";
import { mixinBehaviors } from "@polymer/polymer/lib/legacy/class";
import { formatTime } from "../../common/datetime/format_time";
import formatTime from "../../common/datetime/format_time";
// eslint-disable-next-line no-unused-vars
/* global Chart moment Color */
+2 -2
View File
@@ -40,9 +40,9 @@ class HaEntitiesPickerLight extends LitElement {
public pickedEntityLabel?: string;
@property({ attribute: "pick-entity-label" }) public pickEntityLabel?: string;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.hass) {
return html``;
return;
}
const currentEntities = this._currentEntities;
+1 -1
View File
@@ -145,7 +145,7 @@ class HaEntityPicker extends LitElement {
}
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const states = this._getStates(
this._hass,
this.includeDomains,
+1 -1
View File
@@ -28,7 +28,7 @@ class HaEntityToggle extends LitElement {
@property() public stateObj?: HassEntity;
@property() private _isOn: boolean = false;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.stateObj) {
return html`
<ha-switch disabled></ha-switch>
@@ -52,7 +52,7 @@ export class HaStateLabelBadge extends LitElement {
this.clearInterval();
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const state = this.state;
if (!state) {
+19 -17
View File
@@ -16,36 +16,26 @@ import { HassEntity } from "home-assistant-js-websocket";
// tslint:disable-next-line
import { HaIcon } from "../ha-icon";
import { HomeAssistant } from "../../types";
import { computeActiveState } from "../../common/entity/compute_active_state";
import { ifDefined } from "lit-html/directives/if-defined";
import { iconColorCSS } from "../../common/style/icon_color_css";
export class StateBadge extends LitElement {
class StateBadge extends LitElement {
public hass?: HomeAssistant;
@property() public stateObj?: HassEntity;
@property() public overrideIcon?: string;
@property() public overrideImage?: string;
@property({ type: Boolean }) public stateColor?: boolean;
@query("ha-icon") private _icon!: HaIcon;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
const stateObj = this.stateObj;
if (!stateObj) {
return html``;
}
const domain = computeStateDomain(stateObj);
return html`
<ha-icon
id="icon"
data-domain=${ifDefined(
this.stateColor || (domain === "light" && this.stateColor !== false)
? domain
: undefined
)}
data-state=${computeActiveState(stateObj)}
data-domain=${computeStateDomain(stateObj)}
data-state=${stateObj.state}
.icon=${this.overrideIcon || stateIcon(stateObj)}
></ha-icon>
`;
@@ -77,14 +67,14 @@ export class StateBadge extends LitElement {
hostStyle.backgroundImage = `url(${imageUrl})`;
iconStyle.display = "none";
} else {
if (stateObj.attributes.hs_color && this.stateColor !== false) {
if (stateObj.attributes.hs_color) {
const hue = stateObj.attributes.hs_color[0];
const sat = stateObj.attributes.hs_color[1];
if (sat > 10) {
iconStyle.color = `hsl(${hue}, 100%, ${100 - sat / 2}%)`;
}
}
if (stateObj.attributes.brightness && this.stateColor !== false) {
if (stateObj.attributes.brightness) {
const brightness = stateObj.attributes.brightness;
if (typeof brightness !== "number") {
const errorMessage = `Type error: state-badge expected number, but type of ${
@@ -121,7 +111,19 @@ export class StateBadge extends LitElement {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
${iconColorCSS}
/* Color the icon if light or sun is on */
ha-icon[data-domain="light"][data-state="on"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="binary_sensor"][data-state="on"],
ha-icon[data-domain="fan"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
/* Color the icon if unavailable */
ha-icon[data-state="unavailable"] {
color: var(--state-icon-unavailable-color);
}
`;
}
}
-229
View File
@@ -1,229 +0,0 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import "@polymer/paper-listbox/paper-listbox";
import {
LitElement,
TemplateResult,
html,
css,
CSSResult,
customElement,
property,
} from "lit-element";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import {
AreaRegistryEntry,
subscribeAreaRegistry,
createAreaRegistryEntry,
} from "../data/area_registry";
import {
showPromptDialog,
showAlertDialog,
} from "../dialogs/generic/show-dialog-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: AreaRegistryEntry }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
paper-item.add-new {
font-weight: 500;
}
</style>
<paper-item>
<paper-item-body two-line>
<div class='name'>[[item.name]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name!;
if (model.item.area_id === "add_new") {
root.querySelector("paper-item")!.className = "add-new";
} else {
root.querySelector("paper-item")!.classList.remove("add-new");
}
};
@customElement("ha-area-picker")
export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public _areas?: AreaRegistryEntry[];
@property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean;
@property() private _opened?: boolean;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = this.noAdd
? areas
: [
...areas,
{
area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"),
},
];
}),
];
}
protected render(): TemplateResult {
if (!this._areas) {
return html``;
}
return html`
<vaadin-combo-box-light
item-value-path="area_id"
item-id-path="area_id"
item-label-path="name"
.items=${this._areas}
.value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area")
: this.label}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.area-picker.clear"
)}
slot="suffix"
class="clear-button"
icon="hass:close"
@click=${this._clearValue}
no-ripple
>
${this.hass.localize("ui.components.area-picker.clear")}
</paper-icon-button>
`
: ""}
${this._areas.length > 0
? html`
<paper-icon-button
aria-label=${this.hass.localize(
"ui.components.area-picker.show_areas"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
${this.hass.localize("ui.components.area-picker.toggle")}
</paper-icon-button>
`
: ""}
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _areaChanged(ev: PolymerChangedEvent<string>) {
const newValue = ev.detail.value;
if (newValue !== "add_new") {
if (newValue !== this._value) {
this._setValue(newValue);
}
return;
}
(ev.target as any).value = this._value;
showPromptDialog(this, {
title: this.hass.localize("ui.components.area-picker.add_dialog.title"),
text: this.hass.localize("ui.components.area-picker.add_dialog.text"),
confirmText: this.hass.localize(
"ui.components.area-picker.add_dialog.add"
),
inputLabel: this.hass.localize(
"ui.components.area-picker.add_dialog.name"
),
confirm: async (name) => {
if (!name) {
return;
}
try {
const area = await createAreaRegistryEntry(this.hass, {
name,
});
this._areas = [...this._areas!, area];
this._setValue(area.area_id);
} catch (err) {
showAlertDialog(this, {
text: this.hass.localize(
"ui.components.area-picker.add_dialog.failed_create_area"
),
});
}
},
});
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
static get styles(): CSSResult {
return css`
paper-input > paper-icon-button {
width: 24px;
height: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-area-picker": HaAreaPicker;
}
}
+6 -26
View File
@@ -10,16 +10,13 @@ import {
import { HassEntity } from "home-assistant-js-websocket";
import hassAttributeUtil from "../util/hass-attributes-util";
import { until } from "lit-html/directives/until";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@property() public stateObj?: HassEntity;
@property() public extraFilters?: string;
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.stateObj) {
return html``;
}
@@ -35,7 +32,7 @@ class HaAttributes extends LitElement {
<div class="data-entry">
<div class="key">${attribute.replace(/_/g, " ")}</div>
<div class="value">
${this.formatAttribute(attribute)}
${this.formatAttributeValue(attribute)}
</div>
</div>
`
@@ -66,10 +63,6 @@ class HaAttributes extends LitElement {
color: var(--secondary-text-color);
text-align: right;
}
pre {
font-family: inherit;
font-size: inherit;
}
`;
}
@@ -82,31 +75,18 @@ class HaAttributes extends LitElement {
});
}
private formatAttribute(attribute: string): string | TemplateResult {
private formatAttributeValue(attribute: string): string {
if (!this.stateObj) {
return "-";
}
const value = this.stateObj.attributes[attribute];
return this.formatAttributeValue(value);
}
private formatAttributeValue(value: any): string | TemplateResult {
if (value === null) {
return "-";
}
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import(/* webpackChunkName: "js-yaml" */ "js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html`
<pre>${until(yaml, "")}</pre>
`;
if (Array.isArray(value)) {
return value.join(", ");
}
return Array.isArray(value) ? value.join(", ") : value;
return value instanceof Object ? JSON.stringify(value, null, 2) : value;
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ class HaCameraStream extends LitElement {
this._attached = false;
}
protected render(): TemplateResult {
protected render(): TemplateResult | void {
if (!this.stateObj || !this._attached) {
return html``;
}
+1 -1
View File
@@ -9,7 +9,7 @@ const MwcCheckbox = customElements.get("mwc-checkbox") as Constructor<Checkbox>;
@customElement("ha-checkbox")
export class HaCheckbox extends MwcCheckbox {
public firstUpdated() {
protected firstUpdated() {
super.firstUpdated();
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
+6 -7
View File
@@ -47,18 +47,17 @@ export class HaChips extends LitElement {
}
private _handleClick(ev) {
fireEvent(this, "chip-clicked", {
index: ev.target.closest("button").index,
});
fireEvent(
this,
"chip-clicked",
{ index: ev.target.closest("button").index },
{ bubbles: false }
);
}
static get styles(): CSSResult {
return css`
${unsafeCSS(chipStyles)}
.mdc-chip {
background-color: rgba(var(--rgb-primary-text-color), 0.15);
color: var(--primary-text-color);
}
`;
}
}
-31
View File
@@ -1,31 +0,0 @@
import { customElement, CSSResult, css } from "lit-element";
import "@material/mwc-dialog";
import { style } from "@material/mwc-dialog/mwc-dialog-css";
// tslint:disable-next-line
import { Dialog } from "@material/mwc-dialog";
import { Constructor } from "../types";
// tslint:disable-next-line
const MwcDialog = customElements.get("mwc-dialog") as Constructor<Dialog>;
@customElement("ha-dialog")
export class HaDialog extends MwcDialog {
protected static get styles(): CSSResult[] {
return [
style,
css`
.mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end);
}
.mdc-dialog__container {
align-items: var(--vertial-align-dialog, center);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog": HaDialog;
}
}

Some files were not shown because too many files have changed in this diff Show More