Skip to content

Commit

Permalink
Merge branch 'main' into payload-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
jbouder committed Apr 8, 2024
2 parents 35c0ffc + 8d424ae commit ae7e816
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 63 deletions.
36 changes: 27 additions & 9 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ jobs:
python-version:
- "3.9"
test_type:
- tests_e2e
- "with_server_options"
- "not with_server_options"
os:
- ubuntu-latest
steps:
Expand All @@ -37,8 +38,26 @@ jobs:
- name: Install chp
run: npm install -g configurable-http-proxy

- name: Add server options if with server types
if: matrix.test_type == 'with_server_options'
run: |
cat >> jupyterhub_config.py <<- EOM
c.KubeSpawner.profile_list = [
{
"description": "Stable environment with 0.5-1 cpu / 0.5-1 GB ram",
"display_name": "Micro Instance",
"slug": "micro-instance"
},
{
"description": "Stable environment with 1 cpu / 1 GB ram",
"display_name": "Small Instance",
"slug": "small-instance"
},
]
EOM
cat jupyterhub_config.py
- name: Start JupyterHub on Ubuntu
if: matrix.test_type == 'tests_e2e' && matrix.os == 'ubuntu-latest'
run: |
nohup jupyterhub -f jupyterhub_config.py > jupyterhub-logs.txt 2>&1 &
# Give it some to time to start properly
Expand All @@ -48,30 +67,29 @@ jobs:
cat jupyterhub-logs.txt
- name: Install Playwright
if: matrix.test_type == 'tests_e2e'
run: |
pip install pytest-playwright
- name: Install Playwright Browser
if: matrix.test_type == 'tests_e2e'
run: |
playwright install
- name: Run Tests
run: |
pytest jhub_apps/${{ matrix.test_type }} -vvv
pytest jhub_apps/tests_e2e/ -vvv -m "${{ matrix.test_type }}"
- name: Upload Playwright Videos
if: matrix.test_type == 'tests_e2e' && always()
if: always()
uses: actions/upload-artifact@v4.3.0
with:
name: ${{ matrix.os }}-playwright-videos
name: ${{ matrix.os }}-${{ matrix.test_type }}-playwright-videos
path: videos

- name: Upload JupyterHub logs
if: matrix.test_type == 'tests_e2e' && always()
if: always()
uses: actions/upload-artifact@v4.3.0
with:
name: ${{ matrix.os }}-jupyterhub-logs
name: ${{ matrix.os }}-${{ matrix.test_type }}-jupyterhub-logs
path: jupyterhub-logs.txt

- name: JupyterHub logs
Expand Down
2 changes: 1 addition & 1 deletion jhub_apps/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2024.4.1rc4"
__version__ = "2024.4.1rc5"
1 change: 1 addition & 0 deletions jhub_apps/hub_client/hub_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ def create_server(self, username, servername, user_options=None):
logger.info("Creating new server", user=username)
servername = self.normalize_server_name(servername)
servername = f"{servername}-{uuid.uuid4().hex[:7]}"
logger.info("Normalized servername", servername=servername)
return self._create_server(username, servername, user_options)

def edit_server(self, username, servername, user_options=None):
Expand Down
3 changes: 1 addition & 2 deletions jhub_apps/service/routes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dataclasses
import os
import typing
from datetime import timedelta
Expand Down Expand Up @@ -256,7 +255,7 @@ async def get_frameworks(user: User = Depends(get_current_user)):
logger.info("Getting all the frameworks")
frameworks = []
for framework in FRAMEWORKS:
frameworks.append(dataclasses.asdict(framework))
frameworks.append(framework.json())
return frameworks


Expand Down
36 changes: 15 additions & 21 deletions jhub_apps/service/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from cachetools import cached, TTLCache
from unittest.mock import Mock

import requests
from jupyterhub.app import JupyterHub
from traitlets.config import LazyConfigValue

Expand Down Expand Up @@ -115,38 +114,33 @@ def encode_file_to_data_url(filename, file_contents):

def get_default_thumbnail(framework_name):
framework: FrameworkConf = FRAMEWORKS_MAPPING.get(framework_name)
thumbnail_url = framework.logo
if thumbnail_url.startswith("/"):
base_url = os.environ["PUBLIC_HOST"]
thumbnail_url = f"{base_url}{thumbnail_url}"
try:
response = requests.get(thumbnail_url)
except Exception as e:
logger.info(f"Unable to fetch thumbnail from url: {thumbnail_url}:")
logger.exception(e)
return
if response.status_code == 200:
thumbnail_content = response.content
thumbnail_filename = thumbnail_url.split("/")[-1]
return encode_file_to_data_url(filename=thumbnail_filename, file_contents=thumbnail_content)
thumbnail_path = framework.logo_path
return encode_file_to_data_url(
filename=thumbnail_path.name, file_contents=thumbnail_path.read_bytes()
)


async def get_thumbnail_data_url(framework_name, thumbnail):
logger.info("Getting thumbnail data url", framework=framework_name)
if thumbnail:
logger.info("Got user provided thumbnail")
thumbnail_contents = await thumbnail.read()
thumbnail_data_url = encode_file_to_data_url(
thumbnail.filename, thumbnail_contents
)
else:
thumbnail_data_url = get_default_thumbnail(framework_name)
logger.info("Getting default thumbnail")
framework: FrameworkConf = FRAMEWORKS_MAPPING.get(framework_name)
thumbnail_path = framework.logo_path
thumbnail_data_url = encode_file_to_data_url(
filename=thumbnail_path.name, file_contents=thumbnail_path.read_bytes()
)
return thumbnail_data_url


def get_theme(config):
"""This will extract theme variables from the JupyterHub config"""
if isinstance(config.theme, dict):
return config.theme.jupyterhub
elif isinstance(config.theme, LazyConfigValue):
return {}
if isinstance(config.JupyterHub.template_vars, dict):
return config.JupyterHub.template_vars
else:
return None
return None
39 changes: 31 additions & 8 deletions jhub_apps/spawner/types.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import typing
from dataclasses import dataclass
from enum import Enum
from pathlib import Path

HERE = Path(__file__).parent.parent.resolve()

LOGO_BASE_PATH = "/services/japps/static/img/logos/",
STATIC_PATH = HERE.joinpath("static/img/logos")


@dataclass
class FrameworkConf:
name: str
display_name: str
logo_path: Path
# logo url
logo: str

def json(self):
return {
"name": self.name,
"display_name": self.display_name,
"logo": self.logo,
}


@dataclass
class UserOptions:
Expand Down Expand Up @@ -42,42 +57,50 @@ def values(cls):
FrameworkConf(
name=Framework.panel.value,
display_name="Panel",
logo="/services/japps/static/img/logos/panel.png",
logo_path=STATIC_PATH.joinpath("panel.png"),
logo=f"{LOGO_BASE_PATH}/panel.png"
),
FrameworkConf(
name=Framework.bokeh.value,
display_name="Bokeh",
logo="/services/japps/static/img/logos/bokeh.png",
logo_path=STATIC_PATH.joinpath("bokeh.png"),
logo=f"{LOGO_BASE_PATH}/bokeh.png"
),
FrameworkConf(
name=Framework.streamlit.value,
display_name="Streamlit",
logo="/services/japps/static/img/logos/streamlit.png",
logo_path=STATIC_PATH.joinpath("streamlit.png"),
logo=f"{LOGO_BASE_PATH}/streamlit.png"
),
FrameworkConf(
name=Framework.voila.value,
display_name="Voila",
logo="/services/japps/static/img/logos/voila.png",
logo_path=STATIC_PATH.joinpath("voila.png"),
logo=f"{LOGO_BASE_PATH}/voila.png"
),
FrameworkConf(
name=Framework.plotlydash.value,
display_name="PlotlyDash",
logo="/services/japps/static/img/logos/plotly-dash.png",
logo_path=STATIC_PATH.joinpath("plotly-dash.png"),
logo=f"{LOGO_BASE_PATH}/plotly-dash.png"
),
FrameworkConf(
name=Framework.gradio.value,
display_name="Gradio",
logo="/services/japps/static/img/logos/gradio.png",
logo_path=STATIC_PATH.joinpath("gradio.png"),
logo=f"{LOGO_BASE_PATH}/gradio.png"
),
# FrameworkConf(
# name=Framework.jupyterlab.value,
# display_name="JupyterLab",
# logo="https://upload.wikimedia.org/wikipedia/commons/thumb/3/38/Jupyter_logo.svg/1200px-Jupyter_logo.svg.png",
# logo_path="",
# logo="",
# ),
FrameworkConf(
name=Framework.custom.value,
display_name="Custom Command",
logo="/services/japps/static/img/logos/custom.png",
logo_path=STATIC_PATH.joinpath("custom.png"),
logo=f"{LOGO_BASE_PATH}/custom.png"
),
]

Expand Down
3 changes: 1 addition & 2 deletions jhub_apps/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import dataclasses
import io
import json
from unittest.mock import patch
Expand Down Expand Up @@ -148,7 +147,7 @@ def test_api_frameworks(client):
)
frameworks = []
for framework in FRAMEWORKS:
frameworks.append(dataclasses.asdict(framework))
frameworks.append(framework.json())
assert response.json() == frameworks


Expand Down
69 changes: 49 additions & 20 deletions jhub_apps/tests_e2e/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
import uuid

import pytest
import structlog
from playwright.sync_api import Playwright, expect

Expand Down Expand Up @@ -29,34 +30,24 @@ def test_jupyterhub_loading(playwright: Playwright):
context.close()


def test_panel_app_creation(playwright: Playwright) -> None:
@pytest.mark.parametrize(
("with_server_options", ), [
pytest.param(True, marks=pytest.mark.with_server_options),
pytest.param(False),
]
)
def test_panel_app_creation(playwright: Playwright, with_server_options) -> None:
browser, context, page = get_page(playwright)
framework = Framework.panel.value
app_suffix = uuid.uuid4().hex[:6]
# for searching app with unique name in the UI
app_name = f"{framework} app {app_suffix}"
app_page_title = "Panel Test App"
wait_for_element_in_app = "div.bk-slider-title >> text=Slider:"
try:
page.goto(BASE_URL)
logger.info("Signing in")
page.get_by_label("Username:").click()
page.get_by_label("Username:").fill(f"admin-{app_suffix}")
page.get_by_label("Password:").fill("admin")
logger.info("Pressing Sign in button")
page.get_by_role("button", name="Sign in").click()
logger.info("Click Authorize button")
page.get_by_role("button", name="Authorize").click()
logger.info("Creating App")
page.get_by_role("button", name="Create App").click()
logger.info("Fill App display Name")
page.get_by_label("Name *").click()
page.get_by_label("Name *").fill(app_name)
logger.info("Select Framework")
page.locator("id=framework").click()
page.get_by_role("option", name="Panel").click()
logger.info("Click Submit")
page.get_by_role("button", name="Create App").click()
sign_in_and_authorize(app_suffix, page)
create_app(app_name, page, with_server_options)
wait_for_element_in_app = "div.bk-slider-title >> text=Slider:"
slider_text_element = page.wait_for_selector(wait_for_element_in_app)
assert slider_text_element is not None, "Slider text element not found!"
logger.info("Checking page title")
Expand All @@ -66,3 +57,41 @@ def test_panel_app_creation(playwright: Playwright) -> None:
context.close()
browser.close()
raise e


def create_app(app_name, page, with_server_options=True):
logger.info("Creating App")
page.get_by_role("button", name="Create App").click()
logger.info("Fill App display Name")
page.get_by_label("Name *").click()
page.get_by_label("Name *").fill(app_name)
logger.info("Select Framework")
page.locator("id=framework").click()
page.get_by_role("option", name="Panel").click()
if with_server_options:
next_page_locator = page.get_by_role("button", name="Next")
logger.info("Select Next Page for Server options")
expect(next_page_locator).to_be_visible()
next_page_locator.click()
assert page.url.endswith('server-types')
small_instance_radio_button = page.get_by_label("Small Instance")
logger.info("Expect Small Instance to be visible")
expect(small_instance_radio_button).to_be_visible()
small_instance_radio_button.check()

create_app_locator = page.get_by_role("button", name="Create App")
logger.info("Expect Create App button to be visible")
expect(create_app_locator).to_be_visible()
logger.info("Click Create App")
create_app_locator.click()


def sign_in_and_authorize(app_suffix, page):
logger.info("Signing in")
page.get_by_label("Username:").click()
page.get_by_label("Username:").fill(f"admin-{app_suffix}")
page.get_by_label("Password:").fill("admin")
logger.info("Pressing Sign in button")
page.get_by_role("button", name="Sign in").click()
logger.info("Click Authorize button")
page.get_by_role("button", name="Authorize").click()

0 comments on commit ae7e816

Please sign in to comment.