Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/merge-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ jobs:
permissions:
contents: read

ui-tests:
name: UI
needs: [ run-slow-tests ]
uses: ./.github/workflows/tests-ui.yml
permissions:
contents: read

# This job ensures inputs have been executed successfully.
approve-merge:
name: Allow Merge
Expand All @@ -63,7 +70,7 @@ jobs:
contents: read
# If you need additional jobs to be part of the merge gate, add them below
# Removed saas-tests from being required
needs: [ fast-checks, large-runner-tests, ordinary-itests, gpu-tests ]
needs: [ fast-checks, large-runner-tests, ordinary-itests, gpu-tests, ui-tests ]

# Each job requires a step, so we added this dummy step.
steps:
Expand Down
48 changes: 48 additions & 0 deletions .github/workflows/tests-ui.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: UI

on:
workflow_call:

jobs:

ui-tests:
name: UI Tests
runs-on: ubuntu-24.04
permissions:
contents: read

steps:
- name: SCM Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Free disk space
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
large-packages: false

- name: Free disk space by removing large directories
run: |
sudo rm -rf /usr/local/graalvm/
sudo rm -rf /usr/local/.ghcup/
sudo rm -rf /usr/local/share/powershell
sudo rm -rf /usr/local/share/chromium
sudo rm -rf /usr/local/lib/node_modules
sudo rm -rf /opt/ghc

- name: Setup Python & Poetry Environment
uses: exasol/python-toolbox/.github/actions/python-environment@v1
with:
poetry-version: 2.1.2

- name: Install All Optional Dependencies (aka. "extras")
run: poetry install --all-extras

- name: Allow unprivileged user namespaces
run: |
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

- name: Tests
run: poetry run -- pytest -rA --setup-show test/ui/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need matrix for this ?

3 changes: 2 additions & 1 deletion doc/changes/unreleased.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Unreleased
* #287: working examples for functional UI
* #287: working examples for functional UI
* #297: Added Access store UI tests and GitHub workflow addition
66 changes: 66 additions & 0 deletions exasol/nb_connector/ui/access_store_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from exasol.nb_connector.ui.ui_styles import get_config_styles
from exasol.nb_connector.ui.popup_message_ui import popup_message

from pathlib import Path
import ipywidgets as widgets
from IPython import get_ipython

from exasol.nb_connector.secret_store import Secrets


def get_access_store_ui(root_dir: str = '.') -> widgets.Widget:

# Try to find the file name in the shared store.
# Create a global variable only temporarily.
ipython = get_ipython()
if ipython and hasattr(ipython, 'run_line_magic'):
ipython.run_line_magic('store', '-r') # reloads variables in the IPython user namespace persistence mechanism.
if 'sb_store_file' in globals():
global sb_store_file
sb_store_file_ = sb_store_file
del sb_store_file
else:
sb_store_file_ = 'ai_lab_secure_configuration_storage.sqlite'

ui_look = get_config_styles()

header_lbl = widgets.Label(value='Configuration Store', style=ui_look.header_style, layout=ui_look.header_layout)
file_lbl = widgets.Label(value='File Path', style=ui_look.label_style, layout=ui_look.label_layout)
password_lbl = widgets.Label(value='Password', style=ui_look.label_style, layout=ui_look.label_layout)
file_txt = widgets.Text(value=sb_store_file_, style=ui_look.input_style, layout=ui_look.input_layout)
password_txt = widgets.Password(style=ui_look.input_style, layout=ui_look.input_layout)
open_btn = widgets.Button(description='Open', style=ui_look.button_style, layout=ui_look.button_layout)

def open_or_create_config_store(btn):
global ai_lab_config, sb_store_file
sb_store_file = file_txt.value
try:
ai_lab_config = Secrets(Path(root_dir) / sb_store_file, password_txt.value)
ai_lab_config.connection()
except:
popup_message('Failed to open the store. Please check that the password is correct')
else:
open_btn.icon = 'check'
finally:
# Save the file in the shared store.
if ipython and hasattr(ipython, 'run_line_magic'):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jana-selva added this, because run_line_magic is throwing

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a comment that we added this to enable testing.

ipython.run_line_magic('store', 'sb_store_file')
del sb_store_file

def on_value_change(change):
open_btn.icon = 'pen'

open_btn.on_click(open_or_create_config_store)

file_txt.observe(on_value_change, names=['value'])
password_txt.observe(on_value_change, names=['value'])

group_items = [
header_lbl,
widgets.Box([file_lbl, file_txt], layout=ui_look.row_layout),
widgets.Box([password_lbl, password_txt], layout=ui_look.row_layout)
]
items = [widgets.Box(group_items, layout=ui_look.group_layout), open_btn]
ui = widgets.Box(items, layout=ui_look.outer_layout)
return ui

13 changes: 13 additions & 0 deletions exasol/nb_connector/ui/popup_message_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ipywidgets as widgets
from IPython.display import Javascript, display, clear_output

# Prepare to display a popup message
notify_output = widgets.Output()
display(notify_output)

@notify_output.capture()
def popup_message(message):
clear_output()
message = message.replace("'", '"')
display(Javascript(f"alert('{message}')"))

45 changes: 45 additions & 0 deletions exasol/nb_connector/ui/ui_styles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
The UI design aims to provide a convenient way of setting up various configuration parameters.
A user form would consist of one or more blocks of data laid out vertically or horizontally. The positioning of these blocks is defined by the `outer_layout` property.
A block includes a collection of UI elements. Each element is represented by a pair of widgets - a Label with the description of an element and the input widget, e.g. a Text. The layout of the elements within the block is defined by the `group_layout` property.
The layout of the description Label and the input widget is defined by the `row_layout` property. As the name suggests, an input and its description are assumed to be placed next to one another in a row, although such design is not strictly required.
A block may include a header Label where one can put the name of the input data group. `header_style` and `header_layout` define respectively the style and the layout of such labels.
Likewise, the Labels of input elements have their style and layout defined by the `label_style` and `label_layout` properties. The input elements themselves have similar properties - `input_style` and `input_layout`.
A user form may include one or more buttons. Their style and layout are defined as `button_style` and `button_layout`.
"""

# In[ ]:


def get_config_styles():

from dataclasses import dataclass, field
import ipywidgets as widgets

@dataclass(frozen=True)
class UI_Look:
header_style: dict = field(default_factory=dict)
header_layout: widgets.Layout = field(default_factory=widgets.Layout)
label_style: dict = field(default_factory=dict)
label_layout: widgets.Layout = field(default_factory=widgets.Layout)
input_style: dict = field(default_factory=dict)
input_layout: widgets.Layout = field(default_factory=widgets.Layout)
button_style: dict = field(default_factory=dict)
button_layout: widgets.Layout = field(default_factory=widgets.Layout)
row_layout: widgets.Layout = field(default_factory=widgets.Layout)
group_layout: widgets.Layout = field(default_factory=widgets.Layout)
outer_layout: widgets.Layout = field(default_factory=widgets.Layout)

return UI_Look(
header_style = {'background': 'Beige'},
header_layout = widgets.Layout(display='flex', justify_content='center', border='solid 1px'),
label_layout = widgets.Layout(max_width='130px'),
input_layout = widgets.Layout(max_width='200px'),
button_style = {'button_color': 'LightCyan'},
button_layout = widgets.Layout(border='solid 1px', margin='12px 0 0 0'),
row_layout = widgets.Layout(display='flex', flex_flow='row', max_width='350px', justify_content='space-between',
padding='0 0 0 10px'),
group_layout = widgets.Layout(display='flex', flex_flow='column', border='solid 1px', max_width='350px',
margin='12px 0 0 0'),
outer_layout = widgets.Layout(display='flex', flex_flow='column', align_items='stretch', padding='0 0 0 50px')
)
Loading
Loading