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)
|
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()
|
@cli.callback()
|
||||||
def main_callback(
|
def main_callback(
|
||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
|
|
@ -621,7 +677,13 @@ def main_callback(
|
||||||
help="Print version and exit.",
|
help="Print version and exit.",
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> 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:
|
if ctx.invoked_subcommand in excluded_commands:
|
||||||
return
|
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