Dynamic panels (#24184)
* Don't require all panel urls to be registered * Allow removing panels, fire event when panels updated
This commit is contained in:
parent
ca89d6184c
commit
acc9fd0382
14 changed files with 133 additions and 58 deletions
|
@ -36,7 +36,7 @@ async def async_setup(hass, config):
|
|||
hass.http.register_view(CalendarEventView(component))
|
||||
|
||||
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
|
||||
# await hass.components.frontend.async_register_built_in_panel(
|
||||
# hass.components.frontend.async_register_built_in_panel(
|
||||
# 'calendar', 'calendar', 'hass:calendar')
|
||||
|
||||
await component.async_setup(config)
|
||||
|
|
|
@ -30,7 +30,7 @@ ON_DEMAND = ('zwave',)
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the config component."""
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'config', 'config', 'hass:settings', require_admin=True)
|
||||
|
||||
async def setup_panel(panel_name):
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
"""Handle the frontend for Home Assistant."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
@ -26,6 +25,7 @@ CONF_EXTRA_HTML_URL = 'extra_html_url'
|
|||
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
|
||||
CONF_FRONTEND_REPO = 'development_repo'
|
||||
CONF_JS_VERSION = 'javascript_version'
|
||||
EVENT_PANELS_UPDATED = 'panels_updated'
|
||||
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
|
||||
|
@ -97,6 +97,28 @@ SCHEMA_GET_TRANSLATIONS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
|||
})
|
||||
|
||||
|
||||
def generate_negative_index_regex():
|
||||
"""Generate regex for index."""
|
||||
skip = [
|
||||
# files
|
||||
"service_worker.js",
|
||||
"robots.txt",
|
||||
"onboarding.html",
|
||||
"manifest.json",
|
||||
]
|
||||
for folder in (
|
||||
"static",
|
||||
"frontend_latest",
|
||||
"frontend_es5",
|
||||
"local",
|
||||
"auth",
|
||||
"api",
|
||||
):
|
||||
# Regex matching static, static/, static/index.html
|
||||
skip.append("{}(/|/.+|)".format(folder))
|
||||
return r"(?!(" + "|".join(skip) + r")).*"
|
||||
|
||||
|
||||
class Panel:
|
||||
"""Abstract class for panels."""
|
||||
|
||||
|
@ -128,15 +150,6 @@ class Panel:
|
|||
self.config = config
|
||||
self.require_admin = require_admin
|
||||
|
||||
@callback
|
||||
def async_register_index_routes(self, router, index_view):
|
||||
"""Register routes for panel to be served by index view."""
|
||||
router.add_route(
|
||||
'get', '/{}'.format(self.frontend_url_path), index_view.get)
|
||||
router.add_route(
|
||||
'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
|
||||
index_view.get)
|
||||
|
||||
@callback
|
||||
def to_response(self):
|
||||
"""Panel as dictionary."""
|
||||
|
@ -151,26 +164,36 @@ class Panel:
|
|||
|
||||
|
||||
@bind_hass
|
||||
async def async_register_built_in_panel(hass, component_name,
|
||||
sidebar_title=None, sidebar_icon=None,
|
||||
frontend_url_path=None, config=None,
|
||||
require_admin=False):
|
||||
@callback
|
||||
def async_register_built_in_panel(hass, component_name,
|
||||
sidebar_title=None, sidebar_icon=None,
|
||||
frontend_url_path=None, config=None,
|
||||
require_admin=False):
|
||||
"""Register a built-in panel."""
|
||||
panel = Panel(component_name, sidebar_title, sidebar_icon,
|
||||
frontend_url_path, config, require_admin)
|
||||
|
||||
panels = hass.data.get(DATA_PANELS)
|
||||
if panels is None:
|
||||
panels = hass.data[DATA_PANELS] = {}
|
||||
panels = hass.data.setdefault(DATA_PANELS, {})
|
||||
|
||||
if panel.frontend_url_path in panels:
|
||||
_LOGGER.warning("Overwriting component %s", panel.frontend_url_path)
|
||||
|
||||
if DATA_FINALIZE_PANEL in hass.data:
|
||||
hass.data[DATA_FINALIZE_PANEL](panel)
|
||||
|
||||
panels[panel.frontend_url_path] = panel
|
||||
|
||||
hass.bus.async_fire(EVENT_PANELS_UPDATED)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
def async_remove_panel(hass, frontend_url_path):
|
||||
"""Remove a built-in panel."""
|
||||
panel = hass.data.get(DATA_PANELS, {}).pop(frontend_url_path, None)
|
||||
|
||||
if panel is None:
|
||||
_LOGGER.warning("Removing unknown panel %s", frontend_url_path)
|
||||
|
||||
hass.bus.async_fire(EVENT_PANELS_UPDATED)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@callback
|
||||
|
@ -233,28 +256,14 @@ async def async_setup(hass, config):
|
|||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local, not is_dev)
|
||||
|
||||
index_view = IndexView(repo_path)
|
||||
hass.http.register_view(index_view)
|
||||
hass.http.register_view(IndexView(repo_path))
|
||||
|
||||
@callback
|
||||
def async_finalize_panel(panel):
|
||||
"""Finalize setup of a panel."""
|
||||
panel.async_register_index_routes(hass.http.app.router, index_view)
|
||||
for panel in ('kiosk', 'states', 'profile'):
|
||||
async_register_built_in_panel(hass, panel)
|
||||
|
||||
await asyncio.wait(
|
||||
[async_register_built_in_panel(hass, panel) for panel in (
|
||||
'kiosk', 'states', 'profile')])
|
||||
await asyncio.wait(
|
||||
[async_register_built_in_panel(hass, panel, require_admin=True)
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template', 'dev-mqtt')])
|
||||
|
||||
hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
|
||||
|
||||
# Finalize registration of panels that registered before frontend was setup
|
||||
# This includes the built-in panels from line above.
|
||||
for panel in hass.data[DATA_PANELS].values():
|
||||
async_finalize_panel(panel)
|
||||
for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
|
||||
'dev-template', 'dev-mqtt'):
|
||||
async_register_built_in_panel(hass, panel, require_admin=True)
|
||||
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
|
@ -324,6 +333,9 @@ class IndexView(HomeAssistantView):
|
|||
url = '/'
|
||||
name = 'frontend:index'
|
||||
requires_auth = False
|
||||
extra_urls = [
|
||||
"/{extra:%s}" % generate_negative_index_regex()
|
||||
]
|
||||
|
||||
def __init__(self, repo_path):
|
||||
"""Initialize the frontend view."""
|
||||
|
@ -349,6 +361,10 @@ class IndexView(HomeAssistantView):
|
|||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
|
||||
if (request.path != '/' and
|
||||
request.url.parts[1] not in hass.data[DATA_PANELS]):
|
||||
raise web.HTTPNotFound
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
return web.Response(status=302, headers={
|
||||
'location': '/onboarding.html'
|
||||
|
|
|
@ -61,7 +61,7 @@ class HassIOAddonPanel(HomeAssistantView):
|
|||
|
||||
async def delete(self, request, addon):
|
||||
"""Handle remove add-on panel requests."""
|
||||
# Currently not supported by backend / frontend
|
||||
self.hass.components.frontend.async_remove_panel(addon)
|
||||
return web.Response()
|
||||
|
||||
async def get_panels(self):
|
||||
|
|
|
@ -252,7 +252,7 @@ async def async_setup(hass, config):
|
|||
use_include_order = conf.get(CONF_ORDER)
|
||||
|
||||
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'history', 'history', 'hass:poll-box')
|
||||
|
||||
return True
|
||||
|
|
|
@ -102,7 +102,7 @@ async def async_setup(hass, config):
|
|||
|
||||
hass.http.register_view(LogbookView(config.get(DOMAIN, {})))
|
||||
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'logbook', 'logbook', 'hass:format-list-bulleted-type')
|
||||
|
||||
hass.services.async_register(
|
||||
|
|
|
@ -53,7 +53,7 @@ async def async_setup(hass, config):
|
|||
# Pass in default to `get` because defaults not set if loaded as dep
|
||||
mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE)
|
||||
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
DOMAIN, config={
|
||||
'mode': mode
|
||||
})
|
||||
|
|
|
@ -30,7 +30,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
|
|||
async def async_setup(hass, config):
|
||||
"""Track states and offer events for mailboxes."""
|
||||
mailboxes = []
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'mailbox', 'mailbox', 'mdi:mailbox')
|
||||
hass.http.register_view(MailboxPlatformsView(mailboxes))
|
||||
hass.http.register_view(MailboxMessageView(mailboxes))
|
||||
|
|
|
@ -4,6 +4,6 @@ DOMAIN = 'map'
|
|||
|
||||
async def async_setup(hass, config):
|
||||
"""Register the built-in map panel."""
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'map', 'map', 'hass:tooltip-account')
|
||||
return True
|
||||
|
|
|
@ -112,7 +112,7 @@ async def async_register_panel(
|
|||
|
||||
config['_panel_custom'] = custom_panel_config
|
||||
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
component_name='custom',
|
||||
sidebar_title=sidebar_title,
|
||||
sidebar_icon=sidebar_icon,
|
||||
|
|
|
@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
async def async_setup(hass, config):
|
||||
"""Set up the iFrame frontend panels."""
|
||||
for url_path, info in config[DOMAIN].items():
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
|
||||
url_path, {'url': info[CONF_URL]},
|
||||
require_admin=info[CONF_REQUIRE_ADMIN])
|
||||
|
|
|
@ -117,7 +117,7 @@ def async_setup(hass, config):
|
|||
'What is on my shopping list'
|
||||
])
|
||||
|
||||
yield from hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'shopping-list', 'shopping_list', 'mdi:cart')
|
||||
|
||||
hass.components.websocket_api.async_register_command(
|
||||
|
|
|
@ -14,11 +14,13 @@ from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED
|
|||
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
|
||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||
from homeassistant.components.frontend import EVENT_PANELS_UPDATED
|
||||
|
||||
# These are events that do not contain any sensitive data
|
||||
# Except for state_changed, which is handled accordingly.
|
||||
SUBSCRIBE_WHITELIST = {
|
||||
EVENT_COMPONENT_LOADED,
|
||||
EVENT_PANELS_UPDATED,
|
||||
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED,
|
||||
EVENT_SERVICE_REGISTERED,
|
||||
EVENT_SERVICE_REMOVED,
|
||||
|
|
|
@ -8,10 +8,11 @@ import pytest
|
|||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components.frontend import (
|
||||
DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL,
|
||||
CONF_EXTRA_HTML_URL_ES5)
|
||||
CONF_EXTRA_HTML_URL_ES5, generate_negative_index_regex,
|
||||
EVENT_PANELS_UPDATED)
|
||||
from homeassistant.components.websocket_api.const import TYPE_RESULT
|
||||
|
||||
from tests.common import mock_coro
|
||||
from tests.common import mock_coro, async_capture_events
|
||||
|
||||
|
||||
CONFIG_THEMES = {
|
||||
|
@ -232,12 +233,21 @@ def test_extra_urls(mock_http_client_with_urls, mock_onboarded):
|
|||
assert text.find('href="https://domain.com/my_extra_url.html"') >= 0
|
||||
|
||||
|
||||
async def test_get_panels(hass, hass_ws_client):
|
||||
async def test_get_panels(hass, hass_ws_client, mock_http_client):
|
||||
"""Test get_panels command."""
|
||||
await async_setup_component(hass, 'frontend', {})
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
events = async_capture_events(hass, EVENT_PANELS_UPDATED)
|
||||
|
||||
resp = await mock_http_client.get('/map')
|
||||
assert resp.status == 404
|
||||
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'map', 'Map', 'mdi:tooltip-account', require_admin=True)
|
||||
|
||||
resp = await mock_http_client.get('/map')
|
||||
assert resp.status == 200
|
||||
|
||||
assert len(events) == 1
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({
|
||||
'id': 5,
|
||||
|
@ -255,14 +265,21 @@ async def test_get_panels(hass, hass_ws_client):
|
|||
assert msg['result']['map']['title'] == 'Map'
|
||||
assert msg['result']['map']['require_admin'] is True
|
||||
|
||||
hass.components.frontend.async_remove_panel('map')
|
||||
|
||||
resp = await mock_http_client.get('/map')
|
||||
assert resp.status == 404
|
||||
|
||||
assert len(events) == 2
|
||||
|
||||
|
||||
async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user):
|
||||
"""Test get_panels command."""
|
||||
hass_admin_user.groups = []
|
||||
await async_setup_component(hass, 'frontend', {})
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'map', 'Map', 'mdi:tooltip-account', require_admin=True)
|
||||
await hass.components.frontend.async_register_built_in_panel(
|
||||
hass.components.frontend.async_register_built_in_panel(
|
||||
'history', 'History', 'mdi:history')
|
||||
|
||||
client = await hass_ws_client(hass)
|
||||
|
@ -331,3 +348,43 @@ async def test_auth_authorize(mock_http_client):
|
|||
resp = await mock_http_client.get(authorizejs.groups(0)[0])
|
||||
assert resp.status == 200
|
||||
assert 'public' in resp.headers.get('cache-control')
|
||||
|
||||
|
||||
def test_index_regex():
|
||||
"""Test the index regex."""
|
||||
pattern = re.compile('/' + generate_negative_index_regex())
|
||||
|
||||
for should_match in (
|
||||
'/',
|
||||
'/lovelace',
|
||||
'/lovelace/default_view',
|
||||
'/map',
|
||||
'/config',
|
||||
):
|
||||
assert pattern.match(should_match), should_match
|
||||
|
||||
for should_not_match in (
|
||||
'/service_worker.js',
|
||||
'/manifest.json',
|
||||
'/onboarding.html',
|
||||
'/manifest.json',
|
||||
'static',
|
||||
'static/',
|
||||
'static/index.html',
|
||||
'frontend_latest',
|
||||
'frontend_latest/',
|
||||
'frontend_latest/index.html',
|
||||
'frontend_es5',
|
||||
'frontend_es5/',
|
||||
'frontend_es5/index.html',
|
||||
'local',
|
||||
'local/',
|
||||
'local/index.html',
|
||||
'auth',
|
||||
'auth/',
|
||||
'auth/index.html',
|
||||
'/api',
|
||||
'/api/',
|
||||
'/api/logbook',
|
||||
):
|
||||
assert not pattern.match(should_not_match), should_not_match
|
||||
|
|
Loading…
Add table
Reference in a new issue