Convert frontend to do client-side modern JS detection (#23618)
* Convert frontend to do client-side async pick * Further cleanup * Updated frontend to 20190502.0 * Fix template caching * Remove es5 test * Lint * Update description
This commit is contained in:
parent
7331eb1f71
commit
592e99947d
5 changed files with 61 additions and 165 deletions
|
@ -3,7 +3,7 @@ import asyncio
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
import pathlib
|
||||
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
@ -11,7 +11,6 @@ import jinja2
|
|||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.components.http.const import KEY_AUTHENTICATED
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.config import find_config_file, load_yaml_config_file
|
||||
from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED
|
||||
|
@ -27,14 +26,13 @@ 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'
|
||||
JS_DEFAULT_OPTION = 'auto'
|
||||
JS_OPTIONS = ['es5', 'latest', 'auto']
|
||||
|
||||
DEFAULT_THEME_COLOR = '#03A9F4'
|
||||
|
||||
MANIFEST_JSON = {
|
||||
'background_color': '#FFFFFF',
|
||||
'description': 'Open-source home automation platform running on Python 3.',
|
||||
'description':
|
||||
'Home automation platform that puts local control and privacy first.',
|
||||
'dir': 'ltr',
|
||||
'display': 'standalone',
|
||||
'icons': [],
|
||||
|
@ -73,10 +71,9 @@ CONFIG_SCHEMA = vol.Schema({
|
|||
}),
|
||||
vol.Optional(CONF_EXTRA_HTML_URL):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_EXTRA_HTML_URL_ES5):
|
||||
vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_JS_VERSION, default=JS_DEFAULT_OPTION):
|
||||
vol.In(JS_OPTIONS)
|
||||
# We no longer use these options.
|
||||
vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all,
|
||||
vol.Optional(CONF_JS_VERSION): cv.match_all,
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -191,6 +188,15 @@ def add_manifest_json_key(key, val):
|
|||
MANIFEST_JSON[key] = val
|
||||
|
||||
|
||||
def _frontend_root(dev_repo_path):
|
||||
"""Return root path to the frontend files."""
|
||||
if dev_repo_path is not None:
|
||||
return pathlib.Path(dev_repo_path) / 'hass_frontend'
|
||||
|
||||
import hass_frontend
|
||||
return hass_frontend.where()
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the serving of the frontend."""
|
||||
await async_setup_frontend_storage(hass)
|
||||
|
@ -207,39 +213,28 @@ async def async_setup(hass, config):
|
|||
|
||||
repo_path = conf.get(CONF_FRONTEND_REPO)
|
||||
is_dev = repo_path is not None
|
||||
hass.data[DATA_JS_VERSION] = js_version = conf.get(CONF_JS_VERSION)
|
||||
root_path = _frontend_root(repo_path)
|
||||
|
||||
if is_dev:
|
||||
hass_frontend_path = os.path.join(repo_path, 'hass_frontend')
|
||||
hass_frontend_es5_path = os.path.join(repo_path, 'hass_frontend_es5')
|
||||
else:
|
||||
import hass_frontend
|
||||
import hass_frontend_es5
|
||||
hass_frontend_path = hass_frontend.where()
|
||||
hass_frontend_es5_path = hass_frontend_es5.where()
|
||||
for path, should_cache in (
|
||||
("service_worker.js", False),
|
||||
("robots.txt", False),
|
||||
("onboarding.html", True),
|
||||
("static", True),
|
||||
("frontend_latest", True),
|
||||
("frontend_es5", True),
|
||||
):
|
||||
hass.http.register_static_path(
|
||||
"/{}".format(path), str(root_path / path), should_cache)
|
||||
|
||||
hass.http.register_static_path(
|
||||
"/service_worker_es5.js",
|
||||
os.path.join(hass_frontend_es5_path, "service_worker.js"), False)
|
||||
hass.http.register_static_path(
|
||||
"/service_worker.js",
|
||||
os.path.join(hass_frontend_path, "service_worker.js"), False)
|
||||
hass.http.register_static_path(
|
||||
"/robots.txt",
|
||||
os.path.join(hass_frontend_path, "robots.txt"), False)
|
||||
hass.http.register_static_path("/static", hass_frontend_path, not is_dev)
|
||||
hass.http.register_static_path(
|
||||
"/frontend_latest", hass_frontend_path, not is_dev)
|
||||
hass.http.register_static_path(
|
||||
"/frontend_es5", hass_frontend_es5_path, not is_dev)
|
||||
"/auth/authorize", str(root_path / "authorize.html"), False)
|
||||
|
||||
local = hass.config.path('www')
|
||||
if os.path.isdir(local):
|
||||
hass.http.register_static_path("/local", local, not is_dev)
|
||||
|
||||
index_view = IndexView(repo_path, js_version)
|
||||
index_view = IndexView(repo_path)
|
||||
hass.http.register_view(index_view)
|
||||
hass.http.register_view(AuthorizeView(repo_path, js_version))
|
||||
|
||||
@callback
|
||||
def async_finalize_panel(panel):
|
||||
|
@ -263,13 +258,9 @@ async def async_setup(hass, config):
|
|||
|
||||
if DATA_EXTRA_HTML_URL not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL] = set()
|
||||
if DATA_EXTRA_HTML_URL_ES5 not in hass.data:
|
||||
hass.data[DATA_EXTRA_HTML_URL_ES5] = set()
|
||||
|
||||
for url in conf.get(CONF_EXTRA_HTML_URL, []):
|
||||
add_extra_html_url(hass, url, False)
|
||||
for url in conf.get(CONF_EXTRA_HTML_URL_ES5, []):
|
||||
add_extra_html_url(hass, url, True)
|
||||
|
||||
_async_setup_themes(hass, conf.get(CONF_THEMES))
|
||||
|
||||
|
@ -327,36 +318,6 @@ def _async_setup_themes(hass, themes):
|
|||
hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes)
|
||||
|
||||
|
||||
class AuthorizeView(HomeAssistantView):
|
||||
"""Serve the frontend."""
|
||||
|
||||
url = '/auth/authorize'
|
||||
name = 'auth:authorize'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, repo_path, js_option):
|
||||
"""Initialize the frontend view."""
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
|
||||
async def get(self, request: web.Request):
|
||||
"""Redirect to the authorize page."""
|
||||
latest = self.repo_path is not None or \
|
||||
_is_latest(self.js_option, request)
|
||||
|
||||
if latest:
|
||||
base = 'frontend_latest'
|
||||
else:
|
||||
base = 'frontend_es5'
|
||||
|
||||
location = "/{}/authorize.html{}".format(
|
||||
base, str(request.url.relative())[15:])
|
||||
|
||||
return web.Response(status=302, headers={
|
||||
'location': location
|
||||
})
|
||||
|
||||
|
||||
class IndexView(HomeAssistantView):
|
||||
"""Serve the frontend."""
|
||||
|
||||
|
@ -364,70 +325,48 @@ class IndexView(HomeAssistantView):
|
|||
name = 'frontend:index'
|
||||
requires_auth = False
|
||||
|
||||
def __init__(self, repo_path, js_option):
|
||||
def __init__(self, repo_path):
|
||||
"""Initialize the frontend view."""
|
||||
self.repo_path = repo_path
|
||||
self.js_option = js_option
|
||||
self._template_cache = {}
|
||||
self._template_cache = None
|
||||
|
||||
def get_template(self, latest):
|
||||
def get_template(self):
|
||||
"""Get template."""
|
||||
if self.repo_path is not None:
|
||||
root = os.path.join(self.repo_path, 'hass_frontend')
|
||||
elif latest:
|
||||
import hass_frontend
|
||||
root = hass_frontend.where()
|
||||
else:
|
||||
import hass_frontend_es5
|
||||
root = hass_frontend_es5.where()
|
||||
|
||||
tpl = self._template_cache.get(root)
|
||||
|
||||
tpl = self._template_cache
|
||||
if tpl is None:
|
||||
with open(os.path.join(root, 'index.html')) as file:
|
||||
with open(
|
||||
str(_frontend_root(self.repo_path) / 'index.html')
|
||||
) as file:
|
||||
tpl = jinja2.Template(file.read())
|
||||
|
||||
# Cache template if not running from repository
|
||||
if self.repo_path is None:
|
||||
self._template_cache[root] = tpl
|
||||
self._template_cache = tpl
|
||||
|
||||
return tpl
|
||||
|
||||
async def get(self, request, extra=None):
|
||||
"""Serve the index view."""
|
||||
hass = request.app['hass']
|
||||
latest = self.repo_path is not None or \
|
||||
_is_latest(self.js_option, request)
|
||||
|
||||
if not hass.components.onboarding.async_is_onboarded():
|
||||
if latest:
|
||||
location = '/frontend_latest/onboarding.html'
|
||||
else:
|
||||
location = '/frontend_es5/onboarding.html'
|
||||
|
||||
return web.Response(status=302, headers={
|
||||
'location': location
|
||||
'location': '/onboarding.html'
|
||||
})
|
||||
|
||||
no_auth = '1'
|
||||
if not request[KEY_AUTHENTICATED]:
|
||||
# do not try to auto connect on load
|
||||
no_auth = '0'
|
||||
template = self._template_cache
|
||||
|
||||
template = await hass.async_add_job(self.get_template, latest)
|
||||
if template is None:
|
||||
template = await hass.async_add_executor_job(self.get_template)
|
||||
|
||||
extra_key = DATA_EXTRA_HTML_URL if latest else DATA_EXTRA_HTML_URL_ES5
|
||||
|
||||
template_params = dict(
|
||||
no_auth=no_auth,
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[extra_key],
|
||||
use_oauth='1'
|
||||
return web.Response(
|
||||
text=template.render(
|
||||
theme_color=MANIFEST_JSON['theme_color'],
|
||||
extra_urls=hass.data[DATA_EXTRA_HTML_URL],
|
||||
),
|
||||
content_type='text/html'
|
||||
)
|
||||
|
||||
return web.Response(text=template.render(**template_params),
|
||||
content_type='text/html')
|
||||
|
||||
|
||||
class ManifestJSONView(HomeAssistantView):
|
||||
"""View to return a manifest.json."""
|
||||
|
@ -443,38 +382,6 @@ class ManifestJSONView(HomeAssistantView):
|
|||
return web.Response(text=msg, content_type="application/manifest+json")
|
||||
|
||||
|
||||
def _is_latest(js_option, request):
|
||||
"""
|
||||
Return whether we should serve latest untranspiled code.
|
||||
|
||||
Set according to user's preference and URL override.
|
||||
"""
|
||||
import hass_frontend
|
||||
|
||||
if request is None:
|
||||
return js_option == 'latest'
|
||||
|
||||
# latest in query
|
||||
if 'latest' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'latest' in urlparse(request.headers['Referer']).query):
|
||||
return True
|
||||
|
||||
# es5 in query
|
||||
if 'es5' in request.query or (
|
||||
request.headers.get('Referer') and
|
||||
'es5' in urlparse(request.headers['Referer']).query):
|
||||
return False
|
||||
|
||||
# non-auto option in config
|
||||
if js_option != 'auto':
|
||||
return js_option == 'latest'
|
||||
|
||||
useragent = request.headers.get('User-Agent')
|
||||
|
||||
return useragent and hass_frontend.version(useragent)
|
||||
|
||||
|
||||
@callback
|
||||
def websocket_get_panels(hass, connection, msg):
|
||||
"""Handle get panels command.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/components/frontend",
|
||||
"requirements": [
|
||||
"home-assistant-frontend==20190427.0"
|
||||
"home-assistant-frontend==20190502.0"
|
||||
],
|
||||
"dependencies": [
|
||||
"api",
|
||||
|
|
|
@ -560,7 +560,7 @@ hole==0.3.0
|
|||
holidays==0.9.10
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20190427.0
|
||||
home-assistant-frontend==20190502.0
|
||||
|
||||
# homeassistant.components.zwave
|
||||
homeassistant-pyozw==0.1.4
|
||||
|
|
|
@ -145,7 +145,7 @@ hdate==0.8.7
|
|||
holidays==0.9.10
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20190427.0
|
||||
home-assistant-frontend==20190502.0
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
homekit[IP]==0.14.0
|
||||
|
|
|
@ -89,10 +89,6 @@ def test_frontend_and_static(mock_http_client, mock_onboarded):
|
|||
@asyncio.coroutine
|
||||
def test_dont_cache_service_worker(mock_http_client):
|
||||
"""Test that we don't cache the service worker."""
|
||||
resp = yield from mock_http_client.get('/service_worker_es5.js')
|
||||
assert resp.status == 200
|
||||
assert 'cache-control' not in resp.headers
|
||||
|
||||
resp = yield from mock_http_client.get('/service_worker.js')
|
||||
assert resp.status == 200
|
||||
assert 'cache-control' not in resp.headers
|
||||
|
@ -233,16 +229,7 @@ def test_extra_urls(mock_http_client_with_urls, mock_onboarded):
|
|||
resp = yield from mock_http_client_with_urls.get('/states?latest')
|
||||
assert resp.status == 200
|
||||
text = yield from resp.text()
|
||||
assert text.find("href='https://domain.com/my_extra_url.html'") >= 0
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def test_extra_urls_es5(mock_http_client_with_urls, mock_onboarded):
|
||||
"""Test that es5 extra urls are loaded."""
|
||||
resp = yield from mock_http_client_with_urls.get('/states?es5')
|
||||
assert resp.status == 200
|
||||
text = yield from resp.text()
|
||||
assert text.find("href='https://domain.com/my_extra_url_es5.html'") >= 0
|
||||
assert text.find('href="https://domain.com/my_extra_url.html"') >= 0
|
||||
|
||||
|
||||
async def test_get_panels(hass, hass_ws_client):
|
||||
|
@ -330,15 +317,17 @@ async def test_auth_authorize(mock_http_client):
|
|||
resp = await mock_http_client.get(
|
||||
'/auth/authorize?response_type=code&client_id=https://localhost/&'
|
||||
'redirect_uri=https://localhost/&state=123%23456')
|
||||
assert resp.status == 200
|
||||
# No caching of auth page.
|
||||
assert 'cache-control' not in resp.headers
|
||||
|
||||
assert str(resp.url.relative()) == (
|
||||
'/frontend_es5/authorize.html?response_type=code&client_id='
|
||||
'https://localhost/&redirect_uri=https://localhost/&state=123%23456')
|
||||
text = await resp.text()
|
||||
|
||||
resp = await mock_http_client.get(
|
||||
'/auth/authorize?latest&response_type=code&client_id='
|
||||
'https://localhost/&redirect_uri=https://localhost/&state=123%23456')
|
||||
# Test we can retrieve authorize.js
|
||||
authorizejs = re.search(
|
||||
r'(?P<app>\/frontend_latest\/authorize.[A-Za-z0-9]{8}.js)', text)
|
||||
|
||||
assert str(resp.url.relative()) == (
|
||||
'/frontend_latest/authorize.html?latest&response_type=code&client_id='
|
||||
'https://localhost/&redirect_uri=https://localhost/&state=123%23456')
|
||||
assert authorizejs is not None, text
|
||||
resp = await mock_http_client.get(authorizejs.groups(0)[0])
|
||||
assert resp.status == 200
|
||||
assert 'public' in resp.headers.get('cache-control')
|
||||
|
|
Loading…
Add table
Reference in a new issue