solara-ui 1.39.0__py2.py3-none-any.whl → 1.40.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solara/__init__.py +1 -1
- solara/components/__init__.py +1 -0
- solara/components/input_text_area.py +86 -0
- solara/components/markdown.py +1 -1
- solara/hooks/use_thread.py +4 -4
- solara/lab/components/chat.py +8 -2
- solara/server/assets/style.css +2 -1
- solara/server/flask.py +1 -1
- solara/server/jupyter/server_extension.py +11 -1
- solara/server/jupyter/solara.py +91 -0
- solara/server/patch.py +1 -0
- solara/server/pyinstaller/__init__.py +9 -0
- solara/server/pyinstaller/hook-ipyreact.py +5 -0
- solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
- solara/server/pyinstaller/hook-solara.py +9 -0
- solara/server/server.py +6 -1
- solara/server/starlette.py +18 -6
- solara/server/static/highlight-dark.css +1 -1
- solara/server/static/main-vuetify.js +1 -1
- solara/server/static/solara_bootstrap.py +1 -1
- solara/server/templates/solara.html.j2 +30 -6
- solara/website/assets/custom.css +20 -57
- solara/website/components/__init__.py +2 -2
- solara/website/components/algolia_api.vue +23 -6
- solara/website/components/breadcrumbs.py +28 -0
- solara/website/components/contact.py +144 -0
- solara/website/components/docs.py +11 -9
- solara/website/components/header.py +31 -20
- solara/website/components/markdown.py +12 -1
- solara/website/components/markdown_nav.vue +34 -0
- solara/website/components/sidebar.py +7 -1
- solara/website/pages/__init__.py +87 -254
- solara/website/pages/about/__init__.py +9 -0
- solara/website/pages/about/about.md +3 -0
- solara/website/pages/careers/__init__.py +27 -0
- solara/website/pages/changelog/__init__.py +2 -2
- solara/website/pages/changelog/changelog.md +12 -0
- solara/website/pages/contact/__init__.py +30 -6
- solara/website/pages/documentation/__init__.py +25 -33
- solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +2 -1
- solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +1 -1
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +2 -1
- solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -1
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +5 -2
- solara/website/pages/documentation/api/hooks/use_thread.md +6 -0
- solara/website/pages/documentation/components/data/pivot_table.py +2 -2
- solara/website/pages/documentation/components/input/input.py +2 -0
- solara/website/pages/documentation/components/output/sql_code.py +3 -3
- solara/website/pages/documentation/examples/__init__.py +2 -2
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +2 -2
- solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +19 -14
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +205 -15
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +3 -1
- solara/website/pages/home.vue +1199 -0
- solara/website/pages/our_team/__init__.py +83 -0
- solara/website/pages/pricing/__init__.py +31 -0
- solara/website/pages/roadmap/__init__.py +11 -0
- solara/website/pages/roadmap/roadmap.md +41 -0
- solara/website/pages/scale_ipywidgets.py +45 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/METADATA +2 -2
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/RECORD +65 -49
- solara/website/components/hero.py +0 -15
- solara/website/pages/contact/contact.md +0 -17
- {solara_ui-1.39.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.39.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/WHEEL +0 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py
CHANGED
solara/components/__init__.py
CHANGED
|
@@ -33,6 +33,7 @@ from .togglebuttons import ( # noqa: F401 F403
|
|
|
33
33
|
ToggleButtonsSingle,
|
|
34
34
|
)
|
|
35
35
|
from .input import InputText, InputFloat, InputInt # noqa: F401 F403
|
|
36
|
+
from .input_text_area import InputTextArea # noqa: F401 F403
|
|
36
37
|
from .pivot_table import PivotTableView, PivotTable, PivotTableCard # noqa: F401 F403
|
|
37
38
|
from .head import Head # noqa: F401 F403
|
|
38
39
|
from .title import Title # noqa: F401 F403
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from typing import Callable, Optional, Union, List
|
|
2
|
+
from .input import use_change
|
|
3
|
+
import solara
|
|
4
|
+
from solara.alias import rv as v
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@solara.component
|
|
8
|
+
def InputTextArea(
|
|
9
|
+
label: str,
|
|
10
|
+
value: Union[str, solara.Reactive[str]] = "",
|
|
11
|
+
on_value: Callable[[str], None] = None,
|
|
12
|
+
disabled: bool = False,
|
|
13
|
+
continuous_update: bool = False,
|
|
14
|
+
update_events: List[str] = ["focusout"],
|
|
15
|
+
error: Union[bool, str] = False,
|
|
16
|
+
message: Optional[str] = None,
|
|
17
|
+
auto_grow: bool = True,
|
|
18
|
+
rows: int = 5,
|
|
19
|
+
):
|
|
20
|
+
r"""Free form text area input.
|
|
21
|
+
|
|
22
|
+
### Basic example:
|
|
23
|
+
|
|
24
|
+
```solara
|
|
25
|
+
import solara
|
|
26
|
+
|
|
27
|
+
text = solara.reactive("Hello\nWorld\n!!!")
|
|
28
|
+
continuous_update = solara.reactive(True)
|
|
29
|
+
|
|
30
|
+
@solara.component
|
|
31
|
+
def Page():
|
|
32
|
+
solara.Checkbox(label="Continuous update", value=continuous_update)
|
|
33
|
+
solara.InputTextArea("Enter some text", value=text, continuous_update=continuous_update.value)
|
|
34
|
+
with solara.Row():
|
|
35
|
+
solara.Button("Clear", on_click=lambda: text.set(""))
|
|
36
|
+
solara.Button("Reset", on_click=lambda: text.set("Hello\nWorld\n!!!"))
|
|
37
|
+
solara.Markdown(f"**You entered**: {text.value}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## Arguments
|
|
42
|
+
|
|
43
|
+
* `label`: Label to display next to the slider.
|
|
44
|
+
* `value`: The currently entered value.
|
|
45
|
+
* `on_value`: Callback to call when the value changes.
|
|
46
|
+
* `disabled`: Whether the input is disabled.
|
|
47
|
+
* `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
|
|
48
|
+
* `update_events`: A list of events that should trigger `on_value`. If continuous update is enabled, this will effectively be ignored,
|
|
49
|
+
since updates will happen every change.
|
|
50
|
+
* `auto_grow`: Whether the text area auto grows with more text.
|
|
51
|
+
* `rows`: Number of empty rows to display.
|
|
52
|
+
* `error`: If truthy, show the input as having an error (in red). If a string is passed, it will be shown as the error message.
|
|
53
|
+
* `message`: Message to show below the input. If `error` is a string, this will be ignored.
|
|
54
|
+
* `classes`: List of CSS classes to apply to the input.
|
|
55
|
+
* `style`: CSS style to apply to the input.
|
|
56
|
+
"""
|
|
57
|
+
reactive_value = solara.use_reactive(value, on_value)
|
|
58
|
+
del value, on_value
|
|
59
|
+
|
|
60
|
+
def set_value_cast(value):
|
|
61
|
+
reactive_value.value = str(value)
|
|
62
|
+
|
|
63
|
+
def on_v_model(value):
|
|
64
|
+
if continuous_update:
|
|
65
|
+
set_value_cast(value)
|
|
66
|
+
|
|
67
|
+
messages = []
|
|
68
|
+
if error and isinstance(error, str):
|
|
69
|
+
messages.append(error)
|
|
70
|
+
elif message:
|
|
71
|
+
messages.append(message)
|
|
72
|
+
text_area = v.Textarea(
|
|
73
|
+
v_model=reactive_value.value,
|
|
74
|
+
on_v_model=on_v_model,
|
|
75
|
+
label=label,
|
|
76
|
+
disabled=disabled,
|
|
77
|
+
error=bool(error),
|
|
78
|
+
messages=messages,
|
|
79
|
+
solo=True,
|
|
80
|
+
hide_details=True,
|
|
81
|
+
outlined=True,
|
|
82
|
+
rows=rows,
|
|
83
|
+
auto_grow=auto_grow,
|
|
84
|
+
)
|
|
85
|
+
use_change(text_area, set_value_cast, enabled=not continuous_update, update_events=update_events)
|
|
86
|
+
return text_area
|
solara/components/markdown.py
CHANGED
|
@@ -128,7 +128,7 @@ module.exports = {
|
|
|
128
128
|
href = location.pathname + href.substr(1);
|
|
129
129
|
a.attributes['href'].href = href;
|
|
130
130
|
}
|
|
131
|
-
let authLink = href.
|
|
131
|
+
let authLink = href.startsWith("/_solara/auth/");
|
|
132
132
|
if( (href.startsWith("./") || href.startsWith("/")) && !authLink) {
|
|
133
133
|
a.onclick = e => {
|
|
134
134
|
console.log("clicked", href)
|
solara/hooks/use_thread.py
CHANGED
|
@@ -15,7 +15,7 @@ logger = logging.getLogger("solara.hooks.use_thread")
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def use_thread(
|
|
18
|
-
callback
|
|
18
|
+
callback: Union[
|
|
19
19
|
Callable[[threading.Event], T],
|
|
20
20
|
Iterator[Callable[[threading.Event], T]],
|
|
21
21
|
Callable[[], T],
|
|
@@ -69,11 +69,11 @@ def use_thread(
|
|
|
69
69
|
# result.current = None
|
|
70
70
|
set_result_state(ResultState.RUNNING)
|
|
71
71
|
|
|
72
|
-
sig = inspect.signature(callback)
|
|
72
|
+
sig = inspect.signature(callback) # type: ignore
|
|
73
73
|
if sig.parameters:
|
|
74
|
-
f = functools.partial(callback, cancel)
|
|
74
|
+
f = functools.partial(callback, cancel) # type: ignore
|
|
75
75
|
else:
|
|
76
|
-
f = callback
|
|
76
|
+
f = callback # type: ignore
|
|
77
77
|
try:
|
|
78
78
|
try:
|
|
79
79
|
# we only use the cancel_guard context manager around
|
solara/lab/components/chat.py
CHANGED
|
@@ -46,7 +46,9 @@ def ChatInput(
|
|
|
46
46
|
send_callback: Optional[Callable] = None,
|
|
47
47
|
disabled: bool = False,
|
|
48
48
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
49
|
+
input_text_style: Optional[Union[str, Dict[str, str]]] = None,
|
|
49
50
|
classes: List[str] = [],
|
|
51
|
+
input_text_classes: List[str] = [],
|
|
50
52
|
):
|
|
51
53
|
"""
|
|
52
54
|
The ChatInput component renders a text input together with a send button.
|
|
@@ -56,11 +58,14 @@ def ChatInput(
|
|
|
56
58
|
* `send_callback`: A callback function for when the user presses enter or clicks the send button.
|
|
57
59
|
* `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying,
|
|
58
60
|
among other things.
|
|
59
|
-
* `style`: CSS styles to apply to the
|
|
61
|
+
* `style`: CSS styles to apply to the `solara.Row` containing the input field and submit button. Either a string or a dictionary.
|
|
62
|
+
* `input_text_style`: CSS styles to apply to the `InputText` part of the component. Either a string or a dictionary.
|
|
60
63
|
* `classes`: A list of CSS classes to apply to the component. Also applied to the container.
|
|
64
|
+
* `input_text_classes`: A list of CSS classes to apply to the `InputText` part of the component.
|
|
61
65
|
"""
|
|
62
66
|
message, set_message = solara.use_state("") # type: ignore
|
|
63
67
|
style_flat = solara.util._flatten_style(style)
|
|
68
|
+
input_text_style_flat = solara.util._flatten_style(input_text_style)
|
|
64
69
|
|
|
65
70
|
if "align-items" not in style_flat:
|
|
66
71
|
style_flat += " align-items: center;"
|
|
@@ -79,8 +84,9 @@ def ChatInput(
|
|
|
79
84
|
rounded=True,
|
|
80
85
|
filled=True,
|
|
81
86
|
hide_details=True,
|
|
82
|
-
style_="flex-grow: 1;",
|
|
87
|
+
style_="flex-grow: 1;" + input_text_style_flat,
|
|
83
88
|
disabled=disabled,
|
|
89
|
+
class_=" ".join(input_text_classes),
|
|
84
90
|
)
|
|
85
91
|
|
|
86
92
|
use_change(message_input, send, update_events=["keyup.enter"])
|
solara/server/assets/style.css
CHANGED
solara/server/flask.py
CHANGED
|
@@ -192,7 +192,7 @@ def assets(path):
|
|
|
192
192
|
return flask.Response("not found", status=404)
|
|
193
193
|
|
|
194
194
|
|
|
195
|
-
@blueprint.route("/
|
|
195
|
+
@blueprint.route("/jupyter/nbextensions/<dir>/<filename>")
|
|
196
196
|
def nbext(dir, filename):
|
|
197
197
|
if not allowed():
|
|
198
198
|
abort(401)
|
|
@@ -2,6 +2,7 @@ from jupyter_server.utils import url_path_join
|
|
|
2
2
|
|
|
3
3
|
from solara.server.cdn_helper import cdn_url_path
|
|
4
4
|
from solara.server.jupyter.cdn_handler import CdnHandler
|
|
5
|
+
from .solara import SolaraHandler, Assets, ReadyZ
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def _jupyter_server_extension_paths():
|
|
@@ -9,6 +10,11 @@ def _jupyter_server_extension_paths():
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _load_jupyter_server_extension(server_app):
|
|
13
|
+
# a dummy app, so that server.read_root can be used
|
|
14
|
+
import solara.server.app
|
|
15
|
+
|
|
16
|
+
solara.server.app.apps["__default__"] = solara.server.app.AppScript("solara.server.jupyter.solara:Page")
|
|
17
|
+
|
|
12
18
|
web_app = server_app.web_app
|
|
13
19
|
|
|
14
20
|
host_pattern = ".*$"
|
|
@@ -17,7 +23,11 @@ def _load_jupyter_server_extension(server_app):
|
|
|
17
23
|
web_app.add_handlers(
|
|
18
24
|
host_pattern,
|
|
19
25
|
[
|
|
20
|
-
(url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}),
|
|
26
|
+
(url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}), # kept for backward compatibility
|
|
27
|
+
(url_path_join(base_url, f"/solara/{cdn_url_path}/(.*)"), CdnHandler, {}),
|
|
28
|
+
(url_path_join(base_url, "/solara/static/assets/(.*)"), Assets, {}),
|
|
29
|
+
(url_path_join(base_url, "/solara/readyz"), ReadyZ, {}),
|
|
30
|
+
(url_path_join(base_url, "/solara(.*)"), SolaraHandler, {}),
|
|
21
31
|
],
|
|
22
32
|
)
|
|
23
33
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import tornado.web
|
|
7
|
+
from jupyter_server.base.handlers import JupyterHandler
|
|
8
|
+
import solara.server.server as server
|
|
9
|
+
|
|
10
|
+
from solara.server.utils import path_is_child_of
|
|
11
|
+
import solara
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("solara.server.jupyter.solara")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@solara.component
|
|
17
|
+
def Page():
|
|
18
|
+
solara.Error("Hi, you should not see this, we only support ipypopout for now")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SolaraHandler(JupyterHandler):
|
|
22
|
+
async def get(self, path=None):
|
|
23
|
+
try:
|
|
24
|
+
# base url ends with /
|
|
25
|
+
base_url = self.settings["base_url"]
|
|
26
|
+
# root_path's do not end with /
|
|
27
|
+
jupyter_root_path = ""
|
|
28
|
+
if base_url and base_url.endswith("/"):
|
|
29
|
+
jupyter_root_path = base_url[:-1]
|
|
30
|
+
root_path = f"{jupyter_root_path}/solara"
|
|
31
|
+
content = server.read_root(path="", root_path=root_path, jupyter_root_path=jupyter_root_path)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.exception(e)
|
|
34
|
+
raise tornado.web.HTTPError(500)
|
|
35
|
+
|
|
36
|
+
if content is None:
|
|
37
|
+
raise tornado.web.HTTPError(404)
|
|
38
|
+
|
|
39
|
+
self.set_header("Content-Type", "text/html")
|
|
40
|
+
self.write(content)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# similar to voila
|
|
44
|
+
class MultiStaticFileHandler(tornado.web.StaticFileHandler):
|
|
45
|
+
"""A static file handler that 'merges' a list of directories
|
|
46
|
+
|
|
47
|
+
If initialized like this::
|
|
48
|
+
|
|
49
|
+
application = web.Application([
|
|
50
|
+
(r"/content/(.*)", web.MultiStaticFileHandler, {"paths": ["/var/1", "/var/2"]}),
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
A file will be looked up in /var/1 first, then in /var/2.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def initialize(self, paths, default_filename=None): # type: ignore
|
|
58
|
+
self.roots = paths
|
|
59
|
+
super().initialize(path=paths[0], default_filename=default_filename)
|
|
60
|
+
|
|
61
|
+
def get_absolute_path(self, root: str, path: str) -> str: # type: ignore
|
|
62
|
+
# find the first absolute path that exists
|
|
63
|
+
self.root = self.roots[0]
|
|
64
|
+
abspath = os.path.abspath(os.path.join(root, path))
|
|
65
|
+
for root in self.roots[1:]:
|
|
66
|
+
abspath = os.path.abspath(os.path.join(root, path))
|
|
67
|
+
if os.path.exists(abspath):
|
|
68
|
+
self.root = root # make sure all the other methods in the base class know how to find the file
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
# tornado probably already does a version of this, to make sure it behaves as the rest of the solara
|
|
72
|
+
# server, we do it again
|
|
73
|
+
if not path_is_child_of(Path(abspath), Path(self.root)):
|
|
74
|
+
raise PermissionError(f"Trying to read from outside of cache directory: {abspath} is not a subdir of {self.root}")
|
|
75
|
+
|
|
76
|
+
return abspath
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Assets(MultiStaticFileHandler):
|
|
80
|
+
def initialize(self): # type: ignore
|
|
81
|
+
super().initialize(server.asset_directories())
|
|
82
|
+
logging.error("Using %r as assets directories", self.roots)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ReadyZ(JupyterHandler):
|
|
86
|
+
def get(self):
|
|
87
|
+
json_data, status = server.readyz()
|
|
88
|
+
json_response = json.dumps(json_data)
|
|
89
|
+
self.set_header("Content-Type", "application/json")
|
|
90
|
+
self.set_status(status)
|
|
91
|
+
self.write(json_response)
|
solara/server/patch.py
CHANGED
|
@@ -38,6 +38,7 @@ class FakeIPython:
|
|
|
38
38
|
# needed for the pyplot interface of matplotlib
|
|
39
39
|
# (although we don't really support it)
|
|
40
40
|
self.events = mock.MagicMock()
|
|
41
|
+
self.user_ns: Dict[Any, Any] = {}
|
|
41
42
|
|
|
42
43
|
def enable_gui(self, gui):
|
|
43
44
|
logger.error("ignoring call to enable_gui(%s)", gui)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
from PyInstaller.utils.hooks import collect_data_files, copy_metadata, collect_submodules
|
|
2
|
+
|
|
3
|
+
hiddenimports = collect_submodules("ipyvuetify")
|
|
4
|
+
datas = collect_data_files("ipyvuetify") # codespell:ignore datas
|
|
5
|
+
datas += copy_metadata("ipyvuetify") # codespell:ignore datas
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
|
|
2
|
+
|
|
3
|
+
hiddenimports = collect_submodules("solara")
|
|
4
|
+
datas = collect_data_files("solara") # codespell:ignore datas
|
|
5
|
+
datas += collect_data_files("solara-ui") # codespell:ignore datas
|
|
6
|
+
|
|
7
|
+
# this makes sure that inspect.getfile works, which is used for the
|
|
8
|
+
# vue component decorator
|
|
9
|
+
module_collection_mode = "pyz+py"
|
solara/server/server.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Dict, List, Optional, Tuple, TypeVar
|
|
9
|
+
from typing import Dict, List, Optional, Tuple, TypeVar, Union
|
|
10
10
|
|
|
11
11
|
import ipykernel
|
|
12
12
|
import ipyvue
|
|
@@ -268,6 +268,7 @@ def asset_directories():
|
|
|
268
268
|
def read_root(
|
|
269
269
|
path: str,
|
|
270
270
|
root_path: str = "",
|
|
271
|
+
jupyter_root_path: Union[str, None] = None,
|
|
271
272
|
render_kwargs={},
|
|
272
273
|
use_nbextensions=True,
|
|
273
274
|
ssg_data=None,
|
|
@@ -373,10 +374,14 @@ def read_root(
|
|
|
373
374
|
else:
|
|
374
375
|
cdn = solara.settings.assets.cdn
|
|
375
376
|
|
|
377
|
+
if jupyter_root_path is None:
|
|
378
|
+
jupyter_root_path = f"{root_path}/jupyter"
|
|
379
|
+
|
|
376
380
|
render_settings = {
|
|
377
381
|
"title": title,
|
|
378
382
|
"path": path,
|
|
379
383
|
"root_path": root_path,
|
|
384
|
+
"jupyter_root_path": jupyter_root_path,
|
|
380
385
|
"resources": resources,
|
|
381
386
|
"theme": settings.theme.dict(),
|
|
382
387
|
"production": settings.main.mode == "production",
|
solara/server/starlette.py
CHANGED
|
@@ -391,16 +391,16 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
|
391
391
|
"""
|
|
392
392
|
if "script-name" in request.headers:
|
|
393
393
|
msg += f"""It looks like the reverse proxy sets the script-name header to {request.headers['script-name']!r}
|
|
394
|
-
|
|
394
|
+
"""
|
|
395
395
|
if "x-script-name" in request.headers:
|
|
396
396
|
msg += f"""It looks like the reverse proxy sets the x-script-name header to {request.headers['x-script-name']!r}
|
|
397
|
-
|
|
397
|
+
"""
|
|
398
398
|
if configured_root_path:
|
|
399
399
|
msg += f"""It looks like the root path was configured to {configured_root_path!r} in the settings
|
|
400
|
-
|
|
400
|
+
"""
|
|
401
401
|
if root_path_asgi:
|
|
402
402
|
msg += f"""It looks like the root path set by the asgi framework was configured to {root_path_asgi!r}
|
|
403
|
-
|
|
403
|
+
"""
|
|
404
404
|
warnings.warn(msg)
|
|
405
405
|
if host and forwarded_host and forwarded_proto:
|
|
406
406
|
port = request.base_url.port
|
|
@@ -451,6 +451,18 @@ See also https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
|
451
451
|
if request.scope["scheme"] == "https" or request.headers.get("x-forwarded-proto", "http") == "https" or request.base_url.hostname == "localhost":
|
|
452
452
|
samesite = "none"
|
|
453
453
|
secure = True
|
|
454
|
+
elif request.base_url.hostname != "localhost":
|
|
455
|
+
warnings.warn(f"""Cookies with samesite=none require https, but according to the asgi framework, the scheme is {request.scope['scheme']!r}
|
|
456
|
+
and the x-forwarded-proto header is {request.headers.get('x-forwarded-proto', 'http')!r}. We will fallback to samesite=lax.
|
|
457
|
+
|
|
458
|
+
If you embed solara in an iframe, make sure you forward the x-forwarded-proto header correctly so that the session cookie can be set.
|
|
459
|
+
|
|
460
|
+
See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite for more information on samesite cookies.
|
|
461
|
+
|
|
462
|
+
Also check out the following Solara documentation:
|
|
463
|
+
* https://solara.dev/documentation/getting_started/deploying/self-hosted
|
|
464
|
+
* https://solara.dev/documentation/advanced/howto/embed
|
|
465
|
+
""")
|
|
454
466
|
response.set_cookie(
|
|
455
467
|
server.COOKIE_KEY_SESSION_ID,
|
|
456
468
|
value=session_id,
|
|
@@ -483,9 +495,9 @@ class StaticNbFiles(StaticFilesOptionalAuth):
|
|
|
483
495
|
# from https://github.com/encode/starlette/pull/1377/files
|
|
484
496
|
def lookup_path(self, path: str) -> typing.Tuple[str, typing.Optional[os.stat_result]]:
|
|
485
497
|
for directory in self.all_directories:
|
|
498
|
+
directory = os.path.realpath(directory)
|
|
486
499
|
original_path = os.path.join(directory, path)
|
|
487
500
|
full_path = os.path.realpath(original_path)
|
|
488
|
-
directory = os.path.realpath(directory)
|
|
489
501
|
# return early if someone tries to access a file outside of the directory
|
|
490
502
|
if not path_is_child_of(Path(original_path), Path(directory)):
|
|
491
503
|
return "", None
|
|
@@ -668,7 +680,7 @@ routes = [
|
|
|
668
680
|
*([Mount(f"/{cdn_url_path}", app=StaticCdn(directory=settings.assets.proxy_cache_dir))] if solara.settings.assets.proxy else []),
|
|
669
681
|
Mount(f"{prefix}/static/public", app=StaticPublic()),
|
|
670
682
|
Mount(f"{prefix}/static/assets", app=StaticAssets()),
|
|
671
|
-
Mount(f"{prefix}/
|
|
683
|
+
Mount(f"{prefix}/jupyter/nbextensions", app=StaticNbFiles()),
|
|
672
684
|
Mount(f"{prefix}/static", app=StaticFilesOptionalAuth(directory=server.solara_static)),
|
|
673
685
|
Route("/{fullpath:path}", endpoint=root),
|
|
674
686
|
]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
and then remove the overlapping first lines with highlight.css
|
|
3
3
|
*/
|
|
4
4
|
.theme--dark.v-application .highlight .hll { background-color: #ffffcc }
|
|
5
|
-
.theme--dark.v-application .highlight { background: #
|
|
5
|
+
.theme--dark.v-application .highlight { background: #1d1f22; color: #ABB2BF }
|
|
6
6
|
.theme--dark.v-application .highlight .c { color: #7F848E } /* Comment */
|
|
7
7
|
.theme--dark.v-application .highlight .err { color: #ABB2BF } /* Error */
|
|
8
8
|
.theme--dark.v-application .highlight .esc { color: #ABB2BF } /* Escape */
|
|
@@ -127,7 +127,7 @@ async function solaraInit(mountId, appName) {
|
|
|
127
127
|
window.navigator.sendBeacon(close_url);
|
|
128
128
|
}
|
|
129
129
|
});
|
|
130
|
-
let kernel = await solara.connectKernel(solara.
|
|
130
|
+
let kernel = await solara.connectKernel(solara.jupyterRootPath, kernelId)
|
|
131
131
|
if (!kernel) {
|
|
132
132
|
return;
|
|
133
133
|
}
|
|
@@ -119,7 +119,7 @@ async def main():
|
|
|
119
119
|
]
|
|
120
120
|
for dep in requirements:
|
|
121
121
|
await micropip.install(dep, keep_going=True)
|
|
122
|
-
await micropip.install("/wheels/solara-1.
|
|
122
|
+
await micropip.install("/wheels/solara-1.40.0-py2.py3-none-any.whl", keep_going=True)
|
|
123
123
|
import solara
|
|
124
124
|
|
|
125
125
|
el = solara.Warning("lala")
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
<script id="jupyter-config-data" type="application/json">
|
|
33
33
|
{
|
|
34
|
-
"baseUrl": "{{
|
|
34
|
+
"baseUrl": "{{jupyter_root_path}}/",
|
|
35
35
|
"kernelId": "1234"
|
|
36
36
|
}
|
|
37
37
|
</script>
|
|
@@ -243,12 +243,13 @@
|
|
|
243
243
|
{% endif %}
|
|
244
244
|
<script>
|
|
245
245
|
solara.rootPath = {{ root_path | tojson | safe}};
|
|
246
|
+
solara.jupyterRootPath = {{ jupyter_root_path | tojson | safe}};
|
|
246
247
|
solara.cdn = {{ cdn | tojson | safe }};
|
|
247
248
|
// the vue templates expect it to not have a trailing slash
|
|
248
249
|
solara.cdn = solara.cdn.replace(/\/$/, '');
|
|
249
250
|
// keep this for backwards compatibility
|
|
250
251
|
window.solara_cdn = solara.cdn;
|
|
251
|
-
console.log("
|
|
252
|
+
console.log("solara config", {rootPath: solara.rootPath, jupyterRootPath: solara.jupyterRootPath, cdn: solara.cdn});
|
|
252
253
|
|
|
253
254
|
async function changeThemeCSS(theme) {
|
|
254
255
|
let css = await fetch(`${solara.rootPath}/static/assets/theme-${theme}.css`).then(r => r.text());
|
|
@@ -269,6 +270,12 @@
|
|
|
269
270
|
solara.production = {{ production | tojson | safe }};
|
|
270
271
|
const themeVariants = ['light', 'dark', 'auto']
|
|
271
272
|
solara.preRendered = {{ pre_rendered_html | safe | length | tojson }} > 0
|
|
273
|
+
if(solara.preRendered) {
|
|
274
|
+
document.body.classList.add('solara-ssg', 'solara-pre-rendered');
|
|
275
|
+
} else {
|
|
276
|
+
document.body.classList.add('solara-no-ssg');
|
|
277
|
+
}
|
|
278
|
+
|
|
272
279
|
</script>
|
|
273
280
|
|
|
274
281
|
<script>
|
|
@@ -329,8 +336,25 @@
|
|
|
329
336
|
// if preRendered, the app is not mounted yet
|
|
330
337
|
// so we mount it when loading becomes false
|
|
331
338
|
if (solara.preRendered && !this.mounted) {
|
|
332
|
-
|
|
333
|
-
|
|
339
|
+
function waitForAnimation(element) {
|
|
340
|
+
return new Promise((resolve) => {
|
|
341
|
+
element.addEventListener('animationend', resolve, { once: true });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function waitForAllAnimations(elements) {
|
|
346
|
+
const animationPromises = Array.from(elements).map(element => waitForAnimation(element));
|
|
347
|
+
return Promise.all(animationPromises);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
(async () => {
|
|
351
|
+
await waitForAllAnimations(document.querySelectorAll('.solara-ssg-wait-for-animation'));
|
|
352
|
+
console.log("animation finished");
|
|
353
|
+
document.body.classList.remove('solara-pre-rendered');
|
|
354
|
+
document.body.classList.add('solara-pre-rendered-finished');
|
|
355
|
+
this.isMounted = true;
|
|
356
|
+
this.$mount("#app")
|
|
357
|
+
})();
|
|
334
358
|
} else {
|
|
335
359
|
this.isMounted = true;
|
|
336
360
|
}
|
|
@@ -418,7 +442,7 @@
|
|
|
418
442
|
{% endif -%}
|
|
419
443
|
nbextensionHashes = {{ resources.nbextensions_hashes | tojson | safe }};
|
|
420
444
|
requirejs.config({
|
|
421
|
-
baseUrl: '{{
|
|
445
|
+
baseUrl: '{{jupyter_root_path}}',
|
|
422
446
|
waitSeconds: 3000,
|
|
423
447
|
map: {
|
|
424
448
|
'*': {
|
|
@@ -443,7 +467,7 @@
|
|
|
443
467
|
});
|
|
444
468
|
requirejs([
|
|
445
469
|
{% for ext in resources.nbextensions if ext != 'jupyter-vuetify/extension' and ext != 'jupyter-vue/extension' -%}
|
|
446
|
-
"{{
|
|
470
|
+
"{{jupyter_root_path}}/nbextensions/{{ ext }}.js",
|
|
447
471
|
{% endfor %}
|
|
448
472
|
]);
|
|
449
473
|
(async function () {
|