2

I need to add a custom button in Swagger UI of my FastAPI application. I found this answer which suggest a good solution to add custom javascript to Swagger UI along with this documentations from FastAPI. But this solution only works for adding custom javascript code. I tried to add some HTML code for adding a new button to it using the swagger UI Authorise button style:

custom_html = '<div class="scheme-containerr"><section class="schemes wrapper block col-12"><div class="auth-wrapper"><button class="btn authorize"><span>Authorize Google</span><svg width="20" height="20"><use href="#unlocked" xlink:href="#unlocked"></use></svg></button></div></section></div>'

@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
    return get_swagger_ui_html(
        openapi_url=app.openapi_url,
        title=app.title + " - Swagger UI",
        oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
        swagger_js_url="/static/swagger-ui-bundle.js",
        swagger_css_url="/static/swagger-ui.css",
        custom_js_url=google_custom_button,
        custom_html=custom_html,
    )

def get_swagger_ui_html(
        *,
        ...
        custom_html: Optional[str] = None,
) -> HTMLResponse:

    ...

    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
    <link type="text/css" rel="stylesheet" href="{swagger_css_url}">
    <link rel="shortcut icon" href="{swagger_favicon_url}">
    <title>{title}</title>
    </head>
    <body>
    <div id="swagger-ui">
    {custom_html if custom_html else ""}  # <-- I added the HTML code here
    </div>
    """
    ....

But looks like whatever I put between <div id="swagger-ui"></div> gets overwritten somehow and won't make it in the Swagger UI.

How to add custom HTML (in this case, buttons like Swagger's Authorise button) for specific needs in Swagger UI using FastAPI?

Update

If I add the custom HTML outside of the <div id="swagger-ui"></div> I can see my custom button in Swagger UI like this:

enter image description here

But I would like to add my button where the original Authorise button is.

Chris
  • 18,724
  • 6
  • 46
  • 80
Ghasem
  • 14,455
  • 21
  • 138
  • 171
  • I think the original answer would work for you if you replace the click-handler on the `Authorize` button. – Maximilian Burszley Jun 05 '23 at 13:10
  • @MaximilianBurszley But I need multiple buttons – Ghasem Jun 05 '23 at 13:24
  • I believe FastAPI packages HTML that makes up the SwaggerUI. You could download that same file and make your changes to serve your custom version instead. Here is [the relevant documentation](https://fastapi.tiangolo.com/advanced/extending-openapi/#self-hosting-javascript-and-css-for-docs). – Maximilian Burszley Jun 05 '23 at 13:29
  • @MaximilianBurszley Thanks, but that document only mentions the `js` and `css` files – Ghasem Jun 05 '23 at 13:36
  • Yes, that is why I mentioned "packages HTML". It looks like the actual HTML is an implementation detail within FastAPI, but you could grab that HTML using `get_swagger_ui_html()` and then modify _that_ with the elements you need via XPath or other mechanisms. – Maximilian Burszley Jun 05 '23 at 13:39
  • What is the "Authorize Google" button in your example supposed to do? If the idea is to authorize API endpoints using Google OAuth or Google OpenID Connect, you might be able to achieve this behavior using OpenAPI's standard [security scheme syntax](https://swagger.io/docs/specification/authentication/) (or whatever the FastAPI equivalent is). – Helen Jun 05 '23 at 14:54
  • For arbitrary customizations, you'll probably need to either [write a Swagger UI plugin](https://stackoverflow.com/a/52815260/113116), or fork and modify Swagger UI code for your own use. In either case, you'll need to configure FastAPI to use your custom Swagger UI version instead of the bundled version. – Helen Jun 05 '23 at 14:57
  • @Helen Thanks for the tip. The problem with the standard security flow is you can only use one oidc provider because swagger [supports only one `client_id`](https://github.com/swagger-api/swagger-ui/issues/4690). but anyway that's another discussion – Ghasem Jun 05 '23 at 15:17

1 Answers1

0

You could modify FastAPI's get_swagger_ui_html() function, in order to inject some custom JavaScript code, as described by @lunaa here, and create the custom HTML button programmatically through the custom_script.js. However, since the Authorize button element is created after the DOM/Window is loaded—and there doesn't seem to be a native way to run your JS code after is defined, even if you use Window.load event to run the JavaScript code—and you need to add your custom button next to it, you could simply wait for that element to be created, using the approach described here, and then create the custom button and add it to the DOM.

Complete Working Example

app.py

from fastapi import FastAPI
from fastapi import Depends
from fastapi.security import OpenIdConnect
from fastapi.staticfiles import StaticFiles
from fastapi.openapi.docs import (
    get_redoc_html,
    get_swagger_ui_oauth2_redirect_html,
)
from custom_swagger import get_swagger_ui_html


app = FastAPI(docs_url=None) 
app.mount("/static", StaticFiles(directory="static"), name="static")
oidc_google = OpenIdConnect(openIdConnectUrl='https://accounts.google.com/.well-known/openid-configuration')


@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
    return get_swagger_ui_html(
        openapi_url=app.openapi_url,
        title="My API",
        oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
        #swagger_js_url="/static/swagger-ui-bundle.js",  # Optional
        #swagger_css_url="/static/swagger-ui.css",  # Optional
        #swagger_favicon_url="/static/favicon-32x32.png",  # Optional
        custom_js_url="/static/custom_script.js",
    )


@app.get('/')
def main(token: str = Depends(oidc_google)):
    return "You are Authenticated"


custom_swagger.py

import json
from typing import Any, Dict, Optional
from fastapi.encoders import jsonable_encoder
from fastapi.openapi.docs import swagger_ui_default_parameters
from starlette.responses import HTMLResponse

def get_swagger_ui_html(
    *,
    openapi_url: str,
    title: str,
    swagger_js_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui-bundle.js",
    swagger_css_url: str = "https://cdn.jsdelivr.net/npm/swagger-ui-dist@4/swagger-ui.css",
    swagger_favicon_url: str = "https://fastapi.tiangolo.com/img/favicon.png",
    oauth2_redirect_url: Optional[str] = None,
    init_oauth: Optional[Dict[str, Any]] = None,
    swagger_ui_parameters: Optional[Dict[str, Any]] = None,
    custom_js_url: Optional[str] = None,
) -> HTMLResponse:
    current_swagger_ui_parameters = swagger_ui_default_parameters.copy()
    if swagger_ui_parameters:
        current_swagger_ui_parameters.update(swagger_ui_parameters)

    html = f"""
    <!DOCTYPE html>
    <html>
    <head>
    <link type="text/css" rel="stylesheet" href="{swagger_css_url}">
    <link rel="shortcut icon" href="{swagger_favicon_url}">
    <title>{title}</title>
    </head>
    <body>
    <div id="swagger-ui">
    </div>
    """
    
    if custom_js_url:
        html += f"""
        <script src="{custom_js_url}"></script>
        """

    html += f"""
    <script src="{swagger_js_url}"></script>
    <!-- `SwaggerUIBundle` is now available on the page -->
    <script>
    const ui = SwaggerUIBundle({{
        url: '{openapi_url}',
    """

    for key, value in current_swagger_ui_parameters.items():
        html += f"{json.dumps(key)}: {json.dumps(jsonable_encoder(value))},\n"

    if oauth2_redirect_url:
        html += f"oauth2RedirectUrl: window.location.origin + '{oauth2_redirect_url}',"

    html += """
    presets: [
        SwaggerUIBundle.presets.apis,
        SwaggerUIBundle.SwaggerUIStandalonePreset
        ],
    })"""

    if init_oauth:
        html += f"""
        ui.initOAuth({json.dumps(jsonable_encoder(init_oauth))})
        """

    html += """
    </script>
    </body>
    </html>
    """
    return HTMLResponse(html)


static/custom_script.js

function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

waitForElm('.auth-wrapper').then((elm) => {
    var authWrapper = document.getElementsByClassName("auth-wrapper")[0];
    var btn = document.createElement("BUTTON");
    btn.innerHTML = "Click me";
    btn.id = "btn-id";
    btn.onclick = function() {
        alert("button is clicked");
    };
    authWrapper.append(btn);
});


Instead of programmatically creating the button through JavaScript, you could load an external HTML file (using JavaScript), which would contain the HTML code for the button and any other elements you would possibly like to insert. Example below:

static/custom_script.js

function waitForElm(selector) {
   // same as in the previous code snippet
}

waitForElm('.auth-wrapper').then((elm) => {
   var authWrapper = document.getElementsByClassName("auth-wrapper")[0];
   fetch('/static/button.html')
      .then(response => response.text())
      .then(text => {
         const newDiv = document.createElement("div");
         newDiv.innerHTML = text;
         authWrapper.append(newDiv);
      });
});

static/button.html

<button onclick="alert('button is clicked');" class="btn authorize unlocked Google">
   <span>Authorize Google</span>
   <svg width="20" height="20">
      <use href="#unlocked" xlink:href="#unlocked"></use>
   </svg>
</button>

Adding Dynamic Custom Content

In case you would like to add some dynamic content, instead of static JS/HTML file content, you could either pass the content directly as a string to the get_swagger_ui_html() function, or use a combination of static content with dynamic variables, which could be added using Jinja2 templates. Example is given below, demonstrating the changes to be made to the code provided earlier—rest of the code should remain the same as above. The dynamic variable in the exmaple below is msg.

Example

app.py

# ...
from jinja2 import Environment, FileSystemLoader

def get_template():
    env = Environment(loader=FileSystemLoader('./static'))
    template = env.get_template('custom_script.js')
    context = {'msg': 'button is clicked!'}
    html = template.render(context)
    return html

@app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html():
    return get_swagger_ui_html(
        openapi_url=app.openapi_url,
        title="My API",
        oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
        custom_js_content=get_template()
    )

custom_swagger.py

def get_swagger_ui_html(
    *,
    # ...
    custom_js_content: Optional[str] = None,
) -> HTMLResponse:
    # ...
    
    if custom_js_content:
        html += f"""
        <script>{custom_js_content}</script>
        """
    # ...

static/custom_script.js

function waitForElm(selector) {
   // ...
}

waitForElm('.auth-wrapper').then((elm) => {
    var authWrapper = document.getElementsByClassName("auth-wrapper")[0];
    var btn = document.createElement("BUTTON");
    btn.innerHTML = `
       <span>Authorize Google</span>
       <svg width="20" height="20">
          <use href="#unlocked" xlink:href="#unlocked"></use>
       </svg>
   `;
    btn.className = "btn authorize unlocked Google";
    btn.onclick = function() {
        alert("{{msg}}");
    };
    authWrapper.append(btn);
});

or

static/custom_script.js

function waitForElm(selector) {
   // ...
}

waitForElm('.auth-wrapper').then((elm) => {
    var authWrapper = document.getElementsByClassName("auth-wrapper")[0];
    var html = `
    <button onclick="alert('{{msg}}');" class="btn authorize unlocked Google">
       <span>Authorize Google</span>
       <svg width="20" height="20">
          <use href="#unlocked" xlink:href="#unlocked"></use>
       </svg>
    </button>
    `;
    var newDiv = document.createElement("div");
    newDiv.innerHTML = html;
    authWrapper.append(newDiv);
});
Chris
  • 18,724
  • 6
  • 46
  • 80
  • I gave it a try, it's working but needs a little change, because `.auth-wrapper` won't exists if you don't add any built-in authorise button from Swagger UI. So I had to find the div with `.information-container` class and insert a div with `.scheme-container` class after it. – Ghasem Jun 15 '23 at 13:20