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:
Eric Gustin 2025-04-28 09:22:55 -08:00 committed by GitHub
parent ef886bb503
commit 817131c6ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 143 additions and 1 deletions

View file

@ -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

View 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()