Skip to content

UI: Allow scoping plugin tabs to specific Dags and tasks - #69148

Merged
bbovenzi merged 4 commits into
apache:mainfrom
dheerajturaga:gate-plugin-tabs-by-dag-tags-and-id
Aug 18, 2026
Merged

bbovenzi merged 4 commits into
apache:mainfrom
dheerajturaga:gate-plugin-tabs-by-dag-tags-and-id

Conversation

@dheerajturaga

@dheerajturaga dheerajturaga commented Jun 30, 2026 •

Copy link
Copy Markdown
Member

Plugin external views and React apps that target the Dag, Dag run, task, or task instance pages appear on every Dag, with no way to limit a tab to the Dags and tasks it is relevant to. A deployment running several Dag-specific plugins ends up with tabs that are dead weight on unrelated Dags, and a plugin cannot hide its own tab from inside itself.

This adds a single optional applies_to block:

"applies_to": {
    "dag_tags": ["ml"],                      # Dag carries any of these tags
    "dag_ids": ["train_pipeline"],           # exact dag_id
    "task_ids": ["train_model"],             # exact task_id
    "operators": ["KubernetesPodOperator"],  # operator class name
    "operator_names": ["@task.bash"],         # UI/display name
}

All keys are optional; omitting applies_to shows the item everywhere, which is the existing behaviour. operators matches operator class names only. operator_names matches UI/display names (custom_operator_name), including decorator names such as @task.bash.

Criteria combine like Kubernetes label selectors — OR within a key, AND across keys — but the AND applies only across the keys the current page can evaluate. A task_ids criterion cannot be judged on a Dag-level page, so it is skipped there rather than failing the match. That lets one applies_to block be shared by a plugin's Dag- and task-level destinations instead of needing one per destination.

Scoping is also applied to the dag_overview and task_overview React app destinations, which filter locally rather than going through usePluginTabs — without that, a scoped plugin still rendered its panel on every Dag.

Plugin showing on all Dags:
all_dags_plugin

Plugins showing only for the tutorial Dag:
tutorial_only_plugin

Plugins showing for example_* Dags:
example_only_plugin

On the feedback in this thread:

  • API sprawl — the schema gains exactly one field, ever. Future criteria (e.g. asset_groups for asset pages) become keys inside PluginAppliesToResponse rather than new top-level parameters. I've deliberately kept this PR to the Dag/task axis so the model can be reviewed on its own; assets are a natural follow-up.
  • Dependency direction — dag_tags puts the opt-in on the Dag side: the Dag author tags their Dag to receive the tab. operators is the case where a plugin legitimately knows what it applies to. dag_ids and task_ids cover deployments where the Airflow operator, not the plugin author, knows the mapping.
  • Since UI: Pass Dag, DagRun, TaskInstance, and Asset objects to React app plugins #70149 landed, a plugin can self-gate its content. applies_to gates the tab affordance, which a plugin cannot do from inside itself — that is the remaining gap.

dag_id_pattern from the earlier revision is dropped: glob matching was the most bespoke part and is not needed.

This is display gating, not an authorization boundary — a user who knows the url_route can still reach the view directly. Noted in the docs.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 5)

Generated-by: Claude Code (Opus 5) following the guidelines

@pierrejeambrun pierrejeambrun left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to pass all those attributes to the plugin already. (dagId, taskInstanceId, runId).

Always displaying the tab, and let the Plugin display 'nothing' or a specific message depending on the relevant parameters, so everything is still self contained in the plugin, and the plugin remains responsible for displaying whatever it feels like depending on the params.

This adds complexity just for the sake of 'hiding the tab', I don't think it's worth it.

@bbovenzi

bbovenzi commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Actually I think this can make sense. If you want a plugin to only work for one dag or task. Then adding an empty tab to hundreds of other tasks or dags is not a great UX. I can definitely see that a plugin would be only relevant to a certain operator.

@pierrejeambrun

pierrejeambrun commented Jul 10, 2026 •

Copy link
Copy Markdown
Member

That's possible and make sense, the thing I don't like is that it reverses the dependency from core to plugin, to decide what to do instead of having plugin self contained, deciding for themselves what they should display. And that's added complexity and code for something that isn't really harmfull. (showing a plugin tab that will render, "Plugin not available for this operator...." is fine to me. (instead of having people wonder where the plugin tab is, why it's not displaying there, I find it actually more confusing)

But if we want this in I'm fine.

@bbovenzi

Copy link
Copy Markdown
Contributor

The dependency order is a fair point.

I also want to think more if the filter patterns in this PR are the way we want to go. I fear that it could quickly becoming unruly with too many specific ways to filter a dag or task, each one being its own optional parameter.

@dheerajturaga

Copy link
Copy Markdown
Member Author

Hi @pierrejeambrun, @bbovenzi
Thanks for the feedback. The challenge I am facing is that we have several plugins that we have created that we need displayed as a new tab within the dag run page. Today, I cannot "hide" the tabs which are irrelevant to the dags. I rather need to display "nothing" or some useful message to indicate this tab isnt supported for this dag.

In my case I have 6-7 plugins (and growing) which are dag specific or tag specific and I dont want to display these tabs for unrelated dags. Hence my approach.

Im happy to take suggestions in alternate ways of implementation. Please let me know

@dheerajturaga

Copy link
Copy Markdown
Member Author

Thanks for the feedback, both points landed. Proposal to address them:

Instead of three flat parameters (which would keep growing — I also want task-level scoping), collapse everything into a single optional applies_to object:

"applies_to": {
    "dag_tags": ["ml"],                      # Dag carries any of these tags
    "dag_ids": ["train_pipeline"],           # exact dag_id
    "task_ids": ["train_model"],             # exact task_id
    "operators": ["KubernetesPodOperator"],  # task's operator class
}

Semantics like Kubernetes label selectors: OR within a key, AND across keys, omit applies_to entirely → tab shows everywhere (current behavior). I'm dropping dag_id_pattern — glob matching was the most bespoke part and I don't need it.

On the concerns raised:

  • API sprawl: the schema gains exactly one field, ever. Future criteria become keys inside one model rather than new top-level parameters.
  • Dependency direction: dag_tags puts the opt-in on the Dag side (the Dag author tags their Dag to receive the tab), and operators is the case where the plugin legitimately knows what it applies to — a plugin built for a specific operator. dag_ids and task_ids round it out as exact-match criteria for plugins tied to a specific Dag or task.

I'd also split the "pass dag (and task/TI) context to React app plugins" change into a separate PR, since it's independent and useful for self-contained plugins regardless.

If this shape looks reasonable I'll rework the branch.


Drafted-by: Claude Code (Fable 5); reviewed by @dheerajturaga before posting

@dheerajturaga
dheerajturaga force-pushed the gate-plugin-tabs-by-dag-tags-and-id branch from 4e70b01 to bdb30a8 Compare August 4, 2026 00:08
@dheerajturaga dheerajturaga changed the title UI: Allow scoping plugin tabs to specific Dags by tag or id UI: Allow scoping plugin tabs to specific Dags and tasks Aug 4, 2026

@bbovenzi bbovenzi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add some validation to the _get_ui_plugins() of plugins_manager.py. So we can log error messages if the applies_to are incorrect. Or log a warning that a task filter is applied to the dag level and therefore won't do anything.

Comment thread airflow-core/src/airflow/ui/src/hooks/usePluginTabs.tsx Outdated
Comment thread airflow-core/src/airflow/api_fastapi/core_api/datamodels/plugins.py Outdated
Comment thread airflow-core/docs/administration-and-deployment/plugins.rst Outdated
Comment thread airflow-core/src/airflow/ui/src/hooks/usePluginAppliesToContext.ts
@dheerajturaga
dheerajturaga force-pushed the gate-plugin-tabs-by-dag-tags-and-id branch from bdb30a8 to 7ffccc8 Compare August 17, 2026 02:19
Plugin external views and React apps that target the Dag, Dag run, task,
or task instance pages appeared on every Dag, with no way to limit a tab
to the Dags and tasks it is relevant to. A deployment running several
Dag-specific plugins ends up with tabs that are dead weight on unrelated
Dags, and a plugin cannot hide its own tab from inside itself.

Scoping lives in a single optional `applies_to` block rather than one
parameter per criterion, so future criteria become keys inside one model
instead of new top-level fields. Criteria are AND-ed only across the keys
the current page can evaluate, which lets one block be shared by a
plugin's Dag- and task-level destinations rather than needing one per
destination.

This gates the tab affordance only. It is not an authorization boundary:
a user who knows the url_route can still reach the view directly.
Addresses review feedback on the plugin tab scoping feature.

Malformed or mis-targeted applies_to blocks were accepted silently, and a
single operators key conflated the operator class name with its display
name — unlike every other operator filter in the UI. Plugin authors also
paid for Dag/task lookups on pages where nothing was scoped.

Now the plugin loader warns about (and strips) malformed applies_to and
warns when a destination cannot honour a criterion, the API model rejects
unknown keys so a typo no longer drops the whole plugin, operators and
operator_names are matched separately, and the context queries run only
when some view for the destination is actually scoped.
Some test files had manual line-wrapping that didn't match prettier's
output, tripping the ts-compile-lint-ui static check in CI.
Historical task instance pages can refer to older Dag versions, while malformed plugin metadata should not prevent UI plugins from loading. Keeping the examples aligned avoids configurations that appear valid but cannot filter their destination.
@dheerajturaga
dheerajturaga force-pushed the gate-plugin-tabs-by-dag-tags-and-id branch from 49d95ec to a9c6a37 Compare August 17, 2026 21:32
@dheerajturaga

Copy link
Copy Markdown
Member Author

The review-level validation request is addressed in _get_ui_plugins(). Malformed applies_to blocks—wrong container type, non-string or unknown criterion keys, and non-string criterion values—now emit a warning and are removed so the plugin item still loads unscoped. Valid criteria that the destination cannot evaluate also emit a warning, but remain configured because runtime matching intentionally skips unevaluable criteria. Tests cover malformed, unevaluable, and valid configurations.


Drafted-by: Codex (GPT-5) (no human review before posting)

@bbovenzi
bbovenzi merged commit c855392 into apache:main Aug 18, 2026
156 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Backport failed to create: airflow-ctl/v0-1-test. View the failure log Run details

Note: As of Merging PRs targeted for Airflow 3.X
the committer who merges the PR is responsible for backporting the PRs that are bug fixes (generally speaking) to the maintenance branches.

In matter of doubt please ask in #release-management Slack channel.

Status Branch Result
❌ airflow-ctl/v0-1-test Commit Link

You can attempt to backport this manually by running:

cherry_picker c855392 airflow-ctl/v0-1-test

This should apply the commit to the airflow-ctl/v0-1-test branch and leave the commit in conflict state marking
the files that need manual conflict resolution.

After you have resolved the conflicts, you can continue the backport process by running:

cherry_picker --continue

If you don't have cherry-picker installed, see the installation guide.

@dheerajturaga

Copy link
Copy Markdown
Member Author

@bbovenzi really appreciate your review on this! This implementation will facilitate operator scoped plugins allowing for features such as AIP-115 https://cwiki.apache.org/confluence/spaces/AIRFLOW/pages/440305022/AIP-115+On-Demand+Task+Sections

@bbovenzi

Copy link
Copy Markdown
Contributor

@dheerajturaga Of course. And I'll add the plugin actions soon too so if you want to surface that task run action without a full tab you can do that too.

imrichardwu pushed a commit to imrichardwu/airflow that referenced this pull request Sep 11, 2026
* UI: Allow scoping plugin tabs to specific Dags and tasks

Plugin external views and React apps that target the Dag, Dag run, task,
or task instance pages appeared on every Dag, with no way to limit a tab
to the Dags and tasks it is relevant to. A deployment running several
Dag-specific plugins ends up with tabs that are dead weight on unrelated
Dags, and a plugin cannot hide its own tab from inside itself.

Scoping lives in a single optional `applies_to` block rather than one
parameter per criterion, so future criteria become keys inside one model
instead of new top-level fields. Criteria are AND-ed only across the keys
the current page can evaluate, which lets one block be shared by a
plugin's Dag- and task-level destinations rather than needing one per
destination.

This gates the tab affordance only. It is not an authorization boundary:
a user who knows the url_route can still reach the view directly.

* UI: Validate plugin applies_to and split operator scoping

Addresses review feedback on the plugin tab scoping feature.

Malformed or mis-targeted applies_to blocks were accepted silently, and a
single operators key conflated the operator class name with its display
name — unlike every other operator filter in the UI. Plugin authors also
paid for Dag/task lookups on pages where nothing was scoped.

Now the plugin loader warns about (and strips) malformed applies_to and
warns when a destination cannot honour a criterion, the API model rejects
unknown keys so a typo no longer drops the whole plugin, operators and
operator_names are matched separately, and the context queries run only
when some view for the destination is actually scoped.

* UI: Fix prettier formatting in plugin scoping tests

Some test files had manual line-wrapping that didn't match prettier's
output, tripping the ts-compile-lint-ui static check in CI.

* UI: Fix plugin scoping edge cases

Historical task instance pages can refer to older Dag versions, while malformed plugin metadata should not prevent UI plugins from loading. Keeping the examples aligned avoids configurations that appear valid but cannot filter their destination.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants