Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 3 additions & 17 deletions src/prefect/cli/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import webbrowser

from prefect.cli._types import PrefectTyper
from prefect.cli._utilities import exit_with_success
from prefect.cli.cloud import get_current_workspace
from prefect.cli._utilities import exit_with_error, exit_with_success
from prefect.cli.root import app
from prefect.client.cloud import CloudUnauthorizedError, get_cloud_client
from prefect.settings import PREFECT_UI_URL
from prefect.utilities.asyncutils import run_sync_in_worker_thread

Expand All @@ -22,22 +20,10 @@ async def open() -> None:
"""

if not (ui_url := PREFECT_UI_URL.value()):
raise RuntimeError(
exit_with_error(
"`PREFECT_UI_URL` must be set to the URL of a running Prefect server or Prefect Cloud workspace."
)

await run_sync_in_worker_thread(webbrowser.open_new_tab, ui_url)

async with get_cloud_client() as client:
try:
current_workspace = get_current_workspace(await client.read_workspaces())
except CloudUnauthorizedError:
current_workspace = None

destination = (
f"{current_workspace.account_handle}/{current_workspace.workspace_handle}"
if current_workspace
else ui_url
)

exit_with_success(f"Opened {destination!r} in browser.")
exit_with_success(f"Opened {ui_url!r} in browser.")
142 changes: 18 additions & 124 deletions tests/cli/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,145 +1,39 @@
import uuid
from contextlib import contextmanager
from typing import Generator
from unittest.mock import MagicMock

import httpx
import pytest
from starlette import status

from prefect.client.schemas import Workspace
from prefect.context import use_profile
from prefect.settings import (
PREFECT_API_KEY,
PREFECT_API_URL,
PREFECT_CLOUD_API_URL,
Profile,
ProfilesCollection,
save_profiles,
PREFECT_UI_URL,
temporary_settings,
)
from prefect.testing.cli import invoke_and_assert


def gen_test_workspace(**kwargs) -> Workspace:
defaults = {
"account_id": uuid.uuid4(),
"account_name": "account name",
"account_handle": "account-handle",
"workspace_id": uuid.uuid4(),
"workspace_name": "workspace name",
"workspace_handle": "workspace-handle",
"workspace_description": "workspace description",
}
defaults.update(kwargs)
return Workspace(**defaults)


@pytest.fixture
def mock_webbrowser(monkeypatch):
def mock_webbrowser(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
mock = MagicMock()
monkeypatch.setattr("prefect.cli.dashboard.webbrowser", mock)
yield mock


@contextmanager
def _use_profile(profile_name: str) -> Generator[None, None, None]:
with use_profile(profile_name, include_current_context=False):
yield
return mock


def test_open_current_workspace_in_browser_success(mock_webbrowser, respx_mock):
foo_workspace = gen_test_workspace(account_handle="test", workspace_handle="foo")

save_profiles(
ProfilesCollection(
[
Profile(
name="logged-in-profile",
settings={
PREFECT_API_URL: foo_workspace.api_url(),
PREFECT_API_KEY: "foo",
},
)
],
active="logged-in-profile",
)
)

respx_mock.get(PREFECT_CLOUD_API_URL.value() + "/me/workspaces").mock(
return_value=httpx.Response(
status.HTTP_200_OK,
json=[foo_workspace.model_dump(mode="json")],
)
def test_open_dashboard_in_browser_success(mock_webbrowser: MagicMock) -> None:
invoke_and_assert(
["dashboard", "open"],
expected_code=0,
expected_output_contains=f"Opened {PREFECT_UI_URL.value()!r} in browser.",
)
with _use_profile("logged-in-profile"):
invoke_and_assert(
["dashboard", "open"],
expected_code=0,
expected_output_contains=f"Opened {foo_workspace.handle!r} in browser.",
)

mock_webbrowser.open_new_tab.assert_called_with(foo_workspace.ui_url())
mock_webbrowser.open_new_tab.assert_called_with(PREFECT_UI_URL.value())


@pytest.mark.usefixtures("mock_webbrowser")
@pytest.mark.parametrize("api_url", ["http://localhost:4200", "https://api.prefect.io"])
def test_open_current_workspace_in_browser_failure_no_workspace_set(
respx_mock, api_url
):
save_profiles(
ProfilesCollection(
[
Profile(
name="logged-in-profile",
settings={
PREFECT_API_URL: api_url,
PREFECT_API_KEY: "foo",
},
)
],
active="logged-in-profile",
)
)

respx_mock.get(PREFECT_CLOUD_API_URL.value() + "/me/workspaces").mock(
return_value=httpx.Response(
status.HTTP_200_OK,
json=[],
)
)

with _use_profile("logged-in-profile"):
invoke_and_assert(["dashboard", "open"], expected_code=0)


@pytest.mark.usefixtures("mock_webbrowser")
@pytest.mark.parametrize("api_url", ["http://localhost:4200", "https://api.prefect.io"])
def test_open_current_workspace_in_browser_failure_unauthorized(respx_mock, api_url):
save_profiles(
ProfilesCollection(
[
Profile(
name="logged-in-profile",
settings={
PREFECT_API_URL: api_url,
PREFECT_API_KEY: "invalid_key",
},
)
],
active="logged-in-profile",
)
)

respx_mock.get(PREFECT_CLOUD_API_URL.value() + "/me/workspaces").mock(
return_value=httpx.Response(
status.HTTP_401_UNAUTHORIZED,
json={"detail": "Unauthorized"},
)
)

with _use_profile("logged-in-profile"):
def test_open_dashboard_in_browser_failure_no_ui_url(
mock_webbrowser: MagicMock,
) -> None:
with temporary_settings({PREFECT_UI_URL: ""}):
invoke_and_assert(
["dashboard", "open"],
expected_code=0,
expected_output_contains=f"Opened {api_url!r} in browser.",
expected_code=1,
expected_output_contains="PREFECT_UI_URL` must be set",
)

mock_webbrowser.open_new_tab.assert_not_called()
Loading