diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2b10e84298..a2271b98a0 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -24,7 +24,7 @@ on: jobs: build_docs: name: Documentation - runs-on: 'macos-latest' + runs-on: 'macos-14' timeout-minutes: 180 defaults: run: @@ -37,18 +37,18 @@ jobs: DISPLAY: ":99.0" PANEL_IPYWIDGET: 1 steps: - - uses: holoviz-dev/holoviz_tasks/install@v0.1a18 + - uses: holoviz-dev/holoviz_tasks/install@v0.1a19 with: name: doc_build - python-version: "3.9" - channels: pyviz/label/dev,bokeh,conda-forge,nodefaults + python-version: "3.11" + channels: pyviz/label/dev,bokeh/label/dev,conda-forge,nodefaults conda-update: 'true' nodejs: true + nodejs-version: 20 # Remove when all examples tools can be installed on 3.10 envs: -o examples -o doc -o build cache: true opengl: true - conda-mamba: mamba - name: doit develop_install if: steps.install.outputs.cache-hit != 'true' run: | diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b6e264dfec..d4375d1280 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -69,6 +69,7 @@ jobs: - 'doc/getting_started/**' - 'doc/how_to/**' - 'scripts/**' + - 'lite/**' - name: Set matrix option run: | if [[ '${{ github.event_name }}' == 'workflow_dispatch' ]]; then diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_layout.md b/doc/how_to/custom_components/reactive_html/reactive_html_layout.md index 7ca624f029..a5f518378f 100644 --- a/doc/how_to/custom_components/reactive_html/reactive_html_layout.md +++ b/doc/how_to/custom_components/reactive_html/reactive_html_layout.md @@ -12,11 +12,8 @@ import param pn.extension() - class LayoutSingleObject(pn.reactive.ReactiveHTML): - object = param.Parameter() - - _ignored_refs = ("object",) + object = param.Parameter(allow_refs=False) _template = """
@@ -26,7 +23,6 @@ class LayoutSingleObject(pn.reactive.ReactiveHTML):
""" - dial = pn.widgets.Dial( name="°C", value=37, @@ -42,7 +38,9 @@ LayoutSingleObject( ).servable() ``` -Please notice +:::{note} + +Please note - We define the HTML layout in the `_template` attribute. - We can refer to the parameter `object` in the `_template` via the *template parameter* `${object}`. @@ -53,6 +51,8 @@ parameter can be called anything. For example `value`, `dial` or `temperature`. - We add the `border` in the `styles` parameter so that we can better see how the `_template` layes out inside the `ReactiveHTML` component. This can be very useful for development. +::: + ## Layout multiple parameters ```{pyodide} @@ -62,10 +62,8 @@ import param pn.extension() class LayoutMultipleValues(pn.reactive.ReactiveHTML): - object1 = param.Parameter() - object2 = param.Parameter() - - _ignored_refs = ("object1", "object2") + object1 = param.Parameter(allow_refs=False) + object2 = param.Parameter(allow_refs=False) _template = """
@@ -121,7 +119,6 @@ class LayoutLiteralValues(pn.reactive.ReactiveHTML): object1 = param.Parameter() object2 = param.Parameter() - _ignored_refs = ("object1", "object2") _child_config = {"object1": "literal", "object2": "literal"} _template = """ @@ -182,17 +179,21 @@ LayoutOfList(objects=[ The component will trigger a rerendering if you update the `List` value. -Please note that you must +:::{note} + +You must - wrap the `{% for object in objects %}` loop in an HTML element with an `id`. Here it is wrapped with `
...
`. - close all HTML tags! `
` is valid HTML, but not valid with `ReactiveHTML`. You must close it as `
`. -Please note you can optionally +You can optionally - get the index of the `{% for object in objects %}` loop via `{{ loop.index0 }}`. +::: + ## Create a list like layout If you want to create a *list like* layout similar to `Column` and `Row`, you can @@ -226,13 +227,15 @@ layout = ListLikeLayout( layout.servable() ``` -Please note that you must +You can now use `[...]` indexing and the `.append`, `.insert`, `pop`, ... methods that you would +expect. + +:::{note} -- list `NamedListLike, ReactiveHTML` in exactly that order when you define the class! The other +You must list `NamedListLike, ReactiveHTML` in exactly that order when you define the class! The other way around `ReactiveHTML, NamedListLike` will not work. -You can now use `[...]` indexing and the `.append`, `.insert`, `pop`, ... methods that you would -expect. +:::: ## Layout a dictionary @@ -266,9 +269,13 @@ LayoutOfDict(object={ ).servable() ``` +:::{note} + Please note - We can insert the `key` as a literal value only using `{{ key }}`. Inserting it as a template -variable ${key} will not work. +variable `${key}` will not work. - We must not give the HTML element containing `{{ key }}` an `id`. If we do, an exception will be raised. + +::: diff --git a/doc/how_to/custom_components/reactive_html/reactive_html_styling.md b/doc/how_to/custom_components/reactive_html/reactive_html_styling.md index 98b47935f7..9fb8339c55 100644 --- a/doc/how_to/custom_components/reactive_html/reactive_html_styling.md +++ b/doc/how_to/custom_components/reactive_html/reactive_html_styling.md @@ -16,9 +16,7 @@ from panel.reactive import ReactiveHTML pn.extension() class SensorLayout(ReactiveHTML): - object = param.Parameter() - - _ignored_refs = ("object",) + object = param.Parameter(allow_refs=False) _template = """
diff --git a/examples/reference/chat/ChatFeed.ipynb b/examples/reference/chat/ChatFeed.ipynb index fa87bc2f71..ca813d98bb 100644 --- a/examples/reference/chat/ChatFeed.ipynb +++ b/examples/reference/chat/ChatFeed.ipynb @@ -75,6 +75,13 @@ "___" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Basics" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -175,6 +182,13 @@ "message = chat_feed.send({\"object\": \"Overtaken!\", \"user\": \"Bot\"}, user=\"MegaBot\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Callbacks" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -339,6 +353,13 @@ "chat_feed.send(\"This will fail...\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Async Callbacks" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -550,6 +571,13 @@ "message = chat_feed.send(\"Hello bots!\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Serialize" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -712,7 +740,16 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "If a returned object is not a generator (notably LangChain output), it's still possible to stream the output with the `stream` method." + "#### Stream" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If a returned object is not a generator (notably LangChain output), it's still possible to stream the output with the `stream` method.\n", + "\n", + "Note, if you're working with `generator`s, use `yield` in your callback instead." ] }, { @@ -774,6 +811,13 @@ " message = chat_feed.stream(n, message=message)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Customization" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/examples/reference/chat/ChatInterface.ipynb b/examples/reference/chat/ChatInterface.ipynb index f04fe70e4c..01f7698cd6 100644 --- a/examples/reference/chat/ChatInterface.ipynb +++ b/examples/reference/chat/ChatInterface.ipynb @@ -59,6 +59,13 @@ "___" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Basics" + ] + }, { "cell_type": "code", "execution_count": null, @@ -110,6 +117,13 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Input Widgets" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -367,6 +381,13 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Buttons" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/examples/reference/chat/ChatMessage.ipynb b/examples/reference/chat/ChatMessage.ipynb index d739d98881..90eb68fa01 100644 --- a/examples/reference/chat/ChatMessage.ipynb +++ b/examples/reference/chat/ChatMessage.ipynb @@ -65,6 +65,13 @@ "___" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Basics" + ] + }, { "cell_type": "code", "execution_count": null, @@ -192,23 +199,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "That way, the `value`, `user`, and/or `avatar` can be dynamically updated either by setting the `value` like this..." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "chat_message.object = pn.pane.Markdown(\"## Cheers!\")" + "#### Updates" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Or updating multiple values at once with the `.param.update` method!" + "That way, the `value`, `user`, and/or `avatar` can be dynamically updated either by setting the `value` like this..." ] }, { @@ -217,14 +215,14 @@ "metadata": {}, "outputs": [], "source": [ - "chat_message.param.update(user=\"Jolly Guy\", avatar=\"🎅\");" + "chat_message.object = pn.pane.Markdown(\"## Cheers!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "If you don't specify an `avatar` on construction, then an `avatar` will be looked up in the `default_avatars` dictionary." + "Or updating multiple values at once with the `.param.update` method!" ] }, { @@ -233,16 +231,20 @@ "metadata": {}, "outputs": [], "source": [ - "ChatMessage.default_avatars" + "chat_message.param.update(user=\"Jolly Guy\", avatar=\"🎅\");" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "You can modify the `ChatMessage.default_avatars` in-place.\n", + "The easiest and most optimal way to stream output to the `ChatMessage` is through async generators.\n", "\n", - "Note, the keys are *only* alphanumeric sensitive, meaning spaces, special characters, and case sensitivity is disregarded, e.g. `\"Chat-GPT3.5\"`, `\"chatgpt 3.5\"` and `\"Chat GPT 3.5\"` all map to the same value." + "If you're unfamiliar with this term, don't fret!\n", + "\n", + "It's simply prefixing your function with `async` and replacing `return` with `yield`.\n", + "\n", + "This example will show you how to **replace** the `ChatMessage` `value`." ] }, { @@ -251,22 +253,19 @@ "metadata": {}, "outputs": [], "source": [ - "ChatMessage.default_avatars[\"Wolfram\"] = \"🐺\"\n", - "ChatMessage.default_avatars[\"#1 good-to-go guy\"] = \"👍\"\n", + "async def replace_response():\n", + " for value in range(0, 28):\n", + " await asyncio.sleep(0.2)\n", + " yield value\n", "\n", - "pn.Column(\n", - " ChatMessage(\"Mathematics!\", user=\"Wolfram\"),\n", - " ChatMessage(\"Good to go!\", user=\"#1 Good-to-Go Guy\"),\n", - " ChatMessage(\"What's up?\", user=\"Other Guy\"),\n", - " max_width=300,\n", - ")" + "ChatMessage(replace_response)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `timestamp` can be formatted using `timestamp_format`. Additionally, a timestamp_tz can be provided to use any timezones supported by `zoneinfo`." + "This example will show you how to **append** to the `ChatMessage` `value`. " ] }, { @@ -275,24 +274,26 @@ "metadata": {}, "outputs": [], "source": [ - "pn.chat.ChatMessage(timestamp_format=\"%b %d, %Y %I:%M %p\", timestamp_tz=\"US/Pacific\")" + "sentence = \"\"\"\n", + " The greatest glory in living lies not in never falling,\n", + " but in rising every time we fall.\n", + "\"\"\"\n", + "\n", + "async def append_response():\n", + " value = \"\"\n", + " for token in sentence.split():\n", + " value += f\" {token}\"\n", + " await asyncio.sleep(0.2)\n", + " yield value\n", + "\n", + "ChatMessage(append_response, user=\"Wise guy\", avatar=\"🤓\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The `ChatMessage` can serialized into a string." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "widget = pn.widgets.FloatSlider(value=3, name=\"Number selected\")\n", - "pn.chat.ChatMessage(widget).serialize()" + "#### Styling" ] }, { @@ -406,6 +407,13 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Reactions" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -468,13 +476,32 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The easiest and most optimal way to stream output to the `ChatMessage` is through async generators.\n", - "\n", - "If you're unfamiliar with this term, don't fret!\n", - "\n", - "It's simply prefixing your function with `async` and replacing `return` with `yield`.\n", + "#### Misc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you don't specify an `avatar` on construction, then an `avatar` will be looked up in the `default_avatars` dictionary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "ChatMessage.default_avatars" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can modify the `ChatMessage.default_avatars` in-place.\n", "\n", - "This example will show you how to **replace** the `ChatMessage` `value`." + "Note, the keys are *only* alphanumeric sensitive, meaning spaces, special characters, and case sensitivity is disregarded, e.g. `\"Chat-GPT3.5\"`, `\"chatgpt 3.5\"` and `\"Chat GPT 3.5\"` all map to the same value." ] }, { @@ -483,19 +510,22 @@ "metadata": {}, "outputs": [], "source": [ - "async def replace_response():\n", - " for value in range(0, 28):\n", - " await asyncio.sleep(0.2)\n", - " yield value\n", + "ChatMessage.default_avatars[\"Wolfram\"] = \"🐺\"\n", + "ChatMessage.default_avatars[\"#1 good-to-go guy\"] = \"👍\"\n", "\n", - "ChatMessage(replace_response)" + "pn.Column(\n", + " ChatMessage(\"Mathematics!\", user=\"Wolfram\"),\n", + " ChatMessage(\"Good to go!\", user=\"#1 Good-to-Go Guy\"),\n", + " ChatMessage(\"What's up?\", user=\"Other Guy\"),\n", + " max_width=300,\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This example will show you how to **append** to the `ChatMessage` `value`. " + "The `timestamp` can be formatted using `timestamp_format`. Additionally, a timestamp_tz can be provided to use any timezones supported by `zoneinfo`." ] }, { @@ -504,19 +534,24 @@ "metadata": {}, "outputs": [], "source": [ - "sentence = \"\"\"\n", - " The greatest glory in living lies not in never falling,\n", - " but in rising every time we fall.\n", - "\"\"\"\n", - "\n", - "async def append_response():\n", - " value = \"\"\n", - " for token in sentence.split():\n", - " value += f\" {token}\"\n", - " await asyncio.sleep(0.2)\n", - " yield value\n", - "\n", - "ChatMessage(append_response, user=\"Wise guy\", avatar=\"🤓\")" + "pn.chat.ChatMessage(timestamp_format=\"%b %d, %Y %I:%M %p\", timestamp_tz=\"US/Pacific\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `ChatMessage` can serialized into a string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "widget = pn.widgets.FloatSlider(value=3, name=\"Number selected\")\n", + "pn.chat.ChatMessage(widget).serialize()" ] }, { diff --git a/examples/reference/panes/Perspective.ipynb b/examples/reference/panes/Perspective.ipynb index 6b87f493db..0b3e09889e 100644 --- a/examples/reference/panes/Perspective.ipynb +++ b/examples/reference/panes/Perspective.ipynb @@ -41,7 +41,7 @@ "* **``selectable``** (bool, default=True): Whether rows are selectable\n", "* **``sort``** (list): List of sorting specs, e.g. `[[\"x\", \"desc\"]]`\n", "* **``split_by``** (list): A list of columns to pivot by. e.g. `[\"x\", \"y\"]`\n", - "* **``theme``** (str): The theme of the viewer, available options include `'material'`, `'material-dark'`, `'monokai'`, `'solarized'`, `'solarized-dark'` and `'vaporwave'`\n", + "* **``theme``** (str): The theme of the viewer, available options include `'pro'`, `'pro-dark'`, `'monokai'`, `'solarized'`, `'solarized-dark'` and `'vaporwave'`\n", "* **``toggle_config``** (bool): Whether to show the config menu. Default is True.\n", "\n", "##### Callbacks\n", diff --git a/panel/_templates/js_resources.html b/panel/_templates/js_resources.html index 2a616d144f..efc5680f22 100644 --- a/panel/_templates/js_resources.html +++ b/panel/_templates/js_resources.html @@ -25,7 +25,7 @@ {%- for name, file in js_module_exports.items() %} {%- endfor %} diff --git a/panel/compiler.py b/panel/compiler.py index 0627047042..fb1096d745 100644 --- a/panel/compiler.py +++ b/panel/compiler.py @@ -66,7 +66,7 @@ def write_bundled_files(name, files, explicit_dir=None, ext=None): filename = str(filename) if ext and not str(filename).endswith(ext): filename += f'.{ext}' - if filename.endswith('.ttf'): + if filename.endswith(('.ttf', '.wasm')): with open(filename, 'wb') as f: f.write(response.content) else: @@ -250,11 +250,17 @@ def bundle_models(verbose=False, external=True): continue if verbose: print(f'Collecting {name} resources') - prev_jsfiles = getattr(model, '__javascript_raw__', None) + prev_jsfiles = ( + getattr(model, '__javascript_raw__', []) + + getattr(model, '__javascript_modules_raw__', []) + ) or None prev_jsbundle = getattr(model, '__tarball__', None) prev_cls = model for cls in model.__mro__[1:]: - jsfiles = getattr(cls, '__javascript_raw__', None) + jsfiles = ( + getattr(cls, '__javascript_raw__', []) + + getattr(cls, '__javascript_modules_raw__', []) + ) or None if ((jsfiles is None and prev_jsfiles is not None) or (jsfiles is not None and jsfiles != prev_jsfiles)): if prev_jsbundle: diff --git a/panel/dist/css/button.css b/panel/dist/css/button.css index 5ae415e813..b86c067213 100644 --- a/panel/dist/css/button.css +++ b/panel/dist/css/button.css @@ -140,12 +140,20 @@ .bk-btn a { align-items: center; display: inline; + flex-grow: 1; height: 100%; justify-content: center; padding: 6px; - width: 100%; } :host(.bk-panel-models-widgets-FileDownload) .bk-btn { + align-items: center; + display: flex; + flex-direction: row; + justify-content: center; padding: 0px; } + +:host(.bk-panel-models-widgets-FileDownload) .bk-btn .bk-TablerIcon { + margin-left: 0.5em; +} diff --git a/panel/dist/css/notifications.css b/panel/dist/css/notifications.css new file mode 100644 index 0000000000..c87f539a0a --- /dev/null +++ b/panel/dist/css/notifications.css @@ -0,0 +1,3 @@ +.notyf__ripple { + height: calc(100% * 2.75); +} diff --git a/panel/io/jupyter_executor.py b/panel/io/jupyter_executor.py index e123d9b4db..7def6193c6 100644 --- a/panel/io/jupyter_executor.py +++ b/panel/io/jupyter_executor.py @@ -55,6 +55,8 @@ class PanelExecutor(WSHandler): to send and receive messages to and from the frontend. """ + _tasks = set() + def __init__(self, path, token, root_url): self.path = path self.token = token @@ -98,7 +100,9 @@ def _set_state(self): state.rel_path = self.root_url def _receive_msg(self, msg) -> None: - asyncio.ensure_future(self._receive_msg_async(msg)) + task = asyncio.ensure_future(self._receive_msg_async(msg)) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) async def _receive_msg_async(self, msg) -> None: try: diff --git a/panel/io/jupyter_server_extension.py b/panel/io/jupyter_server_extension.py index 4c866f4e0d..876c8924ef 100644 --- a/panel/io/jupyter_server_extension.py +++ b/panel/io/jupyter_server_extension.py @@ -331,6 +331,8 @@ class PanelWSProxy(WSHandler, JupyterHandler): receives Bokeh protocol messages via a Jupyter Comm. """ + _tasks = set() + def __init__(self, tornado_app, *args, **kw) -> None: # Note: tornado_app is stored as self.application kw['application_context'] = None @@ -398,7 +400,9 @@ async def open(self, path, *args, **kwargs) -> None: await self.send_message(msg) self._ping_job.start() - asyncio.create_task(self._check_for_message()) + task = asyncio.create_task(self._check_for_message()) + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) async def _check_for_message(self): while True: diff --git a/panel/io/notifications.py b/panel/io/notifications.py index fc5ae53c43..11771f30c2 100644 --- a/panel/io/notifications.py +++ b/panel/io/notifications.py @@ -10,7 +10,7 @@ from ..reactive import ReactiveHTML from ..util import classproperty from .datamodel import _DATA_MODELS, construct_data_model -from .resources import CSS_URLS, bundled_files +from .resources import CSS_URLS, bundled_files, get_dist_path from .state import state if TYPE_CHECKING: @@ -176,7 +176,9 @@ def __js_skip__(cls): @classproperty def __css__(cls): - return bundled_files(cls, 'css') + return bundled_files(cls, 'css') + [ + f"{get_dist_path()}css/notifications.css" + ] _template = "" diff --git a/panel/io/pyodide.py b/panel/io/pyodide.py index a774764300..cd7eca8a57 100644 --- a/panel/io/pyodide.py +++ b/panel/io/pyodide.py @@ -114,10 +114,14 @@ def _read_json(*args, **kwargs): return _read_json_original(*args, **kwargs) pandas.read_json = _read_json +_tasks = set() + def async_execute(func: Any): event_loop = asyncio.get_running_loop() if event_loop.is_running(): - asyncio.create_task(func()) + task = asyncio.create_task(func()) + _tasks.add(task) + task.add_done_callback(_tasks.discard) else: event_loop.run_until_complete(func()) return @@ -410,7 +414,9 @@ def render_script(obj: Any, target: str) -> str: render_item.roots._roots[root] = target document.getElementById(target).classList.add('bk-root') script = script_for_render_items(docs_json, [render_item]) - asyncio.create_task(_link_model(doc.roots[0].ref['id'], doc)) + task = asyncio.create_task(_link_model(doc.roots[0].ref['id'], doc)) + _tasks.add(task) + task.add_done_callback(_tasks.discard) return wrap_in_script_tag(script) def serve(*args, **kwargs): diff --git a/panel/io/resources.py b/panel/io/resources.py index 169ad3772a..21c709487a 100644 --- a/panel/io/resources.py +++ b/panel/io/resources.py @@ -604,7 +604,7 @@ def extra_resources(self, resources, resource_type): if not (getattr(model, resource_type, None) and model._loaded()): continue for resource in getattr(model, resource_type, []): - if not isurl(resource) and not resource.startswith('static/extensions'): + if not isurl(resource) and not resource.lstrip('./').startswith('static/extensions'): resource = component_resource_path(model, resource_type, resource) if resource not in resources: resources.append(resource) @@ -751,6 +751,10 @@ def js_modules(self): from ..reactive import ReactiveHTML modules = list(config.js_modules.values()) + for model in Model.model_class_reverse_map.values(): + if hasattr(model, '__javascript_modules__'): + modules.extend(model.__javascript_modules__) + self.extra_resources(modules, '__javascript_modules__') if config.design: design_resources = config.design().resolve_resources( diff --git a/panel/models/ace.ts b/panel/models/ace.ts index 792e5aecbe..d35d6a362b 100644 --- a/panel/models/ace.ts +++ b/panel/models/ace.ts @@ -24,18 +24,6 @@ export class AcePlotView extends HTMLBoxView { protected _modelist: ModeList protected _container: HTMLDivElement - initialize(): void { - super.initialize() - this._container = div({ - id: ID(), - style: { - width: "100%", - height: "100%", - zIndex: 0, - } - }) - } - connect_signals(): void { super.connect_signals() this.connect(this.model.properties.code.change, () => this._update_code_from_model()) @@ -51,24 +39,32 @@ export class AcePlotView extends HTMLBoxView { render(): void { super.render() - if (!(this._container === this.shadow_el.childNodes[0])) - this.shadow_el.append(this._container) - this._container.textContent = this.model.code - this._editor = ace.edit(this._container) - this._editor.renderer.attachToShadowRoot() - this._langTools = ace.require('ace/ext/language_tools') - this._modelist = ace.require("ace/ext/modelist") - this._editor.setOptions({ - enableBasicAutocompletion: true, - enableSnippets: true, - fontFamily: "monospace", //hack for cursor position - }); - this._update_theme() - this._update_filename() - this._update_language() - this._editor.setReadOnly(this.model.readonly) - this._editor.setShowPrintMargin(this.model.print_margin); - this._editor.on('change', () => this._update_code_from_editor()) + + this._container = div({ + id: ID(), + style: { + width: "100%", + height: "100%", + zIndex: 0, + } + }) + this.shadow_el.append(this._container) + this._container.textContent = this.model.code + this._editor = ace.edit(this._container) + this._editor.renderer.attachToShadowRoot() + this._langTools = ace.require('ace/ext/language_tools') + this._modelist = ace.require("ace/ext/modelist") + this._editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + fontFamily: "monospace", //hack for cursor position + }); + this._update_theme() + this._update_filename() + this._update_language() + this._editor.setReadOnly(this.model.readonly) + this._editor.setShowPrintMargin(this.model.print_margin); + this._editor.on('change', () => this._update_code_from_editor()) } _update_code_from_model(): void { @@ -109,7 +105,9 @@ export class AcePlotView extends HTMLBoxView { after_layout(): void{ super.after_layout() - this._editor.resize() + if (this._editor !== undefined) { + this._editor.resize() + } } } diff --git a/panel/models/layout.ts b/panel/models/layout.ts index 95d06128eb..0d9c5f1c5d 100644 --- a/panel/models/layout.ts +++ b/panel/models/layout.ts @@ -34,16 +34,16 @@ export class PanelMarkupView extends WidgetView { for (const sts of this._applied_stylesheets) { const style_el = (sts as any).el if (style_el instanceof HTMLLinkElement) { - this._initialized_stylesheets[style_el.href] = false - style_el.addEventListener("load", () => { - this._initialized_stylesheets[style_el.href] = true - if ( - Object.values(this._initialized_stylesheets).every(Boolean) - ) - this.style_redraw() - }) + this._initialized_stylesheets[style_el.href] = false + style_el.addEventListener('load', () => { + this._initialized_stylesheets[style_el.href] = true + if (Object.values(this._initialized_stylesheets).every(Boolean)) + this.style_redraw() + }) } } + if (Object.keys(this._initialized_stylesheets).length === 0) + this.style_redraw() } style_redraw(): void { diff --git a/panel/models/perspective.py b/panel/models/perspective.py index 58574326d9..b513772bef 100644 --- a/panel/models/perspective.py +++ b/panel/models/perspective.py @@ -1,5 +1,5 @@ from bokeh.core.properties import ( - Any, Bool, Dict, Either, Enum, Instance, List, Null, Nullable, String, + Any, Bool, Dict, Either, Enum, Instance, List, Null, String, ) from bokeh.events import ModelEvent from bokeh.models import ColumnDataSource @@ -10,10 +10,11 @@ from .layout import HTMLBox PERSPECTIVE_THEMES = [ - 'material', 'material-dark', 'monokai', 'solarized', 'solarized-dark', 'vaporwave' + 'monokai', 'solarized', 'solarized-dark', 'vaporwave', 'dracula', + 'pro', 'pro-dark', 'gruvbox', 'gruvbox-dark', ] -PERSPECTIVE_VERSION = '1.9.3' +PERSPECTIVE_VERSION = '2.8.0' THEME_PATH = f"@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/css/" THEME_URL = f"{config.npm_cdn}/{THEME_PATH}" @@ -51,7 +52,7 @@ class Perspective(HTMLBox): columns = Either(List(Either(String, Null)), Null()) - expressions = Nullable(List(String)) + expressions = Either(Dict(String, Any), Null()) editable = Bool(default=True) @@ -73,26 +74,34 @@ class Perspective(HTMLBox): toggle_config = Bool(True) - theme = Enum(*PERSPECTIVE_THEMES, default="material") + theme = Enum(*PERSPECTIVE_THEMES, default="pro") - # pylint: disable=line-too-long - __javascript__ = [ - f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/umd/perspective.js", - f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer.js", - f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-datagrid.js", - f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-d3fc.js", + __javascript_module_exports__ = ['perspective'] + + __javascript_modules_raw__ = [ + f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective.js", + f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective.worker.js", + f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective.cpp.wasm", + f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer.js", + f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/cdn/perspective_bg.wasm", + f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-datagrid.js", + f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-d3fc.js", ] + @classproperty + def __javascript_modules__(cls): + return [js for js in bundled_files(cls, 'javascript_modules') if 'wasm' not in js and 'worker' not in js] + __js_skip__ = { - "perspective": __javascript__, + "perspective": __javascript_modules__, } __js_require__ = { "paths": { - "perspective": f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/umd/perspective", - "perspective-viewer": f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer", - "perspective-viewer-datagrid": f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-datagrid", - "perspective-viewer-d3fc": f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/umd/perspective-viewer-d3fc", + "perspective": f"{config.npm_cdn}/@finos/perspective@{PERSPECTIVE_VERSION}/dist/cdn/perspective", + "perspective-viewer": f"{config.npm_cdn}/@finos/perspective-viewer@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer", + "perspective-viewer-datagrid": f"{config.npm_cdn}/@finos/perspective-viewer-datagrid@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-datagrid", + "perspective-viewer-d3fc": f"{config.npm_cdn}/@finos/perspective-viewer-d3fc@{PERSPECTIVE_VERSION}/dist/cdn/perspective-viewer-d3fc", }, "exports": { "perspective": "perspective", diff --git a/panel/models/perspective.ts b/panel/models/perspective.ts index f04fed58af..8d1d09e16d 100644 --- a/panel/models/perspective.ts +++ b/panel/models/perspective.ts @@ -6,8 +6,8 @@ import {HTMLBox, HTMLBoxView, set_size} from "./layout" import type {Attrs} from "@bokehjs/core/types" const THEMES: any = { - 'material-dark': 'Material Dark', - 'material': 'Material Light', + 'pro-dark': 'Pro Dark', + 'pro': 'Pro Light', 'vaporwave': 'Vaporwave', 'solarized': 'Solarized', 'solarized-dark': 'Solarized Dark', @@ -200,7 +200,7 @@ export class PerspectiveView extends HTMLBoxView { const props: any = {} for (let option in config) { let value = config[option] - if (value === undefined || (option == 'plugin' && value === "debug") || option === 'settings') + if (value === undefined || (option == 'plugin' && value === "debug") || this.model.properties.hasOwnProperty(option) === undefined) continue if (option === 'filter') option = 'filters' @@ -256,7 +256,7 @@ export namespace Perspective { aggregates: p.Property split_by: p.Property columns: p.Property - expressions: p.Property + expressions: p.Property editable: p.Property filters: p.Property group_by: p.Property @@ -288,7 +288,7 @@ export class Perspective extends HTMLBox { this.define(({Any, Array, Boolean, Ref, Nullable, String}) => ({ aggregates: [ Any, {} ], columns: [ Array(Nullable(String)), [] ], - expressions: [ Nullable(Array(String)), null ], + expressions: [ Any, {} ], split_by: [ Nullable(Array(String)), null ], editable: [ Boolean, true ], filters: [ Nullable(Array(Any)), null ], @@ -300,7 +300,7 @@ export class Perspective extends HTMLBox { toggle_config: [ Boolean, true ], sort: [ Nullable(Array(Array(String))), null ], source: [ Ref(ColumnDataSource), ], - theme: [ String, 'material' ] + theme: [ String, 'pro' ] })) } } diff --git a/panel/models/tabulator.ts b/panel/models/tabulator.ts index 83c9e78a2f..8d84a793f3 100644 --- a/panel/models/tabulator.ts +++ b/panel/models/tabulator.ts @@ -302,8 +302,10 @@ export class DataTabulatorView extends HTMLBoxView { _selection_updating: boolean = false _initializing: boolean _lastVerticalScrollbarTopPosition: number = 0; + _lastHorizontalScrollbarLeftPosition: number = 0; _applied_styles: boolean = false _building: boolean = false + _restore_scroll: boolean = false connect_signals(): void { super.connect_signals() @@ -378,7 +380,7 @@ export class DataTabulatorView extends HTMLBoxView { this.connect(this.model.source.patching, () => { const inds = this.model.source.selected.indices this.updateOrAddData(); - this.tabulator.rowManager.element.scrollTop = this._lastVerticalScrollbarTopPosition; + this.record_scroll() // Restore indices since updating data may have reset checkbox column this.model.source.selected.indices = inds; }) @@ -427,6 +429,7 @@ export class DataTabulatorView extends HTMLBoxView { this.renderChildren() this.setStyles() } + this._restore_scroll = true } after_layout(): void { @@ -436,6 +439,11 @@ export class DataTabulatorView extends HTMLBoxView { this._initializing = false } + after_resize(): void { + super.after_resize() + this.redraw() + } + setCSSClasses(el: HTMLDivElement): void { el.className = "pnx-tabulator tabulator" for (const cls of this.model.theme_classes) @@ -492,14 +500,19 @@ export class DataTabulatorView extends HTMLBoxView { return cell.getColumn().getField() + ": " + cell.getValue(); }) this.tabulator.on("scrollVertical", debounce(() => { + this.record_scroll() this.setStyles() }, 50, false)) + this.tabulator.on("scrollHorizontal", debounce(() => { + this.record_scroll() + }, 50, false)) // Sync state with model this.tabulator.on("rowSelectionChanged", (data: any, rows: any, selected: any, deselected: any) => this.rowSelectionChanged(data, rows, selected, deselected)) this.tabulator.on("rowClick", (e: any, row: any) => this.rowClicked(e, row)) this.tabulator.on("cellEdited", (cell: any) => this.cellEdited(cell)) this.tabulator.on("dataFiltering", (filters: any) => { + this.record_scroll() this.model.filters = filters }) this.tabulator.on("dataFiltered", (_: any, rows: any[]) => { @@ -925,6 +938,10 @@ export class DataTabulatorView extends HTMLBoxView { postUpdate(): void { this.setSelection() this.setStyles() + if (this._restore_scroll) { + this.restore_scroll() + this._restore_scroll = false + } } updateOrAddData(): void { @@ -1058,7 +1075,22 @@ export class DataTabulatorView extends HTMLBoxView { this._selection_updating = false } + restore_scroll(): void { + const opts = { + top: this._lastVerticalScrollbarTopPosition, + left: this._lastHorizontalScrollbarLeftPosition, + behavior: "instant" + } + setTimeout(() => this.tabulator.rowManager.element.scrollTo(opts), 0) + } + // Update model + + record_scroll() { + this._lastVerticalScrollbarTopPosition = this.tabulator.rowManager.element.scrollTop + this._lastHorizontalScrollbarLeftPosition = this.tabulator.rowManager.element.scrollLeft + } + rowClicked(e: any, row: any) { if ( this._selection_updating || diff --git a/panel/pane/image.py b/panel/pane/image.py index 4a31f56e98..144b263b5d 100644 --- a/panel/pane/image.py +++ b/panel/pane/image.py @@ -23,6 +23,8 @@ if TYPE_CHECKING: from bokeh.model import Model +_tasks = set() + class FileBase(HTMLBasePane): embed = param.Boolean(default=False, doc=""" @@ -110,7 +112,9 @@ def _data(self, obj: Any) -> bytes | None: from pyodide.http import pyfetch async def replace_content(): self.object = await (await pyfetch(obj)).bytes() - asyncio.create_task(replace_content()) + task = asyncio.create_task(replace_content()) + _tasks.add(task) + task.add_done_callback(_tasks.discard) else: import requests r = requests.request(url=obj, method='GET') diff --git a/panel/pane/perspective.py b/panel/pane/perspective.py index 2f870bd672..45babb0aea 100644 --- a/panel/pane/perspective.py +++ b/panel/pane/perspective.py @@ -29,10 +29,11 @@ from ..model.perspective import PerspectiveClickEvent -DEFAULT_THEME = "material" +DEFAULT_THEME = "pro" THEMES = [ - 'material', 'material-dark', 'monokai', 'solarized', 'solarized-dark', 'vaporwave' + 'material', 'material-dark', 'monokai', 'solarized', 'solarized-dark', + 'vaporwave', 'pro', 'pro-dark' ] class Plugin(Enum): @@ -264,7 +265,7 @@ class Perspective(ModelPane, ReactiveData): :Example: - >>> Perspective(df, plugin='hypergrid', theme='material-dark') + >>> Perspective(df, plugin='hypergrid', theme='pro-dark') """ aggregates = param.Dict(default=None, nested_refs=True, doc=""" @@ -276,7 +277,7 @@ class Perspective(ModelPane, ReactiveData): editable = param.Boolean(default=True, allow_None=True, doc=""" Whether items are editable.""") - expressions = param.List(default=None, nested_refs=True, doc=""" + expressions = param.ClassSelector(class_=(dict, list), default=None, nested_refs=True, doc=""" A list of expressions computing new columns from existing columns. For example [""x"+"index""]""") @@ -310,8 +311,8 @@ class Perspective(ModelPane, ReactiveData): toggle_config = param.Boolean(default=True, doc=""" Whether to show the config menu.""") - theme = param.ObjectSelector(default='material', objects=THEMES, doc=""" - The style of the PerspectiveViewer. For example material-dark""") + theme = param.ObjectSelector(default='pro', objects=THEMES, doc=""" + The style of the PerspectiveViewer. For example pro-dark""") priority: ClassVar[float | bool | None] = None @@ -373,6 +374,8 @@ def _filter_properties(self, properties): def _get_properties(self, doc, source=None): props = super()._get_properties(doc) + if 'theme' in props and 'material' in props['theme']: + props['theme'] = props['theme'].replace('material', 'pro') del props['object'] if props.get('toggle_config'): props['height'] = self.height or 300 @@ -431,6 +434,8 @@ def _process_param_change(self, params): params['stylesheets'] = [ ImportedStyleSheet(url=ss) for ss in css ] + params.get('stylesheets', self.stylesheets) + if 'theme' in params and 'material' in params['theme']: + params['theme'] = params['theme'].replace('material', 'pro') props = super()._process_param_change(params) for p in ('columns', 'group_by', 'split_by'): if props.get(p): @@ -441,6 +446,8 @@ def _process_param_change(self, params): props['filters'] = [[str(col), *args] for col, *args in props['filters']] if props.get('aggregates'): props['aggregates'] = {str(col): agg for col, agg in props['aggregates'].items()} + if isinstance(props.get('expressions'), list): + props['expressions'] = {f'expression_{i}': exp for i, exp in enumerate(props['expressions'])} return props def _as_digit(self, col): diff --git a/panel/template/base.py b/panel/template/base.py index f58842caae..cda235bae8 100644 --- a/panel/template/base.py +++ b/panel/template/base.py @@ -555,7 +555,10 @@ class TemplateActions(ReactiveHTML): _template: ClassVar[str] = "" _scripts: ClassVar[Dict[str, List[str] | str]] = { - 'open_modal': ["document.getElementById('pn-Modal').style.display = 'block'"], + 'open_modal': [""" + document.getElementById('pn-Modal').style.display = 'block' + window.dispatchEvent(new Event('resize')); + """], 'close_modal': ["document.getElementById('pn-Modal').style.display = 'none'"], } diff --git a/panel/template/bootstrap/bootstrap.css b/panel/template/bootstrap/bootstrap.css index 6f05eb9115..7646a02f4b 100644 --- a/panel/template/bootstrap/bootstrap.css +++ b/panel/template/bootstrap/bootstrap.css @@ -162,6 +162,7 @@ img.app-logo { .pn-modal-content { display: block; min-height: 42px; + overflow-x: auto; } .pn-modal-close { @@ -182,8 +183,8 @@ img.app-logo { .pn-busy-container { align-items: center; - justify-content: center; display: flex; + justify-content: center; margin-left: auto; } diff --git a/panel/template/fast/fast.css b/panel/template/fast/fast.css index 9210315638..e75c38df64 100644 --- a/panel/template/fast/fast.css +++ b/panel/template/fast/fast.css @@ -366,3 +366,8 @@ fast-card { --background-color: var(--neutral-layer-floating); z-index: 10; } + +.pn-modal-content { + display: block; + overflow-x: auto; +} diff --git a/panel/template/fast/grid/fast_grid_template.html b/panel/template/fast/grid/fast_grid_template.html index 702e8e2f4d..a4765cdaef 100644 --- a/panel/template/fast/grid/fast_grid_template.html +++ b/panel/template/fast/grid/fast_grid_template.html @@ -183,7 +183,7 @@