From a66bd4ec20e4d7a31d6b8e1233db0bc98cfdbc62 Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Fri, 28 Feb 2025 15:57:43 -0700 Subject: [PATCH] [UI] Display Camelized Operation ID in API Explorer (#29785) * updates swagger-ui to display camelized operation ids in development * attempt to fix test timing issue * fixes issue stubbing environment in swagger-ui test * adds test for operation ids in production for swagger-ui component --- ui/app/styles/utils/swagger.scss | 5 ++ .../addon/components/swagger-ui.js | 33 ++++++++- ui/mirage/factories/open-api-explorer.js | 67 +++++++++++-------- .../open-api-explorer/swagger-ui-test.js | 47 +++++++++---- 4 files changed, 106 insertions(+), 46 deletions(-) diff --git a/ui/app/styles/utils/swagger.scss b/ui/app/styles/utils/swagger.scss index e1ab095421..3161f679ea 100644 --- a/ui/app/styles/utils/swagger.scss +++ b/ui/app/styles/utils/swagger.scss @@ -8,4 +8,9 @@ /* align list items with container */ .swagger-ember .swagger-ui .wrapper { padding: 0; + + .opblock-summary-path-description-wrapper { + width: min-content; + flex-grow: 1; + } } diff --git a/ui/lib/open-api-explorer/addon/components/swagger-ui.js b/ui/lib/open-api-explorer/addon/components/swagger-ui.js index dae7a86ca0..5f3ab2d29e 100644 --- a/ui/lib/open-api-explorer/addon/components/swagger-ui.js +++ b/ui/lib/open-api-explorer/addon/components/swagger-ui.js @@ -8,11 +8,13 @@ import { service } from '@ember/service'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import parseURL from 'core/utils/parse-url'; -import config from 'open-api-explorer/config/environment'; +import config from 'vault/config/environment'; +import openApiExplorerConfig from 'open-api-explorer/config/environment'; import { guidFor } from '@ember/object/internals'; import SwaggerUIBundle from 'swagger-ui-dist/swagger-ui-bundle.js'; +import { camelize } from '@ember/string'; -const { APP } = config; +const { APP } = openApiExplorerConfig; export default class SwaggerUiComponent extends Component { @service auth; @@ -49,16 +51,41 @@ export default class SwaggerUiComponent extends Component { }; } + // the operationId values in the spec are dasherized + // camelize the values so they match the function names in the generated API client SDK + CamelizeOperationIdPlugin() { + return { + wrapComponents: { + operation: + (Original, { React }) => + (props) => { + const { operation } = props; + const operationId = operation.get('operationId'); + + if (operationId) { + return React.createElement(Original, { + ...props, + operation: operation.set('operationId', camelize(operationId)), + }); + } + + return React.createElement(Original, props); + }, + }, + }; + } + CONFIG = (SwaggerUIBundle, componentInstance) => { return { dom_id: `#${componentInstance.inputId}`, url: '/v1/sys/internal/specs/openapi', deepLinking: false, presets: [SwaggerUIBundle.presets.apis], - plugins: [SwaggerUIBundle.plugins.DownloadUrl, this.SearchFilterPlugin], + plugins: [SwaggerUIBundle.plugins.DownloadUrl, this.SearchFilterPlugin, this.CamelizeOperationIdPlugin], // 'list' expands tags, but not operations docExpansion: 'list', operationsSorter: 'alpha', + displayOperationId: config.environment === 'development', filter: true, // this makes sure we show the x-vault- options showExtensions: true, diff --git a/ui/mirage/factories/open-api-explorer.js b/ui/mirage/factories/open-api-explorer.js index cfda21d693..57dbfbaf2e 100644 --- a/ui/mirage/factories/open-api-explorer.js +++ b/ui/mirage/factories/open-api-explorer.js @@ -4,42 +4,51 @@ */ import { Factory } from 'miragejs'; -/* eslint-disable ember/avoid-leaking-state-in-ember-objects */ + export default Factory.extend({ openapi: '3.0.2', - info: { - title: 'HashiCorp Vault API', - description: 'HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.', - version: '1.0.0', - license: { - name: 'Mozilla Public License 2.0', - url: 'https://www.mozilla.org/en-US/MPL/2.0', - }, - }, - paths: { - '/auth/token/create': { - description: 'The token create path is used to create new tokens.', - post: { - summary: 'The token create path is used to create new tokens.', - tags: ['auth'], - responses: { - 200: { - description: 'OK', + // set in afterCreate to avoid leaking state lint error + info: null, + paths: null, + + afterCreate(spec) { + spec.info = { + title: 'HashiCorp Vault API', + description: 'HTTP API that gives you full access to Vault. All API routes are prefixed with `/v1/`.', + version: '1.0.0', + license: { + name: 'Mozilla Public License 2.0', + url: 'https://www.mozilla.org/en-US/MPL/2.0', + }, + }; + + spec.paths = { + '/auth/token/create': { + description: 'The token create path is used to create new tokens.', + post: { + summary: 'The token create path is used to create new tokens.', + tags: ['auth'], + operationId: 'token-create', + responses: { + 200: { + description: 'OK', + }, }, }, }, - }, - '/secret/data/{path}': { - description: 'Location of a secret.', - post: { - summary: 'Location of a secret.', - tags: ['secret'], - responses: { - 200: { - description: 'OK', + '/secret/data/{path}': { + description: 'Location of a secret.', + post: { + summary: 'Location of a secret.', + tags: ['secret'], + operationId: 'kv-v2-write', + responses: { + 200: { + description: 'OK', + }, }, }, }, - }, + }; }, }); diff --git a/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js b/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js index 024c93a001..f7c212ecec 100644 --- a/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js +++ b/ui/tests/integration/components/open-api-explorer/swagger-ui-test.js @@ -5,50 +5,48 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; -import { fillIn, render, typeIn, waitFor } from '@ember/test-helpers'; +import { fillIn, render, typeIn } from '@ember/test-helpers'; import { setupEngine } from 'ember-engines/test-support'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { hbs } from 'ember-cli-htmlbars'; +import sinon from 'sinon'; +import config from 'vault/config/environment'; +import { camelize } from '@ember/string'; const SELECTORS = { container: '[data-test-swagger-ui]', searchInput: 'input.operation-filter-input', apiPathBlock: '.opblock-post', + operationId: '.opblock-summary-operation-id', }; module('Integration | Component | open-api-explorer | swagger-ui', function (hooks) { setupRenderingTest(hooks); setupEngine(hooks, 'open-api-explorer'); setupMirage(hooks); + hooks.beforeEach(function () { this.store = this.owner.lookup('service:store'); - const openApiResponse = this.server.create('open-api-explorer'); + this.openApiResponse = this.server.create('open-api-explorer'); this.server.get('sys/internal/specs/openapi', () => { - return openApiResponse; + return this.openApiResponse; }); - this.totalApiPaths = Object.keys(openApiResponse.paths).length; + this.totalApiPaths = Object.keys(this.openApiResponse.paths).length; - this.renderComponent = async () => { - await render(hbs``, { - owner: this.engine, - }); - }; + this.renderComponent = () => render(hbs``, { owner: this.engine }); }); - test(`it renders`, async function (assert) { + test('it renders', async function (assert) { await this.renderComponent(); - await waitFor(SELECTORS.container); - assert.dom(SELECTORS.container).exists('renders component'); assert.dom(SELECTORS.apiPathBlock).exists({ count: this.totalApiPaths }, 'renders all api paths'); }); - test(`it can search`, async function (assert) { + test('it can search', async function (assert) { await this.renderComponent(); - // in testing only the input is not filling correctly except after the second time await fillIn(SELECTORS.searchInput, 'moot'); await typeIn(SELECTORS.searchInput, 'token'); @@ -57,4 +55,25 @@ module('Integration | Component | open-api-explorer | swagger-ui', function (hoo // if the search fn breaks, this test will fail assert.dom(SELECTORS.searchInput).hasValue('token', 'search input has value'); }); + + test('it should render camelized operation ids', async function (assert) { + const envStub = sinon.stub(config, 'environment').value('development'); + + await this.renderComponent(); + + const id = this.openApiResponse.paths['/auth/token/create'].post.operationId; + assert.dom(SELECTORS.operationId).hasText(camelize(id), 'renders camelized operation id'); + + envStub.restore(); + }); + + test('it should not render operation ids in production', async function (assert) { + const envStub = sinon.stub(config, 'environment').value('production'); + + await this.renderComponent(); + + assert.dom(SELECTORS.operationId).doesNotExist('operation ids are hidden in production environment'); + + envStub.restore(); + }); });