Skip to content
Snippets Groups Projects
Unverified Commit f6eb9e79 authored by Paulus Schoutsen's avatar Paulus Schoutsen Committed by GitHub
Browse files

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
parent ab3717af
No related branches found
No related tags found
No related merge requests found
...@@ -4,7 +4,6 @@ Register a custom front end panel. ...@@ -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' ...@@ -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): ...@@ -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
......
"""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
@pytest.fixture(autouse=True)
def mock_frontend_loaded(hass):
"""Mock frontend is loaded."""
mock_component(hass, 'frontend')
async def test_webcomponent_custom_path_not_found(hass):
@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): ...@@ -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.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
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.sidebar_icon == 'mdi:iconicon'
assert panel.sidebar_title == 'Sidebar Title'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment