Add arcade dashboard CLI Command (#330)
* `arcade dashboard` opens the Arcade Dashboard in a web browser. Defaults to `https://api.arcade.dev/dashboard`, but is configurable via flags. * `arcade dashboard --local` opens your locally hosted Arcade Dashboard in a web browser. * Performs a health check of the engine and will print a warning to the console if the Engine is not healthy / not running. ---------------------------------------- * Inspiration from https://minikube.sigs.k8s.io/docs/handbook/dashboard/
This commit is contained in:
parent
ef886bb503
commit
817131c6ce
2 changed files with 143 additions and 1 deletions
|
|
@ -609,6 +609,62 @@ def deploy(
|
|||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
@cli.command(help="Open the Arcade Dashboard in a web browser", rich_help_panel="User")
|
||||
def dashboard(
|
||||
host: str = typer.Option(
|
||||
PROD_ENGINE_HOST,
|
||||
"-h",
|
||||
"--host",
|
||||
help="The Arcade Engine host that serves the dashboard.",
|
||||
),
|
||||
port: Optional[int] = typer.Option(
|
||||
None,
|
||||
"-p",
|
||||
"--port",
|
||||
help="The port of the Arcade Engine.",
|
||||
),
|
||||
local: bool = typer.Option(
|
||||
False,
|
||||
"--local",
|
||||
"-l",
|
||||
help="Open the local dashboard instead of the default remote dashboard.",
|
||||
),
|
||||
force_tls: bool = typer.Option(
|
||||
False,
|
||||
"--tls",
|
||||
help="Whether to force TLS for the connection to the Arcade Engine.",
|
||||
),
|
||||
force_no_tls: bool = typer.Option(
|
||||
False,
|
||||
"--no-tls",
|
||||
help="Whether to disable TLS for the connection to the Arcade Engine.",
|
||||
),
|
||||
) -> None:
|
||||
"""Opens the Arcade Dashboard in a web browser.
|
||||
|
||||
The Dashboard is a web-based Arcade user interface that is served by the Arcade Engine.
|
||||
"""
|
||||
if local:
|
||||
host = "localhost"
|
||||
|
||||
# Construct base URL (for both health check and dashboard)
|
||||
base_url = compute_base_url(force_tls, force_no_tls, host, port)
|
||||
dashboard_url = f"{base_url}/dashboard"
|
||||
|
||||
# Try to hit /health endpoint on engine and warn if it is down
|
||||
config = validate_and_get_config()
|
||||
with Arcade(api_key=config.api.key, base_url=base_url) as client:
|
||||
log_engine_health(client)
|
||||
|
||||
# Open the dashboard in a browser
|
||||
console.print(f"Opening Arcade Dashboard at {dashboard_url}")
|
||||
if not webbrowser.open(dashboard_url):
|
||||
console.print(
|
||||
f"If a browser doesn't open automatically, copy this URL and paste it into your browser: {dashboard_url}",
|
||||
style="dim",
|
||||
)
|
||||
|
||||
|
||||
@cli.callback()
|
||||
def main_callback(
|
||||
ctx: typer.Context,
|
||||
|
|
@ -621,7 +677,13 @@ def main_callback(
|
|||
help="Print version and exit.",
|
||||
),
|
||||
) -> None:
|
||||
excluded_commands = {login.__name__, logout.__name__, serve.__name__, workerup.__name__}
|
||||
excluded_commands = {
|
||||
login.__name__,
|
||||
logout.__name__,
|
||||
serve.__name__,
|
||||
workerup.__name__,
|
||||
dashboard.__name__,
|
||||
}
|
||||
if ctx.invoked_subcommand in excluded_commands:
|
||||
return
|
||||
|
||||
|
|
|
|||
80
arcade/tests/cli/test_dashboard.py
Normal file
80
arcade/tests/cli/test_dashboard.py
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from arcade.cli.constants import PROD_ENGINE_HOST
|
||||
from arcade.cli.main import cli
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, expected_url",
|
||||
[
|
||||
([], f"https://{PROD_ENGINE_HOST}/dashboard"),
|
||||
(["--local"], "http://localhost:9099/dashboard"),
|
||||
(["--host", "custom.host.com"], "https://custom.host.com/dashboard"),
|
||||
(["-h", "api.arcade.dev", "-p", "9099"], "https://api.arcade.dev:9099/dashboard"),
|
||||
(["--local", "--port", "9099"], "http://localhost:9099/dashboard"),
|
||||
(["--local", "--tls"], "https://localhost:9099/dashboard"),
|
||||
(["--no-tls"], f"http://{PROD_ENGINE_HOST}/dashboard"),
|
||||
],
|
||||
)
|
||||
def test_dashboard_url_construction(args, expected_url):
|
||||
"""Test that the dashboard command constructs the correct URL with various args."""
|
||||
with (
|
||||
patch("webbrowser.open") as mock_open,
|
||||
patch("arcade.cli.main.validate_and_get_config") as mock_validate,
|
||||
patch("arcade.cli.main.log_engine_health") as mock_health_check,
|
||||
):
|
||||
# Setup mocks
|
||||
mock_open.return_value = True # Successfully opened browser
|
||||
mock_validate.return_value = MagicMock()
|
||||
mock_health_check.return_value = None # Successful health check
|
||||
|
||||
# Run command
|
||||
result = runner.invoke(cli, ["dashboard", *args])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_open.assert_called_once_with(expected_url)
|
||||
mock_health_check.assert_called_once()
|
||||
|
||||
|
||||
def test_fallback_when_browser_fails():
|
||||
"""Test fallback message when browser.open fails."""
|
||||
with (
|
||||
patch("webbrowser.open") as mock_open,
|
||||
patch("arcade.cli.main.validate_and_get_config") as mock_validate,
|
||||
patch("arcade.cli.main.log_engine_health") as mock_health_check,
|
||||
patch("arcade.cli.main.console.print") as mock_print,
|
||||
):
|
||||
mock_open.return_value = False # Failed to open browser
|
||||
mock_validate.return_value = MagicMock()
|
||||
mock_health_check.return_value = None
|
||||
|
||||
result = runner.invoke(cli, ["dashboard"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_print.assert_any_call(
|
||||
f"If a browser doesn't open automatically, copy this URL and paste it into your browser: https://{PROD_ENGINE_HOST}/dashboard",
|
||||
style="dim",
|
||||
)
|
||||
|
||||
|
||||
def test_health_check_success():
|
||||
"""Test successful health check."""
|
||||
with (
|
||||
patch("webbrowser.open") as mock_open,
|
||||
patch("arcade.cli.main.validate_and_get_config") as mock_validate,
|
||||
patch("arcade.cli.main.log_engine_health") as mock_health_check,
|
||||
):
|
||||
mock_open.return_value = True
|
||||
mock_validate.return_value = MagicMock()
|
||||
mock_health_check.return_value = None # Successful health check
|
||||
|
||||
result = runner.invoke(cli, ["dashboard"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_health_check.assert_called_once()
|
||||
mock_open.assert_called_once()
|
||||
Loading…
Reference in a new issue