Repository: astronomer/astronomer-cosmos

Issue:
[Bug] DAG owner becomes concatenation of model owners
### Astronomer Cosmos Version

1.9.2

### dbt-core version

1.8.1

### Versions of dbt adapters

_No response_

### LoadMode

DBT_LS_MANIFEST

### ExecutionMode

LOCAL

### InvocationMode

None

### airflow version

2.9.3

### Operating System

macOS

### If a you think it's an UI issue, what browsers are you seeing the problem on?

_No response_

### Deployment

Other

### Deployment details

_No response_

### What happened?

Since the changes introduced in PR #1082 (https://github.com/astronomer/astronomer-cosmos/pull/1082), the owner attribute of the generated Airflow DAG appears to be set to a concatenation (or possibly a list formatted as a string) of all unique owners specified in the meta.owner field of the dbt models processed by Cosmos.

This overrides the owner specified via `DEFAULT_ARGS` passed to the parent Airflow DAG when using `DbtTaskGroup`, or potentially passed directly during instantiation.

### Observed behavior
When Cosmos processes multiple dbt models within a DbtTaskGroup, each having a meta: `{owner: 'model_owner_x'}` tag, the resulting Airflow DAG's owner field becomes a combined string like `'model_owner_1, model_owner_2, model_owner_3'`. This happens even if the parent DAG has `default_args={"owner": "specific_dag_owner"}` defined.

### Expected behavior
The DAG owner should be configurable independently of the individual model owners. 

Specifically:
- The owner defined in the DAG's default_args (e.g., DAG(..., `default_args={"owner": "my_dag_owner"}`)) should be respected and assigned to the DAG.
- If no owner is provided in the DAG's default_args, the behavior might default to Airflow's standard behavior (often airflow). 
- The concatenation of model owners should ideally not be the default or only behavior without explicit configuration.

### Impact
This change negatively impacts organizations where DAG ownership and model ownership are distinct roles. 

For example:
- A central platform/foundation team might own the Airflow DAG execution (owner: 'foundation_team').
- Many different analysts or teams might own the individual dbt models (owner: 'analyst_a', owner: 'team_b', etc.).

With the current behavior, the Airflow UI shows a potentially very long list of owners (e.g., hundreds) for the DAG, making it difficult to identify the true DAG operational owner and cluttering the UI.

The long list of owners inserted is also causing issues in Airflow's database
```python
 sqlalchemy.exc.PendingRollbackError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). Original exception was: (psycopg2.errors.StringDataRightTruncation) value too long for type character varying(2000)
```

### How to reproduce

Define multiple dbt models, each with a different value in meta: {owner: ...}.
```
  Example model 1: meta: {owner: 'team_alpha'}
  Example model 2: meta: {owner: 'team_beta'}
  Example model 3: meta: {owner: 'data_science'}
```

Define an Airflow DAG using DbtTaskGroup to include these models. Set the desired owner in the DAG's default_args.

```python
from airflow.models.dag import DAG
from cosmos import DbtTaskGroup, ProjectConfig, ProfileConfig # Assuming configs are defined
from pendulum import datetime

# Define configs (replace with actual paths/methods)
# project_config = ProjectConfig(...)
# profile_config = ProfileConfig(...)

# Define DAG default args, including the desired owner
dag_default_args = {
    "owner": "foundation_team", # Explicit DAG owner
    "start_date": datetime(2023, 1, 1),
    # Add other necessary default_args like email, retries etc.
}

with DAG(
    dag_id="my_dag_with_dbt_task_group",
    default_args=dag_default_args, # Pass default_args here
    schedule_interval="@daily",
    catchup=False,
    tags=["dbt", "cosmos"],
) as dag:

    my_dbt_tasks = DbtTaskGroup(
        group_id="dbt_pipeline", # ID for the TaskGroup
        project_config=project_config,
        profile_config=profile_config,
        operator_args={ # Args passed to the underlying dbt operators
            "install_deps": True,
        },
        # render_config=... # Optional RenderConfig if needed
    )
    # Potentially add other Airflow tasks before/after my_dbt_tasks
```
Observe the DAG in the Airflow UI or inspect the dag.owner attribute.
The owner will be something like 'team_alpha, team_beta, data_science' instead of the expected 'foundation_team'.


### Anything else :)?

### Attempted Workarounds

Defining `default_args={"owner": "foundation_team"}` at the DAG level does not prevent the concatenation behavior affecting the DAG's displayed owner.

### Proposed Solution / Enhancement

Instead of the current behavior being the default, consider introducing a configuration option (e.g., a boolean toggle in `RenderConfig` or passed to DbtTaskGroup/DbtDag) to allow users to opt-in to the behavior of concatenating model owners into the DAG owner field.

Benefits of this approach:
- Restores the previously expected behavior for users relying on default_args for DAG ownership.
- Avoids cluttering the UI for organizations with distinct DAG vs. model ownership.
- Still allows users who find the concatenated owner list useful to enable it explicitly.

### Are you willing to submit PR?

- [x] Yes I am willing to submit a PR!

### Contact Details

_No response_

Hints from issue comments:
@dosubot: <!-- Answer -->
The issue you're experiencing is due to changes introduced in PR #1082, which modify how the owner attribute of Airflow DAGs is set when using `DbtTaskGroup`. The current behavior concatenates the `meta.owner` values from multiple dbt models into a single string for the DAG's owner, overriding the owner specified via `default_args` for the parent DAG <sup>[[1]](https://github.com/astronomer/astronomer-cosmos/blob/main/cosmos/dbt/graph.py)</sup>.

To address this, a proposed solution suggests adding a configuration option that allows users to opt-in to this concatenation behavior. This would enable the DAG's owner to be set independently via `default_args`, restoring the expected functionality and improving usability, especially for organizations with distinct roles for DAG and model ownership.

Currently, there is no existing configuration option in the Cosmos codebase that allows users to control this concatenation behavior or specify a custom owner for the DAG independently of model owners <sup>[[1]](https://github.com/astronomer/astronomer-cosmos/blob/main/cosmos/dbt/graph.py)</sup>. Implementing such a feature would require changes to the codebase to introduce this configuration option.


<!-- Dosu Comment Footer -->
*To continue talking to [Dosu](https://dosu.dev), mention @dosu.*

---
<sup>Help Dosu learn! Give it feedback: [Great Response](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=great_response) | [Irrelevant Answer](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=irrelevant_answer) | [Incorrect Sources](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=incorrect_sources) | [Too Verbose](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=hallucination) | [Bug Report](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/c04c6fcc-4a41-42cc-b1af-58e3d128df65?feedback_type=other)</sup>
@linchun3: Closed by https://github.com/astronomer/astronomer-cosmos/pull/1787

Solution patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 93d4bebd..97ceba61 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -28,11 +28,11 @@ jobs:
 
       - uses: actions/setup-python@v5
         with:
-          python-version: "3.9"
+          python-version: "3.10"
           architecture: "x64"
 
       - run: pip3 install hatch
-      - run: hatch run tests.py3.9-2.8-1.9:type-check
+      - run: hatch run tests.py3.10-2.10-1.9:type-check
 
   Run-Unit-Tests:
     runs-on: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ffb776ba..2a145156 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -57,7 +57,7 @@ repos:
           - --py37-plus
           - --keep-runtime-typing
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.11.11
+    rev: v0.11.12
     hooks:
       - id: ruff
         args:
diff --git a/cosmos/airflow/graph.py b/cosmos/airflow/graph.py
index 20229f3f..c7c37a79 100644
--- a/cosmos/airflow/graph.py
+++ b/cosmos/airflow/graph.py
@@ -129,6 +129,7 @@ def create_test_task_metadata(
     node: DbtNode | None = None,
     render_config: RenderConfig | None = None,
     detached_from_parent: dict[str, DbtNode] | None = None,
+    enable_owner_inheritance: bool | None = None,
 ) -> TaskMetadata:
     """
     Create the metadata that will be used to instantiate the Airflow Task that will be used to run the Dbt test node.
@@ -172,6 +173,9 @@ def create_test_task_metadata(
         exclude_detached_tests_if_needed(node, task_args, detached_from_parent)
         _override_profile_if_needed(task_args, node.profile_config_to_override)
 
+    if not enable_owner_inheritance:
+        task_owner = ""
+
     args_to_override: dict[str, Any] = {}
     if node:
         args_to_override = node.operator_kwargs_to_override
@@ -250,6 +254,7 @@ def create_task_metadata(
     test_indirect_selection: TestIndirectSelection = TestIndirectSelection.EAGER,
     on_warning_callback: Callable[..., Any] | None = None,
     detached_from_parent: dict[str, DbtNode] | None = None,
+    enable_owner_inheritance: bool | None = None,
 ) -> TaskMetadata | None:
     """
     Create the metadata that will be used to instantiate the Airflow Task used to run the Dbt node.
@@ -314,9 +319,14 @@ def create_task_metadata(
 
         _override_profile_if_needed(args, node.profile_config_to_override)
 
+        task_owner = node.owner
+
+        if not enable_owner_inheritance:
+            task_owner = ""
+
         task_metadata = TaskMetadata(
             id=task_id,
-            owner=node.owner,
+            owner=task_owner,
             operator_class=calculate_operator_class(
                 execution_mode=execution_mode, dbt_class=dbt_resource_to_class[node.resource_type]
             ),
@@ -356,6 +366,7 @@ def generate_task_or_group(
     on_warning_callback: Callable[..., Any] | None,
     normalize_task_id: Callable[..., Any] | None = None,
     detached_from_parent: dict[str, DbtNode] | None = None,
+    enable_owner_inheritance: bool | None = None,
     **kwargs: Any,
 ) -> BaseOperator | TaskGroup | None:
     task_or_group: BaseOperator | TaskGroup | None = None
@@ -379,6 +390,7 @@ def generate_task_or_group(
         test_indirect_selection=test_indirect_selection,
         on_warning_callback=on_warning_callback,
         detached_from_parent=detached_from_parent,
+        enable_owner_inheritance=enable_owner_inheritance,
     )
 
     # In most cases, we'll  map one DBT node to one Airflow task
@@ -396,6 +408,7 @@ def generate_task_or_group(
                     node=node,
                     on_warning_callback=on_warning_callback,
                     detached_from_parent=detached_from_parent,
+                    enable_owner_inheritance=enable_owner_inheritance,
                 )
                 test_task = create_airflow_task(test_meta, dag, task_group=model_task_group)
                 task >> test_task
@@ -582,6 +595,7 @@ def build_airflow_graph(
     test_behavior = render_config.test_behavior
     source_rendering_behavior = render_config.source_rendering_behavior
     normalize_task_id = render_config.normalize_task_id
+    enable_owner_inheritance = render_config.enable_owner_inheritance
     tasks_map: dict[str, Union[TaskGroup, BaseOperator]] = {}
     task_or_group: TaskGroup | BaseOperator
 
@@ -612,6 +626,7 @@ def build_airflow_graph(
             normalize_task_id=normalize_task_id,
             node=node,
             detached_from_parent=detached_from_parent,
+            enable_owner_inheritance=enable_owner_inheritance,
         )
         if task_or_group is not None:
             logger.debug(f"Conversion of <{node.unique_id}> was successful!")
@@ -627,6 +642,7 @@ def build_airflow_graph(
             task_args=task_args,
             on_warning_callback=on_warning_callback,
             render_config=render_config,
+            enable_owner_inheritance=enable_owner_inheritance,
         )
         test_task = create_airflow_task(test_meta, dag, task_group=task_group)
         leaves_ids = calculate_leaves(tasks_ids=list(tasks_map.keys()), nodes=nodes)
@@ -644,6 +660,7 @@ def build_airflow_graph(
                 on_warning_callback=on_warning_callback,
                 render_config=render_config,
                 node=node,
+                enable_owner_inheritance=enable_owner_inheritance,
             )
             test_task = create_airflow_task(test_meta, dag, task_group=task_group)
             tasks_map[node_id] = test_task
diff --git a/cosmos/config.py b/cosmos/config.py
index ff36bee8..cfba5185 100644
--- a/cosmos/config.py
+++ b/cosmos/config.py
@@ -66,6 +66,7 @@ class RenderConfig:
     :param airflow_vars_to_purge_dbt_ls_cache: Specify Airflow variables that will affect the LoadMode.DBT_LS cache.
     :param normalize_task_id: A callable that takes a dbt node as input and returns the task ID. This allows users to assign a custom node ID separate from the display name.
     :param should_detach_multiple_parents_tests: A boolean that allows users to decide whether to run tests with multiple parent dependencies in separate tasks.
+    :param enable_owner_inheritance: A boolean that allows users to enable the owner inheritance from dbt models to airflow tasks. Defaults to True.
     """
 
     emit_datasets: bool = True
@@ -87,6 +88,7 @@ class RenderConfig:
     airflow_vars_to_purge_dbt_ls_cache: list[str] = field(default_factory=list)
     normalize_task_id: Callable[..., Any] | None = None
     should_detach_multiple_parents_tests: bool = False
+    enable_owner_inheritance: bool | None = True
 
     def __post_init__(self, dbt_project_path: str | Path | None) -> None:
         if self.env_vars:
diff --git a/cosmos/plugin/plugin_impl.py b/cosmos/plugin/plugin_impl.py
index 1b68b0b8..241fd9f6 100644
--- a/cosmos/plugin/plugin_impl.py
+++ b/cosmos/plugin/plugin_impl.py
@@ -7,7 +7,7 @@ from airflow.plugins_manager import AirflowPlugin
 from airflow.security import permissions
 from airflow.www.auth import has_access
 from airflow.www.views import AirflowBaseView
-from flask import abort, url_for
+from flask import abort
 from flask_appbuilder import AppBuilder, expose
 
 from cosmos.listeners import dag_run_listener
@@ -122,60 +122,6 @@ def open_file(path: str, conn_id: Optional[str] = None) -> str:
 
 iframe_script = """
 <script>
-  function getMaxElement(side, elements_query) {
-    var elements = document.querySelectorAll(elements_query)
-    var elementsLength = elements.length,
-      elVal = 0,
-      maxVal = 0,
-      Side = capitalizeFirstLetter(side),
-      timer = Date.now()
-
-    for (var i = 0; i < elementsLength; i++) {
-      elVal =
-        elements[i].getBoundingClientRect()[side] +
-        getComputedStyleWrapper('margin' + Side, elements[i])
-      if (elVal > maxVal) {
-        maxVal = elVal
-      }
-    }
-
-    timer = Date.now() - timer
-
-    chkEventThottle(timer)
-
-    return maxVal
-  }
-  var throttledTimer = 16
-  function chkEventThottle(timer) {
-    if (timer > throttledTimer / 2) {
-      throttledTimer = 2 * timer
-    }
-  }
-  function capitalizeFirstLetter(string) {
-    return string.charAt(0).toUpperCase() + string.slice(1)
-  }
-  function getComputedStyleWrapper(prop, el) {
-    var retVal = 0
-    el = el || document.body // Not testable in phantonJS
-
-    retVal = document.defaultView.getComputedStyle(el, null)
-    retVal = null === retVal ? 0 : retVal[prop]
-
-    return parseInt(retVal)
-  }
-  window.iFrameResizer = {
-    heightCalculationMethod: function getHeight() {
-      return Math.max(
-        // Overview page
-        getMaxElement('bottom', 'div.panel.panel-default') + 50,
-        // Model page
-        getMaxElement('bottom', 'section.section') + 75,
-        // Search page
-        getMaxElement('bottom', 'div.result-body') + 125
-      )
-    }
-  }
-
   // Prevent parent hash changes from sending a message back to the parent.
   // This is necessary for making sure the browser back button works properly.
   let hashChangeLock = true;
@@ -186,6 +132,7 @@ iframe_script = """
     }
     hashChangeLock = false;
   });
+
   window.addEventListener('message', function (event) {
     let msgData = event.data;
     if (typeof msgData === 'string' && msgData.startsWith('#!')) {
@@ -228,9 +175,7 @@ class DbtDocsView(AirflowBaseView):  # type: ignore
         except FileNotFoundError:
             abort(404)
         else:
-            # Hack the dbt docs to render properly in an iframe
-            iframe_resizer_url = url_for(".static", filename="iframeResizer.contentWindow.min.js")
-            html = html.replace("</head>", f'{iframe_script}<script src="{iframe_resizer_url}"></script></head>', 1)
+            html = html.replace("</head>", f"{iframe_script}</head>")
             return html, 200, {"Content-Security-Policy": "frame-ancestors 'self'"}
 
     @expose("/catalog.json")  # type: ignore[misc]
diff --git a/cosmos/plugin/static/iframeResizer.contentWindow.min.js b/cosmos/plugin/static/iframeResizer.contentWindow.min.js
deleted file mode 100644
index 914161c0..00000000
--- a/cosmos/plugin/static/iframeResizer.contentWindow.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/*! iFrame Resizer (iframeSizer.contentWindow.min.js) - v4.3.5 - 2023-03-08
- *  Desc: Include this file in any page being loaded into an iframe
- *        to force the iframe to resize to the content size.
- *  Requires: iframeResizer.min.js on host page.
- *  Copyright: (c) 2023 David J. Bradshaw - dave@bradshaw.net
- *  License: MIT
- */
-!function(a){if("undefined"!=typeof window){var r=!0,P="",u=0,c="",s=null,D="",d=!1,j={resize:1,click:1},l=128,q=!0,f=1,n="bodyOffset",m=n,H=!0,W="",h={},g=32,B=null,p=!1,v=!1,y="[iFrameSizer]",J=y.length,w="",U={max:1,min:1,bodyScroll:1,documentElementScroll:1},b="child",V=!0,X=window.parent,T="*",E=0,i=!1,Y=null,O=16,S=1,K="scroll",M=K,Q=window,G=function(){x("onMessage function not defined")},Z=function(){},$=function(){},_={height:function(){return x("Custom height calculation function not defined"),document.documentElement.offsetHeight},width:function(){return x("Custom width calculation function not defined"),document.body.scrollWidth}},ee={},te=!1;try{var ne=Object.create({},{passive:{get:function(){te=!0}}});window.addEventListener("test",ae,ne),window.removeEventListener("test",ae,ne)}catch(e){}var oe,o,I,ie,N,A,C={bodyOffset:function(){return document.body.offsetHeight+ye("marginTop")+ye("marginBottom")},offset:function(){return C.bodyOffset()},bodyScroll:function(){return document.body.scrollHeight},custom:function(){return _.height()},documentElementOffset:function(){return document.documentElement.offsetHeight},documentElementScroll:function(){return document.documentElement.scrollHeight},max:function(){return Math.max.apply(null,e(C))},min:function(){return Math.min.apply(null,e(C))},grow:function(){return C.max()},lowestElement:function(){return Math.max(C.bodyOffset()||C.documentElementOffset(),we("bottom",Te()))},taggedElement:function(){return be("bottom","data-iframe-height")}},z={bodyScroll:function(){return document.body.scrollWidth},bodyOffset:function(){return document.body.offsetWidth},custom:function(){return _.width()},documentElementScroll:function(){return document.documentElement.scrollWidth},documentElementOffset:function(){return document.documentElement.offsetWidth},scroll:function(){return Math.max(z.bodyScroll(),z.documentElementScroll())},max:function(){return Math.max.apply(null,e(z))},min:function(){return Math.min.apply(null,e(z))},rightMostElement:function(){return we("right",Te())},taggedElement:function(){return be("right","data-iframe-width")}},re=(oe=Ee,N=null,A=0,function(){var e=Date.now(),t=O-(e-(A=A||e));return o=this,I=arguments,t<=0||O<t?(N&&(clearTimeout(N),N=null),A=e,ie=oe.apply(o,I),N||(o=I=null)):N=N||setTimeout(Oe,t),ie});k(window,"message",function(t){var n={init:function(){W=t.data,X=t.source,se(),q=!1,setTimeout(function(){H=!1},l)},reset:function(){H?R("Page reset ignored by init"):(R("Page size reset by host page"),Me("resetPage"))},resize:function(){L("resizeParent","Parent window requested size check")},moveToAnchor:function(){h.findTarget(i())},inPageLink:function(){this.moveToAnchor()},pageInfo:function(){var e=i();R("PageInfoFromParent called from parent: "+e),$(JSON.parse(e)),R(" --")},message:function(){var e=i();R("onMessage called from parent: "+e),G(JSON.parse(e)),R(" --")}};function o(){return t.data.split("]")[1].split(":")[0]}function i(){return t.data.slice(t.data.indexOf(":")+1)}function r(){return t.data.split(":")[2]in{true:1,false:1}}function e(){var e=o();e in n?n[e]():("undefined"==typeof module||!module.exports)&&"iFrameResize"in window||window.jQuery!==a&&"iFrameResize"in window.jQuery.prototype||r()||x("Unexpected message ("+t.data+")")}y===(""+t.data).slice(0,J)&&(!1===q?e():r()?n.init():R('Ignored message of type "'+o()+'". Received before initialization.'))}),k(window,"readystatechange",Ne),Ne()}function ae(){}function k(e,t,n,o){e.addEventListener(t,n,!!te&&(o||{}))}function ue(e){return e.charAt(0).toUpperCase()+e.slice(1)}function ce(e){return y+"["+w+"] "+e}function R(e){p&&"object"==typeof window.console&&console.log(ce(e))}function x(e){"object"==typeof window.console&&console.warn(ce(e))}function se(){function e(e){return"true"===e}function t(e,t){return"function"==typeof e&&(R("Setup custom "+t+"CalcMethod"),_[t]=e,e="custom"),e}{var n;n=W.slice(J).split(":"),w=n[0],u=a===n[1]?u:Number(n[1]),d=a===n[2]?d:e(n[2]),p=a===n[3]?p:e(n[3]),g=a===n[4]?g:Number(n[4]),r=a===n[6]?r:e(n[6]),c=n[7],m=a===n[8]?m:n[8],P=n[9],D=n[10],E=a===n[11]?E:Number(n[11]),h.enable=a!==n[12]&&e(n[12]),b=a===n[13]?b:n[13],M=a===n[14]?M:n[14],v=a===n[15]?v:e(n[15]),R("Initialising iFrame ("+window.location.href+")"),"iFrameResizer"in window&&Object===window.iFrameResizer.constructor&&(n=window.iFrameResizer,R("Reading data from page: "+JSON.stringify(n)),Object.keys(n).forEach(de,n),G="onMessage"in n?n.onMessage:G,Z="onReady"in n?n.onReady:Z,T="targetOrigin"in n?n.targetOrigin:T,m="heightCalculationMethod"in n?n.heightCalculationMethod:m,M="widthCalculationMethod"in n?n.widthCalculationMethod:M,m=t(m,"height"),M=t(M,"width"))}function o(e){F(0,0,e.type,e.screenY+":"+e.screenX)}function i(e,t){R("Add event listener: "+t),k(window.document,e,o)}R("TargetOrigin for parent set to: "+T),le("margin",function(e,t){-1!==t.indexOf("-")&&(x("Negative CSS value ignored for "+e),t="");return t}("margin",c=a===c?u+"px":c)),le("background",P),le("padding",D),(n=document.createElement("div")).style.clear="both",n.style.display="block",n.style.height="0",document.body.appendChild(n),he(),ge(),document.documentElement.style.height="",document.body.style.height="",R('HTML & body height set to "auto"'),R("Enable public methods"),Q.parentIFrame={autoResize:function(e){return!0===e&&!1===r?(r=!0,pe()):!1===e&&!0===r&&(r=!1,fe("remove"),null!==s&&s.disconnect(),clearInterval(B)),F(0,0,"autoResize",JSON.stringify(r)),r},close:function(){F(0,0,"close")},getId:function(){return w},getPageInfo:function(e){"function"==typeof e?($=e,F(0,0,"pageInfo")):($=function(){},F(0,0,"pageInfoStop"))},moveToAnchor:function(e){h.findTarget(e)},reset:function(){Ie("parentIFrame.reset")},scrollTo:function(e,t){F(t,e,"scrollTo")},scrollToOffset:function(e,t){F(t,e,"scrollToOffset")},sendMessage:function(e,t){F(0,0,"message",JSON.stringify(e),t)},setHeightCalculationMethod:function(e){m=e,he()},setWidthCalculationMethod:function(e){M=e,ge()},setTargetOrigin:function(e){R("Set targetOrigin: "+e),T=e},size:function(e,t){L("size","parentIFrame.size("+((e||"")+(t?","+t:""))+")",e,t)}},!0===v&&(i("mouseenter","Mouse Enter"),i("mouseleave","Mouse Leave")),pe(),h=function(){function n(e){var e=e.getBoundingClientRect(),t={x:window.pageXOffset===a?document.documentElement.scrollLeft:window.pageXOffset,y:window.pageYOffset===a?document.documentElement.scrollTop:window.pageYOffset};return{x:parseInt(e.left,10)+parseInt(t.x,10),y:parseInt(e.top,10)+parseInt(t.y,10)}}function o(e){var e=e.split("#")[1]||e,t=decodeURIComponent(e),t=document.getElementById(t)||document.getElementsByName(t)[0];a===t?(R("In page link (#"+e+") not found in iFrame, so sending to parent"),F(0,0,"inPageLink","#"+e)):(t=n(t=t),R("Moving to in page link (#"+e+") at x: "+t.x+" y: "+t.y),F(t.y,t.x,"scrollToOffset"))}function e(){var e=window.location.hash,t=window.location.href;""!==e&&"#"!==e&&o(t)}function t(){Array.prototype.forEach.call(document.querySelectorAll('a[href^="#"]'),function(e){"#"!==e.getAttribute("href")&&k(e,"click",function(e){e.preventDefault(),o(this.getAttribute("href"))})})}function i(){Array.prototype.forEach&&document.querySelectorAll?(R("Setting up location.hash handlers"),t(),k(window,"hashchange",e),setTimeout(e,l)):x("In page linking not fully supported in this browser! (See README.md for IE8 workaround)")}h.enable?i():R("In page linking not enabled");return{findTarget:o}}(),L("init","Init message from host page"),Z()}function de(e){var t=e.split("Callback");2===t.length&&(this[t="on"+t[0].charAt(0).toUpperCase()+t[0].slice(1)]=this[e],delete this[e],x("Deprecated: '"+e+"' has been renamed '"+t+"'. The old method will be removed in the next major version."))}function le(e,t){a!==t&&""!==t&&"null"!==t&&R("Body "+e+' set to "'+(document.body.style[e]=t)+'"')}function t(n){var e={add:function(e){function t(){L(n.eventName,n.eventType)}ee[e]=t,k(window,e,t,{passive:!0})},remove:function(e){var t=ee[e];delete ee[e],window.removeEventListener(e,t,!1)}};n.eventNames&&Array.prototype.map?(n.eventName=n.eventNames[0],n.eventNames.map(e[n.method])):e[n.method](n.eventName),R(ue(n.method)+" event listener: "+n.eventType)}function fe(e){t({method:e,eventType:"Animation Start",eventNames:["animationstart","webkitAnimationStart"]}),t({method:e,eventType:"Animation Iteration",eventNames:["animationiteration","webkitAnimationIteration"]}),t({method:e,eventType:"Animation End",eventNames:["animationend","webkitAnimationEnd"]}),t({method:e,eventType:"Input",eventName:"input"}),t({method:e,eventType:"Mouse Up",eventName:"mouseup"}),t({method:e,eventType:"Mouse Down",eventName:"mousedown"}),t({method:e,eventType:"Orientation Change",eventName:"orientationchange"}),t({method:e,eventType:"Print",eventNames:["afterprint","beforeprint"]}),t({method:e,eventType:"Ready State Change",eventName:"readystatechange"}),t({method:e,eventType:"Touch Start",eventName:"touchstart"}),t({method:e,eventType:"Touch End",eventName:"touchend"}),t({method:e,eventType:"Touch Cancel",eventName:"touchcancel"}),t({method:e,eventType:"Transition Start",eventNames:["transitionstart","webkitTransitionStart","MSTransitionStart","oTransitionStart","otransitionstart"]}),t({method:e,eventType:"Transition Iteration",eventNames:["transitioniteration","webkitTransitionIteration","MSTransitionIteration","oTransitionIteration","otransitioniteration"]}),t({method:e,eventType:"Transition End",eventNames:["transitionend","webkitTransitionEnd","MSTransitionEnd","oTransitionEnd","otransitionend"]}),"child"===b&&t({method:e,eventType:"IFrame Resized",eventName:"resize"})}function me(e,t,n,o){return t!==e&&(e in n||(x(e+" is not a valid option for "+o+"CalculationMethod."),e=t),R(o+' calculation method set to "'+e+'"')),e}function he(){m=me(m,n,C,"height")}function ge(){M=me(M,K,z,"width")}function pe(){var e;!0===r?(fe("add"),e=g<0,window.MutationObserver||window.WebKitMutationObserver?e?ve():s=function(){function t(e){function t(e){!1===e.complete&&(R("Attach listeners to "+e.src),e.addEventListener("load",i,!1),e.addEventListener("error",r,!1),u.push(e))}"attributes"===e.type&&"src"===e.attributeName?t(e.target):"childList"===e.type&&Array.prototype.forEach.call(e.target.querySelectorAll("img"),t)}function o(e){R("Remove listeners from "+e.src),e.removeEventListener("load",i,!1),e.removeEventListener("error",r,!1),u.splice(u.indexOf(e),1)}function n(e,t,n){o(e.target),L(t,n+": "+e.target.src)}function i(e){n(e,"imageLoad","Image loaded")}function r(e){n(e,"imageLoadFailed","Image load failed")}function a(e){L("mutationObserver","mutationObserver: "+e[0].target+" "+e[0].type),e.forEach(t)}var u=[],c=window.MutationObserver||window.WebKitMutationObserver,s=function(){var e=document.querySelector("body");return s=new c(a),R("Create body MutationObserver"),s.observe(e,{attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0}),s}();return{disconnect:function(){"disconnect"in s&&(R("Disconnect body MutationObserver"),s.disconnect(),u.forEach(o))}}}():(R("MutationObserver not supported in this browser!"),ve())):R("Auto Resize disabled")}function ve(){0!==g&&(R("setInterval: "+g+"ms"),B=setInterval(function(){L("interval","setInterval: "+g)},Math.abs(g)))}function ye(e,t){return t=t||document.body,t=null===(t=document.defaultView.getComputedStyle(t,null))?0:t[e],parseInt(t,10)}function we(e,t){for(var n,o=t.length,i=0,r=ue(e),a=Date.now(),u=0;u<o;u++)i<(n=t[u].getBoundingClientRect()[e]+ye("margin"+r,t[u]))&&(i=n);return a=Date.now()-a,R("Parsed "+o+" HTML elements"),R("Element position calculated in "+a+"ms"),O/2<(a=a)&&R("Event throttle increased to "+(O=2*a)+"ms"),i}function e(e){return[e.bodyOffset(),e.bodyScroll(),e.documentElementOffset(),e.documentElementScroll()]}function be(e,t){var n=document.querySelectorAll("["+t+"]");return 0===n.length&&(x("No tagged elements ("+t+") found on page"),document.querySelectorAll("body *")),we(e,n)}function Te(){return document.querySelectorAll("body *")}function Ee(e,t,n,o){function i(){e in{init:1,interval:1,size:1}||!(m in U||d&&M in U)?e in{interval:1}||R("No change in size detected"):Ie(t)}function r(e,t){return!(Math.abs(e-t)<=E)}n=a===n?C[m]():n,o=a===o?z[M]():o,r(f,n)||d&&r(S,o)||"init"===e?(Se(),F(f=n,S=o,e)):i()}function Oe(){A=Date.now(),N=null,ie=oe.apply(o,I),N||(o=I=null)}function L(e,t,n,o){i&&e in j?R("Trigger event cancelled: "+e):(e in{reset:1,resetPage:1,init:1}||R("Trigger event: "+t),("init"===e?Ee:re)(e,t,n,o))}function Se(){i||(i=!0,R("Trigger event lock on")),clearTimeout(Y),Y=setTimeout(function(){i=!1,R("Trigger event lock off"),R("--")},l)}function Me(e){f=C[m](),S=z[M](),F(f,S,e)}function Ie(e){var t=m;m=n,R("Reset trigger event: "+e),Se(),Me("reset"),m=t}function F(e,t,n,o,i){!0===V&&(a===i?i=T:R("Message targetOrigin: "+i),R("Sending message to host page ("+(e=w+":"+(e+":"+t)+":"+n+(a===o?"":":"+o))+")"),X.postMessage(y+e,i))}function Ne(){"loading"!==document.readyState&&window.parent.postMessage("[iFrameResizerChild]Ready","*")}}();
-//# sourceMappingURL=iframeResizer.contentWindow.map
diff --git a/cosmos/plugin/static/iframeResizer.min.js b/cosmos/plugin/static/iframeResizer.min.js
deleted file mode 100644
index 11b0db0a..00000000
--- a/cosmos/plugin/static/iframeResizer.min.js
+++ /dev/null
@@ -1,8 +0,0 @@
-/*! iFrame Resizer (iframeSizer.min.js ) - v4.3.5 - 2023-03-08
- *  Desc: Force cross domain iframes to size to content.
- *  Requires: iframeResizer.contentWindow.min.js to be loaded into the target frame.
- *  Copyright: (c) 2023 David J. Bradshaw - dave@bradshaw.net
- *  License: MIT
- */
-!function(d){var c,u,a,v,x,I,M,r,f,k,i,l,z;function m(){return window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver}function F(e,n,i){e.addEventListener(n,i,!1)}function B(e,n,i){e.removeEventListener(n,i,!1)}function p(e){return x+"["+(n="Host page: "+(e=e),n=window.top!==window.self?window.parentIFrame&&window.parentIFrame.getId?window.parentIFrame.getId()+": "+e:"Nested host page: "+e:n)+"]";var n}function t(e){return k[e]?k[e].log:u}function O(e,n){o("log",e,n,t(e))}function E(e,n){o("info",e,n,t(e))}function R(e,n){o("warn",e,n,!0)}function o(e,n,i,t){!0===t&&"object"==typeof window.console&&console[e](p(n),i)}function w(e){function i(){t("Height"),t("Width"),P(function(){H(w),C(b),l("onResized",w)},w,"init")}function n(){var e=p.slice(I).split(":"),n=e[1]?parseInt(e[1],10):0,i=k[e[0]]&&k[e[0]].iframe,t=getComputedStyle(i);return{iframe:i,id:e[0],height:n+function(e){if("border-box"!==e.boxSizing)return 0;var n=e.paddingTop?parseInt(e.paddingTop,10):0,e=e.paddingBottom?parseInt(e.paddingBottom,10):0;return n+e}(t)+function(e){if("border-box"!==e.boxSizing)return 0;var n=e.borderTopWidth?parseInt(e.borderTopWidth,10):0,e=e.borderBottomWidth?parseInt(e.borderBottomWidth,10):0;return n+e}(t),width:e[2],type:e[3]}}function t(e){var n=Number(k[b]["max"+e]),i=Number(k[b]["min"+e]),e=e.toLowerCase(),t=Number(w[e]);O(b,"Checking "+e+" is in range "+i+"-"+n),t<i&&(t=i,O(b,"Set "+e+" to min value")),n<t&&(t=n,O(b,"Set "+e+" to max value")),w[e]=""+t}function o(){var t=e.origin,o=k[b]&&k[b].checkOrigin;if(o&&""+t!="null"&&!function(){if(o.constructor!==Array)return e=k[b]&&k[b].remoteHost,O(b,"Checking connection is from: "+e),t===e;var e,n=0,i=!1;for(O(b,"Checking connection is from allowed list of origins: "+o);n<o.length;n++)if(o[n]===t){i=!0;break}return i}())throw new Error("Unexpected message received from: "+t+" for "+w.iframe.id+". Message was: "+e.data+". This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.");return 1}function a(e){return p.slice(p.indexOf(":")+v+e)}function s(i,t){var e,n,o;e=function(){var e,n;A("Send Page Info","pageInfo:"+(e=document.body.getBoundingClientRect(),n=w.iframe.getBoundingClientRect(),JSON.stringify({iframeHeight:n.height,iframeWidth:n.width,clientHeight:Math.max(document.documentElement.clientHeight,window.innerHeight||0),clientWidth:Math.max(document.documentElement.clientWidth,window.innerWidth||0),offsetTop:parseInt(n.top-e.top,10),offsetLeft:parseInt(n.left-e.left,10),scrollTop:window.pageYOffset,scrollLeft:window.pageXOffset,documentHeight:document.documentElement.clientHeight,documentWidth:document.documentElement.clientWidth,windowHeight:window.innerHeight,windowWidth:window.innerWidth})),i,t)},n=32,z[o=t]||(z[o]=setTimeout(function(){z[o]=null,e()},n))}function r(e){e=e.getBoundingClientRect();return W(b),{x:Math.floor(Number(e.left)+Number(M.x)),y:Math.floor(Number(e.top)+Number(M.y))}}function d(e){var n=e?r(w.iframe):{x:0,y:0},i={x:Number(w.width)+n.x,y:Number(w.height)+n.y};O(b,"Reposition requested from iFrame (offset x:"+n.x+" y:"+n.y+")"),window.top===window.self?(M=i,c(),O(b,"--")):window.parentIFrame?window.parentIFrame["scrollTo"+(e?"Offset":"")](i.x,i.y):R(b,"Unable to scroll to requested position, window.parentIFrame not found")}function c(){!1===l("onScroll",M)?S():C(b)}function u(e){var e=e.split("#")[1]||"",n=decodeURIComponent(e),n=document.getElementById(n)||document.getElementsByName(n)[0];n?(n=r(n),O(b,"Moving to in page link (#"+e+") at x: "+n.x+" y: "+n.y),M={x:n.x,y:n.y},c(),O(b,"--")):window.top===window.self?O(b,"In page link #"+e+" not found"):window.parentIFrame?window.parentIFrame.moveToAnchor(e):O(b,"In page link #"+e+" not found and window.parentIFrame not found")}function f(e){var n,i={};i=0===Number(w.width)&&0===Number(w.height)?{x:(n=a(9).split(":"))[1],y:n[0]}:{x:w.width,y:w.height},l(e,{iframe:w.iframe,screenX:Number(i.x),screenY:Number(i.y),type:w.type})}function l(e,n){return T(b,e,n)}function m(){switch(k[b]&&k[b].firstRun&&k[b]&&(k[b].firstRun=!1),w.type){case"close":N(w.iframe);break;case"message":n=a(6),O(b,"onMessage passed: {iframe: "+w.iframe.id+", message: "+n+"}"),l("onMessage",{iframe:w.iframe,message:JSON.parse(n)}),O(b,"--");break;case"mouseenter":f("onMouseEnter");break;case"mouseleave":f("onMouseLeave");break;case"autoResize":k[b].autoResize=JSON.parse(a(9));break;case"scrollTo":d(!1);break;case"scrollToOffset":d(!0);break;case"pageInfo":s(k[b]&&k[b].iframe,b),r=b,e("Add ",F),k[r]&&(k[r].stopPageInfo=o);break;case"pageInfoStop":k[b]&&k[b].stopPageInfo&&(k[b].stopPageInfo(),delete k[b].stopPageInfo);break;case"inPageLink":u(a(9));break;case"reset":j(w);break;case"init":i(),l("onInit",w.iframe);break;default:0===Number(w.width)&&0===Number(w.height)?R("Unsupported message received ("+w.type+"), this is likely due to the iframe containing a later version of iframe-resizer than the parent page"):i()}function e(n,i){function t(){k[r]?s(k[r].iframe,r):o()}["scroll","resize"].forEach(function(e){O(r,n+e+" listener for sendPageInfo"),i(window,e,t)})}function o(){e("Remove ",B)}var r,n}var g,h,p=e.data,w={},b=null;if("[iFrameResizerChild]Ready"===p)for(var y in k)A("iFrame requested init",L(y),k[y].iframe,y);else x===(""+p).slice(0,I)&&p.slice(I).split(":")[0]in k?(w=n(),b=w.id,k[b]&&(k[b].loaded=!0),(h=w.type in{true:1,false:1,undefined:1})&&O(b,"Ignoring init message from meta parent page"),!h&&(h=!0,k[g=b]||(h=!1,R(w.type+" No settings for "+g+". Message was: "+p)),h)&&(O(b,"Received: "+p),g=!0,null===w.iframe&&(R(b,"IFrame ("+w.id+") not found"),g=!1),g&&o()&&m())):E(b,"Ignored: "+p)}function T(e,n,i){var t=null,o=null;if(k[e]){if("function"!=typeof(t=k[e][n]))throw new TypeError(n+" on iFrame["+e+"] is not a function");o=t(i)}return o}function g(e){e=e.id;delete k[e]}function N(e){var n=e.id;if(!1===T(n,"onClose",n))O(n,"Close iframe cancelled by onClose event");else{O(n,"Removing iFrame: "+n);try{e.parentNode&&e.parentNode.removeChild(e)}catch(e){R(e)}T(n,"onClosed",n),O(n,"--"),g(e)}}function W(e){null===M&&O(e,"Get page position: "+(M={x:window.pageXOffset===d?document.documentElement.scrollLeft:window.pageXOffset,y:window.pageYOffset===d?document.documentElement.scrollTop:window.pageYOffset}).x+","+M.y)}function C(e){null!==M&&(window.scrollTo(M.x,M.y),O(e,"Set page position: "+M.x+","+M.y),S())}function S(){M=null}function j(e){O(e.id,"Size reset requested by "+("init"===e.type?"host page":"iFrame")),W(e.id),P(function(){H(e),A("reset","reset",e.iframe,e.id)},e,"reset")}function H(o){function i(e){var n;function i(){Object.keys(k).forEach(function(e){function n(e){return"0px"===(k[i]&&k[i].iframe.style[e])}var i;k[i=e]&&null!==k[i].iframe.offsetParent&&(n("height")||n("width"))&&A("Visibility change","resize",k[i].iframe,i)})}function t(e){O("window","Mutation observed: "+e[0].target+" "+e[0].type),h(i,16)}!a&&"0"===o[e]&&(a=!0,O(r,"Hidden iFrame detected, creating visibility listener"),e=m())&&(n=document.querySelector("body"),new e(t).observe(n,{attributes:!0,attributeOldValue:!1,characterData:!0,characterDataOldValue:!1,childList:!0,subtree:!0}))}function e(e){var n;n=e,o.id?(o.iframe.style[n]=o[n]+"px",O(o.id,"IFrame ("+r+") "+n+" set to "+o[n]+"px")):O("undefined","messageData id not set"),i(e)}var r=o.iframe.id;k[r]&&(k[r].sizeHeight&&e("height"),k[r].sizeWidth)&&e("width")}function P(e,n,i){i!==n.type&&r&&!window.jasmine?(O(n.id,"Requesting animation frame"),r(e)):e()}function A(n,i,t,o,e){function r(){var e;t&&"contentWindow"in t&&null!==t.contentWindow?(e=k[o]&&k[o].targetOrigin,O(o,"["+n+"] Sending msg to iframe["+o+"] ("+i+") targetOrigin: "+e),t.contentWindow.postMessage(x+i,e)):R(o,"["+n+"] IFrame("+o+") not found")}function a(){e&&k[o]&&k[o].warningTimeout&&(k[o].msgTimeout=setTimeout(function(){!k[o]||k[o].loaded||s||(s=!0,R(o,"IFrame has not responded within "+k[o].warningTimeout/1e3+" seconds. Check iFrameResizer.contentWindow.js has been loaded in iFrame. This message can be ignored if everything is working, or you can set the warningTimeout option to a higher value or zero to suppress this warning."))},k[o].warningTimeout))}var s=!1;o=o||t.id,k[o]&&(r(),a())}function L(e){return e+":"+k[e].bodyMarginV1+":"+k[e].sizeWidth+":"+k[e].log+":"+k[e].interval+":"+k[e].enablePublicMethods+":"+k[e].autoResize+":"+k[e].bodyMargin+":"+k[e].heightCalculationMethod+":"+k[e].bodyBackground+":"+k[e].bodyPadding+":"+k[e].tolerance+":"+k[e].inPageLinks+":"+k[e].resizeFrom+":"+k[e].widthCalculationMethod+":"+k[e].mouseEvents}function s(t,i){function e(i){var e=m();e&&(e=e,t.parentNode)&&new e(function(e){e.forEach(function(e){Array.prototype.slice.call(e.removedNodes).forEach(function(e){e===t&&N(t)})})}).observe(t.parentNode,{childList:!0}),F(t,"load",function(){var e,n;A("iFrame.onload",i,t,d,!0),e=k[r]&&k[r].firstRun,n=k[r]&&k[r].heightCalculationMethod in f,!e&&n&&j({iframe:t,height:0,width:0,type:"init"})}),A("init",i,t,d,!0)}function o(e){var n=e.split("Callback");2===n.length&&(this[n="on"+n[0].charAt(0).toUpperCase()+n[0].slice(1)]=this[e],delete this[e],R(r,"Deprecated: '"+e+"' has been renamed '"+n+"'. The old method will be removed in the next major version."))}function n(e){if(e=e||{},k[r]=Object.create(null),k[r].iframe=t,k[r].firstRun=!0,k[r].remoteHost=t.src&&t.src.split("/").slice(0,3).join("/"),"object"!=typeof e)throw new TypeError("Options is not an object");Object.keys(e).forEach(o,e);var n,i=e;for(n in l)Object.prototype.hasOwnProperty.call(l,n)&&(k[r][n]=(Object.prototype.hasOwnProperty.call(i,n)?i:l)[n]);k[r]&&(k[r].targetOrigin=!0!==k[r].checkOrigin||""===(e=k[r].remoteHost)||null!==e.match(/^(about:blank|javascript:|file:\/\/)/)?"*":e)}var r=function(e){if("string"!=typeof e)throw new TypeError("Invaild id for iFrame. Expected String");var n;return""===e&&(t.id=(n=i&&i.id||l.id+c++,null!==document.getElementById(n)&&(n+=c++),e=n),u=(i||{}).log,O(e,"Added missing iframe ID: "+e+" ("+t.src+")")),e}(t.id);if(r in k&&"iFrameResizer"in t)R(r,"Ignored iFrame, already setup.");else{switch(n(i),O(r,"IFrame scrolling "+(k[r]&&k[r].scrolling?"enabled":"disabled")+" for "+r),t.style.overflow=!1===(k[r]&&k[r].scrolling)?"hidden":"auto",k[r]&&k[r].scrolling){case"omit":break;case!0:t.scrolling="yes";break;case!1:t.scrolling="no";break;default:t.scrolling=k[r]?k[r].scrolling:"no"}s("Height"),s("Width"),a("maxHeight"),a("minHeight"),a("maxWidth"),a("minWidth"),"number"!=typeof(k[r]&&k[r].bodyMargin)&&"0"!==(k[r]&&k[r].bodyMargin)||(k[r].bodyMarginV1=k[r].bodyMargin,k[r].bodyMargin=k[r].bodyMargin+"px"),e(L(r)),k[r]&&(k[r].iframe.iFrameResizer={close:N.bind(null,k[r].iframe),removeListeners:g.bind(null,k[r].iframe),resize:A.bind(null,"Window resize","resize",k[r].iframe),moveToAnchor:function(e){A("Move to anchor","moveToAnchor:"+e,k[r].iframe,r)},sendMessage:function(e){A("Send Message","message:"+(e=JSON.stringify(e)),k[r].iframe,r)}})}function a(e){var n=k[r][e];1/0!==n&&0!==n&&(t.style[e]="number"==typeof n?n+"px":n,O(r,"Set "+e+" = "+t.style[e]))}function s(e){if(k[r]["min"+e]>k[r]["max"+e])throw new Error("Value for min"+e+" can not be greater than max"+e)}}function h(e,n){null===i&&(i=setTimeout(function(){i=null,e()},n))}function e(){"hidden"!==document.visibilityState&&(O("document","Trigger event: Visibility change"),h(function(){b("Tab Visible","resize")},16))}function b(i,t){Object.keys(k).forEach(function(e){var n;k[n=e]&&"parent"===k[n].resizeFrom&&k[n].autoResize&&!k[n].firstRun&&A(i,t,k[e].iframe,e)})}function y(){F(window,"message",w),F(window,"resize",function(){var e;O("window","Trigger event: "+(e="resize")),h(function(){b("Window "+e,"resize")},16)}),F(document,"visibilitychange",e),F(document,"-webkit-visibilitychange",e)}function n(){function t(e,n){if(n){if(!n.tagName)throw new TypeError("Object is not a valid DOM element");if("IFRAME"!==n.tagName.toUpperCase())throw new TypeError("Expected <IFRAME> tag, found <"+n.tagName+">");s(n,e),o.push(n)}}for(var o,e=["moz","webkit","o","ms"],n=0;n<e.length&&!r;n+=1)r=window[e[n]+"RequestAnimationFrame"];return r?r=r.bind(window):O("setup","RequestAnimationFrame not supported"),y(),function(e,n){var i;switch(o=[],(i=e)&&i.enablePublicMethods&&R("enablePublicMethods option has been removed, public methods are now always available in the iFrame"),typeof n){case"undefined":case"string":Array.prototype.forEach.call(document.querySelectorAll(n||"iframe"),t.bind(d,e));break;case"object":t(e,n);break;default:throw new TypeError("Unexpected data type ("+typeof n+")")}return o}}function q(e){e.fn?e.fn.iFrameResize||(e.fn.iFrameResize=function(i){return this.filter("iframe").each(function(e,n){s(n,i)}).end()}):E("","Unable to bind to jQuery, it is not fully loaded.")}"undefined"!=typeof window&&(c=0,a=u=!1,v="message".length,I=(x="[iFrameSizer]").length,M=null,r=window.requestAnimationFrame,f=Object.freeze({max:1,scroll:1,bodyScroll:1,documentElementScroll:1}),k={},i=null,l=Object.freeze({autoResize:!0,bodyBackground:null,bodyMargin:null,bodyMarginV1:8,bodyPadding:null,checkOrigin:!0,inPageLinks:!1,enablePublicMethods:!0,heightCalculationMethod:"bodyOffset",id:"iFrameResizer",interval:32,log:!1,maxHeight:1/0,maxWidth:1/0,minHeight:0,minWidth:0,mouseEvents:!0,resizeFrom:"parent",scrolling:!1,sizeHeight:!0,sizeWidth:!1,warningTimeout:5e3,tolerance:0,widthCalculationMethod:"scroll",onClose:function(){return!0},onClosed:function(){},onInit:function(){},onMessage:function(){R("onMessage function not defined")},onMouseEnter:function(){},onMouseLeave:function(){},onResized:function(){},onScroll:function(){return!0}}),z={},window.jQuery!==d&&q(window.jQuery),"function"==typeof define&&define.amd?define([],n):"object"==typeof module&&"object"==typeof module.exports&&(module.exports=n()),window.iFrameResize=window.iFrameResize||n())}();
-//# sourceMappingURL=iframeResizer.map
diff --git a/cosmos/plugin/templates/dbt_docs.html b/cosmos/plugin/templates/dbt_docs.html
index fe3f794b..58f0dfa3 100644
--- a/cosmos/plugin/templates/dbt_docs.html
+++ b/cosmos/plugin/templates/dbt_docs.html
@@ -1,35 +1,46 @@
 {% extends base_template %}
 {% block page_title %}dbt docs - {{ appbuilder.app_name }}{% endblock %}
 {% block content %}
-    <script src="{{ url_for('.static', filename='iframeResizer.min.js') }}"></script>
-    <iframe id="dbtIframe" src="{{ url_for('.dbt_docs_index') }}" style="min-width: 100%; border: 0;"></iframe>
-    <script>
-        iFrameResize(
-            {
-                heightCalculationMethod: 'custom',
-                resizeFrom: 'child',
-                minHeight: 500
-            },
-            '#dbtIframe'
-        );
+<style>
+    :root {
+        --height-offset: 140px;
+        /*
+            Airflow uses a padding of 70px for the header and 70px
+            for the footer in versions 2.X
+            [Ref: main.css](https://github.com/apache/airflow/blob/c083e456fa02c6cb32cdbe0c9ed3c3b2380beccd/airflow/www/static/css/main.css#L22)
 
-        window.addEventListener('message', function (event) {
-            let msgData = event.data;
-            if (msgData.startsWith('#!')) {
-                let updateUrl = new URL(window.location);
-                updateUrl.hash = msgData;
-                history.replaceState(null, null, updateUrl);
-            }
-        });
+            We use this value to offset the height of the iframe. These values are fixed in the body css.
 
-        window.addEventListener('popstate', function () {
-            dbtIframe.contentWindow.postMessage(window.location.hash);
-        });
+        */
+    }
 
-        let dbtIframe = document.getElementById('dbtIframe');
-        let iframeUrl = new URL(dbtIframe.src);
-        iframeUrl.hash = window.location.hash;
-        dbtIframe.src = iframeUrl.href;
+    #dbtIframe {
+        min-width: 100%;
+        border: 0;
+        height: calc(100vh - var(--height-offset));
+    }
+</style>
+<iframe id="dbtIframe" src="{{ url_for('.dbt_docs_index') }}"
+    style="min-width: 100%; border: 0; height: calc(100vh - 140px);"></iframe>
+<script>
 
-    </script>
+    window.addEventListener('message', function (event) {
+        let msgData = event.data;
+        if (msgData.startsWith('#!')) {
+            let updateUrl = new URL(window.location);
+            updateUrl.hash = msgData;
+            history.replaceState(null, null, updateUrl);
+        }
+    });
+
+    window.addEventListener('popstate', function () {
+        dbtIframe.contentWindow.postMessage(window.location.hash);
+    });
+
+    let dbtIframe = document.getElementById('dbtIframe');
+    let iframeUrl = new URL(dbtIframe.src);
+    iframeUrl.hash = window.location.hash;
+    dbtIframe.src = iframeUrl.href;
+
+</script>
 {% endblock %}
diff --git a/docs/configuration/render-config.rst b/docs/configuration/render-config.rst
index 7c860323..7c50afe3 100644
--- a/docs/configuration/render-config.rst
+++ b/docs/configuration/render-config.rst
@@ -22,8 +22,9 @@ The ``RenderConfig`` class takes the following arguments:
 - ``dbt_project_path``: Configures the DBT project location accessible on their airflow controller for DAG rendering - Required when using ``load_method=LoadMode.DBT_LS`` or ``load_method=LoadMode.CUSTOM``
 - ``airflow_vars_to_purge_cache``: (new in v1.5) Specify Airflow variables that will affect the ``LoadMode.DBT_LS`` cache. See `Caching <./caching.html>`_ for more information.
 - ``source_rendering_behavior``: Determines how source nodes are rendered when using cosmos default source node rendering (ALL, NONE, WITH_TESTS_OR_FRESHNESS). Defaults to "NONE" (since Cosmos 1.6). See `Source Nodes Rendering <./source-nodes-rendering.html>`_ for more information.
-- ``normalize_task_id``: A callable that takes a dbt node as input and returns the task ID. This function allows users to set a custom task_id independently of the model name, which can be specified as the task’s display_name. This way, task_id can be modified using a user-defined function, while the model name remains as the task’s display name. The display_name parameter is available in Airflow 2.9 and above. See `Task display name <./task-display-name.html>`_ for more information.
+- ``normalize_task_id``: A callable that takes a dbt node as input and returns the task ID. This function allows users to set a custom task_id independently of the model name, which can be specified as the task's display_name. This way, task_id can be modified using a user-defined function, while the model name remains as the task's display name. The display_name parameter is available in Airflow 2.9 and above. See `Task display name <./task-display-name.html>`_ for more information.
 - ``should_detach_multiple_parents_tests``: A boolean to control if tests that depend on multiple parents should be run as standalone tasks. See `Parsing Methods <testing-behavior.html>`_ for more information.
+- ``enable_owner_inheritance``: A boolean to control if dbt owners should be imported as part of the airflow DAG owners. Defaults to True.
 
 
 How to run dbt ls (invocation mode)


Test patch:
diff --git a/tests/airflow/test_graph.py b/tests/airflow/test_graph.py
index 1cc24189..74f6723f 100644
--- a/tests/airflow/test_graph.py
+++ b/tests/airflow/test_graph.py
@@ -980,6 +980,7 @@ def test_owner(dbt_extra_config, expected_owner):
         test_behavior=TestBehavior.AFTER_EACH,
         on_warning_callback=None,
         source_rendering_behavior=SOURCE_RENDERING_BEHAVIOR,
+        enable_owner_inheritance=True,
     )
 
     assert len(output.leaves) == 1
@@ -1037,3 +1038,269 @@ def test_add_teardown_task_raises_error_without_async_py_requirements():
 
     with pytest.raises(CosmosValueError, match="ExecutionConfig.AIRFLOW_ASYNC needs async_py_requirements to be set"):
         _add_teardown_task(sample_dag, ExecutionMode.AIRFLOW_ASYNC, task_args, sample_tasks_map, None, None)
+
+
+@pytest.mark.parametrize(
+    "enable_owner_inheritance,node_owner,expected_owner",
+    [
+        (True, "dbt-owner", "dbt-owner"),  # Default behavior - inherit owner
+        (False, "dbt-owner", ""),  # Disable inheritance - empty string
+        (True, "", ""),  # No owner to inherit - empty string
+        (False, "", ""),  # No owner to inherit, disable inheritance - empty string
+    ],
+)
+def test_create_task_metadata_disable_owner_inheritance(enable_owner_inheritance, node_owner, expected_owner):
+    """Test that enable_owner_inheritance parameter works correctly in create_task_metadata."""
+    node = DbtNode(
+        unique_id=f"{DbtResourceType.MODEL.value}.my_folder.my_model",
+        resource_type=DbtResourceType.MODEL,
+        file_path=SAMPLE_PROJ_PATH / "gen2/models/parent.sql",
+        tags=["has_child"],
+        config={"materialized": "view", "meta": {"owner": node_owner}},
+        depends_on=[],
+    )
+
+    task_metadata = create_task_metadata(
+        node=node,
+        execution_mode=ExecutionMode.LOCAL,
+        args={"project_dir": SAMPLE_PROJ_PATH},
+        dbt_dag_task_group_identifier="test_dag",
+        enable_owner_inheritance=enable_owner_inheritance,
+    )
+
+    assert task_metadata is not None
+    assert task_metadata.owner == expected_owner
+
+
+@pytest.mark.parametrize(
+    "enable_owner_inheritance,node_owner,expected_owner",
+    [
+        (True, "dbt-owner", "dbt-owner"),  # Default behavior - inherit owner
+        (False, "dbt-owner", ""),  # Disable inheritance - empty string
+        (True, "", ""),  # No owner to inherit - empty string
+        (False, "", ""),  # No owner to inherit, disable inheritance - empty string
+    ],
+)
+def test_create_test_task_metadata_disable_owner_inheritance(enable_owner_inheritance, node_owner, expected_owner):
+    """Test that enable_owner_inheritance parameter works correctly in create_test_task_metadata."""
+    node = DbtNode(
+        unique_id=f"{DbtResourceType.MODEL.value}.my_folder.my_model",
+        resource_type=DbtResourceType.MODEL,
+        file_path=SAMPLE_PROJ_PATH / "gen2/models/parent.sql",
+        tags=["has_child"],
+        config={"materialized": "view", "meta": {"owner": node_owner}},
+        depends_on=[],
+    )
+
+    test_metadata = create_test_task_metadata(
+        test_task_name="test_my_model",
+        execution_mode=ExecutionMode.LOCAL,
+        test_indirect_selection=TestIndirectSelection.EAGER,
+        task_args={"project_dir": SAMPLE_PROJ_PATH},
+        node=node,
+        enable_owner_inheritance=enable_owner_inheritance,
+    )
+
+    assert test_metadata.owner == expected_owner
+
+
+def test_create_test_task_metadata_disable_owner_inheritance_without_node():
+    """Test that enable_owner_inheritance has no effect when node is None."""
+    test_metadata = create_test_task_metadata(
+        test_task_name="test_all",
+        execution_mode=ExecutionMode.LOCAL,
+        test_indirect_selection=TestIndirectSelection.EAGER,
+        task_args={"project_dir": SAMPLE_PROJ_PATH},
+        node=None,
+        enable_owner_inheritance=False,
+    )
+
+    assert test_metadata.owner == ""
+
+
+@pytest.mark.parametrize(
+    "enable_owner_inheritance,node_owner,expected_owner",
+    [
+        (True, "dbt-owner", "dbt-owner"),  # Default behavior - inherit owner
+        (False, "dbt-owner", DEFAULT_OWNER),  # Disable inheritance - use default owner
+        (True, "", DEFAULT_OWNER),  # No owner to inherit - use default owner
+        (False, "", DEFAULT_OWNER),  # No owner to inherit, disable inheritance - use default owner
+    ],
+)
+def test_generate_task_or_group_disable_owner_inheritance(enable_owner_inheritance, node_owner, expected_owner):
+    """Test that enable_owner_inheritance parameter works correctly in generate_task_or_group."""
+    with DAG("test-disable-owner-inheritance", start_date=datetime(2022, 1, 1)) as dag:
+        node = DbtNode(
+            unique_id=f"{DbtResourceType.MODEL.value}.my_folder.my_model",
+            resource_type=DbtResourceType.MODEL,
+            file_path=SAMPLE_PROJ_PATH / "gen2/models/parent.sql",
+            tags=["has_child"],
+            config={"materialized": "view", "meta": {"owner": node_owner}},
+            depends_on=[],
+        )
+
+        task_or_group = generate_task_or_group(
+            dag=dag,
+            task_group=None,
+            node=node,
+            execution_mode=ExecutionMode.LOCAL,
+            task_args={
+                "project_dir": SAMPLE_PROJ_PATH,
+                "profile_config": ProfileConfig(
+                    profile_name="default",
+                    target_name="default",
+                    profile_mapping=PostgresUserPasswordProfileMapping(
+                        conn_id="fake_conn",
+                        profile_args={"schema": "public"},
+                    ),
+                ),
+            },
+            test_behavior=TestBehavior.NONE,
+            source_rendering_behavior=SOURCE_RENDERING_BEHAVIOR,
+            test_indirect_selection=TestIndirectSelection.EAGER,
+            on_warning_callback=None,
+            enable_owner_inheritance=enable_owner_inheritance,
+        )
+
+        assert task_or_group is not None
+        assert task_or_group.owner == expected_owner
+
+
+@pytest.mark.parametrize(
+    "test_behavior,enable_owner_inheritance",
+    [
+        (TestBehavior.AFTER_EACH, True),
+        (TestBehavior.AFTER_EACH, False),
+        (TestBehavior.AFTER_ALL, True),
+        (TestBehavior.AFTER_ALL, False),
+        (TestBehavior.BUILD, True),
+        (TestBehavior.BUILD, False),
+    ],
+)
+def test_build_airflow_graph_disable_owner_inheritance(test_behavior, enable_owner_inheritance):
+    """Test that enable_owner_inheritance parameter works correctly in build_airflow_graph."""
+    with DAG("test-disable-owner-inheritance-graph", start_date=datetime(2022, 1, 1)) as dag:
+        node_with_owner = DbtNode(
+            unique_id=f"{DbtResourceType.MODEL.value}.my_folder.model_with_owner",
+            resource_type=DbtResourceType.MODEL,
+            file_path=SAMPLE_PROJ_PATH / "gen2/models/parent.sql",
+            tags=["has_child"],
+            config={"materialized": "view", "meta": {"owner": "test-owner"}},
+            depends_on=[],
+            has_test=True,
+        )
+
+        nodes = {node_with_owner.unique_id: node_with_owner}
+
+        task_args = {
+            "project_dir": SAMPLE_PROJ_PATH,
+            "conn_id": "fake_conn",
+            "profile_config": ProfileConfig(
+                profile_name="default",
+                target_name="default",
+                profile_mapping=PostgresUserPasswordProfileMapping(
+                    conn_id="fake_conn",
+                    profile_args={"schema": "public"},
+                ),
+            ),
+        }
+
+        tasks_map = build_airflow_graph(
+            nodes=nodes,
+            dag=dag,
+            execution_mode=ExecutionMode.LOCAL,
+            test_indirect_selection=TestIndirectSelection.EAGER,
+            task_args=task_args,
+            render_config=RenderConfig(
+                test_behavior=test_behavior,
+                source_rendering_behavior=SOURCE_RENDERING_BEHAVIOR,
+                enable_owner_inheritance=enable_owner_inheritance,
+            ),
+            dbt_project_name="test_project",
+        )
+
+        # Check the main model task
+        model_task = tasks_map[node_with_owner.unique_id]
+        if test_behavior == TestBehavior.AFTER_EACH:
+            assert isinstance(model_task, TaskGroup)
+
+            run_task = model_task.children["model_with_owner.run"]
+            expected_owner = DEFAULT_OWNER if not enable_owner_inheritance else "test-owner"
+            assert run_task.owner == expected_owner
+
+            test_task = model_task.children["model_with_owner.test"]
+            assert test_task.owner == expected_owner
+        else:
+            expected_owner = DEFAULT_OWNER if not enable_owner_inheritance else "test-owner"
+            assert model_task.owner == expected_owner
+
+        if test_behavior == TestBehavior.AFTER_ALL:
+            test_tasks = [task for task in dag.tasks if task.task_id.endswith("_test")]
+            assert len(test_tasks) == 1
+            test_task = test_tasks[0]
+            assert test_task.owner == DEFAULT_OWNER
+
+
+def test_build_airflow_graph_disable_owner_inheritance_with_detached_tests():
+    """Test that enable_owner_inheritance works correctly with detached test nodes."""
+    with DAG("test-disable-owner-inheritance-detached", start_date=datetime(2022, 1, 1)) as dag:
+        parent_node1 = DbtNode(
+            unique_id=f"{DbtResourceType.MODEL.value}.my_folder.parent1",
+            resource_type=DbtResourceType.MODEL,
+            file_path=SAMPLE_PROJ_PATH / "gen2/models/parent1.sql",
+            config={"materialized": "view", "meta": {"owner": "parent1-owner"}},
+            depends_on=[],
+        )
+
+        parent_node2 = DbtNode(
+            unique_id=f"{DbtResourceType.MODEL.value}.my_folder.parent2",
+            resource_type=DbtResourceType.MODEL,
+            file_path=SAMPLE_PROJ_PATH / "gen2/models/parent2.sql",
+            config={"materialized": "view", "meta": {"owner": "parent2-owner"}},
+            depends_on=[],
+        )
+
+        test_node = DbtNode(
+            unique_id=f"{DbtResourceType.TEST.value}.my_folder.test_both_parents",
+            resource_type=DbtResourceType.TEST,
+            file_path=SAMPLE_PROJ_PATH / "gen2/tests/test_both_parents.sql",
+            config={"meta": {"owner": "test-owner"}},
+            depends_on=[parent_node1.unique_id, parent_node2.unique_id],
+        )
+
+        nodes = {
+            parent_node1.unique_id: parent_node1,
+            parent_node2.unique_id: parent_node2,
+            test_node.unique_id: test_node,
+        }
+
+        task_args = {
+            "project_dir": SAMPLE_PROJ_PATH,
+            "conn_id": "fake_conn",
+            "profile_config": ProfileConfig(
+                profile_name="default",
+                target_name="default",
+                profile_mapping=PostgresUserPasswordProfileMapping(
+                    conn_id="fake_conn",
+                    profile_args={"schema": "public"},
+                ),
+            ),
+        }
+
+        tasks_map = build_airflow_graph(
+            nodes=nodes,
+            dag=dag,
+            execution_mode=ExecutionMode.LOCAL,
+            test_indirect_selection=TestIndirectSelection.EAGER,
+            task_args=task_args,
+            render_config=RenderConfig(
+                test_behavior=TestBehavior.BUILD,
+                source_rendering_behavior=SOURCE_RENDERING_BEHAVIOR,
+                should_detach_multiple_parents_tests=True,
+                enable_owner_inheritance=False,
+            ),
+            dbt_project_name="test_project",
+        )
+
+        for task_id, task in tasks_map.items():
+            assert task.owner == DEFAULT_OWNER, f"Task {task_id} should have default owner when inheritance is disabled"