Add experimental UI (#1205)

* Add experimental UI

* Allow theming a view

* Name it css

* Allow applying themes

* Add filter card

* Add normal column layout logic
This commit is contained in:
Paulus Schoutsen 2018-06-16 17:29:18 -04:00 committed by GitHub
parent 75e3f1f37b
commit 5f226d1809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 451 additions and 0 deletions

View File

@ -49,6 +49,10 @@ function ensureLoaded(panel) {
imported = import(/* webpackChunkName: "panel-dev-template" */ '../panels/dev-template/ha-panel-dev-template.js'); imported = import(/* webpackChunkName: "panel-dev-template" */ '../panels/dev-template/ha-panel-dev-template.js');
break; break;
case 'experimental-ui':
imported = import(/* webpackChunkName: "panel-experimental-ui" */ '../panels/experimental-ui/ha-panel-experimental-ui.js');
break;
case 'history': case 'history':
imported = import(/* webpackChunkName: "panel-history" */ '../panels/history/ha-panel-history.js'); imported = import(/* webpackChunkName: "panel-history" */ '../panels/history/ha-panel-history.js');
break; break;

View File

@ -0,0 +1,96 @@
import '@polymer/app-layout/app-header-layout/app-header-layout.js';
import '@polymer/app-layout/app-header/app-header.js';
import '@polymer/app-layout/app-toolbar/app-toolbar.js';
import '@polymer/paper-icon-button/paper-icon-button.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import './hui-view.js';
class ExperimentalUI extends PolymerElement {
static get template() {
return html`
<style include='ha-style'>
app-header-layout {
height: 100%;
}
</style>
<app-header-layout>
<app-header slot="header" fixed>
<app-toolbar>
<ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
<div main-title>Experimental UI</div>
<paper-icon-button icon='hass:refresh' on-click='_fetchConfig'></paper-icon-button>
</app-toolbar>
</app-header>
<hui-view
hass='[[hass]]'
config='[[_curView]]'
columns='[[_columns]]'
></hui-view>
</app-header-layout>
`;
}
static get properties() {
return {
hass: Object,
narrow: {
type: Boolean,
value: false,
},
showMenu: {
type: Boolean,
value: false,
},
_columns: {
type: Number,
value: 1,
},
_config: {
type: Object,
value: null,
observer: '_configChanged',
},
_curView: Object
};
}
ready() {
super.ready();
this._fetchConfig();
this._handleWindowChange = this._handleWindowChange.bind(this);
this.mqls = [300, 600, 900, 1200].map((width) => {
const mql = matchMedia(`(min-width: ${width}px)`);
mql.addListener(this._handleWindowChange);
return mql;
});
this._handleWindowChange();
}
_handleWindowChange() {
const matchColumns = this.mqls.reduce((cols, mql) => cols + mql.matches, 0);
// Do -1 column if the menu is docked and open
this._columns = Math.max(1, matchColumns - (!this.narrow && this.showMenu));
}
_fetchConfig() {
this.hass.connection.sendMessagePromise({ type: 'frontend/experimental_ui' })
.then((conf) => { this._config = conf.result; });
}
_configChanged(config) {
if (!config) return;
// Currently hardcode to first view.
this._curView = config.views[0];
}
}
customElements.define('ha-panel-experimental-ui', ExperimentalUI);

View File

@ -0,0 +1,114 @@
import '@polymer/iron-flex-layout/iron-flex-layout-classes.js';
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import stateCardType from '../../common/entity/state_card_type.js';
import '../../components/ha-card.js';
// just importing this now as shortcut to import correct state-card-*
import '../../state-summary/state-card-content.js';
// Support for overriding type from attributes should be removed
// Instead, it should be coded inside entity config.
// stateCardType requires `hass` because we check if a service exists.
// This should also be determined during runtime.
function stateElement(hass, entityId, stateObj) {
if (!stateObj) {
return 'state-card-display';
} else if (stateObj.attributes && 'custom_ui_state_card' in stateObj.attributes) {
return stateObj.attributes.custom_ui_state_card;
}
return 'state-card-' + stateCardType(hass, stateObj);
}
class HuiEntitiesCard extends PolymerElement {
static get template() {
return html`
<style>
ha-card {
padding: 16px;
}
.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;
}
</style>
<ha-card>
<div class='header'>
<div class="name">[[_computeTitle(config)]]</div>
</div>
<div id="states"></div>
</ha-card>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: '_hassChanged',
},
config: {
type: Object,
observer: '_configChanged',
}
};
}
constructor() {
super();
this._elements = [];
}
getCardSize() {
// +1 for the header
return 1 + (this.config ? this.config.entities.length : 0);
}
_computeTitle(config) {
return config.title;
}
_configChanged(config) {
const root = this.$.states;
while (root.lastChild) {
root.removeChild(root.lastChild);
}
this._elements = [];
for (let i = 0; i < config.entities.length; i++) {
const entityId = config.entities[i];
const stateObj = this.hass.states[entityId];
const tag = stateElement(this.hass, entityId, stateObj);
const element = document.createElement(tag);
element.stateObj = stateObj;
element.hass = this.hass;
this._elements.push({ entityId, element });
root.appendChild(element);
}
}
_hassChanged(hass) {
for (let i = 0; i < this._elements.length; i++) {
const { entityId, element } = this._elements[i];
const stateObj = hass.states[entityId];
element.stateObj = stateObj;
element.hass = hass;
}
}
}
customElements.define('hui-entities-card', HuiEntitiesCard);

View File

@ -0,0 +1,55 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import './hui-entities-card.js';
import computeStateDomain from '../../common/entity/compute_state_domain.js';
class HuiEntitiesCard extends PolymerElement {
static get template() {
return html`
<hui-entities-card
hass='[[hass]]'
config='[[_computeCardConfig(hass, config)]]'
></hui-entities-card>
`;
}
static get properties() {
return {
hass: Object,
config: Object,
};
}
getCardSize() {
// +1 for the header
return 1 + this._getEntities(this.hass, this.config.filter).length;
}
// Return a list of entities based on a filter.
_getEntities(hass, filter) {
const filters = [];
if (filter.domain) {
const domain = filter.domain;
filters.push(stateObj => computeStateDomain(stateObj) === domain);
}
if (filter.state) {
const state = filter.state;
filters.push(stateObj => stateObj.state === state);
}
return Object.values(hass.states)
.filter(stateObj => filters.every(filterFunc => filterFunc(stateObj)))
.map(stateObj => stateObj.entity_id);
}
_computeCardConfig(hass, config) {
return Object.assign({}, config.card_config || {}, {
entities: this._getEntities(hass, config.filter),
});
}
}
customElements.define('hui-entity-filter-card', HuiEntitiesCard);

View File

@ -0,0 +1,182 @@
import { html } from '@polymer/polymer/lib/utils/html-tag.js';
import { PolymerElement } from '@polymer/polymer/polymer-element.js';
import './hui-entities-card.js';
import './hui-entity-filter-card.js';
import applyThemesOnElement from '../../common/dom/apply_themes_on_element.js';
const VALID_TYPES = ['entities', 'entity-filter'];
const CUSTOM_TYPE_PREFIX = 'custom:';
function cardElement(type) {
if (VALID_TYPES.includes(type)) {
return `hui-${type}-card`;
} else if (type.startsWith(CUSTOM_TYPE_PREFIX)) {
return type.substr(CUSTOM_TYPE_PREFIX.length);
}
return null;
}
class HaView extends PolymerElement {
static get template() {
return html`
<style>
:host {
display: block;
padding-top: 8px;
padding-right: 8px;
transform: translateZ(0);
position: relative;
}
#columns {
display: flex;
flex-direction: row;
justify-content: center;
}
.column {
flex-basis: 0;
flex-grow: 1;
max-width: 500px;
overflow-x: hidden;
}
.column > * {
display: block;
margin-left: 8px;
margin-bottom: 8px;
}
@media (max-width: 500px) {
:host {
padding-right: 0;
}
.column > * {
margin-left: 0;
}
}
@media (max-width: 599px) {
.column {
max-width: 600px;
}
}
</style>
<div id='columns'></div>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: '_hassChanged',
},
columns: {
type: Number,
observer: '_configChanged',
},
config: {
type: Object,
observer: '_configChanged',
},
};
}
constructor() {
super();
this._elements = [];
}
_getElements(cards) {
const elements = [];
for (let i = 0; i < cards.length; i++) {
const cardConfig = cards[i];
const tag = cardElement(cardConfig.type);
if (!tag) {
// eslint-disable-next-line
console.error('Unknown type encountered:', cardConfig.type);
continue;
}
const element = document.createElement(tag);
element.config = cardConfig;
element.hass = this.hass;
elements.push(element);
}
return elements;
}
_configChanged() {
const root = this.$.columns;
const config = this.config;
while (root.lastChild) {
root.removeChild(root.lastChild);
}
if (!config) {
this._elements = [];
return;
}
const elements = this._getElements(config.cards);
let columns = [];
const columnEntityCount = [];
for (let i = 0; i < this.columns; i++) {
columns.push([]);
columnEntityCount.push(0);
}
// Find column with < 5 entities, else column with lowest count
function getColumnIndex(size) {
let minIndex = 0;
for (let i = 0; i < columnEntityCount.length; i++) {
if (columnEntityCount[i] < 5) {
minIndex = i;
break;
}
if (columnEntityCount[i] < columnEntityCount[minIndex]) {
minIndex = i;
}
}
columnEntityCount[minIndex] += size;
return minIndex;
}
elements.forEach(el =>
columns[getColumnIndex(el.getCardSize())].push(el));
// Remove empty columns
columns = columns.filter(val => val.length > 0);
columns.forEach((column) => {
const columnEl = document.createElement('div');
columnEl.classList.add('column');
column.forEach(el => columnEl.appendChild(el));
root.appendChild(columnEl);
});
this._elements = elements;
if ('theme' in config) {
applyThemesOnElement(root, this.hass.themes, config.theme);
}
}
_hassChanged(hass) {
for (let i = 0; i < this._elements.length; i++) {
this._elements[i].hass = hass;
}
}
}
customElements.define('hui-view', HaView);