]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
dashboard: detect language and serve correct frontend app
authorRicardo Dias <rdias@suse.com>
Mon, 30 Sep 2019 14:53:41 +0000 (15:53 +0100)
committerRicardo Dias <rdias@suse.com>
Thu, 10 Oct 2019 15:03:31 +0000 (16:03 +0100)
Signed-off-by: Ricardo Dias <rdias@suse.com>
src/pybind/mgr/dashboard/__init__.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/home.py [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/locale.helper.ts
src/pybind/mgr/dashboard/module.py

index 3c0050e8ccff76143a5edc9f8b07ead5edc2a6aa..8c825305400bb2c5b31502bfe6dcb4132f4a3965 100644 (file)
@@ -56,3 +56,4 @@ else:
     sys.modules['ceph_module'] = mock.Mock()
 
     mgr = mock.Mock()
+    mgr.get_frontend_path.side_effect = lambda: "./frontend/dist"
index fbbf7252f4ec06ec58f8eb664d58db8024d38722..04169b3997331791e0db4072b02ed4afe785ee04 100644 (file)
@@ -8,6 +8,7 @@ import inspect
 import json
 import os
 import pkgutil
+import re
 import sys
 
 import six
@@ -292,24 +293,32 @@ ENDPOINT_MAP = collections.defaultdict(list)
 def generate_controller_routes(endpoint, mapper, base_url):
     inst = endpoint.inst
     ctrl_class = endpoint.ctrl
-    endp_base_url = None
 
     if endpoint.proxy:
         conditions = None
     else:
         conditions = dict(method=[endpoint.method])
 
+    # base_url can be empty or a URL path that starts with "/"
+    # we will remove the trailing "/" if exists to help with the
+    # concatenation with the endpoint url below
+    if base_url.endswith("/"):
+        base_url = base_url[:-1]
+
     endp_url = endpoint.url
-    if base_url == "/":
-        base_url = ""
-    if endp_url == "/" and base_url:
-        endp_url = ""
-    url = "{}{}".format(base_url, endp_url)
 
-    if '/' in url[len(base_url)+1:]:
-        endp_base_url = url[:len(base_url)+1+endp_url[1:].find('/')]
+    if endp_url.find("/", 1) == -1:
+        parent_url = "{}{}".format(base_url, endp_url)
     else:
-        endp_base_url = url
+        parent_url = "{}{}".format(base_url, endp_url[:endp_url.find("/", 1)])
+
+    # parent_url might be of the form "/.../{...}" where "{...}" is a path parameter
+    # we need to remove the path parameter definition
+    parent_url = re.sub(r'(?:/\{[^}]+\})$', '', parent_url)
+    if not parent_url:  # root path case
+        parent_url = "/"
+
+    url = "{}{}".format(base_url, endp_url)
 
     logger.debug("Mapped [%s] to %s:%s restricted to %s",
                  url, ctrl_class.__name__, endpoint.action,
@@ -327,7 +336,7 @@ def generate_controller_routes(endpoint, mapper, base_url):
     mapper.connect(name, url, controller=inst, action=endpoint.action,
                    conditions=conditions)
 
-    return endp_base_url
+    return parent_url
 
 
 def generate_routes(url_prefix):
@@ -500,10 +509,13 @@ class BaseController(object):
 
         @property
         def url(self):
+            ctrl_path = self.ctrl.get_path()
+            if ctrl_path == "/":
+                ctrl_path = ""
             if self.config['path'] is not None:
-                url = "{}{}".format(self.ctrl.get_path(), self.config['path'])
+                url = "{}{}".format(ctrl_path, self.config['path'])
             else:
-                url = "{}/{}".format(self.ctrl.get_path(), self.func.__name__)
+                url = "{}/{}".format(ctrl_path, self.func.__name__)
 
             ctrl_path_params = self.ctrl.get_path_param_names(
                 self.config['path'])
diff --git a/src/pybind/mgr/dashboard/controllers/home.py b/src/pybind/mgr/dashboard/controllers/home.py
new file mode 100644 (file)
index 0000000..32613f9
--- /dev/null
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import os
+import re
+import json
+try:
+    from functools import lru_cache
+except ImportError:
+    from ..plugins.lru_cache import lru_cache
+
+import cherrypy
+from cherrypy.lib.static import serve_file
+
+from . import Controller, BaseController, Proxy
+from .. import mgr, logger
+
+
+LANGUAGES = {f for f in os.listdir(mgr.get_frontend_path())
+             if os.path.isdir(os.path.join(mgr.get_frontend_path(), f))}
+LANGUAGES_PATH_MAP = {f.lower(): {'lang': f, 'path': os.path.join(mgr.get_frontend_path(), f)}
+                      for f in LANGUAGES}
+# pre-populating with the primary language subtag
+for _lang in list(LANGUAGES_PATH_MAP.keys()):
+    if '-' in _lang:
+        LANGUAGES_PATH_MAP[_lang.split('-')[0]] = {
+            'lang': LANGUAGES_PATH_MAP[_lang]['lang'], 'path': LANGUAGES_PATH_MAP[_lang]['path']}
+
+
+def _get_default_language():
+    with open("{}/../package.json".format(mgr.get_frontend_path()), "r") as f:
+        config = json.load(f)
+    return config['config']['locale']
+
+
+DEFAULT_LANGUAGE = _get_default_language()
+DEFAULT_LANGUAGE_PATH = os.path.join(mgr.get_frontend_path(), DEFAULT_LANGUAGE)
+
+
+@Controller("/", secure=False)
+class HomeController(BaseController):
+    LANG_TAG_SEQ_RE = re.compile(r'\s*([^,]+)\s*,?\s*')
+    LANG_TAG_RE = re.compile(
+        r'^(?P<locale>[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})?)(;q=(?P<weight>[01]\.\d{0,3}))?$')
+    MAX_ACCEPTED_LANGS = 10
+
+    @lru_cache()
+    def _parse_accept_language(self, accept_lang_header):
+        result = []
+        for i, m in enumerate(self.LANG_TAG_SEQ_RE.finditer(accept_lang_header)):
+            if i >= self.MAX_ACCEPTED_LANGS:
+                logger.debug("reached max accepted languages, skipping remaining")
+                break
+
+            tag_match = self.LANG_TAG_RE.match(m[1])
+            if tag_match is None:
+                raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header")
+            locale = tag_match.group('locale').lower()
+            weight = tag_match.group('weight')
+            if weight:
+                try:
+                    ratio = float(weight)
+                except ValueError:
+                    raise cherrypy.HTTPError(400, "Malformed 'Accept-Language' header")
+            else:
+                ratio = 1.0
+            result.append((locale, ratio))
+
+        result.sort(key=lambda l: l[0])
+        result.sort(key=lambda l: l[1], reverse=True)
+        logger.debug("language preference: %s", result)
+        return [l[0] for l in result]
+
+    def _language_dir(self, langs):
+        for lang in langs:
+            if lang in LANGUAGES_PATH_MAP:
+                logger.debug("found directory for language '%s'", lang)
+                cherrypy.response.headers['Content-Language'] = LANGUAGES_PATH_MAP[lang]['lang']
+                return LANGUAGES_PATH_MAP[lang]['path']
+
+        logger.debug("Languages '%s' not available, falling back to %s", langs, DEFAULT_LANGUAGE)
+        cherrypy.response.headers['Content-Language'] = DEFAULT_LANGUAGE
+        return DEFAULT_LANGUAGE_PATH
+
+    @Proxy()
+    def __call__(self, path, **params):
+        if not path:
+            path = "index.html"
+
+        if 'cd-lang' in cherrypy.request.cookie:
+            langs = [cherrypy.request.cookie['cd-lang'].value.lower()]
+            logger.debug("frontend language from cookie: %s", langs)
+        else:
+            if 'Accept-Language' in cherrypy.request.headers:
+                accept_lang_header = cherrypy.request.headers['Accept-Language']
+                langs = self._parse_accept_language(accept_lang_header)
+            else:
+                langs = [DEFAULT_LANGUAGE]
+            logger.debug("frontend language from headers: %s", langs)
+
+        base_dir = self._language_dir(langs)
+        full_path = os.path.join(base_dir, path)
+        logger.debug("serving static content: %s", full_path)
+        if 'Vary' in cherrypy.response.headers:
+            cherrypy.response.headers['Vary'] = "{}, Accept-Language"
+        else:
+            cherrypy.response.headers['Vary'] = "Accept-Language"
+        return serve_file(full_path)
index 21fb857050ddcd9ad2e74ce784d1289f47886e31..5c5b9d2209b569902eef5b4adc4a9f883a59a037 100644 (file)
@@ -52,6 +52,7 @@ export class LocaleHelper {
   }
 
   static setLocale(lang: string) {
+    document.cookie = `cd-lang=${lang}`;
     window.localStorage.setItem('lang', lang);
   }
 
index dcfa42fcaa9e103c3d96207819ead86a1c64d9f5..7cf4605e16f385238e573250cb173b75f4bb2605 100644 (file)
@@ -327,13 +327,7 @@ class Module(MgrModule, CherryPyConfig):
 
         mapper, parent_urls = generate_routes(self.url_prefix)
 
-        config = {
-            self.url_prefix or '/': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.dir': self.get_frontend_path(),
-                'tools.staticdir.index': 'index.html'
-            }
-        }
+        config = {}
         for purl in parent_urls:
             config[purl] = {
                 'request.dispatch': mapper