Custom panel (#14708)

* Add support for custom panels in JS

* Allow specifying JS custom panels

* Add trust external option

* Fix tests

* Do I/O outside event loop

* Change config to avoid breaking change
This commit is contained in:
Paulus Schoutsen 2018-06-01 10:06:17 -04:00 committed by GitHub
parent ab3717af76
commit f6eb9e79d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 99 additions and 33 deletions

View File

@ -4,7 +4,6 @@ Register a custom front end panel.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/panel_custom/ https://home-assistant.io/components/panel_custom/
""" """
import asyncio
import logging import logging
import os import os
@ -21,27 +20,33 @@ CONF_SIDEBAR_ICON = 'sidebar_icon'
CONF_URL_PATH = 'url_path' CONF_URL_PATH = 'url_path'
CONF_CONFIG = 'config' CONF_CONFIG = 'config'
CONF_WEBCOMPONENT_PATH = 'webcomponent_path' CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
CONF_JS_URL = 'js_url'
CONF_EMBED_IFRAME = 'embed_iframe'
CONF_TRUST_EXTERNAL_SCRIPT = 'trust_external_script'
DEFAULT_ICON = 'mdi:bookmark' DEFAULT_ICON = 'mdi:bookmark'
LEGACY_URL = '/api/panel_custom/{}'
PANEL_DIR = 'panels' PANEL_DIR = 'panels'
CONFIG_SCHEMA = vol.Schema({ CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [{ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
vol.Required(CONF_COMPONENT_NAME): cv.slug, vol.Required(CONF_COMPONENT_NAME): cv.string,
vol.Optional(CONF_SIDEBAR_TITLE): cv.string, vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon, vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
vol.Optional(CONF_URL_PATH): cv.string, vol.Optional(CONF_URL_PATH): cv.string,
vol.Optional(CONF_CONFIG): cv.match_all, vol.Optional(CONF_CONFIG): dict,
vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile, vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
}]) vol.Optional(CONF_JS_URL): cv.string,
vol.Optional(CONF_EMBED_IFRAME, default=False): cv.boolean,
vol.Optional(CONF_TRUST_EXTERNAL_SCRIPT, default=False): cv.boolean,
})])
}, extra=vol.ALLOW_EXTRA) }, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@asyncio.coroutine async def async_setup(hass, config):
def async_setup(hass, config):
"""Initialize custom panel.""" """Initialize custom panel."""
success = False success = False
@ -52,17 +57,39 @@ def async_setup(hass, config):
if panel_path is None: if panel_path is None:
panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name)) panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
if not os.path.isfile(panel_path): custom_panel_config = {
'name': name,
'embed_iframe': panel[CONF_EMBED_IFRAME],
'trust_external': panel[CONF_TRUST_EXTERNAL_SCRIPT],
}
if CONF_JS_URL in panel:
custom_panel_config['js_url'] = panel[CONF_JS_URL]
elif not await hass.async_add_job(os.path.isfile, panel_path):
_LOGGER.error('Unable to find webcomponent for %s: %s', _LOGGER.error('Unable to find webcomponent for %s: %s',
name, panel_path) name, panel_path)
continue continue
yield from hass.components.frontend.async_register_panel( else:
name, panel_path, url = LEGACY_URL.format(name)
hass.http.register_static_path(url, panel_path)
custom_panel_config['html_url'] = LEGACY_URL.format(name)
if CONF_CONFIG in panel:
# Make copy because we're mutating it
config = dict(panel[CONF_CONFIG])
else:
config = {}
config['_panel_custom'] = custom_panel_config
await hass.components.frontend.async_register_built_in_panel(
component_name='custom',
sidebar_title=panel.get(CONF_SIDEBAR_TITLE), sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
sidebar_icon=panel.get(CONF_SIDEBAR_ICON), sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
frontend_url_path=panel.get(CONF_URL_PATH), frontend_url_path=panel.get(CONF_URL_PATH),
config=panel.get(CONF_CONFIG), config=config
) )
success = True success = True

View File

@ -1,23 +1,11 @@
"""The tests for the panel_custom component.""" """The tests for the panel_custom component."""
import asyncio
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
import pytest
from homeassistant import setup from homeassistant import setup
from homeassistant.components import frontend from homeassistant.components import frontend
from tests.common import mock_component
async def test_webcomponent_custom_path_not_found(hass):
@pytest.fixture(autouse=True)
def mock_frontend_loaded(hass):
"""Mock frontend is loaded."""
mock_component(hass, 'frontend')
@asyncio.coroutine
def test_webcomponent_custom_path_not_found(hass):
"""Test if a web component is found in config panels dir.""" """Test if a web component is found in config panels dir."""
filename = 'mock.file' filename = 'mock.file'
@ -33,45 +21,96 @@ def test_webcomponent_custom_path_not_found(hass):
} }
with patch('os.path.isfile', Mock(return_value=False)): with patch('os.path.isfile', Mock(return_value=False)):
result = yield from setup.async_setup_component( result = await setup.async_setup_component(
hass, 'panel_custom', config hass, 'panel_custom', config
) )
assert not result assert not result
assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0 assert len(hass.data.get(frontend.DATA_PANELS, {})) == 0
@asyncio.coroutine async def test_webcomponent_custom_path(hass):
def test_webcomponent_custom_path(hass):
"""Test if a web component is found in config panels dir.""" """Test if a web component is found in config panels dir."""
filename = 'mock.file' filename = 'mock.file'
config = { config = {
'panel_custom': { 'panel_custom': {
'name': 'todomvc', 'name': 'todo-mvc',
'webcomponent_path': filename, 'webcomponent_path': filename,
'sidebar_title': 'Sidebar Title', 'sidebar_title': 'Sidebar Title',
'sidebar_icon': 'mdi:iconicon', 'sidebar_icon': 'mdi:iconicon',
'url_path': 'nice_url', 'url_path': 'nice_url',
'config': 5, 'config': {
'hello': 'world',
}
} }
} }
with patch('os.path.isfile', Mock(return_value=True)): with patch('os.path.isfile', Mock(return_value=True)):
with patch('os.access', Mock(return_value=True)): with patch('os.access', Mock(return_value=True)):
result = yield from setup.async_setup_component( result = await setup.async_setup_component(
hass, 'panel_custom', config hass, 'panel_custom', config
) )
assert result assert result
panels = hass.data.get(frontend.DATA_PANELS, []) panels = hass.data.get(frontend.DATA_PANELS, [])
assert len(panels) == 1 assert panels
assert 'nice_url' in panels assert 'nice_url' in panels
panel = panels['nice_url'] panel = panels['nice_url']
assert panel.config == 5 assert panel.config == {
'hello': 'world',
'_panel_custom': {
'html_url': '/api/panel_custom/todo-mvc',
'name': 'todo-mvc',
'embed_iframe': False,
'trust_external': False,
},
}
assert panel.frontend_url_path == 'nice_url'
assert panel.sidebar_icon == 'mdi:iconicon'
assert panel.sidebar_title == 'Sidebar Title'
async def test_js_webcomponent(hass):
"""Test if a web component is found in config panels dir."""
config = {
'panel_custom': {
'name': 'todo-mvc',
'js_url': '/local/bla.js',
'sidebar_title': 'Sidebar Title',
'sidebar_icon': 'mdi:iconicon',
'url_path': 'nice_url',
'config': {
'hello': 'world',
},
'embed_iframe': True,
'trust_external_script': True,
}
}
result = await setup.async_setup_component(
hass, 'panel_custom', config
)
assert result
panels = hass.data.get(frontend.DATA_PANELS, [])
assert panels
assert 'nice_url' in panels
panel = panels['nice_url']
assert panel.config == {
'hello': 'world',
'_panel_custom': {
'js_url': '/local/bla.js',
'name': 'todo-mvc',
'embed_iframe': True,
'trust_external': True,
}
}
assert panel.frontend_url_path == 'nice_url' assert panel.frontend_url_path == 'nice_url'
assert panel.sidebar_icon == 'mdi:iconicon' assert panel.sidebar_icon == 'mdi:iconicon'
assert panel.sidebar_title == 'Sidebar Title' assert panel.sidebar_title == 'Sidebar Title'
assert panel.path == filename