mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
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:
parent
75e3f1f37b
commit
5f226d1809
@ -49,6 +49,10 @@ function ensureLoaded(panel) {
|
||||
imported = import(/* webpackChunkName: "panel-dev-template" */ '../panels/dev-template/ha-panel-dev-template.js');
|
||||
break;
|
||||
|
||||
case 'experimental-ui':
|
||||
imported = import(/* webpackChunkName: "panel-experimental-ui" */ '../panels/experimental-ui/ha-panel-experimental-ui.js');
|
||||
break;
|
||||
|
||||
case 'history':
|
||||
imported = import(/* webpackChunkName: "panel-history" */ '../panels/history/ha-panel-history.js');
|
||||
break;
|
||||
|
96
src/panels/experimental-ui/ha-panel-experimental-ui.js
Normal file
96
src/panels/experimental-ui/ha-panel-experimental-ui.js
Normal 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);
|
114
src/panels/experimental-ui/hui-entities-card.js
Normal file
114
src/panels/experimental-ui/hui-entities-card.js
Normal 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);
|
55
src/panels/experimental-ui/hui-entity-filter-card.js
Normal file
55
src/panels/experimental-ui/hui-entity-filter-card.js
Normal 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);
|
182
src/panels/experimental-ui/hui-view.js
Normal file
182
src/panels/experimental-ui/hui-view.js
Normal 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);
|
Loading…
x
Reference in New Issue
Block a user