Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
177 commits
Select commit Hold shift + click to select a range
0045d37
feat(artifacts): add RichArtifact for HTML/JS content
aaazzam Dec 11, 2025
f80bbec
init
aaazzam Dec 11, 2025
95719d8
Update artifacts.py
aaazzam Dec 14, 2025
5858cfd
Merge branch 'main' into feat/rich-artifact-type
aaazzam Dec 14, 2025
9d894a7
chore(deps): bump actions/upload-artifact from 4 to 6 (#19773)
dependabot[bot] Dec 15, 2025
42be7df
chore(deps): bump actions/download-artifact from 5 to 7 (#19774)
dependabot[bot] Dec 15, 2025
4939097
Fix mattermost botname issue (#19780)
tre-ert Dec 15, 2025
3a61ce9
feat(ui-v2): add deployment filter to runs page (#19781)
devin-ai-integration[bot] Dec 15, 2025
b3fd7af
fix(ui-v2): Remove documentation link from flows page header (#19782)
devin-ai-integration[bot] Dec 15, 2025
beca0a7
feat(ui-v2): add EventsLineChart component (#19783)
devin-ai-integration[bot] Dec 15, 2025
cc28c35
feat: convert scheduler and proactive triggers to docket, remove Loop…
zzstoatzz Dec 15, 2025
d65bb87
feat(ui-v2): add useResourceOptions hook for events resource filterin…
devin-ai-integration[bot] Dec 15, 2025
f449524
feat(ui-v2): add work pool filter component for flow runs filtering (…
devin-ai-integration[bot] Dec 15, 2025
e33a48e
feat(ui-v2): add hover-based prefetching for flows table pagination (…
devin-ai-integration[bot] Dec 15, 2025
218dc43
fix: correct docket_url setting path to server.docket.url (#19791)
zzstoatzz Dec 15, 2025
c43c670
feat(ui-v2): add toast notification when copying flow ID (#19789)
devin-ai-integration[bot] Dec 15, 2025
5a90534
Add work pool filter URL state and API integration for runs route (#1…
devin-ai-integration[bot] Dec 15, 2025
97d3356
feat(ui-v2): OSS-7264 - Event Resource Display Core with icon support…
devin-ai-integration[bot] Dec 15, 2025
12fd825
fix: exclude unset grace_period_seconds from deployment concurrency_o…
zzstoatzz Dec 15, 2025
e552c19
fix: use tag input for testbed image update in workflow_dispatch (#19…
zzstoatzz Dec 15, 2025
ba81af8
Migrate `resume_flow_run` from `sync_compatible` to `async_dispatch` …
devin-ai-integration[bot] Dec 15, 2025
c91b978
feat(ui-v2): add interactive zoom and selection controls for events l…
devin-ai-integration[bot] Dec 15, 2025
4dff0c7
feat(ui-v2): add work pool filter to runs page (OSS-7293) (#19796)
devin-ai-integration[bot] Dec 15, 2025
2300def
feat(ui-v2): add TagsInput component to RunsPage filter bar [OSS-7296…
devin-ai-integration[bot] Dec 15, 2025
8b7adc6
feat(ui-v2): add tags filtering infrastructure to runs route (#19800)
devin-ai-integration[bot] Dec 15, 2025
0780653
feat(ui-v2): add EventsResourceFilter component (#19799)
devin-ai-integration[bot] Dec 16, 2025
d13a331
feat(ui-v2): extract useRunsFilters hook from runs route (#19801)
devin-ai-integration[bot] Dec 16, 2025
1b0e7d4
Add EventsTypeFilter component for filtering events by type (#19803)
devin-ai-integration[bot] Dec 16, 2025
dfce3f2
Add missing API query factories for work queues and concurrency limit…
devin-ai-integration[bot] Dec 16, 2025
2ce0448
feat(ui-v2): Add useEventsPagination hook with token vault pattern (#…
devin-ai-integration[bot] Dec 16, 2025
59cc12a
Add release notes for `prefect-dbt==0.7.12` (#19809)
desertaxle Dec 16, 2025
a459395
Add FlowRunsScatterPlot component for flow run history visualization …
devin-ai-integration[bot] Dec 16, 2025
7c2aac9
Add resource-specific display components for event resources (#19810)
devin-ai-integration[bot] Dec 16, 2025
9baeb20
16658 work pool queue update events (#19688)
F4RAN Dec 16, 2025
879fed9
feat(ui-v2): add EventsPage container component (#19812)
devin-ai-integration[bot] Dec 16, 2025
9618402
Fix flow run scatter plot tooltip to show flow and flow run names (#1…
devin-ai-integration[bot] Dec 16, 2025
e75011a
Reduce noisy websocket reconnection warnings in events client (#19814)
jakekaplan Dec 16, 2025
24a28b2
Refactor `Task.__call__` typing to be mypy compliant (#19811)
peterbygrave Dec 16, 2025
652314a
Replace events route placeholder with functional TanStack Router impl…
devin-ai-integration[bot] Dec 16, 2025
10a5fd2
Remove `parameters` and `context` fields from TaskRunResponse (#19816)
chrisguidry Dec 16, 2025
3d56c1a
feat(ui-v2): Add shared filter support to task runs tab (#19818)
devin-ai-integration[bot] Dec 16, 2025
3a81b0d
feat(ui-v2): Update runs page counts to respect current filters (#19823)
devin-ai-integration[bot] Dec 16, 2025
b3fa08d
Hide X-axis and add baseline line to event activity chart (#19819)
devin-ai-integration[bot] Dec 16, 2025
3100791
Add resource type labels to event feed cards (#19820)
devin-ai-integration[bot] Dec 16, 2025
218140d
fix(prefect-dbt): skip asset creation for ephemeral models in _call_t…
devin-ai-integration[bot] Dec 16, 2025
f0672ca
Update Events feed filter layout to match Vue UI (#19825)
devin-ai-integration[bot] Dec 16, 2025
94d2b43
Remove drag-to-select time range from event activity graph (#19828)
devin-ai-integration[bot] Dec 16, 2025
2c234cf
fix(ui-v2): prevent page suspension during filter changes on events f…
devin-ai-integration[bot] Dec 16, 2025
370afe4
fix: event type filter dropdown shows all types regardless of selecti…
devin-ai-integration[bot] Dec 16, 2025
e1413cc
feat(ui-v2): add navigation links for related resources in event card…
devin-ai-integration[bot] Dec 17, 2025
9ab7a32
Remove zoom handling from event feed graph (#19838)
devin-ai-integration[bot] Dec 17, 2025
a4ae0c1
chore(deps-dev): bump the eslint group across 1 directory with 5 upda…
dependabot[bot] Dec 17, 2025
d3fb846
feat(ui-v2): integrate FlowRunsScatterPlot into runs page (#19839)
devin-ai-integration[bot] Dec 17, 2025
79f0095
feat(ui-v2): add event detail query factory (#19843)
devin-ai-integration[bot] Dec 17, 2025
2ead64d
feat(ui-v2): Add localStorage persistence for pagination limits (#19841)
devin-ai-integration[bot] Dec 17, 2025
2d921ec
chore(deps): bump the ui-v2-dependencies group across 1 directory wit…
dependabot[bot] Dec 17, 2025
9c527fc
feat(ui-v2): add EventDetailsHeader breadcrumb component for event de…
devin-ai-integration[bot] Dec 17, 2025
0d23d19
fix: prevent command injection in npm_update_latest_prefect workflow …
ColeMurray Dec 17, 2025
8d35090
chore: remove downstream workflow trigger for oss testbed (#19846)
jamiezieziula Dec 17, 2025
962e5a0
fix: properly drain workers in prefect_test_harness when used in asyn…
devin-ai-integration[bot] Dec 17, 2025
1e9af6c
fix(prefect-redis): add retry logic for consumer reconnection on Redi…
zzstoatzz Dec 17, 2025
6d6a88c
Add EventDetailsDisplay component for event details page (#19847)
devin-ai-integration[bot] Dec 17, 2025
df4c152
feat(ui-v2): add saved filters data model and hook (#19849)
devin-ai-integration[bot] Dec 17, 2025
ed83c5a
Fix deployment concurrency violation when lease expires during provis…
bunchesofdonald Dec 17, 2025
820e08b
feat(ui-v2): add EventActionMenu component with Copy ID and Automate …
devin-ai-integration[bot] Dec 17, 2025
80ce54b
Event Details Tabs Component (#19853)
devin-ai-integration[bot] Dec 17, 2025
815a9ad
Add SavedFiltersMenu component for flow run filters (#19852)
devin-ai-integration[bot] Dec 17, 2025
d3f6fbb
feat(ui-v2): add EventDetailsPage component (#19854)
devin-ai-integration[bot] Dec 17, 2025
0e1575c
Add max persist retries to task run recorder (#19855)
devin-ai-integration[bot] Dec 17, 2025
8c2924d
Add just recipes for starting ui-v2 dev server (#19859)
desertaxle Dec 17, 2025
a3a1636
make uv an optional dependency (#19668)
zzstoatzz Dec 17, 2025
56693d4
Add explicit stream config to logging handlers for env var override (…
joshuastagner Dec 18, 2025
1ac4545
Fix KeyError crash caused by dead cancellation tracking code in worke…
jakekaplan Dec 18, 2025
2d2af9f
feat(ui-v2): add TanStack Router route for event detail pages (#19857)
devin-ai-integration[bot] Dec 18, 2025
89fe615
feat(ui-v2): Add saved filters integration for Runs page (#19856)
devin-ai-integration[bot] Dec 18, 2025
462e916
Add event query limits to rate limits documentation (#19867)
chrisguidry Dec 18, 2025
15f6b1c
Only authenticate to Docker registry when image pull is required (#19…
desertaxle Dec 18, 2025
080230e
feat(prefect-aws): Add AWS AssumeRole support to AWSCredentials Block…
bdalpe Dec 18, 2025
bcc62ca
fix: handle pydantic generic models in JSON serializer (#19868)
zzstoatzz Dec 18, 2025
26a09e3
fix: restore netloc-based credential formatting for YAML deployments …
zzstoatzz Dec 18, 2025
94fea04
feat(ui-v2): add work pool edit API mutation hook and validation sche…
devin-ai-integration[bot] Dec 18, 2025
ebfef64
docs: Add release notes for 3.6.7 (#19872)
devin-ai-integration[bot] Dec 18, 2025
5084485
feat(ui-v2): add WorkPoolEditPageHeader component (#19874)
devin-ai-integration[bot] Dec 18, 2025
8256be6
Match runs page filter layout to legacy Vue app (#19870)
devin-ai-integration[bot] Dec 18, 2025
69c151c
docs: add security considerations to Prefect MCP server guide (#19875)
zzstoatzz Dec 18, 2025
87ad59a
Add default system filters to React runs page (#19878)
devin-ai-integration[bot] Dec 18, 2025
f4286ed
feat(ui-v2): add WorkPoolEditForm component for editing work pools (#…
devin-ai-integration[bot] Dec 18, 2025
15b4f7f
Fix x-axis tick mark spacing in flow runs scatter plot (#19877)
devin-ai-integration[bot] Dec 18, 2025
1d1deaf
fix(ui-v2): prevent scatter plot flickering when filters change (#19880)
devin-ai-integration[bot] Dec 18, 2025
8283c00
feat(ui-v2): Add Base Job Template section to work pool edit form (#1…
devin-ai-integration[bot] Dec 19, 2025
63b7406
feat(ui-v2): Add dark mode support to JsonInput component (#19882)
devin-ai-integration[bot] Dec 19, 2025
9401eeb
feat(ui-v2): Update work pool edit page container styling (#19883)
devin-ai-integration[bot] Dec 19, 2025
36cecb4
fix(ui-v2): Fix flakey SearchInput tests by fixing debounce logic (#1…
devin-ai-integration[bot] Dec 19, 2025
ff6ad8c
ci: run lint, build, and test in parallel for ui-v2 checks (#19884)
devin-ai-integration[bot] Dec 19, 2025
8ae41c3
fix(ui-v2): update code-banner component to support dark mode (#19887)
devin-ai-integration[bot] Dec 19, 2025
16b3a96
feat(ui-v2): add WorkPoolQueuePageHeader component (#19886)
devin-ai-integration[bot] Dec 19, 2025
4870431
Add url property to server-side ReceivedEvent for automation template…
devin-ai-integration[bot] Dec 19, 2025
abe3ad1
Add release notes for integration releases (#19889)
desertaxle Dec 19, 2025
308da4b
feat(ui-v2): Add edit work queue functionality to work pool queue men…
devin-ai-integration[bot] Dec 19, 2025
0625157
Fix CDK warning for fromAwsManagedPolicyName in EcsTaskExecutionRole …
devin-ai-integration[bot] Dec 19, 2025
0c78696
feat(ui-v2): Add WorkPoolQueueDetails component (#19892)
devin-ai-integration[bot] Dec 19, 2025
3e34031
Make flow run name clickable in TaskRunDetails component (#19891)
devin-ai-integration[bot] Dec 19, 2025
8047742
Add buildGetTaskRunResultQuery for fetching task run result artifacts…
devin-ai-integration[bot] Dec 19, 2025
390cfd6
Add FlowPageHeader breadcrumb component for flow detail pages (#19895)
devin-ai-integration[bot] Dec 19, 2025
aac6d10
Add WorkPoolQueueRunsTab component for displaying flow runs filtered …
devin-ai-integration[bot] Dec 19, 2025
5d41548
Fix work pool status badge dark mode styling (#19896)
devin-ai-integration[bot] Dec 19, 2025
da13cb4
Add Result Artifact Display to TaskRunDetails (#19897)
devin-ai-integration[bot] Dec 19, 2025
759a1bc
Fix logs component dark mode styling (#19898)
devin-ai-integration[bot] Dec 19, 2025
f14ad4d
Fix docker deploy integration test failing due to uv import (#19899)
desertaxle Dec 19, 2025
6c3c56e
feat(ui-v2): add WorkPoolQueueUpcomingRunsTab component (#19901)
devin-ai-integration[bot] Dec 19, 2025
70de5aa
feat(ui-v2): add StateSelect component for flow run state selection (…
devin-ai-integration[bot] Dec 20, 2025
4f13dd0
feat(ui-v2): add FlowMenu component with Copy ID and Delete actions (…
devin-ai-integration[bot] Dec 20, 2025
8e02649
feat(ui-v2): implement work pool queue route component (#19908)
devin-ai-integration[bot] Dec 20, 2025
375d26d
feat(ui-v2): add ChangeStateDialog component for task run state chang…
devin-ai-integration[bot] Dec 20, 2025
0130b2b
feat(ui-v2): add flow deletion confirmation dialog (#19911)
devin-ai-integration[bot] Dec 20, 2025
adf77ef
feat(ui-v2): integrate ChangeStateDialog into TaskRunDetailsPage head…
devin-ai-integration[bot] Dec 21, 2025
fba8d43
chore(deps): bump actions/cache from 4 to 5 (#19919)
dependabot[bot] Dec 22, 2025
578ae84
test(ui-v2): add comprehensive tests for TaskRunsPagination component…
devin-ai-integration[bot] Dec 22, 2025
e91197a
feat(ui-v2): add Storybook stories for TaskRunDetailsPage component (…
devin-ai-integration[bot] Dec 22, 2025
5397eec
feat(ui-v2): replace FlowRunsBarChart with FlowRunActivityBarChart in…
devin-ai-integration[bot] Dec 22, 2025
f21fa68
feat(ui-v2): add Storybook stories for TaskRunArtifacts component (#1…
devin-ai-integration[bot] Dec 22, 2025
7d73faa
chore(deps-dev): bump the eslint group in /ui-v2 with 3 updates (#19921)
dependabot[bot] Dec 22, 2025
bd7baef
ci: Update kickoff-release schedule from Monday to Thursday (#19930)
devin-ai-integration[bot] Dec 22, 2025
5e777d0
feat(ui-v2): add state filter dropdown to flow detail runs tab (#19928)
devin-ai-integration[bot] Dec 22, 2025
0d60bc1
Fix `TASK_SOURCE` cache policy for remote execution with `cloudpickle…
desertaxle Dec 22, 2025
639b3fa
chore(deps): bump the ui-v2-dependencies group in /ui-v2 with 8 updat…
dependabot[bot] Dec 22, 2025
985ef41
feat(ui-v2): update flow detail page to use FlowRunsList component (#…
devin-ai-integration[bot] Dec 23, 2025
79265ab
Add timeout to Docket worker shutdown (#19940)
desertaxle Dec 23, 2025
845ad10
feat(ui-v2): add FlowStatsSummary component for flow detail page (#19…
devin-ai-integration[bot] Dec 23, 2025
e0ae2a5
feat(ui-v2): add usePageTitle hook for Flow detail pages (#19942)
devin-ai-integration[bot] Dec 23, 2025
4a438bc
feat(ui-v2): add route configuration for flow run details page (#19944)
devin-ai-integration[bot] Dec 23, 2025
6abdbf7
feat(ui-v2): add search and tag filtering for deployments tab on flow…
devin-ai-integration[bot] Dec 23, 2025
ae8ed34
feat(ui-v2): add date formatting with tooltips and align Flow details…
devin-ai-integration[bot] Dec 23, 2025
d389383
feat(ui-v2): add mini activity charts to flow detail deployment rows …
devin-ai-integration[bot] Dec 23, 2025
e112f05
fix: prevent dictionary mutation during iteration in preprocess_schem…
zzstoatzz Dec 23, 2025
5021c5a
feat(ui-v2): implement FlowRunDetailsPage component shell (#19948)
devin-ai-integration[bot] Dec 23, 2025
5f83d24
feat(ui-v2): use StatusBadge for deployment status in flow detail (#1…
devin-ai-integration[bot] Dec 23, 2025
ca720fb
feat(ui-v2): add FlowRunHeader component (#19952)
devin-ai-integration[bot] Dec 23, 2025
0ab9a79
feat(ui-v2): improve task run details page parity with Vue (#19951)
devin-ai-integration[bot] Dec 23, 2025
509a5c9
chore(deps): update typer requirement from <0.20.0,>=0.16.0 to >=0.16…
dependabot[bot] Dec 23, 2025
297ee2f
feat(ui-v2): add bulk delete functionality to flow detail page (#19955)
devin-ai-integration[bot] Dec 23, 2025
acde061
feat(ui-v2): add FlowRunDetails component for flow run metadata displ…
devin-ai-integration[bot] Dec 23, 2025
eb717d4
feat(ui-v2): integrate FlowRunDetails into sidebar and tab (#19959)
devin-ai-integration[bot] Dec 23, 2025
156d775
Add semaphore to limit concurrent API calls during Kubernetes observe…
desertaxle Dec 23, 2025
ffacf04
fix(ui-v2): update flow page spacing to match Vue implementation (#19…
devin-ai-integration[bot] Dec 23, 2025
592315a
Add FlowRunLogs component for flow run details page (#19960)
devin-ai-integration[bot] Dec 24, 2025
62d96c9
Add UI redirect for deprecated flow run URLs (#19964)
devin-ai-integration[bot] Dec 24, 2025
bdec1a0
Wrap FlowRunLogs in Suspense with LogsSkeleton fallback (#19965)
devin-ai-integration[bot] Dec 24, 2025
4baedb2
feat(ui-v2): implement Parameters and Job Variables tab content for F…
devin-ai-integration[bot] Dec 24, 2025
56aff46
Document AWS IAM role assumption (#19963)
seanpwlms Dec 24, 2025
beef03b
fix: allow string form_data in CustomWebhookNotificationBlock (#19953)
zzstoatzz Dec 24, 2025
e1b5996
Implement FlowRunArtifacts component for flow run details page (#19967)
devin-ai-integration[bot] Dec 24, 2025
2cba63a
Bump `pydocket` version and remove background worker shutdown timeout…
desertaxle Dec 24, 2025
9e0d72a
chore(deps-dev): bump ruff from 0.14.8 to 0.14.10 (#19881)
dependabot[bot] Dec 24, 2025
0b337d8
feat(ui-v2): Integrate Artifacts Tab in FlowRunDetailsPage (#19970)
devin-ai-integration[bot] Dec 24, 2025
e0675e7
Add redirect from /flow-runs/task-run/$id to /runs/task-run/$id (#19971)
devin-ai-integration[bot] Dec 24, 2025
09aaf6d
Fix text overlap in flow run logs (#19973)
devin-ai-integration[bot] Dec 24, 2025
0bcb708
feat(ui-v2): Add FlowRunTaskRuns component for flow run details page …
devin-ai-integration[bot] Dec 24, 2025
01d8b83
feat(cli): add `prefect flow-run retry` command (#19858)
desertaxle Dec 24, 2025
469d25e
feat(ui-v2): add conditional visibility for Task Runs tab (#19974)
devin-ai-integration[bot] Dec 24, 2025
7919ddc
Add trigger schemas for automation wizard form validation (#19975)
devin-ai-integration[bot] Dec 24, 2025
8ae19ff
docs: Add release notes for 3.6.8 (#19976)
devin-ai-integration[bot] Dec 24, 2025
bb10db8
Add TriggerStep component shell for automations wizard (#19977)
devin-ai-integration[bot] Dec 24, 2025
b996d07
Add FlowRunSubflows component for displaying child flow runs (#19978)
devin-ai-integration[bot] Dec 24, 2025
bdb89b6
Add prefect-kubernetes 0.7.2 release notes (#19981)
devin-ai-integration[bot] Dec 24, 2025
515caf7
Add conditional visibility for SubflowRuns tab when flow run is PENDI…
devin-ai-integration[bot] Dec 24, 2025
dec218a
feat(ui-v2): Add Flow Run State Trigger Form components for automatio…
devin-ai-integration[bot] Dec 24, 2025
e9611bb
Add FlowRunGraph component to FlowRunDetailsPage (#19982)
devin-ai-integration[bot] Dec 24, 2025
8f441a4
Fix flakey flow-run-details-page test by using overridden state (#19984)
devin-ai-integration[bot] Dec 24, 2025
58d1cf4
feat(ui-v2): Add sorting, multi-delete, and grid layout to flow deplo…
devin-ai-integration[bot] Dec 29, 2025
5ad1304
add JSON schema for prefect.yaml files (#19996)
zzstoatzz Dec 29, 2025
dd09fef
feat(artifacts): add RichArtifact for HTML/JS content
aaazzam Dec 11, 2025
811c6e6
init
aaazzam Dec 11, 2025
bcf87c8
Update artifacts.py
aaazzam Dec 14, 2025
fc59c82
Merge branch 'feat/rich-artifact-type' of https://github.com/PrefectH…
aaazzam Dec 29, 2025
fad5ef9
Update package.json
aaazzam Dec 29, 2025
6015569
Update package-lock.json
aaazzam Dec 29, 2025
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
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 109 additions & 0 deletions src/prefect/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,40 @@ def format(self) -> str:
return self.image_url


class RichArtifact(Artifact):
"""
An artifact that renders HTML/JS content in a sandboxed iframe.

Arguments:
html: The HTML content to render (can include inline CSS/JS).
sandbox: List of iframe sandbox permissions. Defaults to ['allow-scripts'].
csp: Optional Content-Security-Policy directive string.
"""

html: str
sandbox: Optional[list[str]] = None
csp: Optional[str] = None
type: Optional[str] = "rich"

def _get_sandbox(self) -> list[str]:
if self.sandbox is not None:
return self.sandbox
return ["allow-scripts"]

async def aformat(self) -> dict[str, Any]:
result: dict[str, Any] = {"html": self.html, "sandbox": self._get_sandbox()}
if self.csp:
result["csp"] = self.csp
return result

@async_dispatch(aformat)
def format(self) -> dict[str, Any]:
result: dict[str, Any] = {"html": self.html, "sandbox": self._get_sandbox()}
if self.csp:
result["csp"] = self.csp
return result


async def acreate_link_artifact(
link: str,
link_text: str | None = None,
Expand Down Expand Up @@ -793,3 +827,78 @@ def create_image_artifact(
artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch

return artifact.id


async def acreate_rich_artifact(
html: str,
key: str | None = None,
description: str | None = None,
sandbox: list[str] | None = None,
csp: str | None = None,
) -> UUID:
"""
Create a rich HTML artifact that renders in a sandboxed iframe.

Arguments:
html: The HTML content to render (can include inline CSS/JS).
key: A user-provided string identifier.
Required for the artifact to show in the Artifacts page in the UI.
The key must only contain lowercase letters, numbers, and dashes.
description: A user-specified description of the artifact.
sandbox: List of iframe sandbox permissions. Defaults to ['allow-scripts'].
csp: Optional Content-Security-Policy directive string.

Returns:
The rich artifact ID.
"""
new_artifact = RichArtifact(
key=key, description=description, html=html, sandbox=sandbox, csp=csp
)
artifact = await new_artifact.acreate()
return artifact.id


@async_dispatch(acreate_rich_artifact)
def create_rich_artifact(
html: str,
key: str | None = None,
description: str | None = None,
sandbox: list[str] | None = None,
csp: str | None = None,
) -> UUID:
"""
Create a rich HTML artifact that renders in a sandboxed iframe.

Arguments:
html: The HTML content to render (can include inline CSS/JS).
key: A user-provided string identifier.
Required for the artifact to show in the Artifacts page in the UI.
The key must only contain lowercase letters, numbers, and dashes.
description: A user-specified description of the artifact.
sandbox: List of iframe sandbox permissions. Defaults to ['allow-scripts'].
csp: Optional Content-Security-Policy directive string.

Returns:
The rich artifact ID.

Example:
```python
from prefect import flow
from prefect.artifacts import create_rich_artifact

@flow
def my_flow():
create_rich_artifact(
html="<html><body><h1>Hello World</h1><script>console.log('hi');</script></body></html>",
key="my-rich-artifact",
description="A rich HTML artifact with JavaScript",
)

my_flow()
```
"""
new_artifact = RichArtifact(
key=key, description=description, html=html, sandbox=sandbox, csp=csp
)
artifact = cast(ArtifactResponse, new_artifact.create(_sync=True)) # pyright: ignore[reportCallIssue] _sync is valid because .create is wrapped in async_dispatch
return artifact.id
172 changes: 172 additions & 0 deletions tests/test_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
from prefect import flow, task
from prefect.artifacts import (
Artifact,
RichArtifact,
acreate_link_artifact,
acreate_progress_artifact,
acreate_rich_artifact,
acreate_table_artifact,
aupdate_progress_artifact,
create_image_artifact,
create_link_artifact,
create_markdown_artifact,
create_progress_artifact,
create_rich_artifact,
create_table_artifact,
update_progress_artifact,
)
Expand Down Expand Up @@ -1093,3 +1096,172 @@ async def test_get_nonexistent_artifact_async(self, prefect_client: PrefectClien
"""Test getting an artifact that doesn't exist returns None"""
retrieved = await Artifact.aget("nonexistent-key", prefect_client)
assert retrieved is None


class TestRichArtifact:
async def test_rich_artifact_format_with_default_sandbox(self):
"""Test RichArtifact format with default sandbox permissions."""
artifact = RichArtifact(
html="<html><body><h1>Hello</h1></body></html>",
key="test-rich",
description="Test rich artifact",
)

formatted = await artifact.aformat()
assert formatted == {
"html": "<html><body><h1>Hello</h1></body></html>",
"sandbox": ["allow-scripts"],
}

sync_formatted = cast(dict, artifact.format(_sync=True)) # pyright: ignore[reportCallIssue]
assert sync_formatted == {
"html": "<html><body><h1>Hello</h1></body></html>",
"sandbox": ["allow-scripts"],
}

async def test_rich_artifact_format_with_custom_sandbox(self):
"""Test RichArtifact format with custom sandbox permissions."""
artifact = RichArtifact(
html="<html><body><h1>Hello</h1></body></html>",
key="test-rich",
description="Test rich artifact",
sandbox=["allow-scripts"],
)

formatted = await artifact.aformat()
assert formatted == {
"html": "<html><body><h1>Hello</h1></body></html>",
"sandbox": ["allow-scripts"],
}

async def test_rich_artifact_format_with_empty_sandbox(self):
"""Test RichArtifact format with empty sandbox (most restrictive)."""
artifact = RichArtifact(
html="<html><body><h1>Hello</h1></body></html>",
key="test-rich",
description="Test rich artifact",
sandbox=[],
)

formatted = await artifact.aformat()
assert formatted == {
"html": "<html><body><h1>Hello</h1></body></html>",
"sandbox": [],
}

async def test_rich_artifact_format_with_csp(self):
"""Test RichArtifact format with CSP directive."""
artifact = RichArtifact(
html="<html><body><h1>Hello</h1></body></html>",
key="test-rich",
description="Test rich artifact",
csp="default-src 'self'; script-src 'unsafe-inline'",
)

formatted = await artifact.aformat()
assert formatted == {
"html": "<html><body><h1>Hello</h1></body></html>",
"sandbox": ["allow-scripts"],
"csp": "default-src 'self'; script-src 'unsafe-inline'",
}

async def test_create_rich_artifact_in_flow(self, client: httpx.AsyncClient):
"""Test creating a rich artifact in a flow."""

@flow
async def my_flow():
return await acreate_rich_artifact(
html="<html><body><h1>Hello World</h1></body></html>",
key="flow-rich-artifact",
description="A rich artifact from a flow",
)

artifact_id = await my_flow()
response = await client.get(f"/artifacts/{artifact_id}")
assert response.status_code == 200
result = schemas.core.Artifact.model_validate(response.json())
assert result.type == "rich"
assert isinstance(result.data, dict)
assert result.data["html"] == "<html><body><h1>Hello World</h1></body></html>"
assert result.data["sandbox"] == ["allow-scripts"]

async def test_create_rich_artifact_in_task(self, client: httpx.AsyncClient):
"""Test creating a rich artifact in a task."""

@task
def my_task():
run_context = get_run_context()
assert isinstance(run_context, TaskRunContext)
task_run_id = run_context.task_run.id
artifact_id = create_rich_artifact(
html="<html><body><script>console.log('test');</script></body></html>",
key="task-rich-artifact",
description="A rich artifact from a task",
)
return artifact_id, task_run_id

@flow
def my_flow():
run_context = get_run_context()
assert isinstance(run_context, FlowRunContext)
assert run_context.flow_run is not None
flow_run_id = run_context.flow_run.id
artifact_id, task_run_id = my_task()
return artifact_id, flow_run_id, task_run_id

my_artifact_id, flow_run_id, task_run_id = my_flow()

response = await client.get(f"/artifacts/{my_artifact_id}")
assert response.status_code == 200
my_rich_artifact = schemas.core.Artifact.model_validate(response.json())

assert my_rich_artifact.flow_run_id == flow_run_id
assert my_rich_artifact.task_run_id == task_run_id
assert my_rich_artifact.type == "rich"
assert isinstance(my_rich_artifact.data, dict)
assert (
my_rich_artifact.data["html"]
== "<html><body><script>console.log('test');</script></body></html>"
)

async def test_create_rich_artifact_with_custom_sandbox(
self, client: httpx.AsyncClient
):
"""Test creating a rich artifact with custom sandbox permissions."""

@flow
async def my_flow():
return await acreate_rich_artifact(
html="<html><body>No scripts allowed</body></html>",
key="restricted-rich-artifact",
description="A restricted rich artifact",
sandbox=[], # Most restrictive - no permissions
)

artifact_id = await my_flow()
response = await client.get(f"/artifacts/{artifact_id}")
assert response.status_code == 200
result = schemas.core.Artifact.model_validate(response.json())
assert result.type == "rich"
assert isinstance(result.data, dict)
assert result.data["sandbox"] == []

async def test_create_rich_artifact_with_csp(self, client: httpx.AsyncClient):
"""Test creating a rich artifact with CSP directive."""

@flow
async def my_flow():
return await acreate_rich_artifact(
html="<html><body><h1>Secure</h1></body></html>",
key="csp-rich-artifact",
description="A rich artifact with CSP",
csp="default-src 'self'",
)

artifact_id = await my_flow()
response = await client.get(f"/artifacts/{artifact_id}")
assert response.status_code == 200
result = schemas.core.Artifact.model_validate(response.json())
assert result.type == "rich"
assert isinstance(result.data, dict)
assert result.data["csp"] == "default-src 'self'"
2 changes: 1 addition & 1 deletion ui/src/pages/Artifact.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@
inline-block
mx-auto
}
</style>
</style>