Skip to content

Commit ebaccdd

Browse files
committed
Apply PR changes (squashed due to cherry-pick conflicts)
1 parent 920990f commit ebaccdd

7 files changed

Lines changed: 69 additions & 10 deletions

File tree

.github/workflows/python-package.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ jobs:
4343
run: |
4444
uv run prefect dev build-ui
4545
46+
- name: Build UI v2
47+
working-directory: ui-v2
48+
run: |
49+
npm ci
50+
npm run build
51+
cp -r dist ../src/prefect/server/ui-v2
52+
4653
- name: Check git diff
4754
run: |
4855
git diff --exit-code

Dockerfile

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ ARG PYTHON_VERSION=3.10
88
ARG BASE_IMAGE=python:${PYTHON_VERSION}-slim
99
# The version used to build the Python distributable.
1010
ARG BUILD_PYTHON_VERSION=3.10
11-
# THe version used to build the UI distributable.
11+
# The version used to build the V1 UI distributable.
1212
ARG NODE_VERSION=20.19.0
13+
# The version used to build the V2 UI distributable (requires Node 22+).
14+
ARG NODE_V2_VERSION=22.12.0
1315
# SQLite version to install (format: X.YY.Z becomes XYYZZOO in filename)
1416
ARG SQLITE_VERSION=3.50.4
1517
ARG SQLITE_YEAR=2025
@@ -40,7 +42,7 @@ RUN wget -q https://sqlite.org/${SQLITE_YEAR}/sqlite-autoconf-${SQLITE_FILE_VERS
4042
cd .. && \
4143
rm -rf sqlite-autoconf-${SQLITE_FILE_VERSION} sqlite-autoconf-${SQLITE_FILE_VERSION}.tar.gz
4244

43-
# Build the UI distributable.
45+
# Build the V1 UI distributable.
4446
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-bullseye-slim AS ui-builder
4547

4648
WORKDIR /opt/ui
@@ -59,6 +61,25 @@ RUN npm ci
5961
COPY ./ui .
6062
RUN npm run build
6163

64+
# Build the V2 UI distributable.
65+
FROM --platform=$BUILDPLATFORM node:${NODE_V2_VERSION}-bullseye-slim AS ui-v2-builder
66+
67+
WORKDIR /opt/ui-v2
68+
69+
RUN apt-get update && \
70+
apt-get install --no-install-recommends -y \
71+
# Required for arm64 builds
72+
chromium \
73+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
74+
75+
# Install dependencies separately so they cache
76+
COPY ./ui-v2/package*.json ./
77+
RUN npm ci
78+
79+
# Build static UI files
80+
COPY ./ui-v2 .
81+
RUN npm run build
82+
6283

6384
# Build the Python distributable.
6485
# Without this build step, versioningit cannot infer the version without git
@@ -78,9 +99,12 @@ COPY --from=ghcr.io/astral-sh/uv:0.6.17 /uv /bin/uv
7899
# Copy the repository in; requires full git history for versions to generate correctly
79100
COPY . ./
80101

81-
# Package the UI into the distributable.
102+
# Package the V1 UI into the distributable.
82103
COPY --from=ui-builder /opt/ui/dist ./src/prefect/server/ui
83104

105+
# Package the V2 UI into the distributable.
106+
COPY --from=ui-v2-builder /opt/ui-v2/dist ./src/prefect/server/ui-v2
107+
84108
# Create a source distributable archive; ensuring existing dists are removed first
85109
RUN rm -rf dist && uv build --sdist --out-dir dist
86110
RUN mv "dist/prefect-"*".tar.gz" "dist/prefect.tar.gz"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
229229
distance-dirty = "{base_version}+{distance}.{vcs}{rev}.dirty"
230230

231231
[tool.hatch.build]
232-
artifacts = ["src/prefect/_build_info.py", "src/prefect/server/ui"]
232+
artifacts = ["src/prefect/_build_info.py", "src/prefect/server/ui", "src/prefect/server/ui-v2"]
233233

234234
[tool.hatch.build.targets.sdist]
235235
include = ["/src/prefect", "/README.md", "/LICENSE", "/pyproject.toml"]

src/prefect/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ class VersionInfo(TypedDict("_FullRevisionId", {"full-revisionid": str})):
6464
# The absolute path to the built UI within the Python module
6565
__ui_static_path__: pathlib.Path = __module_path__ / "server" / "ui"
6666

67+
# The absolute path to the built V2 UI within the Python module, used by
68+
# `prefect server start` to serve a dynamic build of the V2 UI
69+
__ui_v2_static_subpath__: pathlib.Path = __module_path__ / "server" / "ui_v2_build"
70+
71+
# The absolute path to the built V2 UI within the Python module
72+
__ui_v2_static_path__: pathlib.Path = __module_path__ / "server" / "ui-v2"
73+
6774
del _build_info, pathlib
6875

6976

src/prefect/server/api/server.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -455,11 +455,22 @@ async def token_validation(request: Request, call_next: Any): # type: ignore[re
455455
def create_ui_app(ephemeral: bool) -> FastAPI:
456456
ui_app = FastAPI(title=UI_TITLE)
457457
base_url = prefect.settings.PREFECT_UI_SERVE_BASE.value()
458-
cache_key = f"{prefect.__version__}:{base_url}"
458+
459+
# Determine which UI to serve based on setting
460+
v2_enabled = prefect.settings.get_current_settings().server.ui.v2_enabled
461+
462+
if v2_enabled:
463+
source_static_path = prefect.__ui_v2_static_path__
464+
static_subpath = prefect.__ui_v2_static_subpath__
465+
cache_key = f"v2:{prefect.__version__}:{base_url}"
466+
else:
467+
source_static_path = prefect.__ui_static_path__
468+
static_subpath = prefect.__ui_static_subpath__
469+
cache_key = f"v1:{prefect.__version__}:{base_url}"
470+
459471
stripped_base_url = base_url.rstrip("/")
460-
static_dir = (
461-
prefect.settings.PREFECT_UI_STATIC_DIRECTORY.value()
462-
or prefect.__ui_static_subpath__
472+
static_dir = prefect.settings.PREFECT_UI_STATIC_DIRECTORY.value() or str(
473+
static_subpath
463474
)
464475
reference_file_name = "UI_SERVE_BASE"
465476

@@ -495,7 +506,7 @@ def create_ui_static_subpath() -> None:
495506
if not os.path.exists(static_dir):
496507
os.makedirs(static_dir)
497508

498-
copy_directory(str(prefect.__ui_static_path__), str(static_dir))
509+
copy_directory(str(source_static_path), str(static_dir))
499510
replace_placeholder_string_in_files(
500511
str(static_dir),
501512
"/PREFECT_UI_SERVE_BASE_REPLACE_PLACEHOLDER",
@@ -511,10 +522,14 @@ def create_ui_static_subpath() -> None:
511522
ui_app.add_middleware(GZipMiddleware)
512523

513524
if (
514-
os.path.exists(prefect.__ui_static_path__)
525+
os.path.exists(source_static_path)
515526
and prefect.settings.PREFECT_UI_ENABLED.value()
516527
and not ephemeral
517528
):
529+
# Log which UI version is being served
530+
if v2_enabled:
531+
logger.info("Serving experimental V2 UI")
532+
518533
# If the static files have already been copied, check if the base_url has changed
519534
# If it has, we delete the subpath directory and copy the files again
520535
if not reference_file_matches_base_url():

src/prefect/settings/models/server/ui.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ class ServerUISettings(PrefectBaseSettings):
1919
),
2020
)
2121

22+
v2_enabled: bool = Field(
23+
default=False,
24+
description="Whether to serve the experimental V2 UI instead of the default V1 UI.",
25+
)
26+
2227
api_url: Optional[str] = Field(
2328
default=None,
2429
description="The connection url for communication from the UI to the API. Defaults to `PREFECT_API_URL` if set. Otherwise, the default URL is generated from `PREFECT_SERVER_API_HOST` and `PREFECT_SERVER_API_PORT`.",

tests/test_settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,7 @@
490490
"PREFECT_SERVER_UI_SERVE_BASE": {"test_value": "/base"},
491491
"PREFECT_SERVER_UI_SHOW_PROMOTIONAL_CONTENT": {"test_value": False},
492492
"PREFECT_SERVER_UI_STATIC_DIRECTORY": {"test_value": "/path/to/static"},
493+
"PREFECT_SERVER_UI_V2_ENABLED": {"test_value": True},
493494
"PREFECT_SILENCE_API_URL_MISCONFIGURATION": {"test_value": True},
494495
"PREFECT_SQLALCHEMY_MAX_OVERFLOW": {"test_value": 10, "legacy": True},
495496
"PREFECT_SQLALCHEMY_POOL_SIZE": {"test_value": 10, "legacy": True},

0 commit comments

Comments
 (0)