From 6ae2693d47397a919370eb19149f5514cb2f99f2 Mon Sep 17 00:00:00 2001 From: Fabian Schneider Date: Wed, 17 Aug 2022 19:42:36 +0200 Subject: [PATCH] feat: Render external docs links and descriptions (#7559) Co-authored-by: Tim Lai --- src/core/components/array-model.jsx | 12 +- src/core/components/object-model.jsx | 16 +- src/core/components/operation-tag.jsx | 10 +- src/core/components/operation.jsx | 8 +- src/core/components/primitive-model.jsx | 15 +- src/style/_layout.scss | 5 + src/style/_models.scss | 19 +++ .../features/external-docs.openapi.yaml | 148 ++++++++++++++++++ .../features/external-docs.swagger.yaml | 116 ++++++++++++++ .../tests/features/external-docs.js | 108 +++++++++++++ 10 files changed, 443 insertions(+), 14 deletions(-) create mode 100644 test/e2e-cypress/static/documents/features/external-docs.openapi.yaml create mode 100644 test/e2e-cypress/static/documents/features/external-docs.swagger.yaml create mode 100644 test/e2e-cypress/tests/features/external-docs.js diff --git a/src/core/components/array-model.jsx b/src/core/components/array-model.jsx index 44730ab8c26..bd2d685ae94 100644 --- a/src/core/components/array-model.jsx +++ b/src/core/components/array-model.jsx @@ -1,6 +1,7 @@ import React, { Component } from "react" import PropTypes from "prop-types" import ImPropTypes from "react-immutable-proptypes" +import { sanitizeUrl } from "core/utils" const propClass = "property" @@ -25,12 +26,16 @@ export default class ArrayModel extends Component { let description = schema.get("description") let items = schema.get("items") let title = schema.get("title") || displayName || name - let properties = schema.filter( ( v, key) => ["type", "items", "description", "$$ref"].indexOf(key) === -1 ) + let properties = schema.filter( ( v, key) => ["type", "items", "description", "$$ref", "externalDocs"].indexOf(key) === -1 ) + let externalDocsUrl = schema.getIn(["externalDocs", "url"]) + let externalDocsDescription = schema.getIn(["externalDocs", "description"]) + const Markdown = getComponent("Markdown", true) const ModelCollapse = getComponent("ModelCollapse") const Model = getComponent("Model") const Property = getComponent("Property") + const Link = getComponent("Link") const titleEl = title && @@ -52,6 +57,11 @@ export default class ArrayModel extends Component { !description ? (properties.size ?
: null) : } + { externalDocsUrl && +
+ {externalDocsDescription || externalDocsUrl} +
+ } ["maxProperties", "minProperties", "nullable", "example"].indexOf(key) !== -1 ) let deprecated = schema.get("deprecated") + let externalDocsUrl = schema.getIn(["externalDocs", "url"]) + let externalDocsDescription = schema.getIn(["externalDocs", "description"]) const JumpToPath = getComponent("JumpToPath", true) const Markdown = getComponent("Markdown", true) const Model = getComponent("Model") const ModelCollapse = getComponent("ModelCollapse") const Property = getComponent("Property") + const Link = getComponent("Link") const JumpToPathSection = () => { return @@ -93,6 +97,17 @@ export default class ObjectModel extends Component { } + { + externalDocsUrl && + + + externalDocs: + + + {externalDocsDescription || externalDocsUrl} + + + } { !deprecated ? null : @@ -103,7 +118,6 @@ export default class ObjectModel extends Component { true - } { !(properties && properties.size) ? null : properties.entrySeq().filter( diff --git a/src/core/components/operation-tag.jsx b/src/core/components/operation-tag.jsx index 48c7b273970..ce50f25f6d4 100644 --- a/src/core/components/operation-tag.jsx +++ b/src/core/components/operation-tag.jsx @@ -88,18 +88,14 @@ export default class OperationTag extends React.Component { } - {!tagExternalDocsDescription ? null : + {!tagExternalDocsUrl ? null :
- {tagExternalDocsDescription} - {tagExternalDocsUrl ? ": " : null} - {tagExternalDocsUrl ? - e.stopPropagation()} target="_blank" - >{tagExternalDocsUrl} : null - } + >{tagExternalDocsDescription || tagExternalDocsUrl}
} diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index d1ce910f9cf..d4a365b7f04 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -133,9 +133,11 @@ export default class Operation extends PureComponent {

Find more details

- - - + {externalDocs.description && + + + + } {externalDocsUrl}
: null diff --git a/src/core/components/primitive-model.jsx b/src/core/components/primitive-model.jsx index 711fab76e6f..4874513ac75 100644 --- a/src/core/components/primitive-model.jsx +++ b/src/core/components/primitive-model.jsx @@ -1,6 +1,6 @@ import React, { Component } from "react" import PropTypes from "prop-types" -import { getExtensions } from "core/utils" +import { getExtensions, sanitizeUrl } from "core/utils" const propClass = "property primitive" @@ -33,12 +33,17 @@ export default class Primitive extends Component { let description = schema.get("description") let extensions = getExtensions(schema) let properties = schema - .filter((_, key) => ["enum", "type", "format", "description", "$$ref"].indexOf(key) === -1) + .filter((_, key) => ["enum", "type", "format", "description", "$$ref", "externalDocs"].indexOf(key) === -1) .filterNot((_, key) => extensions.has(key)) + let externalDocsUrl = schema.getIn(["externalDocs", "url"]) + let externalDocsDescription = schema.getIn(["externalDocs", "description"]) + const Markdown = getComponent("Markdown", true) const EnumModel = getComponent("EnumModel") const Property = getComponent("Property") const ModelCollapse = getComponent("ModelCollapse") + const Link = getComponent("Link") + const titleEl = title && {title} @@ -60,6 +65,12 @@ export default class Primitive extends Component { !description ? null : } + { + externalDocsUrl && +
+ {externalDocsDescription || externalDocsUrl} +
+ } { xml && xml.size ? (
xml: { diff --git a/src/style/_layout.scss b/src/style/_layout.scss index 910a1e544df..e5b979ee0ba 100644 --- a/src/style/_layout.scss +++ b/src/style/_layout.scss @@ -118,6 +118,11 @@ flex: 1; } } + + .info__externaldocs + { + text-align: right; + } } .parameter__type diff --git a/src/style/_models.scss b/src/style/_models.scss index 9d25fb922c9..6683c567a62 100644 --- a/src/style/_models.scss +++ b/src/style/_models.scss @@ -108,6 +108,12 @@ color: #6b6b6b; } } + + .external-docs + { + color: #666; + font-weight: normal; + } } table.model @@ -157,6 +163,19 @@ table.model vertical-align: top; } } + + &.external-docs + { + td:first-child + { + font-weight: bold; + } + } + + .renderedMarkdown p:first-child + { + margin-top: 0; + } } } diff --git a/test/e2e-cypress/static/documents/features/external-docs.openapi.yaml b/test/e2e-cypress/static/documents/features/external-docs.openapi.yaml new file mode 100644 index 00000000000..0195f8a346f --- /dev/null +++ b/test/e2e-cypress/static/documents/features/external-docs.openapi.yaml @@ -0,0 +1,148 @@ +openapi: 3.0.2 + +info: + title: External Docs + version: "1" + +externalDocs: + description: Read external docs + url: http://swagger.io + +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Pet Documentation + url: http://swagger.io +- name: petWithoutDescription + externalDocs: + url: http://swagger.io + +paths: + /pet: + put: + externalDocs: + description: More details about putting a pet + url: http://swagger.io + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + post: + externalDocs: + url: http://swagger.io + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + "405": + description: Invalid input + +components: + schemas: + Pet: + required: + - name + - photoUrls + type: object + description: This is a Pet + externalDocs: + description: More Docs About Pet + url: http://swagger.io + deprecated: true + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + Object: + type: object + externalDocs: + description: Object Docs + url: http://swagger.io + properties: + name: + type: string + ObjectWithoutDescription: + type: object + externalDocs: + url: http://swagger.io + properties: + name: + type: string + Primitive: + description: Just a string schema + type: string + externalDocs: + description: Primitive Docs + url: http://swagger.io + PrimitiveWithoutDescription: + description: Just a string schema + type: string + externalDocs: + url: http://swagger.io + Array: + description: Just an array schema + type: array + externalDocs: + description: Array Docs + url: http://swagger.io + items: + type: string + ArrayWithoutDescription: + description: Just an array schema + type: array + externalDocs: + url: http://swagger.io + items: + type: string diff --git a/test/e2e-cypress/static/documents/features/external-docs.swagger.yaml b/test/e2e-cypress/static/documents/features/external-docs.swagger.yaml new file mode 100644 index 00000000000..10d5275de26 --- /dev/null +++ b/test/e2e-cypress/static/documents/features/external-docs.swagger.yaml @@ -0,0 +1,116 @@ +swagger: "2.0" + +info: + title: External Docs + version: "1" + +externalDocs: + description: Read external docs + url: http://swagger.io + +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Pet Documentation + url: http://swagger.io +- name: petWithoutDescription + externalDocs: + url: http://swagger.io + +paths: + /pet: + put: + externalDocs: + description: More details about putting a pet + url: http://swagger.io + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + responses: + 200: + description: OK + post: + externalDocs: + url: http://swagger.io + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + responses: + 201: + description: Created + +definitions: + Pet: + required: + - name + - photoUrls + type: object + description: This is a Pet + externalDocs: + description: More Docs About Pet + url: http://swagger.io + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + Object: + type: object + externalDocs: + description: Object Docs + url: http://swagger.io + properties: + name: + type: string + ObjectWithoutDescription: + type: object + externalDocs: + url: http://swagger.io + properties: + name: + type: string + Primitive: + description: Just a string schema + type: string + externalDocs: + description: Primitive Docs + url: http://swagger.io + PrimitiveWithoutDescription: + description: Just a string schema + type: string + externalDocs: + url: http://swagger.io + Array: + description: Just an array schema + type: array + externalDocs: + description: Array Docs + url: http://swagger.io + items: + type: string + ArrayWithoutDescription: + description: Just an array schema + type: array + externalDocs: + url: http://swagger.io + items: + type: string diff --git a/test/e2e-cypress/tests/features/external-docs.js b/test/e2e-cypress/tests/features/external-docs.js new file mode 100644 index 00000000000..e9d1c9ae526 --- /dev/null +++ b/test/e2e-cypress/tests/features/external-docs.js @@ -0,0 +1,108 @@ +describe("External docs feature", () => { + describe("in Swagger 2", () => { + ExternalDocsTest("/?url=/documents/features/external-docs.swagger.yaml") + }) + describe("in OpenAPI 3", () => { + ExternalDocsTest("/?url=/documents/features/external-docs.openapi.yaml") + }) +}) + +function ExternalDocsTest(baseUrl) { + describe("for Root", () => { + it("should display link to external docs with description", () => { + cy.visit(baseUrl) + .get(".info__extdocs") + .should("exist") + .and("contain.text", "Read external docs") + .and("have.attr", "href", "http://swagger.io") + }) + + it("should display link to external docs without description", () => { + cy + .intercept({ + path: /^\/documents\/features\/external-docs\.(swagger|openapi)\.yaml\?intercept$/ + }, (req) => { + delete req.headers["if-none-match"] + delete req.headers["if-modified-since"] + req.continue((res) => { + res.send({body: res.body.replace(" description: Read external docs\n", "")}) + }) + }) + .visit(`${baseUrl}?intercept`) + .get(".info__extdocs") + .should("exist") + .and("contain.text", "http://swagger.io") + .and("have.attr", "href", "http://swagger.io") + }) + }) + + describe("for Tags", () => { + it("should display link to external docs with description", () => { + cy.visit(baseUrl) + .get(`.opblock-tag[data-tag="pet"] .info__externaldocs`) + .should("exist") + .find("a") + .should("contain.text", "Pet Documentation") + .and("have.attr", "href", "http://swagger.io") + }) + + it("should display link to external docs without description", () => { + cy.visit(baseUrl) + .get(`.opblock-tag[data-tag="petWithoutDescription"] .info__externaldocs`) + .should("exist") + .find("a") + .should("contain.text", "http://swagger.io") + .and("have.attr", "href", "http://swagger.io") + }) + }) + + describe("for Schemas", () => { + function SchemaTestFactory(type) { + return () => { + it("should display link with description", () => { + cy.visit(baseUrl) + .get(`.models #model-${type} button`) + .click() + .get(`.models #model-${type} .external-docs a`) + .should("contain.text", `${type} Docs`) + .and("have.attr", "href", "http://swagger.io") + }) + + it("should display link without description", () => { + cy.visit(baseUrl) + .get(`.models #model-${type}WithoutDescription button`) + .click() + .get(`.models #model-${type}WithoutDescription .external-docs a`) + .should("contain.text", "http://swagger.io") + .and("have.attr", "href", "http://swagger.io") + }) + } + } + + describe("Primitive Schema", SchemaTestFactory("Primitive")) + describe("Array Schema", SchemaTestFactory("Array")) + describe("Object Schema", SchemaTestFactory("Object")) + }) + + describe("for Operation", () => { + it("should display link to external docs with description", () => { + cy.visit(baseUrl) + .get("#operations-pet-updatePet button") + .click() + .get("#operations-pet-updatePet .opblock-external-docs-wrapper .opblock-external-docs__description") + .should("contain.text", "More details about putting a pet") + .get("#operations-pet-updatePet .opblock-external-docs-wrapper .opblock-external-docs__link") + .should("have.attr", "href", "http://swagger.io") + }) + + it("should display link to external docs without description", () => { + cy.visit(baseUrl) + .get("#operations-pet-addPet button") + .click() + .get("#operations-pet-addPet .opblock-external-docs-wrapper .opblock-external-docs__description") + .should("not.exist") + .get("#operations-pet-addPet .opblock-external-docs-wrapper .opblock-external-docs__link") + .should("have.attr", "href", "http://swagger.io") + }) + }) +}