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();
+ });
});