mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 16:56:35 +00:00
commit
463c7eae54
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,7 +22,8 @@ bin
|
||||
dist
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
# Secrets
|
||||
.lokalise_token
|
||||
|
7
.vscode/extensions.json
vendored
Executable file
7
.vscode/extensions.json
vendored
Executable file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eg2.tslint",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
@ -38,9 +38,9 @@ class DemoCard extends PolymerElement {
|
||||
}
|
||||
</style>
|
||||
<h2>[[config.heading]]</h2>
|
||||
<div class='root'>
|
||||
<div class="root">
|
||||
<div id="card"></div>
|
||||
<template is='dom-if' if='[[showConfig]]'>
|
||||
<template is="dom-if" if="[[showConfig]]">
|
||||
<pre>[[_trim(config.config)]]</pre>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -25,18 +25,18 @@ class DemoCards extends PolymerElement {
|
||||
}
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class='filters'>
|
||||
<paper-toggle-button
|
||||
checked='{{_showConfig}}'
|
||||
>Show config</paper-toggle-button>
|
||||
<div class="filters">
|
||||
<paper-toggle-button checked="{{_showConfig}}"
|
||||
>Show config</paper-toggle-button
|
||||
>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div class='cards'>
|
||||
<template is='dom-repeat' items='[[configs]]'>
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[configs]]">
|
||||
<demo-card
|
||||
config='[[item]]'
|
||||
show-config='[[_showConfig]]'
|
||||
hass='[[hass]]'
|
||||
config="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-card>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -8,56 +8,56 @@ import "../../../src/components/ha-card";
|
||||
class DemoMoreInfo extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
width: 333px;
|
||||
}
|
||||
|
||||
state-card-content {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
more-info-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 16px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
width: 333px;
|
||||
}
|
||||
|
||||
state-card-content {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
more-info-content {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
pre {
|
||||
width: 400px;
|
||||
margin: 16px;
|
||||
overflow: auto;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<state-card-content
|
||||
state-obj="[[_stateObj]]"
|
||||
hass="[[hass]]"
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
:host {
|
||||
flex-direction: column;
|
||||
}
|
||||
pre {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<ha-card>
|
||||
<state-card-content
|
||||
state-obj="[[_stateObj]]"
|
||||
hass="[[hass]]"
|
||||
in-dialog
|
||||
></state-card-content>
|
||||
|
||||
<more-info-content
|
||||
hass='[[hass]]'
|
||||
state-obj='[[_stateObj]]'
|
||||
></more-info-content>
|
||||
</ha-card>
|
||||
<template is='dom-if' if='[[showConfig]]'>
|
||||
<pre>[[_jsonEntity(_stateObj)]]</pre>
|
||||
</template>
|
||||
`;
|
||||
<more-info-content
|
||||
hass="[[hass]]"
|
||||
state-obj="[[_stateObj]]"
|
||||
></more-info-content>
|
||||
</ha-card>
|
||||
<template is="dom-if" if="[[showConfig]]">
|
||||
<pre>[[_jsonEntity(_stateObj)]]</pre>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -25,18 +25,18 @@ class DemoMoreInfos extends PolymerElement {
|
||||
}
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class='filters'>
|
||||
<paper-toggle-button
|
||||
checked='{{_showConfig}}'
|
||||
>Show entity</paper-toggle-button>
|
||||
<div class="filters">
|
||||
<paper-toggle-button checked="{{_showConfig}}"
|
||||
>Show entity</paper-toggle-button
|
||||
>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div class='cards'>
|
||||
<template is='dom-repeat' items='[[entities]]'>
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[entities]]">
|
||||
<demo-more-info
|
||||
entity-id='[[item]]'
|
||||
show-config='[[_showConfig]]'
|
||||
hass='[[hass]]'
|
||||
entity-id="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-more-info>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -29,6 +29,12 @@ export default (elements, { initialStates = {} } = {}) => {
|
||||
resources: demoResources,
|
||||
states: initialStates,
|
||||
themes: {},
|
||||
connection: {
|
||||
subscribeEvents: async (callback, event) => {
|
||||
console.log("subscribeEvents", event);
|
||||
return () => console.log("unsubscribeEvents", event);
|
||||
},
|
||||
},
|
||||
|
||||
// Mock properties
|
||||
mockEntities: entities,
|
||||
|
@ -51,7 +51,11 @@ const CONFIGS = [
|
||||
class DemoAlarmPanelEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards id='demos' hass='[[hass]]' configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -57,8 +57,8 @@ class DemoConditional extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
hass='[[hass]]'
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
|
@ -175,10 +175,7 @@ const CONFIGS = [
|
||||
class DemoEntities extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,11 @@ const CONFIGS = [
|
||||
class DemoEntityButtonEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards id='demos' hass='[[hass]]' configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -92,10 +92,7 @@ const CONFIGS = [
|
||||
class DemoFilter extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -217,10 +217,7 @@ const CONFIGS = [
|
||||
class DemoPicEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ const CONFIGS = [
|
||||
class DemoLightEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards id='demos' configs="[[_configs]]"></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -78,8 +78,8 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
hass='[[hass]]'
|
||||
id="demos"
|
||||
hass="[[hass]]"
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
`;
|
||||
|
@ -80,10 +80,7 @@ const CONFIGS = [
|
||||
class DemoPicElements extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
52
gallery/src/demos/demo-hui-shopping-list-card.js
Normal file
52
gallery/src/demos/demo-hui-shopping-list-card.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
|
||||
import provideHass from "../data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const CONFIGS = [
|
||||
{
|
||||
heading: "List example",
|
||||
config: `
|
||||
- type: shopping-list
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "List with title example",
|
||||
config: `
|
||||
- type: shopping-list
|
||||
title: Shopping List
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoShoppingListEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
_configs: {
|
||||
type: Object,
|
||||
value: CONFIGS,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
|
||||
hass.mockAPI("shopping_list", () => [
|
||||
{ name: "list", id: 1, complete: false },
|
||||
{ name: "all", id: 2, complete: false },
|
||||
{ name: "the", id: 3, complete: false },
|
||||
{ name: "things", id: 4, complete: true },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-shopping-list-card", DemoShoppingListEntity);
|
@ -91,10 +91,7 @@ const CONFIGS = [
|
||||
class DemoStack extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -62,10 +62,7 @@ const CONFIGS = [
|
||||
class DemoThermostatEntity extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-cards
|
||||
id='demos'
|
||||
configs="[[_configs]]"
|
||||
></demo-cards>
|
||||
<demo-cards id="demos" configs="[[_configs]]"></demo-cards>
|
||||
`;
|
||||
}
|
||||
|
||||
|
@ -34,8 +34,8 @@ class DemoMoreInfoLight extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<demo-more-infos
|
||||
hass='[[hass]]'
|
||||
entities='[[_entities]]'
|
||||
hass="[[hass]]"
|
||||
entities="[[_entities]]"
|
||||
></demo-more-infos>
|
||||
`;
|
||||
}
|
||||
|
@ -111,4 +111,5 @@ gulp.task("gen-icons", ["gen-icons-hass", "gen-icons-mdi"], () => {});
|
||||
module.exports = {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
};
|
||||
|
@ -11,4 +11,4 @@ OUTPUT_DIR=build
|
||||
rm -rf $OUTPUT_DIR
|
||||
|
||||
node script/gen-icons.js
|
||||
NODE_ENV=production ../node_modules/.bin/webpack -p --config webpack.config.js
|
||||
NODE_ENV=production CI=false ../node_modules/.bin/webpack -p --config webpack.config.js
|
||||
|
@ -4,6 +4,8 @@
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
OUTPUT_DIR=build
|
||||
|
||||
rm -rf $OUTPUT_DIR
|
||||
|
@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const { findIcons, generateIconset } = require("../../gulp/tasks/gen-icons.js");
|
||||
const {
|
||||
findIcons,
|
||||
generateIconset,
|
||||
genMDIIcons,
|
||||
} = require("../../gulp/tasks/gen-icons.js");
|
||||
|
||||
const MENU_BUTTON_ICON = "menu";
|
||||
|
||||
@ -9,4 +13,5 @@ function genHassioIcons() {
|
||||
fs.writeFileSync("./hassio-icons.html", generateIconset("hassio", iconNames));
|
||||
}
|
||||
|
||||
genMDIIcons();
|
||||
genHassioIcons();
|
||||
|
@ -9,34 +9,52 @@ import NavigateMixin from "../../../src/mixins/navigate-mixin";
|
||||
class HassioAddonRepository extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style hassio-style">
|
||||
paper-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
a.repo {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[addons.length]]">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
[[repo.name]]
|
||||
<div class="description">
|
||||
Maintained by [[repo.maintainer]]
|
||||
<a class="repo" href="[[repo.url]]" target="_blank">[[repo.url]]</a>
|
||||
</div>
|
||||
</div>
|
||||
<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]]" icon="[[computeIcon(addon)]]" icon-title="[[computeIconTitle(addon)]]" icon-class="[[computeIconClass(addon)]]"></hassio-card-content>
|
||||
<style include="iron-flex ha-style hassio-style">
|
||||
paper-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.not_available {
|
||||
opacity: 0.6;
|
||||
}
|
||||
a.repo {
|
||||
display: block;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[addons.length]]">
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
[[repo.name]]
|
||||
<div class="description">
|
||||
Maintained by [[repo.maintainer]]
|
||||
<a class="repo" href="[[repo.url]]" target="_blank"
|
||||
>[[repo.url]]</a
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
</div>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[addons]]"
|
||||
as="addon"
|
||||
sort="sortAddons"
|
||||
>
|
||||
<paper-card class$="[[computeClass(addon)]]" 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>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@ -62,13 +80,19 @@ class HassioAddonRepository extends NavigateMixin(PolymerElement) {
|
||||
return addon.installed !== addon.version
|
||||
? "New version available"
|
||||
: "Add-on is installed";
|
||||
return "Add-on is not installed";
|
||||
return addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system";
|
||||
}
|
||||
|
||||
computeIconClass(addon) {
|
||||
if (addon.installed)
|
||||
return addon.installed !== addon.version ? "update" : "installed";
|
||||
return "";
|
||||
return !addon.available ? "not_available" : "";
|
||||
}
|
||||
|
||||
computeClass(addon) {
|
||||
return !addon.available ? "not_available" : "";
|
||||
}
|
||||
|
||||
addonTapped(ev) {
|
||||
|
@ -7,17 +7,24 @@ import "./hassio-repositories-editor";
|
||||
class HassioAddonStore extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
<hassio-repositories-editor hass="[[hass]]" repos="[[repos]]"></hassio-repositories-editor>
|
||||
<style include="iron-flex ha-style">
|
||||
hassio-addon-repository {
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
<hassio-repositories-editor
|
||||
hass="[[hass]]"
|
||||
repos="[[repos]]"
|
||||
></hassio-repositories-editor>
|
||||
|
||||
<template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos">
|
||||
<hassio-addon-repository hass="[[hass]]" repo="[[repo]]" addons="[[computeAddons(repo.slug)]]"></hassio-addon-repository>
|
||||
</template>
|
||||
`;
|
||||
<template is="dom-repeat" items="[[repos]]" as="repo" sort="sortRepos">
|
||||
<hassio-addon-repository
|
||||
hass="[[hass]]"
|
||||
repo="[[repo]]"
|
||||
addons="[[computeAddons(repo.slug)]]"
|
||||
></hassio-addon-repository>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -11,48 +11,73 @@ import "../resources/hassio-style";
|
||||
class HassioRepositoriesEditor extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
.add {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-input {
|
||||
width: calc(100% - 49px);
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
<style include="ha-style hassio-style">
|
||||
.add {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
iron-icon {
|
||||
color: var(--secondary-text-color);
|
||||
margin-right: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-input {
|
||||
width: calc(100% - 49px);
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div class="card-group">
|
||||
<div class="title">
|
||||
Repositories
|
||||
<div class="description">
|
||||
Configure which add-on repositories to fetch data from:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template id="list" is="dom-repeat" items="[[repoList]]" as="repo" sort="sortRepos">
|
||||
<template
|
||||
id="list"
|
||||
is="dom-repeat"
|
||||
items="[[repoList]]"
|
||||
as="repo"
|
||||
sort="sortRepos"
|
||||
>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
hass="[[hass]]"
|
||||
title="[[repo.name]]"
|
||||
description="[[repo.url]]"
|
||||
icon="hassio:github-circle"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
hass="[[hass]]"
|
||||
path="hassio/supervisor/options"
|
||||
data="[[computeRemoveRepoData(repoList, repo.url)]]"
|
||||
class="warning"
|
||||
>Remove</ha-call-api-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[repo.name]]" description="[[repo.url]]" icon="hassio:github-circle"></hassio-card-content>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input
|
||||
label="Add new repository by URL"
|
||||
value="{{repoUrl}}"
|
||||
></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[computeRemoveRepoData(repoList, repo.url)]]" class="warning">Remove</ha-call-api-button>
|
||||
<ha-call-api-button
|
||||
hass="[[hass]]"
|
||||
path="hassio/supervisor/options"
|
||||
data="[[computeAddRepoData(repoList, repoUrl)]]"
|
||||
>Add</ha-call-api-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<paper-card>
|
||||
<div class="card-content add">
|
||||
<iron-icon icon="hassio:github-circle"></iron-icon>
|
||||
<paper-input label="Add new repository by URL" value="{{repoUrl}}"></paper-input>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/supervisor/options" data="[[computeAddRepoData(repoList, repoUrl)]]">Add</ha-call-api-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
`;
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -14,49 +14,61 @@ 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>
|
||||
<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">
|
||||
<paper-button on-click="_saveSettings">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
<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">
|
||||
<paper-button on-click="_saveSettings">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -9,42 +9,53 @@ 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>
|
||||
<paper-button on-click="saveTapped" disabled="[[!configParsed]]">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
<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
|
||||
>
|
||||
<paper-button on-click="saveTapped" disabled="[[!configParsed]]"
|
||||
>Save</paper-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -5,149 +5,404 @@ import "@polymer/paper-toggle-button/paper-toggle-button";
|
||||
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/components/ha-label-badge";
|
||||
import "../../../src/components/ha-markdown";
|
||||
import "../../../src/components/buttons/ha-call-api-button";
|
||||
import "../../../src/resources/ha-style";
|
||||
import EventsMixin from "../../../src/mixins/events-mixin";
|
||||
|
||||
import "../components/hassio-card-content";
|
||||
|
||||
const PERMIS_DESC = {
|
||||
rating: {
|
||||
title: "Addon 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 addon 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 addon full access to the network capabilities of the host machine. This gives the addon 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 addon.",
|
||||
},
|
||||
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 addon as well, which enables an addon to interact with Home Assistant without the need for additional authentication tokens.",
|
||||
},
|
||||
full_access: {
|
||||
title: "Full Hardware Access",
|
||||
description:
|
||||
"This addon is given full access to the hardware of your system, by request of the addon author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the addon 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 addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
|
||||
},
|
||||
hassio_api: {
|
||||
title: "Hass.io API Access",
|
||||
description:
|
||||
"The addon was given access to the Hass.io API, by request of the addon author. By default, the addon can access general version information of your system. When the addon 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 addon author has requested the addon to have management access to the Docker instance running on your system. This mode gives the addon 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 addon 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 addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
|
||||
},
|
||||
host_pid: {
|
||||
title: "Host Processes Namespace",
|
||||
description:
|
||||
"Usually, the processes the addon runs, are isolated from all other system processes. The addon author has requested the addon to have access to the system processes running on the host system instance, and allow the addon to spawn processes on the host system as well. This mode gives the addon 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 addon 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 addon manually. Only disable the protection mode if you know, need AND trust the source of this addon.",
|
||||
},
|
||||
apparmor: {
|
||||
title: "AppArmor",
|
||||
description:
|
||||
"AppArmor ('Application Armor') is a Linux kernel security module that restricts addons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAddon authors can provide their security profiles, optimized for the addon, 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 addon.",
|
||||
},
|
||||
auth_api: {
|
||||
title: "Home Assistant Authentication",
|
||||
description:
|
||||
"An addon 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.",
|
||||
},
|
||||
};
|
||||
|
||||
class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
paper-card {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.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 div{
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-toggle-button {
|
||||
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%;
|
||||
}
|
||||
</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>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/addons/[[addonSlug]]/update">Update</ha-call-api-button>
|
||||
<template is="dom-if" if="[[addon.changelog]]">
|
||||
<paper-button on-click="openChangelog">Changelog</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<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 paper-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 div {
|
||||
width: 150px;
|
||||
display: inline-block;
|
||||
}
|
||||
paper-toggle-button {
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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 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>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
hass="[[hass]]"
|
||||
path="hassio/addons/[[addonSlug]]/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
<template is="dom-if" if="[[addon.changelog]]">
|
||||
<paper-button on-click="openChangelog">Changelog</paper-button>
|
||||
</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>
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<div class="state">
|
||||
<div>Start on boot</div>
|
||||
<paper-toggle-button on-change="startOnBootToggled" checked="[[computeStartOnBoot(addon.boot)]]"></paper-toggle-button>
|
||||
</div>
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<paper-toggle-button on-change="autoUpdateToggled" checked="[[addon.auto_update]]"></paper-toggle-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<paper-button class="warning" on-click="_unistallClicked">Uninstall</paper-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.webui, isRunning)]]">
|
||||
<a href="[[pathWebui(addon.webui)]]" tabindex="-1" target="_blank" class="right"><paper-button>Open web UI</paper-button></a>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!addon.version]]">
|
||||
<ha-call-api-button 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>
|
||||
</template>
|
||||
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<ha-markdown content="[[addon.long_description]]"></ha-markdown>
|
||||
<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>
|
||||
<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 addon 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 addon.
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="protectionToggled">Enable Protection mode</paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<div class="security">
|
||||
<h3>Addon Security Rating</h3>
|
||||
<div class="description light-color">
|
||||
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 addon requires on your system, the lower the score, thus raising the possible security risks.
|
||||
</div>
|
||||
<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="[[addon.apparmor]]"
|
||||
></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>
|
||||
</div>
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<div class="state">
|
||||
<div>Start on boot</div>
|
||||
<paper-toggle-button
|
||||
on-change="startOnBootToggled"
|
||||
checked="[[computeStartOnBoot(addon.boot)]]"
|
||||
></paper-toggle-button>
|
||||
</div>
|
||||
<div class="state">
|
||||
<div>Auto update</div>
|
||||
<paper-toggle-button
|
||||
on-change="autoUpdateToggled"
|
||||
checked="[[addon.auto_update]]"
|
||||
></paper-toggle-button>
|
||||
</div>
|
||||
<div class="state">
|
||||
<div>Protection mode</div>
|
||||
<paper-toggle-button
|
||||
on-change="protectionToggled"
|
||||
checked="[[addon.protected]]"
|
||||
></paper-toggle-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<paper-button class="warning" on-click="_unistallClicked"
|
||||
>Uninstall</paper-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.webui, isRunning)]]"
|
||||
>
|
||||
<a
|
||||
href="[[pathWebui(addon.webui)]]"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
class="right"
|
||||
><paper-button>Open web UI</paper-button></a
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!addon.version]]">
|
||||
<template is="dom-if" if="[[!addon.available]]">
|
||||
<p class="warning">This addon 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>
|
||||
`;
|
||||
<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() {
|
||||
@ -155,10 +410,7 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
hass: Object,
|
||||
addon: Object,
|
||||
addonSlug: String,
|
||||
isRunning: {
|
||||
type: Boolean,
|
||||
computed: "computeIsRunning(addon)",
|
||||
},
|
||||
isRunning: { type: Boolean, computed: "computeIsRunning(addon)" },
|
||||
};
|
||||
}
|
||||
|
||||
@ -175,6 +427,23 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@ -187,6 +456,16 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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);
|
||||
@ -197,6 +476,20 @@ class HassioAddonInfo extends EventsMixin(PolymerElement) {
|
||||
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);
|
||||
}
|
||||
|
||||
showMoreInfo(e) {
|
||||
const id = e.target.getAttribute("id");
|
||||
this.fire("hassio-markdown-dialog", {
|
||||
title: PERMIS_DESC[id].title,
|
||||
content: PERMIS_DESC[id].description,
|
||||
});
|
||||
}
|
||||
|
||||
openChangelog() {
|
||||
this.hass
|
||||
.callApi("get", `hassio/addons/${this.addonSlug}/changelog`)
|
||||
|
@ -8,24 +8,22 @@ 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;
|
||||
}
|
||||
</style>
|
||||
<paper-card heading="Log">
|
||||
<div class="card-content">
|
||||
<pre>[[log]]</pre>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
<style include="ha-style">
|
||||
:host,
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
<paper-card heading="Log">
|
||||
<div class="card-content"><pre>[[log]]</pre></div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refresh">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -10,51 +10,60 @@ 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>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[config]]">
|
||||
<tr>
|
||||
<td>
|
||||
[[item.container]]
|
||||
</td>
|
||||
<td>
|
||||
<paper-input value="{{item.host}}" no-label-float=""></paper-input>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</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>
|
||||
<paper-button on-click="saveTapped">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Container</th>
|
||||
<th>Host</th>
|
||||
</tr>
|
||||
<template is="dom-repeat" items="[[config]]">
|
||||
<tr>
|
||||
<td>[[item.container]]</td>
|
||||
<td>
|
||||
<paper-input
|
||||
value="{{item.host}}"
|
||||
no-label-float=""
|
||||
></paper-input>
|
||||
</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
|
||||
>
|
||||
<paper-button on-click="saveTapped">Save</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -18,69 +18,102 @@ import "./hassio-addon-network";
|
||||
class HassioAddonView extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-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) {
|
||||
<style include="iron-flex ha-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,
|
||||
hassio-addon-logs {
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
hassio-addon-config {
|
||||
margin-bottom: 24px;
|
||||
width: 600px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<app-route route="[[route]]" pattern="/addon/:slug" data="{{routeData}}" active="{{routeMatches}}"></app-route>
|
||||
<app-header-layout has-scrolling-region="">
|
||||
<app-header fixed="" slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button hassio narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||
<paper-icon-button icon="hassio:arrow-left" on-click="backTapped"></paper-icon-button>
|
||||
<div main-title="">Hass.io: add-on details</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<hassio-addon-info hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-info>
|
||||
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>
|
||||
<app-route
|
||||
route="[[route]]"
|
||||
pattern="/addon/:slug"
|
||||
data="{{routeData}}"
|
||||
active="{{routeMatches}}"
|
||||
></app-route>
|
||||
<app-header-layout has-scrolling-region="">
|
||||
<app-header fixed="" slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
hassio
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></ha-menu-button>
|
||||
<paper-icon-button
|
||||
icon="hassio:arrow-left"
|
||||
on-click="backTapped"
|
||||
></paper-icon-button>
|
||||
<div main-title="">Hass.io: add-on details</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="content">
|
||||
<hassio-addon-info
|
||||
hass="[[hass]]"
|
||||
addon="[[addon]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
></hassio-addon-info>
|
||||
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<hassio-addon-config hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-config>
|
||||
<template is="dom-if" if="[[addon.version]]">
|
||||
<hassio-addon-config
|
||||
hass="[[hass]]"
|
||||
addon="[[addon]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
></hassio-addon-config>
|
||||
|
||||
<template is="dom-if" if="[[addon.audio]]">
|
||||
<hassio-addon-audio hass="[[hass]]" addon="[[addon]]"></hassio-addon-audio>
|
||||
<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="[[routeData.slug]]"
|
||||
></hassio-addon-network>
|
||||
</template>
|
||||
|
||||
<hassio-addon-logs
|
||||
hass="[[hass]]"
|
||||
addon-slug="[[routeData.slug]]"
|
||||
></hassio-addon-logs>
|
||||
</template>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
<template is="dom-if" if="[[addon.network]]">
|
||||
<hassio-addon-network hass="[[hass]]" addon="[[addon]]" addon-slug="[[routeData.slug]]"></hassio-addon-network>
|
||||
</template>
|
||||
|
||||
<hassio-addon-logs hass="[[hass]]" addon-slug="[[routeData.slug]]"></hassio-addon-logs>
|
||||
</template>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog title="[[markdownTitle]]" content="[[markdownContent]]"></hassio-markdown-dialog>
|
||||
`;
|
||||
<hassio-markdown-dialog
|
||||
title="[[markdownTitle]]"
|
||||
content="[[markdownContent]]"
|
||||
></hassio-markdown-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -7,54 +7,68 @@ import "../../../src/components/ha-relative-time";
|
||||
class HassioCardContent extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
iron-icon.update {
|
||||
color: var(--paper-orange-400);
|
||||
}
|
||||
iron-icon.running,
|
||||
iron-icon.installed {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.hassupdate,
|
||||
iron-icon.snapshot {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
.title {
|
||||
color: var(--primary-text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<iron-icon icon="[[icon]]" class\$="[[iconClass]]" title="[[iconTitle]]"></iron-icon>
|
||||
<div>
|
||||
<div class="title">[[title]]</div>
|
||||
<div class="addition">
|
||||
<template is="dom-if" if="[[description]]">
|
||||
[[description]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[datetime]]">
|
||||
<ha-relative-time hass="[[hass]]" class="addition" datetime="[[datetime]]"></ha-relative-time>
|
||||
</template>
|
||||
<style>
|
||||
iron-icon {
|
||||
margin-right: 16px;
|
||||
margin-top: 16px;
|
||||
float: left;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
iron-icon.update {
|
||||
color: var(--paper-orange-400);
|
||||
}
|
||||
iron-icon.running,
|
||||
iron-icon.installed {
|
||||
color: var(--paper-green-400);
|
||||
}
|
||||
iron-icon.hassupdate,
|
||||
iron-icon.snapshot {
|
||||
color: var(--paper-item-icon-color);
|
||||
}
|
||||
iron-icon.not_available {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
.title {
|
||||
color: var(--primary-text-color);
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.addition {
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
height: 2.4em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
ha-relative-time {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<iron-icon
|
||||
icon="[[icon]]"
|
||||
class\$="[[iconClass]]"
|
||||
title="[[iconTitle]]"
|
||||
></iron-icon>
|
||||
<div>
|
||||
<div class="title">[[title]]</div>
|
||||
<div class="addition">
|
||||
<template is="dom-if" if="[[description]]">
|
||||
[[description]]
|
||||
</template>
|
||||
<template is="dom-if" if="[[!available]]">
|
||||
(Not available)
|
||||
</template>
|
||||
<template is="dom-if" if="[[datetime]]">
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
class="addition"
|
||||
datetime="[[datetime]]"
|
||||
></ha-relative-time>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@ -62,6 +76,7 @@ class HassioCardContent extends PolymerElement {
|
||||
hass: Object,
|
||||
title: String,
|
||||
description: String,
|
||||
available: Boolean,
|
||||
datetime: String,
|
||||
icon: {
|
||||
type: String,
|
||||
|
@ -9,29 +9,44 @@ 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]]" icon="[[computeIcon(addon)]]" icon-title="[[computeIconTitle(addon)]]" icon-class="[[computeIconClass(addon)]]"></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
<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() {
|
||||
|
@ -8,16 +8,22 @@ import EventsMixin from "../../../src/mixins/events-mixin";
|
||||
class HassioDashboard extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<hassio-hass-update hass="[[hass]]" hass-info="[[hassInfo]]"></hassio-hass-update>
|
||||
<hassio-addons hass="[[hass]]" addons="[[supervisorInfo.addons]]"></hassio-addons>
|
||||
</div>
|
||||
`;
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<hassio-hass-update
|
||||
hass="[[hass]]"
|
||||
hass-info="[[hassInfo]]"
|
||||
></hassio-hass-update>
|
||||
<hassio-addons
|
||||
hass="[[hass]]"
|
||||
addons="[[supervisorInfo.addons]]"
|
||||
></hassio-addons>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -10,40 +10,60 @@ import "../resources/hassio-style";
|
||||
class HassioHassUpdate extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">Update available! 🎉</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="Home Assistant [[hassInfo.last_version]] is available" description="You are currently running version [[hassInfo.version]]" icon="hassio:home-assistant" icon-class="hassupdate"></hassio-card-content>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="error">Error: [[error]]</div>
|
||||
</template>
|
||||
<p><a href='https://www.home-assistant.io/latest-release-notes/' target='_blank'>Read the release notes</a></p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/homeassistant/update">Update</ha-call-api-button>
|
||||
<a href="https://github.com/home-assistant/home-assistant/releases" target="_blank"><paper-button>Release notes</paper-button></a>
|
||||
</div>
|
||||
</paper-card>
|
||||
<style include="ha-style hassio-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[computeUpdateAvailable(hassInfo)]]">
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
<div class="title">Update available! 🎉</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
hass="[[hass]]"
|
||||
title="Home Assistant [[hassInfo.last_version]] is available"
|
||||
description="You are currently running version [[hassInfo.version]]"
|
||||
icon="hassio:home-assistant"
|
||||
icon-class="hassupdate"
|
||||
></hassio-card-content>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<div class="error">Error: [[error]]</div>
|
||||
</template>
|
||||
<p>
|
||||
<a
|
||||
href="https://www.home-assistant.io/latest-release-notes/"
|
||||
target="_blank"
|
||||
>Read the release notes</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-call-api-button
|
||||
hass="[[hass]]"
|
||||
path="hassio/homeassistant/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
<a
|
||||
href="https://github.com/home-assistant/home-assistant/releases"
|
||||
target="_blank"
|
||||
><paper-button>Release notes</paper-button></a
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -7,10 +7,15 @@ import "./resources/hassio-icons";
|
||||
class HassioApp extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<template is="dom-if" if="[[hass]]">
|
||||
<hassio-main hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-main>
|
||||
</template>
|
||||
`;
|
||||
<template is="dom-if" if="[[hass]]">
|
||||
<hassio-main
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
route="[[route]]"
|
||||
></hassio-main>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -14,22 +14,48 @@ import NavigateMixin from "../../src/mixins/navigate-mixin";
|
||||
class HassioMain extends EventsMixin(NavigateMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<app-route route="[[route]]" pattern="/:page" data="{{routeData}}"></app-route>
|
||||
<hassio-data id="data" hass="[[hass]]" supervisor="{{supervisorInfo}}" homeassistant="{{hassInfo}}" host="{{hostInfo}}"></hassio-data>
|
||||
<app-route
|
||||
route="[[route]]"
|
||||
pattern="/:page"
|
||||
data="{{routeData}}"
|
||||
></app-route>
|
||||
<hassio-data
|
||||
id="data"
|
||||
hass="[[hass]]"
|
||||
supervisor="{{supervisorInfo}}"
|
||||
homeassistant="{{hassInfo}}"
|
||||
host="{{hostInfo}}"
|
||||
></hassio-data>
|
||||
|
||||
<template is="dom-if" if="[[!loaded]]">
|
||||
<hass-loading-screen narrow="[[narrow]]" show-menu="[[showMenu]]"></hass-loading-screen>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!loaded]]">
|
||||
<hass-loading-screen
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></hass-loading-screen>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[loaded]]">
|
||||
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
|
||||
<hassio-pages-with-tabs hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" page="[[routeData.page]]" supervisor-info="[[supervisorInfo]]" hass-info="[[hassInfo]]" host-info="[[hostInfo]]"></hassio-pages-with-tabs>
|
||||
<template is="dom-if" if="[[loaded]]">
|
||||
<template is="dom-if" if="[[!equalsAddon(routeData.page)]]">
|
||||
<hassio-pages-with-tabs
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
page="[[routeData.page]]"
|
||||
supervisor-info="[[supervisorInfo]]"
|
||||
hass-info="[[hassInfo]]"
|
||||
host-info="[[hostInfo]]"
|
||||
></hassio-pages-with-tabs>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equalsAddon(routeData.page)]]">
|
||||
<hassio-addon-view
|
||||
hass="[[hass]]"
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
route="[[route]]"
|
||||
></hassio-addon-view>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equalsAddon(routeData.page)]]">
|
||||
<hassio-addon-view hass="[[hass]]" narrow="[[narrow]]" show-menu="[[showMenu]]" route="[[route]]"></hassio-addon-view>
|
||||
</template>
|
||||
</template>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -11,55 +11,58 @@ import "../../src/resources/ha-style";
|
||||
class HassioMarkdownDialog extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
app-toolbar {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
max-height: 100%;
|
||||
}
|
||||
paper-dialog::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: inherit;
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
app-toolbar {
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
</paper-dialog>
|
||||
`;
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
paper-dialog {
|
||||
max-height: 100%;
|
||||
}
|
||||
paper-dialog::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
background-color: inherit;
|
||||
}
|
||||
app-toolbar {
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<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>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -23,53 +23,85 @@ import NavigateMixin from "../../src/mixins/navigate-mixin";
|
||||
class HassioPagesWithTabs extends NavigateMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-positioning ha-style">
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: #FFF;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<app-header-layout id="layout" has-scrolling-region>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button hassio narrow="[[narrow]]" show-menu="[[showMenu]]"></ha-menu-button>
|
||||
<div main-title>Hass.io</div>
|
||||
<template is="dom-if" if="[[showRefreshButton(page)]]">
|
||||
<paper-icon-button icon="hassio:refresh" on-click="refreshClicked"></paper-icon-button>
|
||||
</template>
|
||||
</app-toolbar>
|
||||
<paper-tabs scrollable="" selected="[[page]]" attr-for-selected="page-name" on-iron-activate="handlePageSelected">
|
||||
<paper-tab page-name="dashboard">Dashboard</paper-tab>
|
||||
<paper-tab page-name="snapshots">Snapshots</paper-tab>
|
||||
<paper-tab page-name="store">Add-on store</paper-tab>
|
||||
<paper-tab page-name="system">System</paper-tab>
|
||||
</paper-tabs>
|
||||
</app-header>
|
||||
<template is="dom-if" if="[[equals(page, "dashboard")]]">
|
||||
<hassio-dashboard hass="[[hass]]" supervisor-info="[[supervisorInfo]]" hass-info="[[hassInfo]]"></hassio-dashboard>
|
||||
</template>
|
||||
<style include="iron-flex iron-positioning ha-style">
|
||||
:host {
|
||||
color: var(--primary-text-color);
|
||||
--paper-card-header-color: var(--primary-text-color);
|
||||
}
|
||||
paper-tabs {
|
||||
margin-left: 12px;
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
<app-header-layout id="layout" has-scrolling-region>
|
||||
<app-header fixed slot="header">
|
||||
<app-toolbar>
|
||||
<ha-menu-button
|
||||
hassio
|
||||
narrow="[[narrow]]"
|
||||
show-menu="[[showMenu]]"
|
||||
></ha-menu-button>
|
||||
<div main-title>Hass.io</div>
|
||||
<template is="dom-if" if="[[showRefreshButton(page)]]">
|
||||
<paper-icon-button
|
||||
icon="hassio:refresh"
|
||||
on-click="refreshClicked"
|
||||
></paper-icon-button>
|
||||
</template>
|
||||
</app-toolbar>
|
||||
<paper-tabs
|
||||
scrollable=""
|
||||
selected="[[page]]"
|
||||
attr-for-selected="page-name"
|
||||
on-iron-activate="handlePageSelected"
|
||||
>
|
||||
<paper-tab page-name="dashboard">Dashboard</paper-tab>
|
||||
<paper-tab page-name="snapshots">Snapshots</paper-tab>
|
||||
<paper-tab page-name="store">Add-on store</paper-tab>
|
||||
<paper-tab page-name="system">System</paper-tab>
|
||||
</paper-tabs>
|
||||
</app-header>
|
||||
<template is="dom-if" if="[[equals(page, "dashboard")]]">
|
||||
<hassio-dashboard
|
||||
hass="[[hass]]"
|
||||
supervisor-info="[[supervisorInfo]]"
|
||||
hass-info="[[hassInfo]]"
|
||||
></hassio-dashboard>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "snapshots")]]">
|
||||
<hassio-snapshots
|
||||
hass="[[hass]]"
|
||||
installed-addons="[[supervisorInfo.addons]]"
|
||||
snapshot-slug="{{snapshotSlug}}"
|
||||
snapshot-deleted="{{snapshotDeleted}}"
|
||||
></hassio-snapshots>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "store")]]">
|
||||
<hassio-addon-store hass="[[hass]]"></hassio-addon-store>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "system")]]">
|
||||
<hassio-system
|
||||
hass="[[hass]]"
|
||||
supervisor-info="[[supervisorInfo]]"
|
||||
host-info="[[hostInfo]]"
|
||||
></hassio-system>
|
||||
</template>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog
|
||||
title="[[markdownTitle]]"
|
||||
content="[[markdownContent]]"
|
||||
></hassio-markdown-dialog>
|
||||
|
||||
<template is="dom-if" if="[[equals(page, "snapshots")]]">
|
||||
<hassio-snapshots hass="[[hass]]" installed-addons="[[supervisorInfo.addons]]" snapshot-slug="{{snapshotSlug}}" snapshot-deleted="{{snapshotDeleted}}"></hassio-snapshots>
|
||||
<hassio-snapshot
|
||||
hass="[[hass]]"
|
||||
snapshot-slug="{{snapshotSlug}}"
|
||||
snapshot-deleted="{{snapshotDeleted}}"
|
||||
></hassio-snapshot>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "store")]]">
|
||||
<hassio-addon-store hass="[[hass]]"></hassio-addon-store>
|
||||
</template>
|
||||
<template is="dom-if" if="[[equals(page, "system")]]">
|
||||
<hassio-system hass="[[hass]]" supervisor-info="[[supervisorInfo]]" host-info="[[hostInfo]]"></hassio-system>
|
||||
</template>
|
||||
</app-header-layout>
|
||||
|
||||
<hassio-markdown-dialog title="[[markdownTitle]]" content="[[markdownContent]]"></hassio-markdown-dialog>
|
||||
|
||||
<template is="dom-if" if="[[equals(page, "snapshots")]]">
|
||||
<hassio-snapshot hass="[[hass]]" snapshot-slug="{{snapshotSlug}}" snapshot-deleted="{{snapshotDeleted}}"></hassio-snapshot>
|
||||
</template>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -7,105 +7,133 @@ import "@polymer/paper-icon-button/paper-icon-button";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { getSignedPath } from "../../../src/auth/data";
|
||||
|
||||
import "../../../src/resources/ha-style";
|
||||
|
||||
class HassioSnapshot extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
app-toolbar {
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-dialog-scrollable {
|
||||
margin: 0;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
<style include="ha-style-dialog">
|
||||
paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
min-width: 350px;
|
||||
font-size: 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
app-toolbar {
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
margin: 0;
|
||||
padding: 0 16px;
|
||||
color: var(--primary-text-color);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.download {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.warning,
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
<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="[[snapshot.addons.length]]">
|
||||
<div>Folders:</div>
|
||||
<template is="dom-repeat" items="[[snapshot.folders]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[snapshot.addons.length]]">
|
||||
<div>Add-ons:</div>
|
||||
<paper-dialog-scrollable>
|
||||
<template is="dom-repeat" items="[[snapshot.addons]]" sort="_sortAddons">
|
||||
app-toolbar [main-title] {
|
||||
margin-left: 16px;
|
||||
}
|
||||
paper-dialog-scrollable {
|
||||
margin: 0;
|
||||
}
|
||||
paper-checkbox {
|
||||
display: block;
|
||||
margin: 4px;
|
||||
}
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
paper-dialog {
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
app-toolbar {
|
||||
color: var(--text-primary-color);
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
.details {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.download {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.warning,
|
||||
.error {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
</style>
|
||||
<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="[[snapshot.addons.length]]">
|
||||
<div>Folders:</div>
|
||||
<template is="dom-repeat" items="[[snapshot.folders]]">
|
||||
<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 class="buttons">
|
||||
<paper-icon-button icon="hassio:delete" on-click="_deleteClicked" class="warning" title="Delete snapshot"></paper-icon-button>
|
||||
<a href="[[_computeDownloadUrl(snapshotSlug)]]" download="[[_computeDownloadName(snapshot)]]">
|
||||
<paper-icon-button icon="hassio:download" class="download" title="Download snapshot"></paper-icon-button>
|
||||
</a>
|
||||
<paper-button on-click="_partialRestoreClicked">Restore selected</paper-button>
|
||||
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
|
||||
<paper-button on-click="_fullRestoreClicked">Wipe & restore</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
<template is="dom-if" if="[[snapshot.addons.length]]">
|
||||
<div>Add-ons:</div>
|
||||
<paper-dialog-scrollable>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[snapshot.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 class="buttons">
|
||||
<paper-icon-button
|
||||
icon="hassio:delete"
|
||||
on-click="_deleteClicked"
|
||||
class="warning"
|
||||
title="Delete snapshot"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
on-click="_downloadClicked"
|
||||
icon="hassio:download"
|
||||
class="download"
|
||||
title="Download snapshot"
|
||||
></paper-icon-button>
|
||||
<paper-button on-click="_partialRestoreClicked"
|
||||
>Restore selected</paper-button
|
||||
>
|
||||
<template is="dom-if" if="[[_isFullSnapshot(snapshot.type)]]">
|
||||
<paper-button on-click="_fullRestoreClicked"
|
||||
>Wipe & restore</paper-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</paper-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@ -251,14 +279,24 @@ class HassioSnapshot extends PolymerElement {
|
||||
);
|
||||
}
|
||||
|
||||
_computeDownloadUrl(snapshotSlug) {
|
||||
const password = encodeURIComponent(this.hass.connection.options.authToken);
|
||||
return `/api/hassio/snapshots/${snapshotSlug}/download?api_password=${password}`;
|
||||
}
|
||||
|
||||
_computeDownloadName(snapshot) {
|
||||
const name = this._computeName(snapshot).replace(/[^a-z0-9]+/gi, "_");
|
||||
return `Hass_io_${name}.tar`;
|
||||
async _downloadClicked() {
|
||||
let signedPath;
|
||||
try {
|
||||
signedPath = await getSignedPath(
|
||||
this.hass,
|
||||
`/api/hassio/snapshots/${this.snapshotSlug}/download`
|
||||
);
|
||||
} catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
return;
|
||||
}
|
||||
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);
|
||||
a.click();
|
||||
this.$.dialog.removeChild(a);
|
||||
}
|
||||
|
||||
_computeName(snapshot) {
|
||||
|
@ -14,90 +14,120 @@ import EventsMixin from "../../../src/mixins/events-mixin";
|
||||
class HassioSnapshots extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style hassio-style">
|
||||
paper-radio-group {
|
||||
display: block;
|
||||
}
|
||||
paper-radio-button {
|
||||
padding: 0 0 2px 2px;
|
||||
}
|
||||
paper-radio-button,
|
||||
paper-checkbox,
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 48px;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<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.
|
||||
<style include="ha-style hassio-style">
|
||||
paper-radio-group {
|
||||
display: block;
|
||||
}
|
||||
paper-radio-button {
|
||||
padding: 0 0 2px 2px;
|
||||
}
|
||||
paper-radio-button,
|
||||
paper-checkbox,
|
||||
paper-input[type="password"] {
|
||||
display: block;
|
||||
margin: 4px 0 4px 48px;
|
||||
}
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<div class="content">
|
||||
<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>
|
||||
</div>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<paper-input autofocus="" label="Name" value="{{snapshotName}}"></paper-input>
|
||||
Type:
|
||||
<paper-radio-group selected="{{snapshotType}}">
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
<template is="dom-if" if="[[!_fullSelected(snapshotType)]]">
|
||||
Folders:
|
||||
<template is="dom-repeat" items="[[folderList]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
Add-ons:
|
||||
<template is="dom-repeat" items="[[addonList]]" sort="_sortAddons">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
Security:
|
||||
<paper-checkbox checked="{{snapshotHasPassword}}">Password protection</paper-checkbox>
|
||||
<template is="dom-if" if="[[snapshotHasPassword]]">
|
||||
<paper-input label="Password" type="password" value="{{snapshotPassword}}"></paper-input>
|
||||
</template>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<p class="error">[[error]]</p>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button disabled="[[creatingSnapshot]]" on-click="_createSnapshot">Create</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
</div>
|
||||
|
||||
<div class="card-group">
|
||||
<div class="title">Available snapshots</div>
|
||||
<template is="dom-if" if="[[!snapshots.length]]">
|
||||
<paper-card>
|
||||
<div class="card-content">You don't have any snapshots yet.</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<template is="dom-repeat" items="[[snapshots]]" as="snapshot" sort="_sortSnapshots">
|
||||
<paper-card class="pointer" on-click="_snapshotClicked">
|
||||
<div class="card-content">
|
||||
<hassio-card-content hass="[[hass]]" title="[[_computeName(snapshot)]]" description="[[_computeDetails(snapshot)]]" datetime="[[snapshot.date]]" icon="[[_computeIcon(snapshot.type)]]" icon-class="snapshot"></hassio-card-content>
|
||||
<paper-input
|
||||
autofocus=""
|
||||
label="Name"
|
||||
value="{{snapshotName}}"
|
||||
></paper-input>
|
||||
Type:
|
||||
<paper-radio-group selected="{{snapshotType}}">
|
||||
<paper-radio-button name="full">
|
||||
Full snapshot
|
||||
</paper-radio-button>
|
||||
<paper-radio-button name="partial">
|
||||
Partial snapshot
|
||||
</paper-radio-button>
|
||||
</paper-radio-group>
|
||||
<template is="dom-if" if="[[!_fullSelected(snapshotType)]]">
|
||||
Folders:
|
||||
<template is="dom-repeat" items="[[folderList]]">
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
Add-ons:
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[addonList]]"
|
||||
sort="_sortAddons"
|
||||
>
|
||||
<paper-checkbox checked="{{item.checked}}">
|
||||
[[item.name]]
|
||||
</paper-checkbox>
|
||||
</template>
|
||||
</template>
|
||||
Security:
|
||||
<paper-checkbox checked="{{snapshotHasPassword}}"
|
||||
>Password protection</paper-checkbox
|
||||
>
|
||||
<template is="dom-if" if="[[snapshotHasPassword]]">
|
||||
<paper-input
|
||||
label="Password"
|
||||
type="password"
|
||||
value="{{snapshotPassword}}"
|
||||
></paper-input>
|
||||
</template>
|
||||
<template is="dom-if" if="[[error]]">
|
||||
<p class="error">[[error]]</p>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button
|
||||
disabled="[[creatingSnapshot]]"
|
||||
on-click="_createSnapshot"
|
||||
>Create</paper-button
|
||||
>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="card-group">
|
||||
<div class="title">Available snapshots</div>
|
||||
<template is="dom-if" if="[[!snapshots.length]]">
|
||||
<paper-card>
|
||||
<div class="card-content">You don't have any snapshots yet.</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[snapshots]]"
|
||||
as="snapshot"
|
||||
sort="_sortSnapshots"
|
||||
>
|
||||
<paper-card class="pointer" on-click="_snapshotClicked">
|
||||
<div class="card-content">
|
||||
<hassio-card-content
|
||||
hass="[[hass]]"
|
||||
title="[[_computeName(snapshot)]]"
|
||||
description="[[_computeDetails(snapshot)]]"
|
||||
datetime="[[snapshot.date]]"
|
||||
icon="[[_computeIcon(snapshot.type)]]"
|
||||
icon-class="snapshot"
|
||||
></hassio-card-content>
|
||||
</div>
|
||||
</paper-card>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -9,90 +9,110 @@ import EventsMixin from "../../../src/mixins/events-mixin";
|
||||
class HassioHostInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-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) {
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
margin-top: 8px;
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.card-content {
|
||||
height: auto;
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
}
|
||||
.info {
|
||||
width: 100%;
|
||||
}
|
||||
.info td:nth-child(2) {
|
||||
text-align: right;
|
||||
}
|
||||
.errors {
|
||||
color: var(--google-red-500);
|
||||
margin-top: 16px;
|
||||
}
|
||||
paper-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>
|
||||
<paper-button raised on-click="_showHardware" class="info">
|
||||
Hardware
|
||||
</paper-button>
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
|
||||
<paper-button raised on-click="_changeHostnameClicked" class="info">
|
||||
Change hostname
|
||||
@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;
|
||||
}
|
||||
paper-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>
|
||||
<paper-button raised on-click="_showHardware" class="info">
|
||||
Hardware
|
||||
</paper-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(_hassOs)]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/hassos/update">Update</ha-call-api-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
<template is="dom-if" if="[[_featureAvailable(data, 'hostname')]]">
|
||||
<paper-button raised on-click="_changeHostnameClicked" class="info">
|
||||
Change hostname
|
||||
</paper-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(_hassOs)]]">
|
||||
<ha-call-api-button hass="[[hass]]" path="hassio/hassos/update"
|
||||
>Update</ha-call-api-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -9,73 +9,96 @@ import EventsMixin from "../../../src/mixins/events-mixin";
|
||||
class HassioSupervisorInfo extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
}
|
||||
.card-content {
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
@media screen and (max-width: 830px) {
|
||||
<style include="iron-flex ha-style">
|
||||
paper-card {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
width: 400px;
|
||||
}
|
||||
.card-content {
|
||||
height: auto;
|
||||
height: 200px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
}
|
||||
.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>
|
||||
@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>
|
||||
</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")]]">
|
||||
<paper-button on-click="_joinBeta" class="warning" title="Get beta updates for Home Assistant (RCs), supervisor and host">Join beta channel</paper-button>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
</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")]]"
|
||||
>
|
||||
<paper-button
|
||||
on-click="_joinBeta"
|
||||
class="warning"
|
||||
title="Get beta updates for Home Assistant (RCs), supervisor and host"
|
||||
>Join beta channel</paper-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -6,23 +6,21 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
class HassioSupervisorLog extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="ha-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<div class="card-content">
|
||||
<pre>[[log]]</pre>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refreshTapped">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
<style include="ha-style">
|
||||
paper-card {
|
||||
display: block;
|
||||
}
|
||||
pre {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
<paper-card>
|
||||
<div class="card-content"><pre>[[log]]</pre></div>
|
||||
<div class="card-actions">
|
||||
<paper-button on-click="refreshTapped">Refresh</paper-button>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -9,26 +9,32 @@ import "./hassio-supervisor-log";
|
||||
class HassioSystem extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 4px;
|
||||
}
|
||||
.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]]"></hassio-host-info>
|
||||
<div class="title">System log</div>
|
||||
<hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log>
|
||||
</div>
|
||||
`;
|
||||
<style include="iron-flex ha-style">
|
||||
.content {
|
||||
margin: 4px;
|
||||
}
|
||||
.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]]"
|
||||
></hassio-host-info>
|
||||
<div class="title">System log</div>
|
||||
<hassio-supervisor-log hass="[[hass]]"></hassio-supervisor-log>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -38,7 +38,7 @@ module.exports = {
|
||||
},
|
||||
}),
|
||||
isProdBuild &&
|
||||
isCI &&
|
||||
!isCI &&
|
||||
new CompressionPlugin({
|
||||
cache: true,
|
||||
exclude: [/\.js\.map$/, /\.LICENSE$/, /\.py$/, /\.txt$/],
|
||||
|
21
package.json
21
package.json
@ -8,8 +8,8 @@
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"build": "script/build_frontend",
|
||||
"lint": "eslint src hassio/src gallery/src test-mocha && tslint -c tslint.json 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'test-mocha/**/*.ts' && polymer lint && tsc",
|
||||
"mocha": "node_modules/.bin/mocha --opts test-mocha/mocha.opts",
|
||||
"lint": "eslint src hassio/src gallery/src && tslint 'src/**/*.ts' 'hassio/src/**/*.ts' 'gallery/src/**/*.ts' 'test-mocha/**/*.ts' && polymer lint && tsc",
|
||||
"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",
|
||||
"bash": "sh ./script/docker_run.sh bash $npm_package_version"
|
||||
@ -35,7 +35,7 @@
|
||||
"@polymer/iron-media-query": "^3.0.1",
|
||||
"@polymer/iron-pages": "^3.0.1",
|
||||
"@polymer/iron-resizable-behavior": "^3.0.1",
|
||||
"@polymer/lit-element": "^0.6.2",
|
||||
"@polymer/lit-element": "0.6.2",
|
||||
"@polymer/neon-animation": "^3.0.1",
|
||||
"@polymer/paper-button": "^3.0.1",
|
||||
"@polymer/paper-card": "^3.0.1",
|
||||
@ -73,12 +73,12 @@
|
||||
"es6-object-assign": "^1.1.0",
|
||||
"eslint-import-resolver-webpack": "^0.10.1",
|
||||
"fecha": "^2.3.3",
|
||||
"home-assistant-js-websocket": "^3.1.5",
|
||||
"home-assistant-js-websocket": "^3.2.4",
|
||||
"intl-messageformat": "^2.2.0",
|
||||
"jquery": "^3.3.1",
|
||||
"js-yaml": "^3.12.0",
|
||||
"leaflet": "^1.3.4",
|
||||
"lit-html": "^0.12.0",
|
||||
"lit-html": "0.12.0",
|
||||
"marked": "^0.5.0",
|
||||
"mdn-polyfills": "^5.12.0",
|
||||
"moment": "^2.22.2",
|
||||
@ -100,10 +100,12 @@
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@babel/preset-typescript": "^7.1.0",
|
||||
"@gfx/zopfli": "^1.0.9",
|
||||
"@types/chai": "^4.1.7",
|
||||
"@types/mocha": "^5.2.5",
|
||||
"babel-eslint": "^10",
|
||||
"babel-loader": "^8.0.4",
|
||||
"babel-minify-webpack-plugin": "^0.3.1",
|
||||
"chai": "^4.1.2",
|
||||
"chai": "^4.2.0",
|
||||
"compression-webpack-plugin": "^2.0.0",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"del": "^3.0.0",
|
||||
@ -135,7 +137,8 @@
|
||||
"raw-loader": "^0.5.1",
|
||||
"reify": "^0.18.1",
|
||||
"require-dir": "^1.0.0",
|
||||
"sinon": "^7.1.0",
|
||||
"sinon": "^7.1.1",
|
||||
"ts-mocha": "^2.0.0",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-prettier": "^1.15.0",
|
||||
"tslint-eslint-rules": "^5.4.0",
|
||||
@ -158,7 +161,9 @@
|
||||
"@webcomponents/shadycss": "^1.5.2",
|
||||
"@vaadin/vaadin-overlay": "3.2.0-alpha3",
|
||||
"@vaadin/vaadin-lumo-styles": "1.2.0",
|
||||
"fecha": "https://github.com/balloob/fecha/archive/51d14fd0eb4781e2ecf265d1c3080706259133b5.tar.gz"
|
||||
"fecha": "https://github.com/taylorhakes/fecha/archive/5e8fe08d982647fdb19fb403459838b02647813c.tar.gz",
|
||||
"lit-html": "0.12.0",
|
||||
"@polymer/lit-element": "0.6.2"
|
||||
},
|
||||
"main": "src/home-assistant.js",
|
||||
"husky": {
|
||||
|
@ -8,9 +8,11 @@ function patch(version) {
|
||||
return `${parts[0]}.${Number(parts[1]) + 1}`;
|
||||
}
|
||||
|
||||
function today(version) {
|
||||
function today() {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}${now.getMonth() + 1}${now.getDate()}.0`;
|
||||
return `${now.getFullYear()}${now.getMonth() + 1}${String(
|
||||
now.getDate()
|
||||
).padStart(2, "0")}.0`;
|
||||
}
|
||||
|
||||
const methods = {
|
||||
|
2
setup.py
2
setup.py
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20181103.3",
|
||||
version="20181121.0",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
7
src/auth/data.ts
Normal file
7
src/auth/data.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { SignedPath } from "./types";
|
||||
|
||||
export const getSignedPath = (
|
||||
hass: HomeAssistant,
|
||||
path: string
|
||||
): Promise<SignedPath> => hass.callWS({ type: "auth/sign_path", path });
|
@ -7,54 +7,61 @@ import LocalizeLiteMixin from "../mixins/localize-lite-mixin";
|
||||
class HaAuthFlow extends LocalizeLiteMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
/* So we can set min-height to avoid jumping during loading */
|
||||
display: block;
|
||||
}
|
||||
.action {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<form>
|
||||
<template is="dom-if" if="[[_equals(_state, "loading")]]">
|
||||
[[localize('ui.panel.page-authorize.form.working')]]:
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "error")]]">
|
||||
<div class='error'>Error: [[_errorMsg]]</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "step")]]">
|
||||
<template is="dom-if" if="[[_equals(_step.type, "abort")]]">
|
||||
[[localize('ui.panel.page-authorize.abort_intro')]]:
|
||||
<ha-markdown content="[[_computeStepAbortedReason(localize, _step)]]"></ha-markdown>
|
||||
<style>
|
||||
:host {
|
||||
/* So we can set min-height to avoid jumping during loading */
|
||||
display: block;
|
||||
}
|
||||
.action {
|
||||
margin: 24px 0 8px;
|
||||
text-align: center;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
<form>
|
||||
<template is="dom-if" if="[[_equals(_state, "loading")]]">
|
||||
[[localize('ui.panel.page-authorize.form.working')]]:
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_equals(_step.type, "form")]]">
|
||||
<template is="dom-if" if="[[_computeStepDescription(localize, _step)]]">
|
||||
<ha-markdown content="[[_computeStepDescription(localize, _step)]]" allow-svg></ha-markdown>
|
||||
<template is="dom-if" if="[[_equals(_state, "error")]]">
|
||||
<div class="error">Error: [[_errorMsg]]</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_equals(_state, "step")]]">
|
||||
<template is="dom-if" if="[[_equals(_step.type, "abort")]]">
|
||||
[[localize('ui.panel.page-authorize.abort_intro')]]:
|
||||
<ha-markdown
|
||||
content="[[_computeStepAbortedReason(localize, _step)]]"
|
||||
></ha-markdown>
|
||||
</template>
|
||||
|
||||
<ha-form
|
||||
data="{{_stepData}}"
|
||||
schema="[[_step.data_schema]]"
|
||||
error="[[_step.errors]]"
|
||||
compute-label="[[_computeLabelCallback(localize, _step)]]"
|
||||
compute-error="[[_computeErrorCallback(localize, _step)]]"
|
||||
></ha-form>
|
||||
<template is="dom-if" if="[[_equals(_step.type, "form")]]">
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_computeStepDescription(localize, _step)]]"
|
||||
>
|
||||
<ha-markdown
|
||||
content="[[_computeStepDescription(localize, _step)]]"
|
||||
allow-svg
|
||||
></ha-markdown>
|
||||
</template>
|
||||
|
||||
<ha-form
|
||||
data="{{_stepData}}"
|
||||
schema="[[_step.data_schema]]"
|
||||
error="[[_step.errors]]"
|
||||
compute-label="[[_computeLabelCallback(localize, _step)]]"
|
||||
compute-error="[[_computeErrorCallback(localize, _step)]]"
|
||||
></ha-form>
|
||||
</template>
|
||||
<div class="action">
|
||||
<paper-button raised on-click="_handleSubmit"
|
||||
>[[_computeSubmitCaption(_step.type)]]</paper-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class='action'>
|
||||
<paper-button
|
||||
raised
|
||||
on-click='_handleSubmit'
|
||||
>[[_computeSubmitCaption(_step.type)]]</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
</form>
|
||||
`;
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -12,49 +12,51 @@ import "./ha-auth-flow";
|
||||
class HaAuthorize extends LocalizeLiteMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-markdown {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown p:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-pick-auth-provider {
|
||||
display: block;
|
||||
margin-top: 48px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
ha-markdown {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-pick-auth-provider {
|
||||
display: block;
|
||||
margin-top: 48px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[!_authProviders]]">
|
||||
<p>[[localize('ui.panel.page-authorize.initializing')]]</p>
|
||||
</template>
|
||||
<template is="dom-if" if="[[!_authProviders]]">
|
||||
<p>[[localize('ui.panel.page-authorize.initializing')]]</p>
|
||||
</template>
|
||||
|
||||
<template is="dom-if" if="[[_authProviders]]">
|
||||
<ha-markdown content='[[_computeIntro(localize, clientId, _authProvider)]]'></ha-markdown>
|
||||
<template is="dom-if" if="[[_authProviders]]">
|
||||
<ha-markdown
|
||||
content="[[_computeIntro(localize, clientId, _authProvider)]]"
|
||||
></ha-markdown>
|
||||
|
||||
<ha-auth-flow
|
||||
resources="[[resources]]"
|
||||
client-id="[[clientId]]"
|
||||
redirect-uri="[[redirectUri]]"
|
||||
oauth2-state="[[oauth2State]]"
|
||||
auth-provider="[[_authProvider]]"
|
||||
step="{{step}}"
|
||||
></ha-auth-flow>
|
||||
|
||||
<template is="dom-if" if="[[_computeMultiple(_authProviders)]]">
|
||||
<ha-pick-auth-provider
|
||||
<ha-auth-flow
|
||||
resources="[[resources]]"
|
||||
client-id="[[clientId]]"
|
||||
auth-providers="[[_computeInactiveProvders(_authProvider, _authProviders)]]"
|
||||
on-pick="_handleAuthProviderPick"
|
||||
></ha-pick-auth-provider>
|
||||
redirect-uri="[[redirectUri]]"
|
||||
oauth2-state="[[oauth2State]]"
|
||||
auth-provider="[[_authProvider]]"
|
||||
step="{{step}}"
|
||||
></ha-auth-flow>
|
||||
|
||||
<template is="dom-if" if="[[_computeMultiple(_authProviders)]]">
|
||||
<ha-pick-auth-provider
|
||||
resources="[[resources]]"
|
||||
client-id="[[clientId]]"
|
||||
auth-providers="[[_computeInactiveProvders(_authProvider, _authProviders)]]"
|
||||
on-pick="_handleAuthProviderPick"
|
||||
></ha-pick-auth-provider>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
`;
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -14,22 +14,22 @@ class HaPickAuthProvider extends EventsMixin(
|
||||
) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<p>[[localize('ui.panel.page-authorize.pick_auth_provider')]]:</p>
|
||||
<template is="dom-repeat" items="[[authProviders]]">
|
||||
<paper-item on-click="_handlePick">
|
||||
<paper-item-body>[[item.name]]</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
</paper-item>
|
||||
</template>
|
||||
`;
|
||||
<style>
|
||||
paper-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
<p>[[localize('ui.panel.page-authorize.pick_auth_provider')]]:</p>
|
||||
<template is="dom-repeat" items="[[authProviders]]">
|
||||
<paper-item on-click="_handlePick">
|
||||
<paper-item-body>[[item.name]]</paper-item-body>
|
||||
<iron-icon icon="hass:chevron-right"></iron-icon>
|
||||
</paper-item>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
3
src/auth/types.ts
Normal file
3
src/auth/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface SignedPath {
|
||||
path: string;
|
||||
}
|
@ -6,16 +6,19 @@ import "../components/entity/ha-state-label-badge";
|
||||
class HaBadgesCard extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
ha-state-label-badge {
|
||||
display: inline-block;
|
||||
margin-bottom: var(--ha-state-label-badge-margin-bottom, 16px);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-repeat" items="[[states]]">
|
||||
<ha-state-label-badge hass="[[hass]]" state="[[item]]"></ha-state-label-badge>
|
||||
</template>
|
||||
`;
|
||||
<style>
|
||||
ha-state-label-badge {
|
||||
display: inline-block;
|
||||
margin-bottom: var(--ha-state-label-badge-margin-bottom, 16px);
|
||||
}
|
||||
</style>
|
||||
<template is="dom-repeat" items="[[states]]">
|
||||
<ha-state-label-badge
|
||||
hass="[[hass]]"
|
||||
state="[[item]]"
|
||||
></ha-state-label-badge>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -14,51 +14,55 @@ const UPDATE_INTERVAL = 10000; // ms
|
||||
class HaCameraCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
min-height: 48px;
|
||||
line-height: 0;
|
||||
}
|
||||
.camera-feed {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.caption {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
<style include="paper-material-styles">
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
min-height: 48px;
|
||||
line-height: 0;
|
||||
}
|
||||
.camera-feed {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.caption {
|
||||
@apply --paper-font-common-nowrap;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 16px;
|
||||
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
|
||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
||||
<img src="[[cameraFeedSrc]]" class="camera-feed" alt="[[_computeStateName(stateObj)]]">
|
||||
</template>
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<template is="dom-if" if="[[!imageLoaded]]">
|
||||
([[localize('ui.card.camera.not_available')]])
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
<template is="dom-if" if="[[cameraFeedSrc]]">
|
||||
<img
|
||||
src="[[cameraFeedSrc]]"
|
||||
class="camera-feed"
|
||||
alt="[[_computeStateName(stateObj)]]"
|
||||
/>
|
||||
</template>
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<template is="dom-if" if="[[!imageLoaded]]">
|
||||
([[localize('ui.card.camera.not_available')]])
|
||||
</template>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -16,54 +16,68 @@ import LocalizeMixin from "../mixins/localize-mixin";
|
||||
class HaEntitiesCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
.states {
|
||||
margin: -4px 0;
|
||||
}
|
||||
.state {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
/* overwriting line-height +8 because entity-toggle can be 40px height,
|
||||
<style include="iron-flex"></style>
|
||||
<style>
|
||||
ha-card {
|
||||
padding: 16px;
|
||||
}
|
||||
.states {
|
||||
margin: -4px 0;
|
||||
}
|
||||
.state {
|
||||
padding: 4px 0;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
/* overwriting line-height +8 because entity-toggle can be 40px height,
|
||||
compensating this with reduced padding */
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 4px 0 12px;
|
||||
}
|
||||
.header .name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
ha-entity-toggle {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.more-info {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 4px 0 12px;
|
||||
}
|
||||
.header .name {
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
ha-entity-toggle {
|
||||
margin-left: 16px;
|
||||
}
|
||||
.more-info {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card>
|
||||
<template is="dom-if" if="[[title]]">
|
||||
<div class$="[[computeTitleClass(groupEntity)]]" on-click="entityTapped">
|
||||
<div class="flex name">[[title]]</div>
|
||||
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
|
||||
<ha-entity-toggle hass="[[hass]]" state-obj="[[groupEntity]]"></ha-entity-toggle>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<div class="states">
|
||||
<template is="dom-repeat" items="[[states]]" on-dom-change="addTapEvents">
|
||||
<div class$="[[computeStateClass(item)]]">
|
||||
<state-card-content hass="[[hass]]" class="state-card" state-obj="[[item]]"></state-card-content>
|
||||
<ha-card>
|
||||
<template is="dom-if" if="[[title]]">
|
||||
<div
|
||||
class$="[[computeTitleClass(groupEntity)]]"
|
||||
on-click="entityTapped"
|
||||
>
|
||||
<div class="flex name">[[title]]</div>
|
||||
<template is="dom-if" if="[[showGroupToggle(groupEntity, states)]]">
|
||||
<ha-entity-toggle
|
||||
hass="[[hass]]"
|
||||
state-obj="[[groupEntity]]"
|
||||
></ha-entity-toggle>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
<div class="states">
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[states]]"
|
||||
on-dom-change="addTapEvents"
|
||||
>
|
||||
<div class$="[[computeStateClass(item)]]">
|
||||
<state-card-content
|
||||
hass="[[hass]]"
|
||||
class="state-card"
|
||||
state-obj="[[item]]"
|
||||
></state-card-content>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -14,39 +14,56 @@ import EventsMixin from "../mixins/events-mixin";
|
||||
class HaHistoryGraphCard extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
paper-card:not([dialog]) .content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
paper-card[dialog] {
|
||||
padding-top: 16px;
|
||||
background-color: transparent;
|
||||
}
|
||||
paper-card {
|
||||
width: 100%;
|
||||
/* prevent new stacking context, chart tooltip needs to overflow */
|
||||
position: static;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 20px 16px 12px;
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
paper-card[dialog] .header {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<ha-state-history-data hass="[[hass]]" filter-type="recent-entity" entity-id="[[computeHistoryEntities(stateObj)]]" data="{{stateHistory}}" is-loading="{{stateHistoryLoading}}" cache-config="[[cacheConfig]]"></ha-state-history-data>
|
||||
<paper-card dialog$="[[inDialog]]" on-click="cardTapped" elevation="[[computeElevation(inDialog)]]">
|
||||
<div class="header">[[computeTitle(stateObj)]]</div>
|
||||
<div class="content">
|
||||
<state-history-charts hass="[[hass]]" history-data="[[stateHistory]]" is-loading-data="[[stateHistoryLoading]]" up-to-now no-single>
|
||||
</state-history-charts>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
<style>
|
||||
paper-card:not([dialog]) .content {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
paper-card[dialog] {
|
||||
padding-top: 16px;
|
||||
background-color: transparent;
|
||||
}
|
||||
paper-card {
|
||||
width: 100%;
|
||||
/* prevent new stacking context, chart tooltip needs to overflow */
|
||||
position: static;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
line-height: 40px;
|
||||
color: var(--primary-text-color);
|
||||
padding: 20px 16px 12px;
|
||||
@apply --paper-font-common-nowrap;
|
||||
}
|
||||
paper-card[dialog] .header {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<ha-state-history-data
|
||||
hass="[[hass]]"
|
||||
filter-type="recent-entity"
|
||||
entity-id="[[computeHistoryEntities(stateObj)]]"
|
||||
data="{{stateHistory}}"
|
||||
is-loading="{{stateHistoryLoading}}"
|
||||
cache-config="[[cacheConfig]]"
|
||||
></ha-state-history-data>
|
||||
<paper-card
|
||||
dialog$="[[inDialog]]"
|
||||
on-click="cardTapped"
|
||||
elevation="[[computeElevation(inDialog)]]"
|
||||
>
|
||||
<div class="header">[[computeTitle(stateObj)]]</div>
|
||||
<div class="content">
|
||||
<state-history-charts
|
||||
hass="[[hass]]"
|
||||
history-data="[[stateHistory]]"
|
||||
is-loading-data="[[stateHistoryLoading]]"
|
||||
up-to-now
|
||||
no-single
|
||||
>
|
||||
</state-history-charts>
|
||||
</div>
|
||||
</paper-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -18,169 +18,200 @@ import LocalizeMixin from "../mixins/localize-mixin";
|
||||
class HaMediaPlayerCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="paper-material-styles iron-flex iron-flex-alignment iron-positioning">
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
<style
|
||||
include="paper-material-styles iron-flex iron-flex-alignment iron-positioning"
|
||||
>
|
||||
:host {
|
||||
@apply --paper-material-elevation-1;
|
||||
display: block;
|
||||
position: relative;
|
||||
font-size: 0px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
.banner {
|
||||
position: relative;
|
||||
background-color: white;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
|
||||
.banner:before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
/* removed .25% from 16:9 ratio to fix YT black bars */
|
||||
padding-top: 56%;
|
||||
transition: padding-top .8s;
|
||||
}
|
||||
.banner:before {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 100%;
|
||||
/* removed .25% from 16:9 ratio to fix YT black bars */
|
||||
padding-top: 56%;
|
||||
transition: padding-top 0.8s;
|
||||
}
|
||||
|
||||
.banner.no-cover {
|
||||
background-position: center center;
|
||||
background-image: url(/static/images/card_media_player_bg.png);
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
.banner.no-cover {
|
||||
background-position: center center;
|
||||
background-image: url(/static/images/card_media_player_bg.png);
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.banner.content-type-music:before {
|
||||
padding-top: 100%;
|
||||
}
|
||||
.banner.content-type-music:before {
|
||||
padding-top: 100%;
|
||||
}
|
||||
|
||||
.banner.no-cover:before {
|
||||
padding-top: 88px;
|
||||
}
|
||||
.banner.no-cover:before {
|
||||
padding-top: 88px;
|
||||
}
|
||||
|
||||
.banner > .cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
.banner > .cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
transition: opacity .8s;
|
||||
opacity: 1;
|
||||
}
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
transition: opacity 0.8s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.banner.is-off > .cover {
|
||||
opacity: 0;
|
||||
}
|
||||
.banner.is-off > .cover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.banner > .caption {
|
||||
@apply --paper-font-caption;
|
||||
.banner > .caption {
|
||||
@apply --paper-font-caption;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||
background-color: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||
|
||||
padding: 8px 16px;
|
||||
padding: 8px 16px;
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
|
||||
transition: background-color .5s;
|
||||
}
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
.banner.is-off > .caption {
|
||||
background-color: initial;
|
||||
}
|
||||
.banner.is-off > .caption {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.banner > .caption .title {
|
||||
@apply --paper-font-common-nowrap;
|
||||
font-size: 1.2em;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
.banner > .caption .title {
|
||||
@apply --paper-font-common-nowrap;
|
||||
font-size: 1.2em;
|
||||
margin: 8px 0 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: var(--paper-progress-height, 4px);
|
||||
margin-top: calc(-1*var(--paper-progress-height, 4px));
|
||||
--paper-progress-active-color: var(--accent-color);
|
||||
--paper-progress-container-color: rgba(200,200,200,0.5);
|
||||
}
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: var(--paper-progress-height, 4px);
|
||||
margin-top: calc(-1 * var(--paper-progress-height, 4px));
|
||||
--paper-progress-active-color: var(--accent-color);
|
||||
--paper-progress-container-color: rgba(200, 200, 200, 0.5);
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
@apply --paper-font-body1;
|
||||
padding: 8px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
.controls {
|
||||
position: relative;
|
||||
@apply --paper-font-body1;
|
||||
padding: 8px;
|
||||
border-bottom-left-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
background-color: var(--paper-card-background-color, white);
|
||||
}
|
||||
|
||||
.controls paper-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.controls paper-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
paper-icon-button {
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
paper-icon-button {
|
||||
opacity: var(--dark-primary-opacity);
|
||||
}
|
||||
|
||||
paper-icon-button[disabled] {
|
||||
opacity: var(--dark-disabled-opacity);
|
||||
}
|
||||
paper-icon-button[disabled] {
|
||||
opacity: var(--dark-disabled-opacity);
|
||||
}
|
||||
|
||||
paper-icon-button.primary {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
transition: background-color .5s;
|
||||
}
|
||||
paper-icon-button.primary {
|
||||
width: 56px !important;
|
||||
height: 56px !important;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
transition: background-color 0.5s;
|
||||
}
|
||||
|
||||
paper-icon-button.primary[disabled] {
|
||||
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
|
||||
}
|
||||
paper-icon-button.primary[disabled] {
|
||||
background-color: rgba(0, 0, 0, var(--dark-disabled-opacity));
|
||||
}
|
||||
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
[invisible] {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class$="[[computeBannerClasses(playerObj)]]">
|
||||
<div class="cover" id="cover"></div>
|
||||
<div class$="[[computeBannerClasses(playerObj)]]">
|
||||
<div class="cover" id="cover"></div>
|
||||
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<div class="title">[[computePrimaryText(localize, playerObj)]]</div>
|
||||
[[playerObj.secondaryTitle]]<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-progress max="[[stateObj.attributes.media_duration]]" value="[[playbackPosition]]" hidden$="[[computeHideProgress(playerObj)]]" class="progress"></paper-progress>
|
||||
|
||||
<div class="controls layout horizontal justified">
|
||||
<paper-icon-button icon="hass:power" on-click="handleTogglePower" invisible$="[[computeHidePowerButton(playerObj)]]" class="self-center secondary"></paper-icon-button>
|
||||
|
||||
<div>
|
||||
<paper-icon-button icon="hass:skip-previous" invisible$="[[!playerObj.supportsPreviousTrack]]" disabled="[[playerObj.isOff]]" on-click="handlePrevious"></paper-icon-button>
|
||||
<paper-icon-button class="primary" icon="[[computePlaybackControlIcon(playerObj)]]" invisible$="[[!computePlaybackControlIcon(playerObj)]]" disabled="[[playerObj.isOff]]" on-click="handlePlaybackControl"></paper-icon-button>
|
||||
<paper-icon-button icon="hass:skip-next" invisible$="[[!playerObj.supportsNextTrack]]" disabled="[[playerObj.isOff]]" on-click="handleNext"></paper-icon-button>
|
||||
<div class="caption">
|
||||
[[_computeStateName(stateObj)]]
|
||||
<div class="title">[[computePrimaryText(localize, playerObj)]]</div>
|
||||
[[playerObj.secondaryTitle]]<br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<paper-icon-button icon="hass:dots-vertical" on-click="handleOpenMoreInfo" class="self-center secondary"></paper-icon-button>
|
||||
<paper-progress
|
||||
max="[[stateObj.attributes.media_duration]]"
|
||||
value="[[playbackPosition]]"
|
||||
hidden$="[[computeHideProgress(playerObj)]]"
|
||||
class="progress"
|
||||
></paper-progress>
|
||||
|
||||
</div>
|
||||
`;
|
||||
<div class="controls layout horizontal justified">
|
||||
<paper-icon-button
|
||||
icon="hass:power"
|
||||
on-click="handleTogglePower"
|
||||
invisible$="[[computeHidePowerButton(playerObj)]]"
|
||||
class="self-center secondary"
|
||||
></paper-icon-button>
|
||||
|
||||
<div>
|
||||
<paper-icon-button
|
||||
icon="hass:skip-previous"
|
||||
invisible$="[[!playerObj.supportsPreviousTrack]]"
|
||||
disabled="[[playerObj.isOff]]"
|
||||
on-click="handlePrevious"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
class="primary"
|
||||
icon="[[computePlaybackControlIcon(playerObj)]]"
|
||||
invisible$="[[!computePlaybackControlIcon(playerObj)]]"
|
||||
disabled="[[playerObj.isOff]]"
|
||||
on-click="handlePlaybackControl"
|
||||
></paper-icon-button>
|
||||
<paper-icon-button
|
||||
icon="hass:skip-next"
|
||||
invisible$="[[!playerObj.supportsNextTrack]]"
|
||||
disabled="[[playerObj.isOff]]"
|
||||
on-click="handleNext"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
|
||||
<paper-icon-button
|
||||
icon="hass:dots-vertical"
|
||||
on-click="handleOpenMoreInfo"
|
||||
class="self-center secondary"
|
||||
></paper-icon-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -15,40 +15,42 @@ import computeObjectId from "../common/entity/compute_object_id";
|
||||
class HaPersistentNotificationCard extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
}
|
||||
ha-markdown {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
ha-markdown p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-markdown p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
paper-button {
|
||||
margin: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:host {
|
||||
@apply --paper-font-body1;
|
||||
}
|
||||
ha-markdown {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
ha-markdown p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-markdown p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-markdown a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-markdown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
paper-button {
|
||||
margin: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card header="[[computeTitle(stateObj)]]">
|
||||
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
|
||||
<paper-button on-click="dismissTap">[[localize('ui.card.persistent_notification.dismiss')]]</paper-button>
|
||||
</ha-card>
|
||||
`;
|
||||
<ha-card header="[[computeTitle(stateObj)]]">
|
||||
<ha-markdown content="[[stateObj.attributes.message]]"></ha-markdown>
|
||||
<paper-button on-click="dismissTap"
|
||||
>[[localize('ui.card.persistent_notification.dismiss')]]</paper-button
|
||||
>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -10,76 +10,93 @@ import EventsMixin from "../mixins/events-mixin";
|
||||
class HaPlantCard extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.has-plant-image .banner {
|
||||
padding-top: 30%;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
line-height: 40px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.has-plant-image .header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
width: 100%;
|
||||
background:rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 32px 24px 32px;
|
||||
}
|
||||
.has-plant-image .content {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
ha-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.attributes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.attributes div {
|
||||
text-align: center;
|
||||
}
|
||||
.problem {
|
||||
color: var(--google-red-500);
|
||||
font-weight: bold;
|
||||
}
|
||||
.uom {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
padding-top: 12px;
|
||||
}
|
||||
.has-plant-image .banner {
|
||||
padding-top: 30%;
|
||||
}
|
||||
.header {
|
||||
@apply --paper-font-headline;
|
||||
line-height: 40px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.has-plant-image .header {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
padding: 16px;
|
||||
color: white;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, var(--dark-secondary-opacity));
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 32px 24px 32px;
|
||||
}
|
||||
.has-plant-image .content {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
ha-icon {
|
||||
color: var(--paper-item-icon-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.attributes {
|
||||
cursor: pointer;
|
||||
}
|
||||
.attributes div {
|
||||
text-align: center;
|
||||
}
|
||||
.problem {
|
||||
color: var(--google-red-500);
|
||||
font-weight: bold;
|
||||
}
|
||||
.uom {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<ha-card class$="[[computeImageClass(stateObj.attributes.entity_picture)]]">
|
||||
<div class="banner" style="background-image:url([[stateObj.attributes.entity_picture]])">
|
||||
<div class="header">[[computeTitle(stateObj)]]</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<template is="dom-repeat" items="[[computeAttributes(stateObj.attributes)]]">
|
||||
<div class="attributes" on-click="attributeClicked">
|
||||
<div><ha-icon icon="[[computeIcon(item, stateObj.attributes.battery)]]"></ha-icon></div>
|
||||
<div class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]">
|
||||
[[computeValue(stateObj.attributes, item)]]
|
||||
<ha-card
|
||||
class$="[[computeImageClass(stateObj.attributes.entity_picture)]]"
|
||||
>
|
||||
<div
|
||||
class="banner"
|
||||
style="background-image:url([[stateObj.attributes.entity_picture]])"
|
||||
>
|
||||
<div class="header">[[computeTitle(stateObj)]]</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<template
|
||||
is="dom-repeat"
|
||||
items="[[computeAttributes(stateObj.attributes)]]"
|
||||
>
|
||||
<div class="attributes" on-click="attributeClicked">
|
||||
<div>
|
||||
<ha-icon
|
||||
icon="[[computeIcon(item, stateObj.attributes.battery)]]"
|
||||
></ha-icon>
|
||||
</div>
|
||||
<div
|
||||
class$="[[computeAttributeClass(stateObj.attributes.problem, item)]]"
|
||||
>
|
||||
[[computeValue(stateObj.attributes, item)]]
|
||||
</div>
|
||||
<div class="uom">
|
||||
[[computeUom(stateObj.attributes.unit_of_measurement_dict,
|
||||
item)]]
|
||||
</div>
|
||||
</div>
|
||||
<div class="uom">[[computeUom(stateObj.attributes.unit_of_measurement_dict, item)]]</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
</template>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
|
@ -106,9 +106,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<ha-card>
|
||||
<div class="header">
|
||||
[[computeState(stateObj.state, localize)]]
|
||||
<div class="name">
|
||||
[[stateObj.attributes.friendly_name]]
|
||||
</div>
|
||||
<div class="name">[[stateObj.attributes.friendly_name]]</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="now">
|
||||
@ -117,26 +115,38 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<ha-icon icon="[[getWeatherIcon(stateObj.state)]]"></ha-icon>
|
||||
</template>
|
||||
<div class="temp">
|
||||
[[stateObj.attributes.temperature]]<span>[[getUnit('temperature')]]</span>
|
||||
[[stateObj.attributes.temperature]]<span
|
||||
>[[getUnit('temperature')]]</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attributes">
|
||||
<template is="dom-if" if="[[_showValue(stateObj.attributes.pressure)]]">
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_showValue(stateObj.attributes.pressure)]]"
|
||||
>
|
||||
<div>
|
||||
[[localize('ui.card.weather.attributes.air_pressure')]]:
|
||||
[[stateObj.attributes.pressure]] [[getUnit('air_pressure')]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showValue(stateObj.attributes.humidity)]]">
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_showValue(stateObj.attributes.humidity)]]"
|
||||
>
|
||||
<div>
|
||||
[[localize('ui.card.weather.attributes.humidity')]]:
|
||||
[[stateObj.attributes.humidity]] %
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showValue(stateObj.attributes.wind_speed)]]">
|
||||
<template
|
||||
is="dom-if"
|
||||
if="[[_showValue(stateObj.attributes.wind_speed)]]"
|
||||
>
|
||||
<div>
|
||||
[[localize('ui.card.weather.attributes.wind_speed')]]:
|
||||
[[getWind(stateObj.attributes.wind_speed, stateObj.attributes.wind_bearing, localize)]]
|
||||
[[getWind(stateObj.attributes.wind_speed,
|
||||
stateObj.attributes.wind_bearing, localize)]]
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -145,22 +155,31 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
|
||||
<div class="forecast">
|
||||
<template is="dom-repeat" items="[[forecast]]">
|
||||
<div>
|
||||
<div class="weekday">[[computeDate(item.datetime)]]<br>
|
||||
<div class="weekday">
|
||||
[[computeDate(item.datetime)]]<br />
|
||||
<template is="dom-if" if="[[!_showValue(item.templow)]]">
|
||||
[[computeTime(item.datetime)]]
|
||||
</template>
|
||||
</div>
|
||||
<template is="dom-if" if="[[_showValue(item.condition)]]">
|
||||
<div class="icon">
|
||||
<ha-icon icon="[[getWeatherIcon(item.condition)]]"></ha-icon>
|
||||
<ha-icon
|
||||
icon="[[getWeatherIcon(item.condition)]]"
|
||||
></ha-icon>
|
||||
</div>
|
||||
</template>
|
||||
<div class="temp">[[item.temperature]] [[getUnit('temperature')]]</div>
|
||||
<div class="temp">
|
||||
[[item.temperature]] [[getUnit('temperature')]]
|
||||
</div>
|
||||
<template is="dom-if" if="[[_showValue(item.templow)]]">
|
||||
<div class="templow">[[item.templow]] [[getUnit('temperature')]]</div>
|
||||
<div class="templow">
|
||||
[[item.templow]] [[getUnit('temperature')]]
|
||||
</div>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_showValue(item.precipitation)]]">
|
||||
<div class="precipitation">[[item.precipitation]] [[getUnit('precipitation')]]</div>
|
||||
<div class="precipitation">
|
||||
[[item.precipitation]] [[getUnit('precipitation')]]
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -6,6 +6,34 @@ import { Auth } from "home-assistant-js-websocket";
|
||||
const CALLBACK_SET_TOKEN = "externalAuthSetToken";
|
||||
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
|
||||
|
||||
interface BasePayload {
|
||||
callback: string;
|
||||
}
|
||||
|
||||
interface RefreshTokenResponse {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
externalApp?: {
|
||||
getExternalAuth(payload: BasePayload);
|
||||
revokeExternalAuth(payload: BasePayload);
|
||||
};
|
||||
webkit?: {
|
||||
messageHandlers: {
|
||||
getExternalAuth: {
|
||||
postMessage(payload: BasePayload);
|
||||
};
|
||||
revokeExternalAuth: {
|
||||
postMessage(payload: BasePayload);
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.externalApp && !window.webkit) {
|
||||
throw new Error(
|
||||
"External auth requires either externalApp or webkit defined on Window object."
|
||||
@ -14,21 +42,24 @@ if (!window.externalApp && !window.webkit) {
|
||||
|
||||
export default class ExternalAuth extends Auth {
|
||||
constructor(hassUrl) {
|
||||
super();
|
||||
|
||||
this.data = {
|
||||
super({
|
||||
hassUrl,
|
||||
clientId: "",
|
||||
refresh_token: "",
|
||||
access_token: "",
|
||||
expires_in: 0,
|
||||
// This will trigger connection to do a refresh right away
|
||||
expires: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async refreshAccessToken() {
|
||||
const responseProm = new Promise((resolve, reject) => {
|
||||
window[CALLBACK_SET_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
});
|
||||
public async refreshAccessToken() {
|
||||
const responseProm = new Promise<RefreshTokenResponse>(
|
||||
(resolve, reject) => {
|
||||
window[CALLBACK_SET_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
}
|
||||
);
|
||||
|
||||
// Allow promise to set resolve on window object.
|
||||
await 0;
|
||||
@ -38,23 +69,18 @@ export default class ExternalAuth extends Auth {
|
||||
if (window.externalApp) {
|
||||
window.externalApp.getExternalAuth(callbackPayload);
|
||||
} else {
|
||||
window.webkit.messageHandlers.getExternalAuth.postMessage(
|
||||
window.webkit!.messageHandlers.getExternalAuth.postMessage(
|
||||
callbackPayload
|
||||
);
|
||||
}
|
||||
|
||||
// Response we expect back:
|
||||
// {
|
||||
// "access_token": "qwere",
|
||||
// "expires_in": 1800
|
||||
// }
|
||||
const tokens = await responseProm;
|
||||
|
||||
this.data.access_token = tokens.access_token;
|
||||
this.data.expires = tokens.expires_in * 1000 + Date.now();
|
||||
}
|
||||
|
||||
async revoke() {
|
||||
public async revoke() {
|
||||
const responseProm = new Promise((resolve, reject) => {
|
||||
window[CALLBACK_REVOKE_TOKEN] = (success, data) =>
|
||||
success ? resolve(data) : reject(data);
|
||||
@ -68,7 +94,7 @@ export default class ExternalAuth extends Auth {
|
||||
if (window.externalApp) {
|
||||
window.externalApp.revokeExternalAuth(callbackPayload);
|
||||
} else {
|
||||
window.webkit.messageHandlers.revokeExternalAuth.postMessage(
|
||||
window.webkit!.messageHandlers.revokeExternalAuth.postMessage(
|
||||
callbackPayload
|
||||
);
|
||||
}
|
@ -1,5 +1,18 @@
|
||||
import { AuthData } from "home-assistant-js-websocket";
|
||||
|
||||
const storage = window.localStorage || {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__tokenCache: {
|
||||
// undefined: we haven't loaded yet
|
||||
// null: none stored
|
||||
tokens?: AuthData | null;
|
||||
writeEnabled?: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// So that core.js and main app hit same shared object.
|
||||
let tokenCache = window.__tokenCache;
|
||||
if (!tokenCache) {
|
||||
@ -15,18 +28,22 @@ export function askWrite() {
|
||||
);
|
||||
}
|
||||
|
||||
export function saveTokens(tokens) {
|
||||
export function saveTokens(tokens: AuthData | null) {
|
||||
tokenCache.tokens = tokens;
|
||||
if (tokenCache.writeEnabled) {
|
||||
try {
|
||||
storage.hassTokens = JSON.stringify(tokens);
|
||||
} catch (err) {} // eslint-disable-line
|
||||
} catch (err) {
|
||||
// write failed, ignore it. Happens if storage is full or private mode.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function enableWrite() {
|
||||
tokenCache.writeEnabled = true;
|
||||
saveTokens(tokenCache.tokens);
|
||||
if (tokenCache.tokens) {
|
||||
saveTokens(tokenCache.tokens);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadTokens() {
|
@ -1,4 +0,0 @@
|
||||
/** Return if a component is loaded. */
|
||||
export default function isComponentLoaded(hass, component) {
|
||||
return hass && hass.config.components.indexOf(component) !== -1;
|
||||
}
|
9
src/common/config/is_component_loaded.ts
Normal file
9
src/common/config/is_component_loaded.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
/** Return if a component is loaded. */
|
||||
export default function isComponentLoaded(
|
||||
hass: HomeAssistant,
|
||||
component: string
|
||||
): boolean {
|
||||
return hass && hass.config.components.indexOf(component) !== -1;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
/** Return if the displaymode is in standalone mode (PWA). */
|
||||
export default function isPwa() {
|
||||
export default function isPwa(): boolean {
|
||||
return window.matchMedia("(display-mode: standalone)").matches;
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/** Get the location name from a hass object. */
|
||||
export default function computeLocationName(hass) {
|
||||
return hass && hass.config.location_name;
|
||||
}
|
6
src/common/config/location_name.ts
Normal file
6
src/common/config/location_name.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
/** Get the location name from a hass object. */
|
||||
export default function computeLocationName(hass: HomeAssistant): string {
|
||||
return hass && hass.config.location_name;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
export default function durationToSeconds(duration) {
|
||||
export default function durationToSeconds(duration: string): number {
|
||||
const parts = duration.split(":").map(Number);
|
||||
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||||
}
|
@ -11,11 +11,10 @@ function toLocaleDateStringSupportsOptions() {
|
||||
}
|
||||
|
||||
export default (toLocaleDateStringSupportsOptions()
|
||||
? (dateObj, locales) =>
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleDateString(locales, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: // eslint-disable-next-line no-unused-vars
|
||||
(dateObj, locales) => fecha.format(dateObj, "mediumDate"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "mediumDate"));
|
@ -11,7 +11,7 @@ function toLocaleStringSupportsOptions() {
|
||||
}
|
||||
|
||||
export default (toLocaleStringSupportsOptions()
|
||||
? (dateObj, locales) =>
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleString(locales, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
@ -19,5 +19,4 @@ export default (toLocaleStringSupportsOptions()
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: // eslint-disable-next-line no-unused-vars
|
||||
(dateObj, locales) => fecha.format(dateObj, "haDateTime"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "haDateTime"));
|
@ -11,10 +11,9 @@ function toLocaleTimeStringSupportsOptions() {
|
||||
}
|
||||
|
||||
export default (toLocaleTimeStringSupportsOptions()
|
||||
? (dateObj, locales) =>
|
||||
? (dateObj: Date, locales: string) =>
|
||||
dateObj.toLocaleTimeString(locales, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: // eslint-disable-next-line no-unused-vars
|
||||
(dateObj, locales) => fecha.format(dateObj, "shortTime"));
|
||||
: (dateObj: Date) => fecha.format(dateObj, "shortTime"));
|
@ -1,33 +0,0 @@
|
||||
/** Calculate a string representing a date object as relative time from now.
|
||||
*
|
||||
* Example output: 5 minutes ago, in 3 days.
|
||||
*/
|
||||
const tests = [60, "second", 60, "minute", 24, "hour", 7, "day"];
|
||||
|
||||
export default function relativeTime(dateObj, localize) {
|
||||
let delta = (new Date() - dateObj) / 1000;
|
||||
const tense = delta >= 0 ? "past" : "future";
|
||||
delta = Math.abs(delta);
|
||||
|
||||
for (let i = 0; i < tests.length; i += 2) {
|
||||
if (delta < tests[i]) {
|
||||
delta = Math.floor(delta);
|
||||
const time = localize(
|
||||
`ui.components.relative_time.duration.${tests[i + 1]}`,
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
return localize(`ui.components.relative_time.${tense}`, "time", time);
|
||||
}
|
||||
|
||||
delta /= tests[i];
|
||||
}
|
||||
|
||||
delta = Math.floor(delta);
|
||||
const time = localize(
|
||||
"ui.components.relative_time.duration.week",
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
return localize(`ui.components.relative_time.${tense}`, "time", time);
|
||||
}
|
40
src/common/datetime/relative_time.ts
Normal file
40
src/common/datetime/relative_time.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { LocalizeFunc } from "../../mixins/localize-base-mixin";
|
||||
|
||||
/**
|
||||
* Calculate a string representing a date object as relative time from now.
|
||||
*
|
||||
* Example output: 5 minutes ago, in 3 days.
|
||||
*/
|
||||
const tests = [60, 60, 24, 7];
|
||||
const langKey = ["second", "minute", "hour", "day"];
|
||||
|
||||
export default function relativeTime(
|
||||
dateObj: Date,
|
||||
localize: LocalizeFunc
|
||||
): string {
|
||||
let delta = (new Date().getTime() - dateObj.getTime()) / 1000;
|
||||
const tense = delta >= 0 ? "past" : "future";
|
||||
delta = Math.abs(delta);
|
||||
|
||||
for (let i = 0; i < tests.length; i++) {
|
||||
if (delta < tests[i]) {
|
||||
delta = Math.floor(delta);
|
||||
const timeDesc = localize(
|
||||
`ui.components.relative_time.duration.${langKey[i]}`,
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
return localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
|
||||
}
|
||||
|
||||
delta /= tests[i];
|
||||
}
|
||||
|
||||
delta = Math.floor(delta);
|
||||
const time = localize(
|
||||
"ui.components.relative_time.duration.week",
|
||||
"count",
|
||||
delta
|
||||
);
|
||||
return localize(`ui.components.relative_time.${tense}`, "time", time);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
const leftPad = (number) => (number < 10 ? `0${number}` : number);
|
||||
const leftPad = (num: number) => (num < 10 ? `0${num}` : num);
|
||||
|
||||
export default function secondsToDuration(d) {
|
||||
export default function secondsToDuration(d: number) {
|
||||
const h = Math.floor(d / 3600);
|
||||
const m = Math.floor((d % 3600) / 60);
|
||||
const s = Math.floor((d % 3600) % 60);
|
@ -1,9 +0,0 @@
|
||||
export default function attributeClassNames(stateObj, attributes) {
|
||||
if (!stateObj) return "";
|
||||
return attributes
|
||||
.map(function(attribute) {
|
||||
return attribute in stateObj.attributes ? "has-" + attribute : "";
|
||||
})
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
}
|
16
src/common/entity/attribute_class_names.ts
Normal file
16
src/common/entity/attribute_class_names.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
export default function attributeClassNames(
|
||||
stateObj: HassEntity,
|
||||
attributes: string[]
|
||||
): string {
|
||||
if (!stateObj) {
|
||||
return "";
|
||||
}
|
||||
return attributes
|
||||
.map((attribute) =>
|
||||
attribute in stateObj.attributes ? "has-" + attribute : ""
|
||||
)
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
/** Return an icon representing a binary sensor state. */
|
||||
|
||||
export default function binarySensorIcon(state) {
|
||||
var activated = state.state && state.state === "off";
|
||||
export default function binarySensorIcon(state: HassEntity) {
|
||||
const activated = state.state && state.state === "off";
|
||||
switch (state.attributes.device_class) {
|
||||
case "battery":
|
||||
return activated ? "hass:battery" : "hass:battery-outline";
|
@ -1,4 +1,6 @@
|
||||
export default function canToggleDomain(hass, domain) {
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export default function canToggleDomain(hass: HomeAssistant, domain: string) {
|
||||
const services = hass.services[domain];
|
||||
if (!services) {
|
||||
return false;
|
@ -1,13 +1,19 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import canToggleDomain from "./can_toggle_domain";
|
||||
import computeStateDomain from "./compute_state_domain";
|
||||
import { HomeAssistant } from "../../types";
|
||||
|
||||
export default function canToggleState(hass, stateObj) {
|
||||
export default function canToggleState(
|
||||
hass: HomeAssistant,
|
||||
stateObj: HassEntity
|
||||
) {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
if (domain === "group") {
|
||||
return stateObj.state === "on" || stateObj.state === "off";
|
||||
}
|
||||
if (domain === "climate") {
|
||||
return !!((stateObj.attributes || {}).supported_features & 4096);
|
||||
// tslint:disable-next-line
|
||||
return (stateObj.attributes.supported_features! & 4096) !== 0;
|
||||
}
|
||||
|
||||
return canToggleDomain(hass, domain);
|
@ -1,3 +0,0 @@
|
||||
export default function computeDomain(entityId) {
|
||||
return entityId.substr(0, entityId.indexOf("."));
|
||||
}
|
3
src/common/entity/compute_domain.ts
Normal file
3
src/common/entity/compute_domain.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export default function computeDomain(entityId: string): string {
|
||||
return entityId.substr(0, entityId.indexOf("."));
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
/** Compute the object ID of a state. */
|
||||
export default function computeObjectId(entityId) {
|
||||
export default function computeObjectId(entityId: string): string {
|
||||
return entityId.substr(entityId.indexOf(".") + 1);
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
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";
|
||||
|
||||
export default function computeStateDisplay(localize, stateObj, language) {
|
||||
if (!stateObj._stateDisplay) {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
if (domain === "binary_sensor") {
|
||||
// Try device class translation, then default binary sensor translation
|
||||
if (stateObj.attributes.device_class) {
|
||||
stateObj._stateDisplay = localize(
|
||||
`state.${domain}.${stateObj.attributes.device_class}.${
|
||||
stateObj.state
|
||||
}`
|
||||
);
|
||||
}
|
||||
if (!stateObj._stateDisplay) {
|
||||
stateObj._stateDisplay = localize(
|
||||
`state.${domain}.default.${stateObj.state}`
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
stateObj.attributes.unit_of_measurement &&
|
||||
!["unknown", "unavailable"].includes(stateObj.state)
|
||||
) {
|
||||
stateObj._stateDisplay =
|
||||
stateObj.state + " " + stateObj.attributes.unit_of_measurement;
|
||||
} else if (domain === "input_datetime") {
|
||||
let date;
|
||||
if (!stateObj.attributes.has_time) {
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day
|
||||
);
|
||||
stateObj._stateDisplay = formatDate(date, language);
|
||||
} else if (!stateObj.attributes.has_date) {
|
||||
const now = new Date();
|
||||
date = new Date(
|
||||
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
|
||||
// don't use artificial 1970 year.
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDay(),
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
);
|
||||
stateObj._stateDisplay = formatTime(date, language);
|
||||
} else {
|
||||
date = new Date(
|
||||
stateObj.attributes.year,
|
||||
stateObj.attributes.month - 1,
|
||||
stateObj.attributes.day,
|
||||
stateObj.attributes.hour,
|
||||
stateObj.attributes.minute
|
||||
);
|
||||
stateObj._stateDisplay = formatDateTime(date, language);
|
||||
}
|
||||
} else if (domain === "zwave") {
|
||||
if (["initializing", "dead"].includes(stateObj.state)) {
|
||||
stateObj._stateDisplay = localize(
|
||||
`state.zwave.query_stage.${stateObj.state}`,
|
||||
"query_stage",
|
||||
stateObj.attributes.query_stage
|
||||
);
|
||||
} else {
|
||||
stateObj._stateDisplay = localize(
|
||||
`state.zwave.default.${stateObj.state}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
stateObj._stateDisplay = localize(`state.${domain}.${stateObj.state}`);
|
||||
}
|
||||
// Fall back to default, component backend translation, or raw state if nothing else matches.
|
||||
stateObj._stateDisplay =
|
||||
stateObj._stateDisplay ||
|
||||
localize(`state.default.${stateObj.state}`) ||
|
||||
localize(`component.${domain}.state.${stateObj.state}`) ||
|
||||
stateObj.state;
|
||||
}
|
||||
|
||||
return stateObj._stateDisplay;
|
||||
}
|
91
src/common/entity/compute_state_display.ts
Normal file
91
src/common/entity/compute_state_display.ts
Normal file
@ -0,0 +1,91 @@
|
||||
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 { LocalizeFunc } from "../../mixins/localize-base-mixin";
|
||||
|
||||
type CachedDisplayEntity = HassEntity & {
|
||||
_stateDisplay?: string;
|
||||
};
|
||||
|
||||
export default function computeStateDisplay(
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
language: string
|
||||
) {
|
||||
const state = stateObj as CachedDisplayEntity;
|
||||
if (!state._stateDisplay) {
|
||||
const domain = computeStateDomain(state);
|
||||
if (domain === "binary_sensor") {
|
||||
// Try device class translation, then default binary sensor translation
|
||||
if (state.attributes.device_class) {
|
||||
state._stateDisplay = localize(
|
||||
`state.${domain}.${state.attributes.device_class}.${state.state}`
|
||||
);
|
||||
}
|
||||
if (!state._stateDisplay) {
|
||||
state._stateDisplay = localize(
|
||||
`state.${domain}.default.${state.state}`
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
state.attributes.unit_of_measurement &&
|
||||
!["unknown", "unavailable"].includes(state.state)
|
||||
) {
|
||||
state._stateDisplay =
|
||||
state.state + " " + state.attributes.unit_of_measurement;
|
||||
} else if (domain === "input_datetime") {
|
||||
let date: Date;
|
||||
if (!state.attributes.has_time) {
|
||||
date = new Date(
|
||||
state.attributes.year,
|
||||
state.attributes.month - 1,
|
||||
state.attributes.day
|
||||
);
|
||||
state._stateDisplay = formatDate(date, language);
|
||||
} else if (!state.attributes.has_date) {
|
||||
const now = new Date();
|
||||
date = new Date(
|
||||
// Due to bugs.chromium.org/p/chromium/issues/detail?id=797548
|
||||
// don't use artificial 1970 year.
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDay(),
|
||||
state.attributes.hour,
|
||||
state.attributes.minute
|
||||
);
|
||||
state._stateDisplay = formatTime(date, language);
|
||||
} else {
|
||||
date = new Date(
|
||||
state.attributes.year,
|
||||
state.attributes.month - 1,
|
||||
state.attributes.day,
|
||||
state.attributes.hour,
|
||||
state.attributes.minute
|
||||
);
|
||||
state._stateDisplay = formatDateTime(date, language);
|
||||
}
|
||||
} else if (domain === "zwave") {
|
||||
if (["initializing", "dead"].includes(state.state)) {
|
||||
state._stateDisplay = localize(
|
||||
`state.zwave.query_stage.${state.state}`,
|
||||
"query_stage",
|
||||
state.attributes.query_stage
|
||||
);
|
||||
} else {
|
||||
state._stateDisplay = localize(`state.zwave.default.${state.state}`);
|
||||
}
|
||||
} else {
|
||||
state._stateDisplay = localize(`state.${domain}.${state.state}`);
|
||||
}
|
||||
// Fall back to default, component backend translation, or raw state if nothing else matches.
|
||||
state._stateDisplay =
|
||||
state._stateDisplay ||
|
||||
localize(`state.default.${state.state}`) ||
|
||||
localize(`component.${domain}.state.${state.state}`) ||
|
||||
state.state;
|
||||
}
|
||||
|
||||
return state._stateDisplay;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import computeDomain from "./compute_domain";
|
||||
|
||||
export default function computeStateDomain(stateObj) {
|
||||
return computeDomain(stateObj.entity_id);
|
||||
}
|
6
src/common/entity/compute_state_domain.ts
Normal file
6
src/common/entity/compute_state_domain.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import computeDomain from "./compute_domain";
|
||||
|
||||
export default function computeStateDomain(stateObj: HassEntity) {
|
||||
return computeDomain(stateObj.entity_id);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import computeObjectId from "./compute_object_id";
|
||||
|
||||
export default function computeStateName(stateObj) {
|
||||
if (stateObj._entityDisplay === undefined) {
|
||||
stateObj._entityDisplay =
|
||||
stateObj.attributes.friendly_name ||
|
||||
computeObjectId(stateObj.entity_id).replace(/_/g, " ");
|
||||
}
|
||||
|
||||
return stateObj._entityDisplay;
|
||||
}
|
18
src/common/entity/compute_state_name.ts
Normal file
18
src/common/entity/compute_state_name.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import computeObjectId from "./compute_object_id";
|
||||
|
||||
type CachedDisplayEntity = HassEntity & {
|
||||
_entityDisplay?: string;
|
||||
};
|
||||
|
||||
export default function computeStateName(stateObj: HassEntity) {
|
||||
const state = stateObj as CachedDisplayEntity;
|
||||
|
||||
if (state._entityDisplay === undefined) {
|
||||
state._entityDisplay =
|
||||
state.attributes.friendly_name ||
|
||||
computeObjectId(state.entity_id).replace(/_/g, " ");
|
||||
}
|
||||
|
||||
return state._entityDisplay;
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
/** Return an icon representing a cover state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import domainIcon from "./domain_icon";
|
||||
|
||||
export default function coverIcon(state) {
|
||||
var open = state.state && state.state !== "closed";
|
||||
export default function coverIcon(state: HassEntity): string {
|
||||
const open = state.state !== "closed";
|
||||
switch (state.attributes.device_class) {
|
||||
case "garage":
|
||||
return open ? "hass:garage-open" : "hass:garage";
|
@ -44,7 +44,7 @@ const fixedIcons = {
|
||||
weblink: "hass:open-in-new",
|
||||
};
|
||||
|
||||
export default function domainIcon(domain, state) {
|
||||
export default function domainIcon(domain: string, state?: string): string {
|
||||
if (domain in fixedIcons) {
|
||||
return fixedIcons[domain];
|
||||
}
|
||||
@ -93,11 +93,10 @@ export default function domainIcon(domain, state) {
|
||||
}
|
||||
|
||||
default:
|
||||
/* eslint-disable no-console */
|
||||
// tslint:disable-next-line
|
||||
console.warn(
|
||||
"Unable to find icon for domain " + domain + " (" + state + ")"
|
||||
);
|
||||
/* eslint-enable no-console */
|
||||
return DEFAULT_DOMAIN_ICON;
|
||||
}
|
||||
}
|
64
src/common/entity/entity_filter.ts
Normal file
64
src/common/entity/entity_filter.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import computeDomain from "./compute_domain";
|
||||
|
||||
export type FilterFunc = (entityId: string) => boolean;
|
||||
|
||||
export const generateFilter = (
|
||||
includeDomains?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeDomains?: string[],
|
||||
excludeEntities?: string[]
|
||||
): FilterFunc => {
|
||||
const includeDomainsSet = new Set(includeDomains);
|
||||
const includeEntitiesSet = new Set(includeEntities);
|
||||
const excludeDomainsSet = new Set(excludeDomains);
|
||||
const excludeEntitiesSet = new Set(excludeEntities);
|
||||
|
||||
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
|
||||
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
|
||||
|
||||
// Case 1 - no includes or excludes - pass all entities
|
||||
if (!haveInclude && !haveExclude) {
|
||||
return () => true;
|
||||
}
|
||||
|
||||
// Case 2 - includes, no excludes - only include specified entities
|
||||
if (haveInclude && !haveExclude) {
|
||||
return (entityId) =>
|
||||
includeEntitiesSet.has(entityId) ||
|
||||
includeDomainsSet.has(computeDomain(entityId));
|
||||
}
|
||||
|
||||
// Case 3 - excludes, no includes - only exclude specified entities
|
||||
if (!haveInclude && haveExclude) {
|
||||
return (entityId) =>
|
||||
!excludeEntitiesSet.has(entityId) &&
|
||||
!excludeDomainsSet.has(computeDomain(entityId));
|
||||
}
|
||||
|
||||
// Case 4 - both includes and excludes specified
|
||||
// Case 4a - include domain specified
|
||||
// - if domain is included, pass if entity not excluded
|
||||
// - if domain is not included, pass if entity is included
|
||||
// note: if both include and exclude domains specified,
|
||||
// the exclude domains are ignored
|
||||
if (includeDomainsSet.size) {
|
||||
return (entityId) =>
|
||||
includeDomainsSet.has(computeDomain(entityId))
|
||||
? !excludeEntitiesSet.has(entityId)
|
||||
: includeEntitiesSet.has(entityId);
|
||||
}
|
||||
|
||||
// Case 4b - exclude domain specified
|
||||
// - if domain is excluded, pass if entity is included
|
||||
// - if domain is not excluded, pass if entity not excluded
|
||||
if (excludeDomainsSet.size) {
|
||||
return (entityId) =>
|
||||
excludeDomainsSet.has(computeDomain(entityId))
|
||||
? includeEntitiesSet.has(entityId)
|
||||
: !excludeEntitiesSet.has(entityId);
|
||||
}
|
||||
|
||||
// Case 4c - neither include or exclude domain specified
|
||||
// - Only pass if entity is included. Ignore entity excludes.
|
||||
return (entityId) => includeEntitiesSet.has(entityId);
|
||||
};
|
@ -1,8 +1,9 @@
|
||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import { DEFAULT_VIEW_ENTITY_ID } from "../const";
|
||||
|
||||
// Return an ordered array of available views
|
||||
export default function extractViews(entities) {
|
||||
const views = [];
|
||||
export default function extractViews(entities: HassEntities): HassEntity[] {
|
||||
const views: HassEntity[] = [];
|
||||
|
||||
Object.keys(entities).forEach((entityId) => {
|
||||
const entity = entities[entityId];
|
@ -1,11 +0,0 @@
|
||||
// Expects classNames to be an object mapping feature-bit -> className
|
||||
export default function featureClassNames(stateObj, classNames) {
|
||||
if (!stateObj || !stateObj.attributes.supported_features) return "";
|
||||
|
||||
const features = stateObj.attributes.supported_features;
|
||||
|
||||
return Object.keys(classNames)
|
||||
.map((feature) => ((features & feature) !== 0 ? classNames[feature] : ""))
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
}
|
21
src/common/entity/feature_class_names.ts
Normal file
21
src/common/entity/feature_class_names.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
// Expects classNames to be an object mapping feature-bit -> className
|
||||
export default function featureClassNames(
|
||||
stateObj: HassEntity,
|
||||
classNames: { [feature: number]: string }
|
||||
) {
|
||||
if (!stateObj || !stateObj.attributes.supported_features) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const features = stateObj.attributes.supported_features;
|
||||
|
||||
return Object.keys(classNames)
|
||||
.map((feature) =>
|
||||
// tslint:disable-next-line
|
||||
(features & Number(feature)) !== 0 ? classNames[feature] : ""
|
||||
)
|
||||
.filter((attr) => attr !== "")
|
||||
.join(" ");
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user