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:
Paulus Schoutsen 2019-05-02 13:59:24 -07:00 committed by GitHub
parent 7331eb1f71
commit 592e99947d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 165 deletions

View file

@ -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.

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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')