Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of server prefix #2159

Merged
merged 8 commits into from
Apr 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions panel/command/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import logging # isort:skip

from glob import glob
from urllib.parse import urljoin

from bokeh.command.subcommands.serve import Serve as _BkServe
from bokeh.command.util import build_single_handler_applications
Expand All @@ -19,7 +18,6 @@
from ..io.reload import record_modules, watch
from ..io.server import INDEX_HTML, get_static_routes
from ..io.state import state
from ..util import edit_readonly

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -147,12 +145,6 @@ def customize_kwargs(self, args, server_kwargs):
else:
files.append(f)

prefix = args.prefix or ''
if not prefix.endswith('/'):
prefix += '/'
with edit_readonly(state):
state.base_url = urljoin('/', prefix)

# Handle tranquilized functions in the supplied functions
if args.rest_provider in REST_PROVIDERS:
pattern = REST_PROVIDERS[args.rest_provider](files, args.rest_endpoint)
Expand Down
34 changes: 29 additions & 5 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from base64 import b64encode
from collections import OrderedDict
from pathlib import Path
from urllib.parse import urljoin

from bokeh.embed.bundle import (
Bundle as BkBundle, _bundle_extensions, extension_dirs,
Expand All @@ -20,6 +19,7 @@
from jinja2 import Environment, Markup, FileSystemLoader

from ..util import url_path
from .state import state


with open(Path(__file__).parent.parent / 'package.json') as f:
Expand All @@ -43,6 +43,7 @@ def conffilter(value):
RESOURCE_MODE = 'server'
PANEL_DIR = Path(__file__).parent.parent
DIST_DIR = PANEL_DIR / 'dist'
BUNDLE_DIR = DIST_DIR / 'bundled'
ASSETS_DIR = PANEL_DIR / 'assets'
BASE_TEMPLATE = _env.get_template('base.html')
DEFAULT_TITLE = "Panel Application"
Expand Down Expand Up @@ -159,12 +160,23 @@ def css_raw(self):
def js_files(self):
from ..config import config
files = super(Resources, self).js_files
js_files = files + list(config.js_files.values())
js_files = []
for js_file in files:
if (js_file.startswith(state.base_url) or js_file.startswith('static/')):
if js_file.startswith(state.base_url):
js_file = js_file[len(state.base_url):]
if state.rel_path:
js_file = f'{state.rel_path}/{js_file}'
js_files.append(js_file)
js_files += list(config.js_files.values())

# Load requirejs last to avoid interfering with other libraries
require_index = [i for i, jsf in enumerate(js_files) if 'require' in jsf]
if self.mode == 'server':
dist_dir = urljoin(self.root_url, LOCAL_DIST)
if state.rel_path:
dist_dir = f'{state.rel_path}/{LOCAL_DIST}'
else:
dist_dir = LOCAL_DIST
else:
dist_dir = CDN_DIST
if require_index:
Expand All @@ -191,7 +203,10 @@ def css_files(self):
continue
files.append(cssf)
if self.mode == 'server':
dist_dir = urljoin(self.root_url, LOCAL_DIST)
if state.rel_path:
dist_dir = f'{state.rel_path}/{LOCAL_DIST}'
else:
dist_dir = LOCAL_DIST
else:
dist_dir = CDN_DIST
for cssf in glob.glob(str(DIST_DIR / 'css' / '*.css')):
Expand Down Expand Up @@ -228,7 +243,16 @@ def from_bokeh(cls, bk_bundle):
)

def _render_js(self):
js_files = []
for js_file in self.js_files:
if (js_file.startswith(state.base_url) or js_file.startswith('static/')):
if js_file.startswith(state.base_url):
js_file = js_file[len(state.base_url):]

if state.rel_path:
js_file = f'{state.rel_path}/{js_file}'
js_files.append(js_file)
return JS_RESOURCES.render(
js_raw=self.js_raw, js_files=self.js_files,
js_raw=self.js_raw, js_files=js_files,
js_modules=self.js_modules, hashes=self.hashes
)
55 changes: 37 additions & 18 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from contextlib import contextmanager
from functools import partial, wraps
from types import FunctionType, MethodType
from urllib.parse import urlparse
from urllib.parse import urljoin, urlparse

import param
import bokeh
Expand Down Expand Up @@ -45,6 +45,7 @@
from tornado.wsgi import WSGIContainer

# Internal imports
from ..util import edit_readonly
from .reload import autoreload_watcher
from .resources import BASE_TEMPLATE, Resources, bundle_resources
from .state import state
Expand Down Expand Up @@ -168,36 +169,52 @@ async def on_session_created(self, session_context):

bokeh.command.util.Application = Application


class SessionPrefixHandler:

@contextmanager
def _session_prefix(self):
prefix = self.request.uri.replace(self.application_context._url, '')
if not prefix.endswith('/'):
prefix += '/'
base_url = urljoin('/', prefix)
rel_path = '/'.join(['..'] * self.application_context._url.strip('/').count('/'))
old_url, old_rel = state.base_url, state.rel_path
with edit_readonly(state):
state.base_url = base_url
state.rel_path = rel_path
try:
yield
finally:
with edit_readonly(state):
state.base_url = old_url
state.rel_path = old_rel

# Patch Bokeh DocHandler URL
class DocHandler(BkDocHandler):
class DocHandler(BkDocHandler, SessionPrefixHandler):

@authenticated
async def get(self, *args, **kwargs):
session = await self.get_session()
r = self.request
prefix = '/'.join(r.uri.split('/')[:-1])
state.root_url = f"{r.protocol}://{r.host}{prefix}"
resources = Resources.from_bokeh(self.application.resources())
page = server_html_page_for_session(
session, resources=resources, title=session.document.title,
template=session.document.template,
template_variables=session.document.template_variables
)

with self._session_prefix():
session = await self.get_session()
resources = Resources.from_bokeh(self.application.resources())
page = server_html_page_for_session(
session, resources=resources, title=session.document.title,
template=session.document.template,
template_variables=session.document.template_variables
)
self.set_header("Content-Type", 'text/html')
self.write(page)

per_app_patterns[0] = (r'/?', DocHandler)

# Patch Bokeh Autoload handler
class AutoloadJsHandler(BkAutoloadJsHandler):
class AutoloadJsHandler(BkAutoloadJsHandler, SessionPrefixHandler):
''' Implements a custom Tornado handler for the autoload JS chunk

'''

async def get(self, *args, **kwargs):
session = await self.get_session()

element_id = self.get_argument("bokeh-autoload-element", default=None)
if not element_id:
self.send_error(status_code=400, reason='No bokeh-autoload-element query parameter')
Expand All @@ -211,8 +228,10 @@ async def get(self, *args, **kwargs):
else:
server_url = None

resources = self.application.resources(server_url)
js = autoload_js_script(resources, session.token, element_id, app_path, absolute_url)
with self._session_prefix():
session = await self.get_session()
resources = Resources.from_bokeh(self.application.resources(server_url))
js = autoload_js_script(resources, session.token, element_id, app_path, absolute_url)

self.set_header("Content-Type", 'application/javascript')
self.write(js)
Expand Down
4 changes: 2 additions & 2 deletions panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class _state(param.Parameterized):
Object with encrypt and decrypt methods to support encryption
of secret variables including OAuth information.""")

root_url = param.String(default=None, doc="""
The root URL of the running server.""")
rel_path = param.String(default='', readonly=True, doc="""
Relative path from the current app being served to the root URL.""")

session_info = param.Dict(default={'total': 0, 'live': 0,
'sessions': OrderedDict()}, doc="""
Expand Down
59 changes: 44 additions & 15 deletions panel/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

from collections import OrderedDict
from functools import partial
from urllib.parse import urljoin

import param

Expand All @@ -22,7 +21,7 @@
from ..config import _base_config, config, panel_extension
from ..io.model import add_to_doc
from ..io.notebook import render_template
from ..io.resources import CDN_DIST, LOCAL_DIST, DIST_DIR
from ..io.resources import CDN_DIST, LOCAL_DIST, BUNDLE_DIR
from ..io.save import save
from ..io.state import state
from ..layout import Column, ListLike, GridSpec
Expand Down Expand Up @@ -403,16 +402,28 @@ class BasicTemplate(BaseTemplate):

location = param.Boolean(default=True, readonly=True)

#############
# Resources #
#############

# Resource locations for bundled resources
_CDN = CDN_DIST
_LOCAL = LOCAL_DIST

# pathlib.Path pointing to local CSS file(s)
_css = None

# pathlib.Path pointing to local JS file(s)
_js = None

# pathlib.Path pointing to local Jinja2 template
_template = None

_modifiers = {}

# External resources
_resources = {'css': {}, 'js': {}, 'js_modules': {}, 'tarball': {}}

_modifiers = {}

__abstract = True

def __init__(self, **params):
Expand Down Expand Up @@ -465,28 +476,32 @@ def _template_resources(self):
name = type(self).__name__.lower()
resources = _settings.resources(default="server")
if resources == 'server':
base_url = state.base_url[1:] if state.base_url.startswith('/') else state.base_url
dist_path = '/' + urljoin(base_url, LOCAL_DIST)
if state.rel_path:
dist_path = f'{state.rel_path}/{self._LOCAL}'
else:
dist_path = self._LOCAL
else:
dist_path = CDN_DIST
dist_path = self._CDN

# External resources
css_files = dict(self._resources.get('css', {}))
for cssname, css in css_files.items():
css_path = url_path(css)
css_files[cssname] = dist_path + f'bundled/css/{css_path}'
if (BUNDLE_DIR / 'css' / css_path.replace('/', os.path.sep)).is_file():
css_files[cssname] = dist_path + f'bundled/css/{css_path}'
js_files = dict(self._resources.get('js', {}))
for jsname, js in js_files.items():
js_path = url_path(js)
js_files[jsname] = dist_path + f'bundled/js/{js_path}'
if (BUNDLE_DIR / 'js' / js_path.replace('/', os.path.sep)).is_file():
js_files[jsname] = dist_path + f'bundled/js/{js_path}'
js_modules = dict(self._resources.get('js_modules', {}))
for jsname, js in js_modules.items():
js_path = url_path(js)
if jsname in self._resources.get('tarball', {}):
js_path += '/index.mjs'
else:
js_path += '.mjs'
if os.path.isfile(DIST_DIR / 'bundled' / 'js' / js_path.replace('/', os.path.sep)):
if os.path.isfile(BUNDLE_DIR / js_path.replace('/', os.path.sep)):
js_modules[jsname] = dist_path + f'bundled/js/{js_path}'
js_files.update(self.config.js_files)
js_modules.update(self.config.js_modules)
Expand All @@ -503,8 +518,12 @@ def _template_resources(self):
tmpl_css = cls._css if isinstance(cls._css, list) else [cls._css]
if css in tmpl_css:
tmpl_name = cls.__name__.lower()
css = os.path.basename(css)
css_files[f'base_{css}'] = dist_path + f'bundled/{tmpl_name}/{css}'
css_file = os.path.basename(css)
if (BUNDLE_DIR / tmpl_name / css_file).is_file():
css_files[f'base_{css_file}'] = dist_path + f'bundled/{tmpl_name}/{css_file}'
else:
with open(css, encoding='utf-8') as f:
raw_css.append(f.read())

# JS files
base_js = self._js
Expand All @@ -517,18 +536,28 @@ def _template_resources(self):
if js in tmpl_js:
tmpl_name = cls.__name__.lower()
js = os.path.basename(js)
js_files[f'base_{js}'] = dist_path + f'bundled/{tmpl_name}/{js}'
if (BUNDLE_DIR / tmpl_name / js).is_file():
js_files[f'base_{js}'] = dist_path + f'bundled/{tmpl_name}/{js}'

if self.theme:
theme = self.theme.find_theme(type(self))
if theme:
if theme.base_css:
basename = os.path.basename(theme.base_css)
owner = theme.param.base_css.owner.__name__.lower()
css_files['theme_base'] = dist_path + f'bundled/{owner}/{basename}'
if (BUNDLE_DIR / owner / basename).is_file():
css_files['theme_base'] = dist_path + f'bundled/{owner}/{basename}'
else:
with open(theme.base_css, encoding='utf-8') as f:
raw_css.append(f.read())
if theme.css:
basename = os.path.basename(theme.css)
css_files['theme'] = dist_path + f'bundled/{name}/{basename}'
if (BUNDLE_DIR / name / basename).is_file():
css_files['theme'] = dist_path + f'bundled/{name}/{basename}'
else:
with open(theme.base_css, encoding='utf-8') as f:
raw_css.append(f.read())

return {
'css': css_files,
'extra_css': extra_css,
Expand Down
Loading