diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 251ef8cc..8229c6e2 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -2,6 +2,8 @@ name: Docker Build on: push: + pull_request: + jobs: test: runs-on: ubuntu-latest diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 824053c8..1efb06a6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,6 +2,8 @@ name: Lint on: push: + pull_request: + jobs: build: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 19e6af86..38ceb032 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,6 +2,8 @@ name: Test on: push: + pull_request: + jobs: test: runs-on: ubuntu-latest diff --git a/jhub_apps/launcher/hub_client.py b/jhub_apps/launcher/hub_client.py index 29bb8f56..2df1fb58 100644 --- a/jhub_apps/launcher/hub_client.py +++ b/jhub_apps/launcher/hub_client.py @@ -1,4 +1,6 @@ import os +import re +import uuid import requests @@ -35,8 +37,20 @@ def get_server(self, username, servername): if name == servername: return server + def normalize_server_name(self, servername): + # Convert text to lowercase + text = servername.lower() + # Remove all special characters except spaces and hyphen + text = re.sub(r"[^a-z0-9\s-]", "", text) + # Replace spaces with hyphens + text = text.replace(" ", "-") + return text + def create_server(self, username, servername, edit=True, params=None): server = self.get_server(username, servername) + if not edit: + servername = self.normalize_server_name(servername) + servername = f"{servername}-{uuid.uuid4().hex[:7]}" if server: if edit: self.delete_server(username, server["name"]) diff --git a/jhub_apps/launcher/panel_app.py b/jhub_apps/launcher/panel_app.py index 82d70cbf..9a1b20cf 100644 --- a/jhub_apps/launcher/panel_app.py +++ b/jhub_apps/launcher/panel_app.py @@ -4,12 +4,53 @@ import panel as pn from jhub_apps.launcher.hub_client import HubClient -from jhub_apps.spawner.types import Framework, FRAMEWORKS_MAPPING, FrameworkConf +from jhub_apps.spawner.types import FRAMEWORKS_MAPPING, FrameworkConf EDIT_APP_BTN_TXT = "Edit App" CREATE_APP_BTN_TXT = "Create App" +css = """ +.custom-font { + font-family: Mukta, sans-serif; + font-size: 1.3em; +} +.bk-input { + font-family: Mukta, sans-serif; + font-size: 1.1em; +} +.bk-btn { + font-family: Mukta, sans-serif; + font-size: 1.4em; +} +.bk-btn:hover { + background: #034c76 !important; + color: white !important; +} +.bk-btn-danger:hover { + background: #dc3545 !important; + color: white !important; +} +.custom-heading { + text-align: center; + word-wrap: break-word; +} + +h2 { + word-wrap: break-word; +} +.center-row-image { + display: flex; + justify-content: center; +} +.bk-Column { + padding-right: 12px; + padding-bottom: 12px; +} +""" +pn.extension(raw_css=[css]) + + @dataclass class InputFormWidget: name_input: Any @@ -69,20 +110,45 @@ def __init__(self, app: App, input_form_widget: InputFormWidget, **params): self.username = params.get("username") # Define Panel buttons - self.view_button = pn.widgets.Button(name="View", button_type="primary") - self.edit_button = pn.widgets.Button(name="Edit", button_type="warning") - self.delete_button = pn.widgets.Button(name="Delete", button_type="danger") + self.view_button = pn.widgets.Button(name="Launch", button_type="primary") + self.edit_button = pn.widgets.Button( + name="Edit", button_type="primary", button_style="outline" + ) + self.delete_button = pn.widgets.Button( + name="Delete", button_type="danger", button_style="outline" + ) # Set up event listeners for the buttons - code = f"""window.location.href = '{self.app.url}'""" + code = f"""window.open('{self.app.url}', '_blank');""" self.view_button.js_on_click(code=code) self.edit_button.on_click(self.on_edit) self.delete_button.on_click(self.on_delete) # Using a Row to group the image, description, and buttons horizontally self.content = pn.Row( - pn.pane.PNG(self.app.logo, width=50), - pn.pane.Markdown(f"**{self.app.name}**", margin=(0, 20, 0, 10)), + pn.pane.Image( + self.app.logo, + link_url=self.app.url, + width=80, + height=80, + align=("center", "center"), + ), + pn.pane.Markdown( + f""" + +
+ + ## {self.app.name} + + {self.app.description or "No description found for app"} +
+ """, + margin=(0, 20, 0, 10), + ), self.view_button, self.edit_button, self.delete_button, @@ -127,6 +193,27 @@ def on_delete(self, event): self.content.visible = False +def heading_markdown(heading): + return pn.pane.Markdown( + f""" + +
+ + # {heading} + +
+ """, + margin=0, + sizing_mode="stretch_width", + ) + + def create_list_apps(input_form_widget, username): print("Create Dashboards Layout") list_items = [] @@ -137,10 +224,9 @@ def create_list_apps(input_form_widget, username): ) list_items.append(list_item) - heading = pn.pane.Markdown("## Your Apps", sizing_mode="stretch_width") # Wrap everything in a Column with the list-container class layout = pn.Column( - heading, + heading_markdown("Your Apps"), *list_items, css_classes=["list-container"], width=800, @@ -152,16 +238,24 @@ def create_list_apps(input_form_widget, username): def get_input_form_widget(): frameworks_display = {f.display_name: f.name for f in FRAMEWORKS_MAPPING.values()} - heading = pn.pane.Markdown("## Create Apps", sizing_mode="stretch_width") + heading = heading_markdown("Create Apps") input_form_widget = InputFormWidget( - name_input=pn.widgets.TextInput(name="Name", id="app_name_input"), - filepath_input=pn.widgets.TextInput(name="Filepath"), - description_input=pn.widgets.TextAreaInput(name="Description"), + name_input=pn.widgets.TextInput( + name="Name", id="app_name_input", css_classes=["custom-font"] + ), + filepath_input=pn.widgets.TextInput( + name="Filepath", css_classes=["custom-font"] + ), + description_input=pn.widgets.TextAreaInput( + name="Description", css_classes=["custom-font"] + ), spinner=pn.indicators.LoadingSpinner( size=30, value=True, color="secondary", bgcolor="dark", visible=True ), button_widget=pn.widgets.Button(name=CREATE_APP_BTN_TXT, button_type="primary"), - framework=pn.widgets.Select(name="Framework", options=frameworks_display), + framework=pn.widgets.Select( + name="Framework", options=frameworks_display, css_classes=["custom-font"] + ), ) input_form = pn.Column( heading, @@ -220,7 +314,18 @@ def _create_server(event, input_form_widget, input_form, username): dashboard_creation_action = "updated" text_with_link = pn.pane.Markdown( f""" - ## 🚀 App {dashboard_creation_action}: [👉🔗]({dashboard_link}) + +
+ + ## 🚀 App {dashboard_creation_action}: [👉🔗]({dashboard_link}) + +
""" ) input_form.append(text_with_link) diff --git a/jhub_apps/spawner/types.py b/jhub_apps/spawner/types.py index 4a4a4ba5..741b5b43 100644 --- a/jhub_apps/spawner/types.py +++ b/jhub_apps/spawner/types.py @@ -27,7 +27,7 @@ def values(cls): FrameworkConf( name=Framework.panel.value, display_name="Panel", - logo="https://raw.githubusercontent.com/holoviz/panel/main/doc/_static/logo_stacked.png", + logo="https://raw.githubusercontent.com/holoviz/panel/5c69f11bc139076a0e55d444dcfbf3e44b3ed8a8/doc/_static/logo.png", ), FrameworkConf( name=Framework.bokeh.value, @@ -47,17 +47,17 @@ def values(cls): FrameworkConf( name=Framework.plotlydash.value, display_name="PlotlyDash", - logo="https://repository-images.githubusercontent.com/33702544/b4400c80-718b-11e9-9f3a-306c07a5f3de", + logo="https://raw.githubusercontent.com/plotly/dash/6eaf2e17c25f7ca1847c41aafeb18e87c586cb9f/components/dash-table/tests/selenium/assets/logo.png", ), FrameworkConf( name=Framework.gradio.value, display_name="Gradio", - logo="https://avatars.githubusercontent.com/u/51063788?s=48&v=4", + logo="https://pbs.twimg.com/profile_images/1526964416834510848/Njy4Kh2q_400x400.jpg", ), FrameworkConf( name=Framework.jupyterlab.value, display_name="JupyterLab", - logo="https://jupyter.org/assets/logos/rectanglelogo-greytext-orangebody-greymoons.svg", + logo="https://upload.wikimedia.org/wikipedia/commons/thumb/3/38/Jupyter_logo.svg/1200px-Jupyter_logo.svg.png", ), ] diff --git a/jhub_apps/tests_e2e/test_playwright.py b/jhub_apps/tests_e2e/test_playwright.py index 3b374e3a..b7c984d9 100644 --- a/jhub_apps/tests_e2e/test_playwright.py +++ b/jhub_apps/tests_e2e/test_playwright.py @@ -62,10 +62,16 @@ def test_panel_dashboard_creation(page: Page, framework, expected_title): page.click(".bk-btn.bk-btn-primary") # Wait for the dashboard to be created time.sleep(5) - # Click on View Dashboard - page.click("text=View") + # Click on Launch Dashboard + launch_button = page.locator('button:text("Launch")') + + assert launch_button.first.is_visible() + + with page.expect_popup() as framework_page_info: + launch_button.click() + framework_page = framework_page_info.value try: - expect(page).to_have_title(re.compile(expected_title)) + expect(framework_page).to_have_title(re.compile(expected_title)) except AssertionError as e: # Go back to japps page page.goto(url)