diff --git a/.gitignore b/.gitignore
index d749a8608fc..b73dcef1073 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,9 @@ config/custom_components/*
!config/custom_components/example.py
!config/custom_components/hello_world.py
!config/custom_components/mqtt_example.py
-!config/custom_components/react_panel
+!config/panels
+config/panels/*
+!config/panels/react.html
tests/testing_config/deps
tests/testing_config/home-assistant.log
diff --git a/config/custom_components/react_panel/__init__.py b/config/custom_components/react_panel/__init__.py
deleted file mode 100644
index 57073b8cddc..00000000000
--- a/config/custom_components/react_panel/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-"""
-Custom panel example showing TodoMVC using React.
-
-Will add a panel to control lights and switches using React. Allows configuring
-the title via configuration.yaml:
-
-react_panel:
- title: 'home'
-
-"""
-import os
-
-from homeassistant.components.frontend import register_panel
-
-DOMAIN = 'react_panel'
-DEPENDENCIES = ['frontend']
-
-PANEL_PATH = os.path.join(os.path.dirname(__file__), 'panel.html')
-
-
-def setup(hass, config):
- """Initialize custom panel."""
- title = config.get(DOMAIN, {}).get('title')
-
- config = None if title is None else {'title': title}
-
- register_panel(hass, 'react', PANEL_PATH,
- title='TodoMVC', icon='mdi:checkbox-marked-outline',
- config=config)
- return True
diff --git a/config/custom_components/react_panel/panel.html b/config/panels/react.html
similarity index 96%
rename from config/custom_components/react_panel/panel.html
rename to config/panels/react.html
index eceee0f0616..dc2735cf759 100644
--- a/config/custom_components/react_panel/panel.html
+++ b/config/panels/react.html
@@ -1,3 +1,20 @@
+
+
diff --git a/homeassistant/components/panel_custom.py b/homeassistant/components/panel_custom.py
new file mode 100644
index 00000000000..e4b2480f6d4
--- /dev/null
+++ b/homeassistant/components/panel_custom.py
@@ -0,0 +1,64 @@
+"""Register a custom front end panel."""
+import logging
+import os
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.frontend import register_panel
+
+DOMAIN = 'panel_custom'
+DEPENDENCIES = ['frontend']
+
+CONF_COMPONENT_NAME = 'name'
+CONF_SIDEBAR_TITLE = 'sidebar_title'
+CONF_SIDEBAR_ICON = 'sidebar_icon'
+CONF_URL_PATH = 'url_path'
+CONF_CONFIG = 'config'
+CONF_WEBCOMPONENT_PATH = 'webcomponent_path'
+
+DEFAULT_ICON = 'mdi:bookmark'
+
+PANEL_DIR = 'panels'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [{
+ vol.Required(CONF_COMPONENT_NAME): cv.slug,
+ vol.Optional(CONF_SIDEBAR_TITLE): cv.string,
+ vol.Optional(CONF_SIDEBAR_ICON, default=DEFAULT_ICON): cv.icon,
+ vol.Optional(CONF_URL_PATH): cv.string,
+ vol.Optional(CONF_CONFIG): cv.match_all,
+ vol.Optional(CONF_WEBCOMPONENT_PATH): cv.isfile,
+ }])
+}, extra=vol.ALLOW_EXTRA)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup(hass, config):
+ """Initialize custom panel."""
+ success = False
+
+ for panel in config.get(DOMAIN):
+ name = panel.get(CONF_COMPONENT_NAME)
+ panel_path = panel.get(CONF_WEBCOMPONENT_PATH)
+
+ if panel_path is None:
+ panel_path = hass.config.path(PANEL_DIR, '{}.html'.format(name))
+
+ if not os.path.isfile(panel_path):
+ _LOGGER.error('Unable to find webcomponent for %s: %s',
+ name, panel_path)
+ continue
+
+ register_panel(
+ hass, name, panel_path,
+ sidebar_title=panel.get(CONF_SIDEBAR_TITLE),
+ sidebar_icon=panel.get(CONF_SIDEBAR_ICON),
+ url_path=panel.get(CONF_URL_PATH),
+ config=panel.get(CONF_CONFIG),
+ )
+
+ success = True
+
+ return success
diff --git a/tests/components/test_panel_custom.py b/tests/components/test_panel_custom.py
new file mode 100644
index 00000000000..6a41706db98
--- /dev/null
+++ b/tests/components/test_panel_custom.py
@@ -0,0 +1,77 @@
+"""The tests for the panel_custom component."""
+import os
+import shutil
+from tempfile import NamedTemporaryFile
+import unittest
+from unittest.mock import patch
+
+from homeassistant import bootstrap
+from homeassistant.components import panel_custom
+
+from tests.common import get_test_home_assistant
+
+
+@patch('homeassistant.components.frontend.setup', return_value=True)
+class TestPanelCustom(unittest.TestCase):
+ """Test the panel_custom component."""
+
+ def setup_method(self, method):
+ """Setup things to be run when tests are started."""
+ self.hass = get_test_home_assistant()
+
+ def teardown_method(self, method):
+ """Stop everything that was started."""
+ self.hass.stop()
+ shutil.rmtree(self.hass.config.path(panel_custom.PANEL_DIR),
+ ignore_errors=True)
+
+ @patch('homeassistant.components.panel_custom.register_panel')
+ def test_webcomponent_in_panels_dir(self, mock_register, _mock_setup):
+ """Test if a web component is found in config panels dir."""
+ config = {
+ 'panel_custom': {
+ 'name': 'todomvc',
+ }
+ }
+
+ assert not bootstrap.setup_component(self.hass, 'panel_custom', config)
+ assert not mock_register.called
+
+ path = self.hass.config.path(panel_custom.PANEL_DIR)
+ os.mkdir(path)
+
+ with open(os.path.join(path, 'todomvc.html'), 'a'):
+ assert bootstrap.setup_component(self.hass, 'panel_custom', config)
+ assert mock_register.called
+
+ @patch('homeassistant.components.panel_custom.register_panel')
+ def test_webcomponent_custom_path(self, mock_register, _mock_setup):
+ """Test if a web component is found in config panels dir."""
+ with NamedTemporaryFile() as fp:
+ config = {
+ 'panel_custom': {
+ 'name': 'todomvc',
+ 'webcomponent_path': fp.name,
+ 'sidebar_title': 'Sidebar Title',
+ 'sidebar_icon': 'mdi:iconicon',
+ 'url_path': 'nice_url',
+ 'config': 5,
+ }
+ }
+
+ with patch('os.path.isfile', return_value=False):
+ assert not bootstrap.setup_component(self.hass, 'panel_custom',
+ config)
+ assert not mock_register.called
+
+ assert bootstrap.setup_component(self.hass, 'panel_custom', config)
+ assert mock_register.called
+ args = mock_register.mock_calls[0][1]
+ kwargs = mock_register.mock_calls[0][2]
+ assert args == (self.hass, 'todomvc', fp.name)
+ assert kwargs == {
+ 'config': 5,
+ 'url_path': 'nice_url',
+ 'sidebar_icon': 'mdi:iconicon',
+ 'sidebar_title': 'Sidebar Title'
+ }