diff --git a/dem/cli/command/create_cmd.py b/dem/cli/command/create_cmd.py index 0f46af5..d783b4f 100644 --- a/dem/cli/command/create_cmd.py +++ b/dem/cli/command/create_cmd.py @@ -25,7 +25,7 @@ def open_dev_env_settings_panel(all_tool_images: dict[str, ToolImage]) -> list[s if "cancel" in dev_env_settings_panel.cancel_save_menu.get_selection(): raise typer.Abort() - return dev_env_settings_panel.selected_tool_images + return dev_env_settings_panel.tool_image_menu.get_selected_tool_images() def create_new_dev_env_descriptor(dev_env_name: str, selected_tool_images: list[str]) -> dict: """ Create a new Development Environment descriptor. diff --git a/dem/cli/command/info_cmd.py b/dem/cli/command/info_cmd.py index 129c26e..edd8d81 100644 --- a/dem/cli/command/info_cmd.py +++ b/dem/cli/command/info_cmd.py @@ -46,7 +46,7 @@ def execute(platform: Platform, arg_dev_env_name: str) -> None: dev_env = catalog.get_dev_env_by_name(arg_dev_env_name) if dev_env: dev_env.assign_tool_image_instances(platform.tool_images) - break + break if dev_env is None: stderr.print(f"[red]Error: Unknown Development Environment: {arg_dev_env_name}[/]\n") diff --git a/dem/cli/command/modify_cmd.py b/dem/cli/command/modify_cmd.py index d82d4c1..d0e564a 100644 --- a/dem/cli/command/modify_cmd.py +++ b/dem/cli/command/modify_cmd.py @@ -112,7 +112,7 @@ def open_dev_env_settings_panel(already_selected_tool_images: list[str], if "cancel" in dev_env_settings_panel.cancel_save_menu.get_selection(): raise typer.Abort() - return dev_env_settings_panel.selected_tool_images + return dev_env_settings_panel.tool_image_menu.get_selected_tool_images() def update_dev_env(dev_env: DevEnv, selected_tool_images: list[str]) -> None: """ Update the Development Environment. diff --git a/dem/cli/tui/printable_tool_image.py b/dem/cli/tui/printable_tool_image.py index ac5a363..2b800c4 100644 --- a/dem/cli/tui/printable_tool_image.py +++ b/dem/cli/tui/printable_tool_image.py @@ -16,7 +16,7 @@ def __init__ (self, tool_image: ToolImage): def convert_to_printable_tool_images(all_tool_images: dict[str, ToolImage]) -> list[PrintableToolImage]: printable_tool_images = [] - for tool_image in all_tool_images.values(): - printable_tool_images.append(PrintableToolImage(tool_image)) + for tool_image in sorted(all_tool_images.items()): + printable_tool_images.append(PrintableToolImage(tool_image[1])) return printable_tool_images \ No newline at end of file diff --git a/dem/cli/tui/renderable/menu.py b/dem/cli/tui/renderable/menu.py index eba3b83..7608d70 100644 --- a/dem/cli/tui/renderable/menu.py +++ b/dem/cli/tui/renderable/menu.py @@ -3,6 +3,8 @@ from dem.cli.tui.printable_tool_image import PrintableToolImage from rich import live, table, align, panel, box +from rich.console import RenderableType + from readchar import readkey, key class BaseMenu(table.Table): @@ -207,8 +209,10 @@ class ToolImageMenu(VerticalMenu): def __init__(self, printable_tool_images: list[PrintableToolImage], already_selected_tool_images: list[str]) -> None: super().__init__() + self.title = "Select the tool images for the Development Environment:" self.add_column("Tool images", no_wrap=True) self.add_column("Availability", no_wrap=True) + self.tool_image_selection = [] for index, tool_image in enumerate(printable_tool_images): row_content = [] @@ -219,9 +223,13 @@ def __init__(self, printable_tool_images: list[PrintableToolImage], row_content = " " + tool_image.name, tool_image.status if tool_image.name in already_selected_tool_images: + self.tool_image_selection.append(tool_image.name) row_content = f"{row_content[0][:2]}[green]{row_content[0][2:]}[/]", str(row_content[1]) self.add_row(*row_content) + + def get_table_width(self) -> int: + return sum(self._measure_column) def handle_user_input(self, input: str) -> None: if input == key.ENTER or input == key.SPACE: @@ -238,12 +246,14 @@ def handle_user_input(self, input: str) -> None: super().handle_user_input(input) def get_selected_tool_images(self) -> list[str]: - selected_tool_images = [] for cell in self.columns[0]._cells: - if "[green]" in cell: - selected_tool_images.append(cell[2:].replace("[/]", "").replace("[green]", "")) + tool_image_name = cell[2:].replace("[/]", "").replace("[green]", "") + if ("[green]" in cell) and (tool_image_name not in self.tool_image_selection): + self.tool_image_selection.append(tool_image_name) + elif ("[green]" not in cell) and (tool_image_name in self.tool_image_selection): + self.tool_image_selection.remove(tool_image_name) - return selected_tool_images + return self.tool_image_selection class SelectMenu(VerticalMenu): def __init__(self, selection: list[str]) -> None: @@ -278,19 +288,17 @@ def set_title(self, title: str) -> None: self.width = len(title) super().set_title(title) -class DevEnvStatusPanel(panel.Panel): - def __init__(self, already_selected_tool_images: list[str]) -> None: - self.outer_table = table.Table(box=None) - super().__init__(self.outer_table, title="Development Environment", expand=True) - self.aligned_renderable = align.Align(self, vertical="middle") - - for tool_image in already_selected_tool_images: - self.outer_table.add_row(tool_image) +class DevEnvStatusPanel(table.Table): + def __init__(self, selected_tool_images: list[str], height: int, width: int) -> None: + super().__init__(title="Dev Env Settings") + self.add_column("Selected Tool Images", no_wrap=True) - def update_table(self, selected_tool_images: list[str]) -> None: - self.outer_table = table.Table(box=None) + if height > 10: + height = 10 for tool_image in selected_tool_images: - self.outer_table.add_row(tool_image) + self.add_row(tool_image) - self.renderable = self.outer_table \ No newline at end of file + if len(selected_tool_images) < height: + for _ in range(height - len(selected_tool_images)): + self.add_row(" " * width) \ No newline at end of file diff --git a/dem/cli/tui/window/dev_env_settings_window.py b/dem/cli/tui/window/dev_env_settings_window.py index 9d067e3..74e5f51 100644 --- a/dem/cli/tui/window/dev_env_settings_window.py +++ b/dem/cli/tui/window/dev_env_settings_window.py @@ -18,43 +18,58 @@ class NavigationHint(Panel): """ def __init__(self) -> None: super().__init__(self.hint_text, title="Navigation", expand=False) - self.aligned_renderable = Align(self, align="center") class DevEnvSettingsWindow(): def __init__(self, printable_tool_images: list[PrintableToolImage], already_selected_tool_images: list[str] = []) -> None: # Panel content + self.dev_env_status_height = len(printable_tool_images) + self.dev_env_status_width = 0 + for printable_tool_image in printable_tool_images: + if len(printable_tool_image.name) > self.dev_env_status_width: + self.dev_env_status_width = len(printable_tool_image.name) + self.tool_image_menu = ToolImageMenu(printable_tool_images, already_selected_tool_images) - self.dev_env_status = DevEnvStatusPanel(already_selected_tool_images) + self.dev_env_status_panel = DevEnvStatusPanel(already_selected_tool_images, + self.dev_env_status_height, + self.dev_env_status_width) self.cancel_save_menu = CancelSaveMenu() - self.navigation_hint = NavigationHint() + self.navigation_hint_panel = NavigationHint() + + self.build_layout() + self.cancel_save_menu.remove_cursor() + self.active_menu = self.tool_image_menu - self.menus = Align(Group( - Align(self.tool_image_menu, align="center", vertical="middle"), - Align(self.cancel_save_menu, align="center", vertical="middle"), - ), align="center", vertical="middle") + def build_layout(self) -> None: + # Set the alignments + aligned_tool_image_menu = Align(self.tool_image_menu, vertical="bottom", align="right") + aligned_dev_env_status_panel = Align(self.dev_env_status_panel, vertical="bottom", align="left") + aligned_cancel_save_menu = Align(self.cancel_save_menu, vertical="middle", align="center") + aligned_navigation_hint_panel = Align(self.navigation_hint_panel, vertical="top", align="center") self.layout = Layout(name="root") self.layout.split_column( - Layout(name="main"), - Layout(name="navigation_hint", size=7), + Layout(name="dev_env_settings"), + Layout(name="navigation"), ) - self.layout["main"].split_row( - Layout(name="menus"), - Layout(name="dev_env_status", size=30) + self.layout["dev_env_settings"].split_row( + Layout(name="available"), + Layout(name="selected") ) - self.layout["menus"].update(self.menus) - self.layout["dev_env_status"].update(self.dev_env_status.aligned_renderable) - self.layout["navigation_hint"].update(self.navigation_hint.aligned_renderable) + self.layout["navigation"].split_column( + Layout(name="cancel_save", ratio=2), + Layout(name="navigation_hint", ratio=7) + ) - self.cancel_save_menu.remove_cursor() - self.active_menu = self.tool_image_menu + self.layout["available"].update(aligned_tool_image_menu) + self.layout["selected"].update(aligned_dev_env_status_panel) + self.layout["cancel_save"].update(aligned_cancel_save_menu) + self.layout["navigation_hint"].update(aligned_navigation_hint_panel) def wait_for_user(self) -> None: - self.selected_tool_images = [] - with Live(self.layout, refresh_per_second=8, screen=True): + with Live(self.layout, refresh_per_second=8, screen=True) as live: while self.cancel_save_menu.is_selected is False: input = readkey() if input is key.TAB: @@ -70,5 +85,8 @@ def wait_for_user(self) -> None: self.active_menu.handle_user_input(input) if (self.active_menu is self.tool_image_menu) and (input in [key.ENTER, key.SPACE]): - self.selected_tool_images = self.tool_image_menu.get_selected_tool_images() - self.dev_env_status.update_table(self.selected_tool_images) \ No newline at end of file + self.dev_env_status_panel = DevEnvStatusPanel(self.tool_image_menu.get_selected_tool_images(), + self.dev_env_status_height, + self.dev_env_status_width) + self.build_layout() + live.update(self.layout) \ No newline at end of file diff --git a/tests/cli/test_create_cmd.py b/tests/cli/test_create_cmd.py index 715400c..d4e488a 100644 --- a/tests/cli/test_create_cmd.py +++ b/tests/cli/test_create_cmd.py @@ -27,7 +27,7 @@ def test_open_dev_env_settings_panel(mock_convert_to_printable_tool_images : Mag mock_dev_env_settings_panel = MagicMock() mock_selected_tool_images = MagicMock() - mock_dev_env_settings_panel.selected_tool_images = mock_selected_tool_images + mock_dev_env_settings_panel.tool_image_menu.get_selected_tool_images.return_value = mock_selected_tool_images mock_DevEnvSettingsWindow.return_value = mock_dev_env_settings_panel mock_dev_env_settings_panel.cancel_save_menu.get_selection.return_value = "save" @@ -42,6 +42,7 @@ def test_open_dev_env_settings_panel(mock_convert_to_printable_tool_images : Mag mock_DevEnvSettingsWindow.assert_called_once_with(mock_printable_tool_images) mock_dev_env_settings_panel.wait_for_user.assert_called_once() mock_dev_env_settings_panel.cancel_save_menu.get_selection.assert_called_once() + mock_dev_env_settings_panel.tool_image_menu.get_selected_tool_images.assert_called_once() @patch("dem.cli.command.create_cmd.DevEnvSettingsWindow") @patch("dem.cli.command.create_cmd.convert_to_printable_tool_images") diff --git a/tests/cli/test_modify_cmd.py b/tests/cli/test_modify_cmd.py index 1803a24..bec4f7b 100644 --- a/tests/cli/test_modify_cmd.py +++ b/tests/cli/test_modify_cmd.py @@ -136,7 +136,7 @@ def test_open_dev_env_settings_panel(mock_DevEnvSettingsWindow: MagicMock) -> No mock_dev_env_settings_panel = MagicMock() mock_DevEnvSettingsWindow.return_value = mock_dev_env_settings_panel expected_selected_tool_images = ["test1:1.0", "test2:2.0"] - mock_dev_env_settings_panel.selected_tool_images = expected_selected_tool_images + mock_dev_env_settings_panel.tool_image_menu.get_selected_tool_images.return_value = expected_selected_tool_images mock_dev_env_settings_panel.cancel_save_menu.get_selection.return_value = "save" mock_printable_tool_images = MagicMock() @@ -146,12 +146,12 @@ def test_open_dev_env_settings_panel(mock_DevEnvSettingsWindow: MagicMock) -> No mock_printable_tool_images) # Check expectations + assert actual_selected_tool_images == expected_selected_tool_images + mock_DevEnvSettingsWindow.assert_called_once_with(mock_printable_tool_images, ["test1:1.0", "test2:2.0"]) mock_dev_env_settings_panel.wait_for_user.assert_called_once() - assert actual_selected_tool_images == expected_selected_tool_images - @patch("dem.cli.command.modify_cmd.DevEnvSettingsWindow") def test_open_dev_env_settings_panel_cancel(mock_DevEnvSettingsWindow: MagicMock) -> None: # Test setup diff --git a/tests/cli/tui/renderable/test_menu.py b/tests/cli/tui/renderable/test_menu.py index c15150b..a4d9fe8 100644 --- a/tests/cli/tui/renderable/test_menu.py +++ b/tests/cli/tui/renderable/test_menu.py @@ -345,6 +345,7 @@ def test_get_selected_tool_images(mock___init__: MagicMock) -> None: mock___init__.return_value = None test_tool_image_menu = menu.ToolImageMenu([], []) + test_tool_image_menu.tool_image_selection = [] mock_columns = [MagicMock()] test_tool_image_menu.columns = mock_columns test_tool_image_menu.columns[0]._cells = [] @@ -440,58 +441,25 @@ def test_set_title(mock_set_title: MagicMock) -> None: mock_set_title.assert_called_once_with(test_title) -@patch("dem.cli.tui.renderable.menu.align.Align") -@patch("dem.cli.tui.renderable.menu.table.Table") -@patch.object(menu.panel.Panel, "__init__") -def test_DevEnvStatusPanel(mock___init__: MagicMock, mock_Table: MagicMock, mock_Align: MagicMock) -> None: - # Test setup - mock___init__.return_value = None - - mock_outer_table = MagicMock() - mock_Table.return_value = mock_outer_table - - mock_aligned_renderable = MagicMock() - mock_Align.return_value = mock_aligned_renderable - - test_already_selected_tool_images = ["test1", "test2"] - - # Run unit under test - test_dev_env_status_panel = menu.DevEnvStatusPanel(test_already_selected_tool_images) - - # Check expectations - mock___init__.assert_called_once() - mock_Table.assert_called_once_with(box=None) - mock___init__.assert_called_once_with(mock_outer_table, title="Development Environment", - expand=True) - mock_Align.assert_called_once_with(test_dev_env_status_panel, vertical="middle") - mock_outer_table.add_row.assert_has_calls = [ - call(test_already_selected_tool_images[0]), - call(test_already_selected_tool_images[1]) - ] - -@patch("dem.cli.tui.renderable.menu.table.Table") -@patch.object(menu.DevEnvStatusPanel, "__init__") -def test_DevEnvStatusPanel_update_table(mock___init__: MagicMock, mock_Table: MagicMock) -> None: +@patch.object(menu.table.Table, "add_row") +@patch.object(menu.table.Table, "add_column") +@patch.object(menu.table.Table, "__init__") +def test_DevEnvStatusPanel(mock___init__: MagicMock, mock_add_column: MagicMock, + mock_add_row: MagicMock) -> None: # Test setup mock___init__.return_value = None - test_dev_env_status_panel = menu.DevEnvStatusPanel([]) - mock_outer_table = MagicMock() - mock_Table.return_value = mock_outer_table - - test_dev_env_status_panel.renderable = None - test_selected_tool_images = ["test1", "test2"] + test_height = 12 + test_width = max(len(test_selected_tool_images[0]), len(test_selected_tool_images[1])) # Run unit under test - test_dev_env_status_panel.update_table(test_selected_tool_images) + menu.DevEnvStatusPanel(test_selected_tool_images, test_height, test_width) # Check expectations - assert test_dev_env_status_panel.renderable is mock_outer_table - - mock___init__.assert_called_once() - mock_Table.assert_called_once_with(box=None) - mock_outer_table.add_row.assert_has_calls = [ - call(test_selected_tool_images[0]), - call(test_selected_tool_images[1]) - ] \ No newline at end of file + mock___init__.assert_called_once_with(title="Dev Env Settings") + mock_add_column.assert_called_once_with("Selected Tool Images", no_wrap=True) + mock_add_row.assert_has_calls([ + call("test1"), + call("test2"), + ] + [call(" " * test_width) for _ in range(8)]) \ No newline at end of file