mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 15:11:07 +02:00
192 lines
6.1 KiB
JavaScript
192 lines
6.1 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
|
|
import Component from '@glimmer/component';
|
|
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 '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';
|
|
import keys from 'core/utils/keys';
|
|
|
|
const { APP } = openApiExplorerConfig;
|
|
|
|
export default class SwaggerUiComponent extends Component {
|
|
@service auth;
|
|
@service namespace;
|
|
|
|
@tracked swaggerLoading = true;
|
|
|
|
inputId = `${guidFor(this)}-swagger`;
|
|
|
|
SearchFilterPlugin() {
|
|
return {
|
|
fn: {
|
|
opsFilter: (taggedOps, phrase) => {
|
|
const filteredOperations = taggedOps.reduce((acc, tagObj) => {
|
|
const operations = tagObj.get('operations');
|
|
|
|
// filter out operations where the path doesn't match search phrase
|
|
const operationsWithMatchingPath = operations.filter((operationObj) => {
|
|
const path = operationObj.get('path');
|
|
return path.includes(phrase);
|
|
});
|
|
|
|
// if there are any operations left after filtering, add the tagObj to the accumulator
|
|
if (operationsWithMatchingPath.size > 0) {
|
|
acc.push(tagObj.set('operations', operationsWithMatchingPath));
|
|
}
|
|
|
|
return acc;
|
|
}, []);
|
|
|
|
return filteredOperations;
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
// 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, 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,
|
|
// we don't have any models defined currently
|
|
defaultModelsExpandDepth: -1,
|
|
defaultModelExpandDepth: 1,
|
|
requestInterceptor: (req) => {
|
|
// we need to add vault authorization header
|
|
// and namespace headers for things to work properly
|
|
req.headers['X-Vault-Token'] = componentInstance.auth.currentToken;
|
|
const namespace = componentInstance.namespace.path;
|
|
if (namespace && !APP.NAMESPACE_ROOT_URLS.some((str) => req.url.includes(str))) {
|
|
req.headers['X-Vault-Namespace'] = namespace;
|
|
}
|
|
// we want to link to the right JSON in swagger UI so
|
|
// it's already been pre-pended
|
|
if (!req.loadSpec) {
|
|
const { protocol, host, pathname, search } = parseURL(req.url);
|
|
//paths in the spec don't have /v1 in them, so we need to add that here
|
|
// http(s): vlt.io:4200 /sys/mounts
|
|
req.url = `${protocol}//${host}/v1${pathname}${search}`;
|
|
}
|
|
return req;
|
|
},
|
|
onComplete: () => {
|
|
componentInstance.swaggerLoading = false;
|
|
this.applyA11yFixes();
|
|
},
|
|
};
|
|
};
|
|
|
|
// using an action to bind the correct "this" context
|
|
@action async swaggerInit() {
|
|
const configSettings = this.CONFIG(SwaggerUIBundle, this);
|
|
SwaggerUIBundle(configSettings);
|
|
}
|
|
|
|
applyA11yFixes() {
|
|
const container = document.querySelector('.swagger-ui');
|
|
if (container) {
|
|
this.observer = new MutationObserver(() => {
|
|
this.updateCaretTabIndex();
|
|
this.updateCopyToClipboard();
|
|
this.updateDisabledFields();
|
|
this.updateTryItOutButtonDescription();
|
|
});
|
|
this.observer.observe(container, { childList: true, subtree: true });
|
|
// Run once on initial load
|
|
this.updateCaretTabIndex();
|
|
this.updateCopyToClipboard();
|
|
this.updateDisabledFields();
|
|
this.updateTryItOutButtonDescription();
|
|
}
|
|
}
|
|
|
|
updateCaretTabIndex() {
|
|
document.querySelectorAll('.opblock-control-arrow').forEach((el) => {
|
|
el.tabIndex = 0;
|
|
});
|
|
}
|
|
|
|
updateCopyToClipboard() {
|
|
document.querySelectorAll('.copy-to-clipboard').forEach((div) => {
|
|
div.tabIndex = 0;
|
|
div.setAttribute('role', 'button');
|
|
div.addEventListener('keydown', function (e) {
|
|
if (e.key === keys.ENTER || e.key === keys.SPACE) {
|
|
const svg = div.querySelector('svg');
|
|
if (svg) {
|
|
svg.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
}
|
|
e.preventDefault();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
updateDisabledFields() {
|
|
document.querySelectorAll('.parameters').forEach((el) => {
|
|
if (!el.disabled) {
|
|
el.removeAttribute('readonly');
|
|
} else {
|
|
el.removeAttribute('disabled');
|
|
el.setAttribute('readonly', true);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateTryItOutButtonDescription() {
|
|
document.querySelectorAll('.try-out button').forEach((el) => {
|
|
const warning =
|
|
'Caution: This will make requests to the Vault server on your behalf which may create or delete items.';
|
|
|
|
el.setAttribute('aria-description', warning);
|
|
});
|
|
}
|
|
|
|
willDestroy() {
|
|
this.observer.disconnect();
|
|
super.willDestroy();
|
|
}
|
|
}
|