Implement improved UX with command transparency and log control
- Show exact Docker commands in parentheses for transparency - Add log control: ask user if they want to see full Docker logs - Clean, minimal output separation with refined ready message - Browser opening option (default: Yes) with concise prompt - Single-line wizard title combining instruction - Removed excessive decorations and emoji for professional look - Clear workflow: checks → selection → setup → ready 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										116
									
								
								setup_wizard.py
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								setup_wizard.py
									
									
									
									
									
								
							@@ -25,6 +25,21 @@ class Scenario:
 | 
				
			|||||||
        """Execute this scenario."""
 | 
					        """Execute this scenario."""
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    def _ask_show_logs(self) -> bool:
 | 
				
			||||||
 | 
					        """Ask user if they want to see full Docker logs."""
 | 
				
			||||||
 | 
					        return questionary.confirm("Show full Docker logs?", default=True).ask()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _show_ready_message(self, port: int) -> None:
 | 
				
			||||||
 | 
					        """Display the setup complete message."""
 | 
				
			||||||
 | 
					        print("\n" + "-"*40)
 | 
				
			||||||
 | 
					        print(f"Ready at http://localhost:{port} (admin/admin)")
 | 
				
			||||||
 | 
					        print("Press Ctrl+C to stop")
 | 
				
			||||||
 | 
					        print("-"*40)
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    def _ask_open_browser(self, port: int) -> bool:
 | 
				
			||||||
 | 
					        """Ask user if they want to open browser."""
 | 
				
			||||||
 | 
					        return questionary.confirm(f"Open in browser?", default=True).ask()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StandaloneGrafana(Scenario):
 | 
					class StandaloneGrafana(Scenario):
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
@@ -35,13 +50,19 @@ class StandaloneGrafana(Scenario):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    def run(self, base_dir: Path, port: int = 3000) -> None:
 | 
					    def run(self, base_dir: Path, port: int = 3000) -> None:
 | 
				
			||||||
        print(f"\n→ Starting {self.name}...")
 | 
					        cmd_str = f"docker run -i -t --rm -p {port}:3000 grafana/grafana:12.0.2"
 | 
				
			||||||
        print(f"Access at: http://localhost:{port} (admin/admin)")
 | 
					        print(f"\n→ Starting {self.name}... ({cmd_str})")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        show_logs = self._ask_show_logs()
 | 
				
			||||||
 | 
					        self._show_ready_message(port)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if self._ask_open_browser(port):
 | 
				
			||||||
 | 
					            webbrowser.open(f"http://localhost:{port}")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        subprocess.run([
 | 
					        subprocess.run([
 | 
				
			||||||
            "docker", "run", "-i", "-t", "--rm", "-p", f"{port}:3000",
 | 
					            "docker", "run", "-i", "-t", "--rm", "-p", f"{port}:3000",
 | 
				
			||||||
            "grafana/grafana:12.0.2"
 | 
					            "grafana/grafana:12.0.2"
 | 
				
			||||||
        ], check=True)
 | 
					        ], check=True, capture_output=not show_logs, text=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ComposeBased(Scenario):
 | 
					class ComposeBased(Scenario):
 | 
				
			||||||
@@ -53,11 +74,19 @@ class ComposeBased(Scenario):
 | 
				
			|||||||
            print(f"Directory '{self.id}' not found")
 | 
					            print(f"Directory '{self.id}' not found")
 | 
				
			||||||
            sys.exit(1)
 | 
					            sys.exit(1)
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        print(f"\n→ Starting {self.name}...")
 | 
					        cmd_str = f"cd {self.id} && docker-compose up"
 | 
				
			||||||
        print(f"Access at: http://localhost:{port} (admin/admin)")
 | 
					        print(f"\n→ Starting {self.name}... ({cmd_str})")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if port != 3000:
 | 
				
			||||||
 | 
					            print(f"Using port {port} instead of default 3000")
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        show_logs = self._ask_show_logs()
 | 
				
			||||||
 | 
					        self._show_ready_message(port)
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if self._ask_open_browser(port):
 | 
				
			||||||
 | 
					            webbrowser.open(f"http://localhost:{port}")
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if port != 3000:
 | 
					        if port != 3000:
 | 
				
			||||||
            print(f"Note: Using port {port} instead of default 3000")
 | 
					 | 
				
			||||||
            # Create a temporary docker-compose override
 | 
					            # Create a temporary docker-compose override
 | 
				
			||||||
            override_content = f"""services:
 | 
					            override_content = f"""services:
 | 
				
			||||||
  grafana:
 | 
					  grafana:
 | 
				
			||||||
@@ -69,13 +98,15 @@ class ComposeBased(Scenario):
 | 
				
			|||||||
                with open(override_file, 'w') as f:
 | 
					                with open(override_file, 'w') as f:
 | 
				
			||||||
                    f.write(override_content)
 | 
					                    f.write(override_content)
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                subprocess.run(["docker-compose", "up"], cwd=scenario_dir, check=True)
 | 
					                subprocess.run(["docker-compose", "up"], cwd=scenario_dir, 
 | 
				
			||||||
 | 
					                             check=True, capture_output=not show_logs, text=True)
 | 
				
			||||||
            finally:
 | 
					            finally:
 | 
				
			||||||
                # Clean up override file
 | 
					                # Clean up override file
 | 
				
			||||||
                if override_file.exists():
 | 
					                if override_file.exists():
 | 
				
			||||||
                    override_file.unlink()
 | 
					                    override_file.unlink()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            subprocess.run(["docker-compose", "up"], cwd=scenario_dir, check=True)
 | 
					            subprocess.run(["docker-compose", "up"], cwd=scenario_dir, 
 | 
				
			||||||
 | 
					                         check=True, capture_output=not show_logs, text=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PrometheusSetup(ComposeBased):
 | 
					class PrometheusSetup(ComposeBased):
 | 
				
			||||||
@@ -125,6 +156,62 @@ def is_port_available(port: int) -> bool:
 | 
				
			|||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def check_docker_containers() -> list[str]:
 | 
				
			||||||
 | 
					    """Check for existing Docker containers that might conflict."""
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        result = subprocess.run(
 | 
				
			||||||
 | 
					            ["docker", "ps", "-a", "--format", "{{.Names}}"],
 | 
				
			||||||
 | 
					            capture_output=True,
 | 
				
			||||||
 | 
					            text=True,
 | 
				
			||||||
 | 
					            check=True
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return result.stdout.strip().split('\n') if result.stdout.strip() else []
 | 
				
			||||||
 | 
					    except (subprocess.CalledProcessError, FileNotFoundError):
 | 
				
			||||||
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def handle_container_conflicts(existing_containers: list[str]) -> bool:
 | 
				
			||||||
 | 
					    """Handle Docker container conflicts."""
 | 
				
			||||||
 | 
					    conflicting = [name for name in ['grafana', 'prometheus', 'loki', 'tempo'] 
 | 
				
			||||||
 | 
					                   if name in existing_containers]
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not conflicting:
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    print(f"\nFound existing containers: {', '.join(conflicting)}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    choice = questionary.select(
 | 
				
			||||||
 | 
					        "These containers may conflict. How would you like to proceed?",
 | 
				
			||||||
 | 
					        choices=[
 | 
				
			||||||
 | 
					            "Remove conflicting containers (recommended)",
 | 
				
			||||||
 | 
					            "Stop conflicting containers", 
 | 
				
			||||||
 | 
					            "Continue anyway (may fail)",
 | 
				
			||||||
 | 
					            "Exit to handle manually"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    ).ask()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if not choice or "Exit" in choice:
 | 
				
			||||||
 | 
					        return False
 | 
				
			||||||
 | 
					    elif "Remove" in choice:
 | 
				
			||||||
 | 
					        for container in conflicting:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                subprocess.run(["docker", "rm", "-f", container], 
 | 
				
			||||||
 | 
					                             capture_output=True, check=True)
 | 
				
			||||||
 | 
					                print(f"Removed container: {container}")
 | 
				
			||||||
 | 
					            except subprocess.CalledProcessError:
 | 
				
			||||||
 | 
					                print(f"Could not remove container: {container}")
 | 
				
			||||||
 | 
					    elif "Stop" in choice:
 | 
				
			||||||
 | 
					        for container in conflicting:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                subprocess.run(["docker", "stop", container], 
 | 
				
			||||||
 | 
					                             capture_output=True, check=True)
 | 
				
			||||||
 | 
					                print(f"Stopped container: {container}")
 | 
				
			||||||
 | 
					            except subprocess.CalledProcessError:
 | 
				
			||||||
 | 
					                print(f"Could not stop container: {container}")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def find_next_available_port(start_port: int = 3000) -> int:
 | 
					def find_next_available_port(start_port: int = 3000) -> int:
 | 
				
			||||||
    """Find the next available port starting from start_port."""
 | 
					    """Find the next available port starting from start_port."""
 | 
				
			||||||
    port = start_port
 | 
					    port = start_port
 | 
				
			||||||
@@ -139,7 +226,7 @@ def handle_port_conflict(port: int = 3000) -> int:
 | 
				
			|||||||
    """Handle port conflicts with user-friendly options."""
 | 
					    """Handle port conflicts with user-friendly options."""
 | 
				
			||||||
    next_port = find_next_available_port(port + 1)
 | 
					    next_port = find_next_available_port(port + 1)
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    print(f"Port {port} is already in use.")
 | 
					    print(f"\nPort {port} is already in use")
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    choice = questionary.select(
 | 
					    choice = questionary.select(
 | 
				
			||||||
        "How would you like to proceed?",
 | 
					        "How would you like to proceed?",
 | 
				
			||||||
@@ -217,8 +304,7 @@ def select_scenario() -> Scenario | None:
 | 
				
			|||||||
    """Display scenario selection menu and return choice."""
 | 
					    """Display scenario selection menu and return choice."""
 | 
				
			||||||
    scenarios = get_scenarios()
 | 
					    scenarios = get_scenarios()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    print("\nGrafana Setup Wizard")
 | 
					    print("Grafana Setup Wizard - Choose a scenario:")
 | 
				
			||||||
    print("Choose a scenario:")
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    choices = [f"{s.id} · {s.name} – {s.description}" for s in scenarios]
 | 
					    choices = [f"{s.id} · {s.name} – {s.description}" for s in scenarios]
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@@ -270,6 +356,12 @@ def main() -> None:
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    print(f"✓ {docker_version}")
 | 
					    print(f"✓ {docker_version}")
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    # Check for container conflicts
 | 
				
			||||||
 | 
					    existing_containers = check_docker_containers()
 | 
				
			||||||
 | 
					    if not handle_container_conflicts(existing_containers):
 | 
				
			||||||
 | 
					        print("Exiting...")
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    # Check port availability
 | 
					    # Check port availability
 | 
				
			||||||
    port = 3000
 | 
					    port = 3000
 | 
				
			||||||
    if not is_port_available(port):
 | 
					    if not is_port_available(port):
 | 
				
			||||||
@@ -277,6 +369,8 @@ def main() -> None:
 | 
				
			|||||||
    else:
 | 
					    else:
 | 
				
			||||||
        print(f"✓ Port {port} available")
 | 
					        print(f"✓ Port {port} available")
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					    print()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    scenario = select_scenario()
 | 
					    scenario = select_scenario()
 | 
				
			||||||
    if scenario:
 | 
					    if scenario:
 | 
				
			||||||
        run_scenario(scenario, port)
 | 
					        run_scenario(scenario, port)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user