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
8 changes: 4 additions & 4 deletions airflow-core/docs/administration-and-deployment/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ definitions in Airflow.
# Name of the external view, this will be displayed in the UI.
"name": "Name of the External View",
# Source URL of the external view. This URL can be templated using context variables, depending on the location where the external view is rendered
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX).
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX, ASSET_ID, ASSET_URI).
"href": "https://example.com/{DAG_ID}/{RUN_ID}/{TASK_ID}/{MAP_INDEX}",
# Destination of the external view. This is used to determine where the view will be loaded in the UI.
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance", "base"], default to "nav".
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance", "asset", "base"], default to "nav".
"destination": "dag_run",
# Optional icon, url to an svg file.
"icon": "https://example.com/icon.svg",
Expand All @@ -265,10 +265,10 @@ definitions in Airflow.
"name": "Name of the React App",
# Bundle URL of the React app. This is the URL where the React app is served from. It can be a static file or a CDN.
# This URL can be templated using context variables, depending on the location where the external view is rendered
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX).
# the context variables available will be different, i.e a subset of (DAG_ID, RUN_ID, TASK_ID, MAP_INDEX, ASSET_ID, ASSET_URI).
"bundle_url": "https://example.com/static/js/my_react_app.js",
# Destination of the react app. This is used to determine where the app will be loaded in the UI.
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance", "base"], default to "nav".
# Supported locations are Literal["nav", "dag", "dag_run", "task", "task_instance", "asset", "base"], default to "nav".
# It can also be put inside of an existing page, the supported views are ["dashboard", "dag_overview", "task_overview"]. You can position
# element in the existing page via the css `order` rule which will determine the flex order.
# Use "base" to mount the app in the base layout (e.g. a toolbar strip); the host uses a flex container so you can set ``order`` in your root JSX to control position.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class AppBuilderMenuItemResponse(BaseModel):
category: str | None = None


BaseDestinationLiteral = Literal["nav", "dag", "dag_run", "task", "task_instance", "base"]
BaseDestinationLiteral = Literal["nav", "dag", "dag_run", "task", "task_instance", "asset", "base"]


class BaseUIResponse(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14768,6 +14768,7 @@ components:
- dag_run
- task
- task_instance
- asset
- base
title: Destination
default: nav
Expand Down Expand Up @@ -15724,6 +15725,7 @@ components:
- dag_run
- task
- task_instance
- asset
- base
- dashboard
title: Destination
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4878,7 +4878,7 @@ export const $ExternalViewResponse = {
},
destination: {
type: 'string',
enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 'base'],
enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 'asset', 'base'],
title: 'Destination',
default: 'nav'
}
Expand Down Expand Up @@ -6270,7 +6270,7 @@ export const $ReactAppResponse = {
},
destination: {
type: 'string',
enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 'base', 'dashboard'],
enum: ['nav', 'dag', 'dag_run', 'task', 'task_instance', 'asset', 'base', 'dashboard'],
title: 'Destination',
default: 'nav'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1280,11 +1280,11 @@ export type ExternalViewResponse = {
category?: string | null;
nav_top_level?: boolean | null;
href: string;
destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'base';
destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'asset' | 'base';
[key: string]: unknown | string;
};

export type destination = 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'base';
export type destination = 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'asset' | 'base';

/**
* Extra Links Response.
Expand Down Expand Up @@ -1655,11 +1655,11 @@ export type ReactAppResponse = {
category?: string | null;
nav_top_level?: boolean | null;
bundle_url: string;
destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'base' | 'dashboard';
destination?: 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'asset' | 'base' | 'dashboard';
[key: string]: unknown | string;
};

export type destination2 = 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'base' | 'dashboard';
export type destination2 = 'nav' | 'dag' | 'dag_run' | 'task' | 'task_instance' | 'asset' | 'base' | 'dashboard';

/**
* Internal enum for setting reprocess behavior in a backfill.
Expand Down
4 changes: 4 additions & 0 deletions airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useAssetServiceGetAsset } from "openapi/queries";
import { BreadcrumbStats } from "src/components/BreadcrumbStats";
import { ProgressBar } from "src/components/ui";
import { GroupsProvider } from "src/context/groups";
import { usePluginTabs } from "src/hooks/usePluginTabs";
import { NavTabs } from "src/layouts/Details/NavTabs";
import { useDocumentTitle } from "src/utils";

Expand Down Expand Up @@ -62,13 +63,16 @@ export const AssetLayout = () => {

const { fitView, getZoom } = useReactFlow();

const externalTabs = usePluginTabs("asset");

const tabs = [
{ icon: <MdTimeline />, label: translate("assets:events"), value: "" },
{
icon: <MdOutlineStorage />,
label: translate("assets:assetStateStore.title"),
value: "asset-state-store",
},
...externalTabs,
];

return (
Expand Down
17 changes: 16 additions & 1 deletion airflow-core/src/airflow/ui/src/pages/Iframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
import { useParams } from "react-router-dom";

import { useAssetServiceGetAsset } from "openapi/queries";
import type { ExternalViewResponse } from "openapi/requests/types.gen";

export const Iframe = ({
Expand All @@ -27,7 +28,15 @@ export const Iframe = ({
readonly externalView: ExternalViewResponse;
readonly sandbox?: string;
}) => {
const { dagId, mapIndex, runId, taskId } = useParams();
const { assetId, dagId, mapIndex, runId, taskId } = useParams();

// The asset URI is not part of the route, so resolve it from the asset record. This is a
// cache hit because the asset details page has already fetched it.
const { data: asset } = useAssetServiceGetAsset(
{ assetId: assetId === undefined ? 0 : parseInt(assetId, 10) },
undefined,
{ enabled: Boolean(assetId) },
);

// Build the href URL with context parameters if the view has a destination
let src = externalView.href;
Expand All @@ -46,6 +55,12 @@ export const Iframe = ({
if (mapIndex !== undefined) {
src = src.replaceAll("{MAP_INDEX}", mapIndex);
}
if (assetId !== undefined) {
src = src.replaceAll("{ASSET_ID}", encodeURIComponent(assetId));
}
if (asset?.uri !== undefined) {
src = src.replaceAll("{ASSET_URI}", encodeURIComponent(asset.uri));
}
}

if (src.startsWith("http://") || src.startsWith("https://")) {
Expand Down
34 changes: 31 additions & 3 deletions airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import { Spinner } from "@chakra-ui/react";
import { type FC, lazy, Suspense } from "react";
import { useParams } from "react-router-dom";

import { useAssetServiceGetAsset } from "openapi/queries";
import type { ReactAppResponse } from "openapi/requests/types.gen";

import { ErrorPage } from "./Error";

export type PluginProps = {
assetId?: string;
assetUri?: string;
dagId?: string;
mapIndex?: string;
runId?: string;
Expand Down Expand Up @@ -62,7 +65,16 @@ const loadPlugin = (reactApp: ReactAppResponse): Promise<{ default: PluginCompon
});

export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse }) => {
const { dagId, mapIndex, runId, taskId } = useParams();
const { assetId, dagId, mapIndex, runId, taskId } = useParams();

// The asset URI is not part of the route, so resolve it from the asset record. This is a
// cache hit because the asset details page has already fetched it.
const { data: asset } = useAssetServiceGetAsset(
{ assetId: assetId === undefined ? 0 : parseInt(assetId, 10) },
undefined,
{ enabled: Boolean(assetId) },
);
const assetUri = asset?.uri;

// If the plugin component was already registered on the global object by a previous load,
// render it directly without going through Suspense/lazy (avoids flashing the spinner).
Expand All @@ -71,7 +83,16 @@ export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse
if (typeof existing === "function") {
const Plugin = existing as PluginComponentType;

return <Plugin dagId={dagId} mapIndex={mapIndex} runId={runId} taskId={taskId} />;
return (
<Plugin
assetId={assetId}
assetUri={assetUri}
dagId={dagId}
mapIndex={mapIndex}
runId={runId}
taskId={taskId}
/>
);
}

// Otherwise, lazy-load the bundle once. When it resolves, it must set a function component
Expand All @@ -80,7 +101,14 @@ export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse

return (
<Suspense fallback={<Spinner />}>
<LazyPlugin dagId={dagId} mapIndex={mapIndex} runId={runId} taskId={taskId} />
<LazyPlugin
assetId={assetId}
assetUri={assetUri}
dagId={dagId}
mapIndex={mapIndex}
runId={runId}
taskId={taskId}
/>
</Suspense>
);
};
1 change: 1 addition & 0 deletions airflow-core/src/airflow/ui/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export const routerConfig = [
children: [
{ element: <AssetEvents />, index: true },
{ element: <AssetStateStore />, path: "asset-state-store" },
pluginRoute,
],
element: <Asset />,
path: "assets/:assetId",
Expand Down
2 changes: 2 additions & 0 deletions airflow-ctl/src/airflowctl/api/datamodels/generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,7 @@ class Destination(str, Enum):
DAG_RUN = "dag_run"
TASK = "task"
TASK_INSTANCE = "task_instance"
ASSET = "asset"
BASE = "base"


Expand Down Expand Up @@ -931,6 +932,7 @@ class Destination1(str, Enum):
DAG_RUN = "dag_run"
TASK = "task"
TASK_INSTANCE = "task_instance"
ASSET = "asset"
BASE = "base"
DASHBOARD = "dashboard"

Expand Down
Loading