diff --git a/src/core/components/copy-component.jsx b/src/core/components/copy-component.jsx new file mode 100644 index 00000000000..de2652cce03 --- /dev/null +++ b/src/core/components/copy-component.jsx @@ -0,0 +1,31 @@ +import React from "react" +import PropTypes from "prop-types" + +function copyToClipboard(e, getBaseUrl, path, method) { + const baseUrl = document.createElement("input") + baseUrl.setAttribute("value", getBaseUrl(path, method)) + baseUrl.addEventListener("copy", () => e.stopPropagation()) + document.body.appendChild(baseUrl) + baseUrl.select() + document.execCommand("copy") + document.body.removeChild(baseUrl) + } + +const CopyComponent = ({ getBaseUrl, path, method }) => { + + return ( +
copyToClipboard(e, getBaseUrl, path, method)}> + + + +
+ ) +} + +CopyComponent.propTypes = { + getBaseUrl: PropTypes.func.isRequired, + path: PropTypes.object.isRequired, + method: PropTypes.object.isRequired + } + + export default CopyComponent diff --git a/src/core/components/curl.jsx b/src/core/components/curl.jsx index ad1a2df4677..bda2d36e084 100644 --- a/src/core/components/curl.jsx +++ b/src/core/components/curl.jsx @@ -19,7 +19,7 @@ export default class Curl extends React.Component { return (

Curl

-
+
diff --git a/src/core/components/operation-summary.jsx b/src/core/components/operation-summary.jsx index bed0b3b4dea..16768232274 100644 --- a/src/core/components/operation-summary.jsx +++ b/src/core/components/operation-summary.jsx @@ -3,6 +3,7 @@ import PropTypes from "prop-types" import { Iterable, List } from "immutable" import ImPropTypes from "react-immutable-proptypes" import toString from "lodash/toString" +import CopyComponent from "./copy-component" export default class OperationSummary extends PureComponent { @@ -15,6 +16,7 @@ export default class OperationSummary extends PureComponent { getConfigs: PropTypes.func.isRequired, authActions: PropTypes.object, authSelectors: PropTypes.object, + getBaseUrl: PropTypes.func.isRequired } static defaultProps = { @@ -32,6 +34,7 @@ export default class OperationSummary extends PureComponent { authSelectors, operationProps, specPath, + getBaseUrl } = this.props let { @@ -43,6 +46,7 @@ export default class OperationSummary extends PureComponent { operationId, originalOperationId, displayOperationId, + path } = operationProps.toJS() let { @@ -80,9 +84,10 @@ export default class OperationSummary extends PureComponent { }} /> } + {/* TODO: use wrapComponents here, swagger-ui doesn't care about jumpToPath */}
) - } + } } diff --git a/src/core/components/operation.jsx b/src/core/components/operation.jsx index f441a5d0fbe..f50632fb994 100644 --- a/src/core/components/operation.jsx +++ b/src/core/components/operation.jsx @@ -1,7 +1,7 @@ import React, { PureComponent } from "react" import PropTypes from "prop-types" import { getList } from "core/utils" -import { getExtensions, sanitizeUrl, escapeDeepLinkPath } from "core/utils" +import { getExtensions, sanitizeUrl, escapeDeepLinkPath, baseUrl } from "core/utils" import { Iterable, List } from "immutable" import ImPropTypes from "react-immutable-proptypes" @@ -110,9 +110,23 @@ export default class Operation extends PureComponent { let onChangeKey = [ path, method ] // Used to add values to _this_ operation ( indexed by path and method ) + const getBaseUrl = (path, method) => { + const obj = { + spec: specSelectors.specJson().toJS(), + contextUrl: specSelectors.url(), + specIsOAS3: specSelectors.isOAS3(specSelectors.specJson().toJS()), + server: oas3Selectors.selectedServer(), + pathName: path, + method, + scheme: specSelectors.operationScheme() + } + return baseUrl(obj) + } + return (
- +
{ (operation && operation.size) || operation === null ? null : @@ -233,4 +247,4 @@ export default class Operation extends PureComponent { ) } -} +} \ No newline at end of file diff --git a/src/core/components/svg-assets.jsx b/src/core/components/svg-assets.jsx index 3b078edda7e..ace497f4bcb 100644 --- a/src/core/components/svg-assets.jsx +++ b/src/core/components/svg-assets.jsx @@ -36,6 +36,10 @@ const SvgAssets = () => + + + +
diff --git a/src/core/utils.js b/src/core/utils.js index d3e6104e4e1..5707aa0a30e 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -9,6 +9,8 @@ import eq from "lodash/eq" import { memoizedSampleFromSchema, memoizedCreateXMLExample } from "core/plugins/samples/fn" import win from "./window" import cssEscape from "css.escape" +import url from "url" +import getIn from "lodash/get" const DEFAULT_RESPONSE_KEY = "default" @@ -842,4 +844,109 @@ export function paramToValue(param, paramValues) { .filter(value => value !== undefined) return values[0] +} + +const stripNonAlpha = str => (str ? str.replace(/\W/g, "") : null) + +const buildOas3UrlWithContext = (ourUrl = "", contextUrl = "") => { + const parsedUrl = url.parse(ourUrl) + const parsedContextUrl = url.parse(contextUrl) + + const computedScheme = stripNonAlpha(parsedUrl.protocol) || stripNonAlpha(parsedContextUrl.protocol) || "" + const computedHost = parsedUrl.host || parsedContextUrl.host + const computedPath = parsedUrl.pathname || "" + let res + + if (computedScheme && computedHost) { + res = `${computedScheme}://${computedHost + computedPath}` + + // If last character is '/', trim it off + } + else { + res = computedPath + } + + return res[res.length - 1] === "/" ? res.slice(0, -1) : res +} + +function getVariableTemplateNames(str) { + const results = [] + const re = /{([^}]+)}/g + let text + + // eslint-disable-next-line no-cond-assign + while (text = re.exec(str)) { + results.push(text[1]) + } + return results +} + +const oas3BaseUrl = ({ spec, pathName, method, server, contextUrl, serverVariables = {} }) => { + const servers = + getIn(spec, ["paths", pathName, (method || "").toLowerCase(), "servers"]) || + getIn(spec, ["paths", pathName, "servers"]) || + getIn(spec, ["servers"]) + + let selectedServerUrl = "" + let selectedServerObj = null + + if (server && servers && servers.length) { + const serverUrls = servers.map(srv => srv.url) + + if (serverUrls.indexOf(server) > -1) { + selectedServerUrl = server + selectedServerObj = servers[serverUrls.indexOf(server)] + } + } + + if (!selectedServerUrl && servers && servers.length) { + // default to the first server if we don't have one by now + selectedServerUrl = servers[0].url + selectedServerObj = servers[0] + } + + if (selectedServerUrl.indexOf("{") > -1) { + // do variable substitution + const varNames = getVariableTemplateNames(selectedServerUrl) + varNames.forEach((vari) => { + if (selectedServerObj.variables && selectedServerObj.variables[vari]) { + // variable is defined in server + const variableDefinition = selectedServerObj.variables[vari] + const variableValue = serverVariables[vari] || variableDefinition.default + + const re = new RegExp(`{${vari}}`, "g") + selectedServerUrl = selectedServerUrl.replace(re, variableValue) + } + }) + } + + //return selectedServerUrl + return buildOas3UrlWithContext(selectedServerUrl, contextUrl) +} + +const swagger2BaseUrl = ({ spec, scheme, contextUrl = "" }) => { + const parsedContextUrl = url.parse(contextUrl) + const firstSchemeInSpec = Array.isArray(spec.schemes) ? spec.schemes[0] : null + + const computedScheme = scheme || firstSchemeInSpec || stripNonAlpha(parsedContextUrl.protocol) || "http" + const computedHost = spec.host || parsedContextUrl.host || "" + const computedPath = spec.basePath || "" + let res + + if (computedScheme && computedHost) { + // we have what we need for an absolute URL + res = `${computedScheme}://${computedHost + computedPath}` + } + else { + // if not, a relative URL will have to do + res = computedPath + } + + // If last character is '/', trim it off + return res[res.length - 1] === "/" ? res.slice(0, -1) : res +} + +export const baseUrl = (obj) => { + const baseUrl = obj.specIsOAS3 ? oas3BaseUrl(obj) : swagger2BaseUrl(obj) + return (`${baseUrl}${obj.pathName}`) } \ No newline at end of file