Jelajahi Sumber

Merge commit 'a72574fc059c79b4f05eacbd17a7b552b929b311' into v1

Falko Schindler 2 tahun lalu
induk
melakukan
3d9d9b5b8c
7 mengubah file dengan 67 tambahan dan 33 penghapusan
  1. 7 7
      main.py
  2. 8 4
      nicegui/client.py
  3. 12 8
      nicegui/nicegui.py
  4. 10 3
      nicegui/page.py
  5. 7 7
      nicegui/templates/index.html
  6. 4 4
      nicegui/vue.py
  7. 19 0
      tests/test_link.py

+ 7 - 7
main.py

@@ -17,14 +17,14 @@ def add_head_html() -> None:
     ui.add_head_html(docutils.core.publish_parts('', writer_name='html')['stylesheet'])
     ui.add_head_html(f'<style>{HtmlFormatter(nobackground=True).get_style_defs(".codehilite")}</style>')
     ui.add_head_html('''
-        <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
-        <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
-        <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
-        <link rel="manifest" href="/favicon/site.webmanifest">
-        <link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#000000">
-        <link rel="shortcut icon" href="/favicon/favicon.ico">
+        <link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png">
+        <link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png">
+        <link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png">
+        <link rel="manifest" href="favicon/site.webmanifest">
+        <link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#000000">
+        <link rel="shortcut icon" href="favicon/favicon.ico">
         <meta name="msapplication-TileColor" content="#ffffff">
-        <meta name="msapplication-config" content="/favicon/browserconfig.xml">
+        <meta name="msapplication-config" content="favicon/browserconfig.xml">
         <meta name="theme-color" content="#ffffff">
     ''')  # https://realfavicongenerator.net/
     ui.add_head_html(f'''

+ 8 - 4
nicegui/client.py

@@ -5,6 +5,7 @@ import uuid
 from pathlib import Path
 from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
 
+from fastapi import Request
 from fastapi.responses import HTMLResponse
 
 from . import globals, vue
@@ -20,7 +21,8 @@ TEMPLATE = (Path(__file__).parent / 'templates' / 'index.html').read_text()
 
 class Client:
 
-    def __init__(self, page: 'page', *, shared: bool = False) -> None:
+    def __init__(self, page: 'page', *, request: Optional[Request] = None, shared: bool = False) -> None:
+        self.request = request
         self.id = globals.next_client_id
         globals.next_client_id += 1
         globals.clients[self.id] = self
@@ -60,7 +62,8 @@ class Client:
     def __exit__(self, *_):
         self.content.__exit__()
 
-    def build_response(self, status_code: int = 200) -> HTMLResponse:
+    def build_response(self, request: Request, status_code: int = 200) -> HTMLResponse:
+        prefix = request.headers.get('X-Forwarded-Prefix', '')
         vue_html, vue_styles, vue_scripts = vue.generate_vue_content()
         elements = json.dumps({id: element.to_dict() for id, element in self.elements.items()})
         return HTMLResponse(
@@ -71,10 +74,11 @@ class Client:
             .replace(r'{{ head_html | safe }}', self.head_html)
             .replace(r'{{ body_html | safe }}', f'{self.body_html}\n{vue_html}\n{vue_styles}')
             .replace(r'{{ vue_scripts | safe }}', vue_scripts)
-            .replace(r'{{ js_imports | safe }}', vue.generate_js_imports())
+            .replace(r'{{ js_imports | safe }}', vue.generate_js_imports(prefix))
             .replace(r'{{ title }}', self.page.resolve_title())
             .replace(r'{{ favicon_url }}', get_favicon_url(self.page))
-            .replace(r'{{ dark }}', str(self.page.resolve_dark())),
+            .replace(r'{{ dark }}', str(self.page.resolve_dark()))
+            .replace(r'{{ prefix | safe }}', prefix),
             status_code
         )
 

+ 12 - 8
nicegui/nicegui.py

@@ -4,7 +4,7 @@ import urllib.parse
 from pathlib import Path
 from typing import Dict, Optional
 
-from fastapi import FastAPI, Request
+from fastapi import FastAPI, HTTPException, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse
 from fastapi.staticfiles import StaticFiles
@@ -28,13 +28,15 @@ globals.index_client = Client(page('/'), shared=True).__enter__()
 
 
 @app.get('/')
-def index():
-    return globals.index_client.build_response()
+def index(request: Request) -> str:
+    return globals.index_client.build_response(request)
 
 
 @app.get('/_vue/dependencies/{path:path}')
 def vue_dependencies(path: str):
-    return FileResponse(path, media_type='text/javascript')
+    if Path(path).exists():
+        return FileResponse(path, media_type='text/javascript')
+    return HTTPException(status_code=404, detail="{path} not found")
 
 
 @app.get('/_vue/components/{name}')
@@ -62,17 +64,19 @@ def shutdown() -> None:
 
 
 @app.exception_handler(404)
-async def exception_handler(_: Request, exception: Exception):
+async def exception_handler(r: Request, exception: Exception):
+    globals.log.warning(f'{r.url} not found')
     with Client(page('')) as client:
         error_content(404, exception)
-    return client.build_response(404)
+    return client.build_response(r, 404)
 
 
 @app.exception_handler(Exception)
-async def exception_handler(_: Request, exception: Exception):
+async def exception_handler(r: Request, exception: Exception):
+    globals.log.exception(f'unexpected exception for {r.url}', exception)
     with Client(page('')) as client:
         error_content(500, exception)
-    return client.build_response(500)
+    return client.build_response(r, 500)
 
 
 @sio.on('connect')

+ 10 - 3
nicegui/page.py

@@ -3,7 +3,7 @@ import inspect
 import time
 from typing import Callable, Optional
 
-from fastapi import Response
+from fastapi import Request, Response
 
 from . import globals
 from .async_updater import AsyncUpdater
@@ -47,9 +47,13 @@ class page:
     def __call__(self, func: Callable) -> Callable:
         # NOTE we need to remove existing routes for this path to make sure only the latest definition is used
         globals.app.routes[:] = [r for r in globals.app.routes if r.path != self.path]
+        parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
 
         async def decorated(*dec_args, **dec_kwargs) -> Response:
-            with Client(self) as client:
+            request = dec_kwargs['request']
+            # NOTE cleaning up the keyword args so the signature is consistent with "func" again
+            dec_kwargs = {k: v for k, v in dec_kwargs.items() if k in parameters_of_decorated_func}
+            with Client(self, request=request) as client:
                 if any(p.name == 'client' for p in inspect.signature(func).parameters.values()):
                     dec_kwargs['client'] = client
                 result = func(*dec_args, **dec_kwargs)
@@ -66,9 +70,12 @@ class page:
                 result = task.result() if task.done() else None
             if isinstance(result, Response):  # NOTE if setup returns a response, we don't need to render the page
                 return result
-            return client.build_response()
+            return client.build_response(request)
 
         parameters = [p for p in inspect.signature(func).parameters.values() if p.name != 'client']
+        # NOTE adding request as a parameter so we can pass it to the client in the decorated function
+        if 'request' not in [p.name for p in parameters]:
+            parameters.append(inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request))
         decorated.__signature__ = inspect.Signature(parameters)
 
         globals.page_routes[decorated] = self.path

+ 7 - 7
nicegui/templates/index.html

@@ -2,16 +2,16 @@
 <html>
   <head>
     <title>{{ title }}</title>
-    <script src="/static/socket.io.min.js"></script>
+    <script src="{{ prefix | safe }}/static/socket.io.min.js"></script>
     <link rel="shortcut icon" href="{{ favicon_url }}" />
-    <link href="/static/fonts.css" rel="stylesheet" type="text/css" />
-    <link href="/static/quasar.prod.css" rel="stylesheet" type="text/css" />
-    <script src="/static/tailwind.min.js"></script>
+    <link href="{{ prefix | safe }}/static/fonts.css" rel="stylesheet" type="text/css" />
+    <link href="{{ prefix | safe }}/static/quasar.prod.css" rel="stylesheet" type="text/css" />
+    <script src="{{ prefix | safe }}/static/tailwind.min.js"></script>
     {{ head_html | safe }}
   </head>
   <body>
-    <script src="/static/vue.global.prod.js"></script>
-    <script src="/static/quasar.umd.prod.js"></script>
+    <script src="{{ prefix | safe }}/static/vue.global.prod.js"></script>
+    <script src="{{ prefix | safe }}/static/quasar.umd.prod.js"></script>
 
     {{ body_html | safe }}
 
@@ -78,7 +78,7 @@
         },
         mounted() {
           const query = { client_id: {{ client_id }} };
-          window.socket = io("{{ socket_address }}", { path: "/ws/socket.io", query: query });
+          window.socket = io(window.location.protocol === 'https:' ? 'wss://' : 'ws://' + window.location.host, { path: "{{ prefix | safe }}/ws/socket.io", query: query });
           window.socket.on("update", (msg) => {
             Object.entries(msg.elements).forEach(([id, element]) => this.elements[element.id] = element);
           });

+ 4 - 4
nicegui/vue.py

@@ -38,18 +38,18 @@ def generate_vue_content() -> Tuple[str]:
     )
 
 
-def generate_js_imports() -> str:
+def generate_js_imports(prefix: str) -> str:
     result = ''
     for name, path in vue_components.items():
         if name in globals.excludes:
             continue
         for path in js_dependencies[name]:
-            result += f'import "/_vue/dependencies/{path}";\n'
+            result += f'import "{prefix}/_vue/dependencies/{path}";\n'
     for name, path in js_components.items():
         if name in globals.excludes:
             continue
         for path in js_dependencies[name]:
-            result += f'import "/_vue/dependencies/{path}";\n'
-        result += f'import {{ default as {name} }} from "/_vue/components/{name}";\n'
+            result += f'import "{prefix}/_vue/dependencies/{path}";\n'
+        result += f'import {{ default as {name} }} from "{prefix}/_vue/components/{name}";\n'
         result += f'app.component("{name}", {name});\n'
     return result

+ 19 - 0
tests/test_link.py

@@ -0,0 +1,19 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_local_target_linking_on_sub_pages(screen: Screen):
+    '''The issue arose when using <base> tag for reverse-proxy path handling. See https://github.com/zauberzeug/nicegui/pull/188#issuecomment-1336313925'''
+    @ui.page('/sub')
+    def main():
+        ui.link('goto target', f'#target').style('margin-bottom: 600px')
+        ui.link_target('target')
+        ui.label('the target')
+
+    ui.label('main page')
+
+    screen.open('/sub')
+    screen.click('goto target')
+    screen.should_contain('the target')
+    screen.should_not_contain('main page')