Views: 614
Number of votes: 3
Average rating:

Integrating Swagger UI on Episerver World with customized components

Documentation section stays for a long time on World, and a new Swagger UI panel has been implemented (available soon), for better usage a long with old Swagger UI pages:

Swagger.IO provided developer with source code and detailed documentation on setting up developing environment, creating custom plugins and Layout. Therefore, investigating and modifying components is fun.

to set up developing environment, simply open powershell/command prompts at parent directory and run command npm run dev, npm will auto rebuilt after each change on source. I personally use Visual studio code for development.

Swagger UI provided with documentation here to support user uses custom layout but material on deeper interaction with the components seems missing. As a newbie to React and Swagger, I tried several methods including duplicate components from core and import it directly to the layout.jsx file within the Standalone layout folder. It seemed to work with several components when some doesn't, as the component has it own initial setup behind the scene. To setup component with initialization we must import those components in layout.jsx and pass it as parameter in return section, which is obstructive.

For secondary approach, I came up with an idea of creating a custom preset called WorldPreset and imported to apis.js

import BasePreset from "./base"
import WorldPreset from "./world"
import OAS3Plugin from "../plugins/oas3"

// Just the base, for now.

export default function PresetApis() {

  return [
    BasePreset,
    WorldPreset,
    OAS3Plugin
  ]
}

Overriding components are declared in world.js file:

import Parameters from "../epiworld/parameters"
import ParameterRow from "../epiworld/parameter-row"
import Responses from "../epiworld/responses"
import Response from "../epiworld/response"
import ModelExample from "../epiworld/model-example"
import Operations from "../epiworld/operations"
import OperationTag from "../epiworld/operation-tag"

export default function() {

  let coreComponents = {
    components: {
      parameters: Parameters,
      parameterRow: ParameterRow,
      responses: Responses,
      response: Response,
      modelExample: ModelExample,
      operations: Operations,
      OperationTag
    }
  }

  return [
    coreComponents
  ]
}

With this method only particular components will be overriden, whereas the rest remains the same. Custom components are placed under the folder "epiworldat the same level of other "core" one. This is to ensure the import command of original source code run well without any modification of directory. For example, customized component parameter-row.jsx import getParameterSchema from "../../helpers/get-parameter-schema.js" runs out-of-the-box.

import React, { Component } from "react"
import { Map, List } from "immutable"
import PropTypes from "prop-types"
import ImPropTypes from "react-immutable-proptypes"
import win from "core/window"
import { getSampleSchema, getExtensions, getCommonExtensions, numberToString, stringify, isEmptyValue } from "core/utils"
import getParameterSchema from "../../helpers/get-parameter-schema.js"

export default class ParameterRow extends Component {
...

However, this method also affected the core bundle. You can see Swagger UI separates code in to 2 files:

<script src="http://localhost:3200/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="http://localhost:3200/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>

In case you use swagger-ui in different locations and purposes, I recommend pack all you custom code in a swagger plugin instead, it will be built to swagger-ui-standalone-preset.js

So that, I mimicked the TopBar plugin by create a new one. It works nicely, and you can override the default comparison method the same as above:

import StandaloneLayout from "./layout"
// Our custom plugin declaration
import CustomPlugin from "plugins/custom"
import ConfigsPlugin from "corePlugins/configs"

// the Standalone preset

export default [
  CustomPlugin,
  ConfigsPlugin,
  () => {
    return {
      components: { StandaloneLayout }
    }
  }
]

File: /src/standalone/index.js

The components' directories need to be modified as we are on the different location now:

import Parameters from "../../core/epiworld/parameters"
import ParameterRow from "../../core/epiworld/parameter-row"
import Responses from "../../core/epiworld/responses"
import Response from "../../core/epiworld/response"
import ModelExample from "../../core/epiworld/model-example"
import Operations from "../../core/epiworld/operations"
import OperationTag from "../../core/epiworld/operation-tag"

export default function() {
  // Only overriding components declared here
  let coreComponents = {
    components: {
      parameters: Parameters,
      parameterRow: ParameterRow,
      responses: Responses,
      response: Response,
      modelExample: ModelExample,
      operations: Operations,
      OperationTag
    },
    // override default filter function
    fn: {
      opsFilter: (taggedOps, phrase) => {
        const phrases = phrase.split(", ")
        return taggedOps.filter((val, key) => {
          return phrases.some(item => key == item)
        })
      }
    }
  }

  return [
    coreComponents
  ]
}

About the custom opsFilter: We would like to override the default filter as it's the LIKE comparision instead of EQUAL one.

By default, we use Standalone Layout without BaseLayout Component, so we would like to use Operations component. This can be done by getComponent function:

import React from "react"
import PropTypes from "prop-types"

export default class StandaloneLayout extends React.Component {

  static propTypes = {
    errSelectors: PropTypes.object.isRequired,
    errActions: PropTypes.object.isRequired,
    specActions: PropTypes.object.isRequired,
    specSelectors: PropTypes.object.isRequired,
    layoutSelectors: PropTypes.object.isRequired,
    layoutActions: PropTypes.object.isRequired,
    getComponent: PropTypes.func.isRequired
  }

  render() {
    let { getComponent } = this.props
    let Operations = getComponent("operations", true)

    return (
      <Operations />
    )
  }

}

The second parameter (a boolean with value equal true) of the function stated that we don't have to penetrade it with parameters (this is mentioned in the instruction file).

With the help of custom components, we can duplicate components from original and put it under "src/core/components" folder. Modification works are done here. Sometimes it's complicated when alternating current layout - by changing position of something as they belong to another one. It means that we must modify current behavior and javascript handler. Rendered DOMs are also an obstacle for styling with responsive issues, peculiarly when you reuse default stylesheet of swagger UI for table display.

During the development process, the address runs at address localhost:3200, I tried to link direct script but it seems not to work, I added CORS header but no error shows on Console.

To resolve this problem, I create a batch command to build and copy built files to my project's location:

call npm run build
call copy C:\code\swagger-ui\dist\swagger-ui-standalone-preset.js C:\code\DevOps\src\Scripts\Custom\SwaggerPanel\
call copy C:\code\swagger-ui\dist\swagger-ui-standalone-preset.js.map C:\code\DevOps\src\Scripts\Custom\SwaggerPanel\
call copy C:\code\swagger-ui\dist\swagger-ui-bundle.js C:\code\DevOps\src\Scripts\Custom\SwaggerPanel\
call copy C:\code\swagger-ui\dist\swagger-ui-bundle.js.map C:\code\DevOps\src\Scripts\Custom\SwaggerPanel\

Call this script by typing in powershell:

./doit.bat

That's it for the modification works, I hope it helps when you have to deal with Swagger UI. The source code was pushed to my personal repository so in case you would like to find a reference. 

Peace out.

Nov 17, 2020

Antti Alasvuo
( By Antti Alasvuo, 11/17/2020 11:16:27 AM)

Nice! Pst, there is a typo "WolrdPreset" in the apis.js code snip on this page.

Kane Made It
( By Kane Made It, 11/18/2020 1:47:10 AM)

Thanks Antti, it's fixed :).

Please login to comment.