diff --git a/.gitignore b/.gitignore index 50c44754..7fa86d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ cli/index.js stats.json /package-lock.json /.idea/ + +# npmrc for local npm +.npmrc diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..3e44267f --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1 @@ +intent.json \ No newline at end of file diff --git a/demo/petstore.json b/demo/petstore.json new file mode 100644 index 00000000..77a25e0d --- /dev/null +++ b/demo/petstore.json @@ -0,0 +1,993 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "tags": [{ + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Access to Petstore orders" + }, + { + "name": "user", + "description": "Operations about user", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + }], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + }], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": true, + "type": "array", + "items": { + "type": "string", + "enum": [ + "available", + "pending", + "sold" + ], + "default": "available" + }, + "collectionFormat": "multi" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": true, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }], + "deprecated": true + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "type": "integer", + "format": "int64" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [{ + "api_key": [ + + ] + }] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "api_key", + "in": "header", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "parameters": [{ + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/ApiResponse" + } + } + }, + "security": [{ + "petstore_auth": [ + "write:pets", + "read:pets" + ] + }] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json" + ], + "parameters": [ + + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [{ + "api_key": [ + + ] + }] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": true, + "schema": { + "$ref": "#/definitions/Order" + } + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", + "operationId": "getOrderById", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "maximum": 10.0, + "minimum": 1.0, + "format": "int64" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "integer", + "minimum": 1.0, + "format": "int64" + }], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "Created user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + }], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + }], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "in": "body", + "name": "body", + "description": "List of user object", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + }], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "query", + "description": "The user name for login", + "required": true, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + }, + "headers": { + "X-Rate-Limit": { + "type": "integer", + "format": "int32", + "description": "calls per hour allowed by the user" + }, + "X-Expires-After": { + "type": "string", + "format": "date-time", + "description": "date in UTC when token expires" + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [ + + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + }], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "path", + "description": "name that need to be updated", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": true, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "400": { + "description": "Invalid user supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/xml", + "application/json" + ], + "parameters": [{ + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + }], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "securityDefinitions": { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + }, + "definitions": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean", + "default": false + } + }, + "xml": { + "name": "Order" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Pet": { + "type": "object", + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + } +} diff --git a/demo/playground/hmr-playground.tsx b/demo/playground/hmr-playground.tsx index adca6279..4f06b045 100644 --- a/demo/playground/hmr-playground.tsx +++ b/demo/playground/hmr-playground.tsx @@ -25,7 +25,10 @@ const specUrl = (userUrl && userUrl[1]) || (swagger ? 'swagger.yaml' : big ? 'big-openapi.json' : 'openapi.yaml'); let store; -const options: RedocRawOptions = { nativeScrollbars: false }; +const headers = { + 'x-nutanix-client': 'ui', +}; +const options: RedocRawOptions = { nativeScrollbars: false, enableConsole: true, providedByName: 'Intent ApiDocs by Nutanix', providedByUri: 'http://www.nutanix.com', additionalHeaders: headers }; async function init() { const spec = await loadAndBundleSpec(specUrl); diff --git a/demo/webpack.config.ts b/demo/webpack.config.ts index d48443ae..0b36e52a 100644 --- a/demo/webpack.config.ts +++ b/demo/webpack.config.ts @@ -56,6 +56,26 @@ const babelHotLoader = { }, }; +let proxy = {}; +let https = false; +//we are using our own proxy here +if (process.env.PROXY && process.env.USERNAME && process.env.PASSWORD) { + proxy = { + "/api": { + auth: `${process.env.USERNAME}:${process.env.PASSWORD}`, + target: process.env.PROXY, + "secure": false, + changeOrigin: true, + ws: true, + xfwd: true + } + } + https = true; + console.log('Using proxy configuration provided with command line, https in use.\n'); +} else { + console.log('Using proxy from PC/PE local server\n'); +} + export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) => ({ entry: [ root('../src/polyfills.ts'), @@ -79,8 +99,12 @@ export default (env: { playground?: boolean; bench?: boolean } = {}, { mode }) = port: 9090, disableHostCheck: true, stats: 'minimal', + https, + proxy, }, + devtool: 'source-map', + resolve: { extensions: ['.ts', '.tsx', '.js', '.json'], alias: diff --git a/package.json b/package.json index 5b199dcf..6c52b1c3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,9 @@ "@types/mark.js": "^8.11.4", "@types/marked": "^0.6.5", "@types/prismjs": "^1.16.0", + "@types/promise": "^7.1.30", "@types/prop-types": "^15.7.1", + "@types/qs": "^6.5.1", "@types/react": "^16.8.23", "@types/react-dom": "^16.8.5", "@types/react-hot-loader": "^4.1.0", @@ -79,6 +81,7 @@ "@types/tapable": "1.0.4", "@types/webpack": "^4.32.1", "@types/webpack-env": "^1.14.0", + "@types/whatwg-fetch": "^0.0.33", "@types/yargs": "^13.0.0", "babel-loader": "8.0.6", "babel-plugin-styled-components": "^1.10.6", @@ -134,6 +137,11 @@ }, "dependencies": { "classnames": "^2.2.6", + "@types/chai": "4.1.3", + "@types/tapable": "1.0.2", + "ajv": "^6.4.0", + "ajv-errors": "^1.0.0", + "brace": "^0.11.1", "decko": "^1.2.0", "dompurify": "^1.0.11", "eventemitter3": "^4.0.0", @@ -153,6 +161,8 @@ "react-hot-loader": "^4.12.10", "react-tabs": "^3.0.0", "slugify": "^1.3.4", + "qs": "^6.5.2", + "react-ace": "^6.0.0", "stickyfill": "^1.1.1", "swagger2openapi": "^5.3.1", "tslib": "^1.10.0", diff --git a/src/common-elements/buttons.ts b/src/common-elements/buttons.ts new file mode 100644 index 00000000..79479a29 --- /dev/null +++ b/src/common-elements/buttons.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import styled, { ResolvedThemeInterface, StyledComponentClass } from '../styled-components'; + +export const Button = styled.button` + background: #248fb2; + border-radius: 0px; + border: none; + color: white; + font-size: 0.929em; + padding: 5px; +`; + +export const SendButton = Button.extend` + background: #B0045E; +`; + +export const ConsoleButton = Button.extend` + background: #e2e2e2; + color: black; + float: right; +`; diff --git a/src/common-elements/index.ts b/src/common-elements/index.ts index 5c9ec542..3ff7e1cc 100644 --- a/src/common-elements/index.ts +++ b/src/common-elements/index.ts @@ -9,3 +9,6 @@ export * from './mixins'; export * from './tabs'; export * from './samples'; export * from './perfect-scrollbar'; +export * from './toggle'; +export * from './input'; +export * from './buttons'; diff --git a/src/common-elements/input.tsx b/src/common-elements/input.tsx new file mode 100644 index 00000000..088307e1 --- /dev/null +++ b/src/common-elements/input.tsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +import styled, { css, ResolvedThemeInterface, StyledComponentClass } from '../styled-components'; + +export const TextField = styled.input` + padding: 0.5em; + margin: 0.5em; + border: 1px solid rgba(38,50,56,0.5); + border-radius: 3px; +`; diff --git a/src/common-elements/panels.ts b/src/common-elements/panels.ts index f41845fc..ef4d3683 100644 --- a/src/common-elements/panels.ts +++ b/src/common-elements/panels.ts @@ -73,3 +73,17 @@ export const Row = styled.div` flex-direction: column; `}; `; + +export const FlexLayout = styled.div` + align-items: flex-end; + display: flex; + width: 100%; +`; + +export const ConsoleActionsRow = FlexLayout.extend` + padding: 5px 0px; +`; + +export const FlexLayoutReverse = FlexLayout.extend` + flex-direction: row-reverse; +`; diff --git a/src/common-elements/toggle.tsx b/src/common-elements/toggle.tsx new file mode 100644 index 00000000..4332f5a1 --- /dev/null +++ b/src/common-elements/toggle.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; +import styled, { css, ResolvedThemeInterface, StyledComponentClass } from '../styled-components'; + +export const Toggle = styled.input` + padding: 0.5em; + margin: 0.5em; + color: palevioletred; + background: papayawhip; + border: none; + border-radius: 3px; +`; diff --git a/src/components/Console/ConsoleEditor.tsx b/src/components/Console/ConsoleEditor.tsx new file mode 100644 index 00000000..e9175569 --- /dev/null +++ b/src/components/Console/ConsoleEditor.tsx @@ -0,0 +1,62 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; + +import AceEditor from 'react-ace'; + +import 'brace/mode/curly'; +import 'brace/mode/json'; +import 'brace/theme/github'; +import 'brace/theme/monokai'; + +import { MediaTypeModel } from '../../services/models'; +import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch'; + +import { MediaTypeSamples } from '../PayloadSamples/MediaTypeSamples'; + +import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel'; +import { InvertedSimpleDropdown, MimeLabel } from '../PayloadSamples/styled.elements'; + +export interface ConsoleEditorProps { + mediaTypes: MediaTypeModel[]; +} + +@observer +export class ConsoleEditor extends React.Component { + + editor: any; + + render() { + const { mediaTypes } = this.props; + + if (!mediaTypes.length) { + return null; + } + let sample = {}; + for (const mediaType of mediaTypes) { + if (mediaType.name.indexOf('json') > -1) { + if (mediaType.examples) { + sample = mediaType.examples && mediaType.examples.default && mediaType.examples.default.value; + } + break; + } + } + + return ( +
+ (this.editor = ace)} + width="100%" + height="400px" + /> +
+ ); + } + +} diff --git a/src/components/Console/ConsoleViewer.tsx b/src/components/Console/ConsoleViewer.tsx new file mode 100644 index 00000000..0931a805 --- /dev/null +++ b/src/components/Console/ConsoleViewer.tsx @@ -0,0 +1,219 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { SendButton } from '../../common-elements/buttons'; +import { ConsoleActionsRow } from '../../common-elements/panels'; +import { FieldModel, OperationModel } from '../../services/models'; +import { OpenAPISchema } from '../../types'; +import { PayloadSamples } from '../PayloadSamples/PayloadSamples'; +import { SourceCodeWithCopy } from '../SourceCode/SourceCode'; +import { ConsoleEditor } from './ConsoleEditor'; + +const qs = require('qs'); + +export interface ConsoleViewerProps { + operation: OperationModel; + additionalHeaders?: object; + queryParamPrefix?: string; + queryParamSuffix?: string; +} + +export interface ConsoleViewerState { + result: any; +} + +export interface Schema { + _$ref?: any; + rawSchema?: OpenAPISchema; +} + +@observer +export class ConsoleViewer extends React.Component { + operation: OperationModel; + additionalHeaders: object; + visited = new Set(); + private consoleEditor: ConsoleEditor; + + constructor(props) { + super(props); + this.state = { + result: null, + }; + } + onClickSend = async () => { + const ace = this.consoleEditor && this.consoleEditor.editor; + // const value = ace && ace.editor && + const schema = this.getSchema(); + const { operation, additionalHeaders = {} } = this.props; + // console.log('Schema: ' + JSON.stringify(schema, null, 2)); + let value = ace && ace.editor.getValue(); + + const ref = schema && schema._$ref; + // var valid = window && window.ajv.validate({ $ref: `specs.json${ref}` }, value); + // console.log(JSON.stringify(window.ajv.errors)); + // if (!valid) { + // console.warn('INVALID REQUEST!'); + // } + const content = operation.requestBody && operation.requestBody.content; + const mediaType = content && content.mediaTypes[content.activeMimeIdx]; + const endpoint = { + method: operation.httpVerb, + path: operation.servers[0].url + operation.path, + }; + // console.log('Value: ' + value); + if (value) { + value = JSON.parse(value); + } + const contentType = mediaType && mediaType.name || 'application/json'; + const contentTypeHeader = { 'Content-Type': contentType }; + const headers = { ...additionalHeaders, ...contentTypeHeader }; + let result; + try { + result = await this.invoke(endpoint, value, headers); + this.setState({ + result, + }); + } catch (error) { + this.setState({ + result: error, + }); + } + }; + + /* + * If we have a url like foo/bar/{uuid} uuid will be replaced with what user has typed in. + */ + addParamsToUrl(url: string, params: FieldModel[]) { + const queryParamPrefix = '{'; + const queryParamSuffix = '}'; + + for (const fieldModel of params) { + console.log(fieldModel.name + ' ' + url); + console.log(fieldModel.$value); + if (url.indexOf(`${queryParamPrefix}${fieldModel.name}${queryParamSuffix}`) > -1 && fieldModel.$value.length > 0) { + url = url.replace(`${queryParamPrefix}${fieldModel.name}${queryParamSuffix}`, fieldModel.$value); + } + } + + if (url.split(queryParamPrefix).length > 1) { + console.error('** we have missing query params ** ', url); + throw Error(`** we have missing query params ** ${url}`); + } + + return url; + + } + + async invoke(endpoint, body, headers = {}) { + try { + const { operation } = this.props; + let url = this.addParamsToUrl(endpoint.path, operation.parameters || []); + if (endpoint.method.toLocaleLowerCase() === 'get') { + url = url + '?' + qs.stringify(body || ''); + } + const myHeaders = new Headers(); + for (const [key, value] of Object.entries(headers)) { + myHeaders.append(key, `${value}`); + } + + const request = new Request(url, { + method: endpoint.method, + credentials: 'include', + redirect: 'manual', + headers: myHeaders, + body: (body) ? JSON.stringify(body) : undefined, + }); + + const result = await fetch(request); + + const contentType = result.headers.get('content-type'); + if (contentType && contentType.indexOf('application/json') !== -1) { + // successful cross-domain connect/ability + const resp = await result.json(); + + return { json: resp, statusCode: result.status, _fetchRes: result }; + } else if (result.status === 200 && contentType && contentType.indexOf('text/plain') !== -1) { + const resp = await result.text(); + return { resp, _fetchRes: result }; + } else { + if (result && result.type && result.type === 'opaqueredirect') { + return { + json: { + endpoint, + error_code: 'RECEIVED_LOGIN_REDIRECT', + details: 'Your session expired. Please refresh the page.', + severity: 'error', + }, + }; + } + + return { + json: { + endpoint, + error_code: 'INVALID_SERVER_RESPONSE', + details: 'Either server not authenticated or error on server', + severity: 'error', + }, + }; + } + } catch (error) { + console.error(error); + } + + } + + render() { + const { operation } = this.props; + const requestBodyContent = operation.requestBody && operation.requestBody.content && operation.requestBody.content; + const hasBodySample = requestBodyContent && requestBodyContent.hasSample; + const samples = operation.codeSamples; + const mediaTypes = (requestBodyContent && requestBodyContent.mediaTypes) ? requestBodyContent.mediaTypes : []; + const { result } = this.state; + return ( +
+

Console

+ {hasBodySample && ( + (this.consoleEditor = editor)} + /> + )} + {false && samples.map(sample => ( + + ))} + + Send Request + + {result && + + } +
+ ); + } + + getSchema() { + const { operation } = this.props; + const requestBodyContent = operation.requestBody && operation.requestBody.content && operation.requestBody.content; + const mediaTypes = (requestBodyContent && requestBodyContent.mediaTypes) ? requestBodyContent.mediaTypes : []; + + if (!mediaTypes.length) { + return null; + } + const schema: Schema = { + }; + for (const mediaType of mediaTypes) { + if (mediaType.name.indexOf('json') > -1) { + if (mediaType.schema) { + // schema = mediaType.schema; + schema.rawSchema = mediaType.schema && mediaType.schema.rawSchema; + console.log('rawSchema : ' + JSON.stringify(schema)); + console.log('schema : ' + JSON.stringify(mediaType.schema.schema)); + schema._$ref = mediaType.schema && mediaType.schema._$ref; + } + break; + } + } + + return schema; + + } +} diff --git a/src/components/Console/index.ts b/src/components/Console/index.ts new file mode 100644 index 00000000..129986fe --- /dev/null +++ b/src/components/Console/index.ts @@ -0,0 +1,2 @@ +export * from './ConsoleEditor'; +export * from './ConsoleViewer'; \ No newline at end of file diff --git a/src/components/Fields/Field.tsx b/src/components/Fields/Field.tsx index e896ed2f..c9fc8ee6 100644 --- a/src/components/Fields/Field.tsx +++ b/src/components/Fields/Field.tsx @@ -13,7 +13,7 @@ import { WrappedShelfIcon, } from '../../common-elements/fields-layout'; -import { ShelfIcon } from '../../common-elements/'; +import { ShelfIcon, TextField } from '../../common-elements/'; import { FieldModel } from '../../services/models'; import { Schema, SchemaOptions } from '../Schema/Schema'; @@ -33,6 +33,12 @@ export class Field extends React.Component { toggle = () => { this.props.field.toggle(); }; + + onFieldChange = e => { + console.log('Textfield value is ' + e.target.placeholder + ' - ' + e.target.value); + this.props.field.setValue(e.target.value); + }; + render() { const { className, field, isLast } = this.props; const { name, expanded, deprecated, required, kind } = field; @@ -53,12 +59,12 @@ export class Field extends React.Component { {required && required } ) : ( - - - {name} - {required && required } - - ); + + + {name} + {required && required } + + ); return ( <> @@ -67,6 +73,9 @@ export class Field extends React.Component { + {field && field.in === 'path' && + + } {field.expanded && withSubSchema && ( diff --git a/src/components/Operation/Operation.tsx b/src/components/Operation/Operation.tsx index 1283a6ad..97bd6f33 100644 --- a/src/components/Operation/Operation.tsx +++ b/src/components/Operation/Operation.tsx @@ -3,11 +3,12 @@ import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement import { observer } from 'mobx-react'; -import { Badge, DarkRightPanel, H2, MiddlePanel, Row } from '../../common-elements'; +import { Badge, ConsoleButton, DarkRightPanel, FlexLayoutReverse, H2, MiddlePanel, Row, Toggle } from '../../common-elements'; import { OptionsContext } from '../OptionsProvider'; import { ShareLink } from '../../common-elements/linkify'; +import { ConsoleViewer } from '../Console/ConsoleViewer'; import { Endpoint } from '../Endpoint/Endpoint'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { Markdown } from '../Markdown/Markdown'; @@ -20,28 +21,57 @@ import { OperationModel as OperationType } from '../../services/models'; import styled from '../../styled-components'; import { Extensions } from '../Fields/Extensions'; -const OperationRow = styled(Row)` +const OperationRow = Row.extend` backface-visibility: hidden; contain: content; overflow: hidden; + position: relative; + + &:after { + position: absolute; + bottom: 0; + width: 100%; + display: block; + content: ''; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + } `; const Description = styled.div` margin-bottom: ${({ theme }) => theme.spacing.unit * 6}px; `; - export interface OperationProps { operation: OperationType; } +export interface OperationState { + executeMode: boolean; +} + @observer -export class Operation extends React.Component { +export class Operation extends React.Component { + + constructor(props) { + super(props); + this.state = { + executeMode: false, + }; + } + + onConsoleClick = () => { + this.setState({ + executeMode: !this.state.executeMode, + }); + } + render() { const { operation } = this.props; + const { executeMode } = this.state; const { name: summary, description, deprecated, externalDocs } = operation; const hasDescription = !!(description || externalDocs); + const consoleButtonLabel = (executeMode) ? 'Hide Console' : 'Show Console'; return ( @@ -52,6 +82,11 @@ export class Operation extends React.Component { {summary} {deprecated && Deprecated } + {options.enableConsole && + + {consoleButtonLabel} + + } {options.pathInMiddlePanel && } {hasDescription && ( @@ -66,8 +101,17 @@ export class Operation extends React.Component { {!options.pathInMiddlePanel && } - - + {executeMode && +
+ +
+ } + {!executeMode && + + } + {!executeMode && + + }
)} diff --git a/src/polyfills.ts b/src/polyfills.ts index 5c8a5635..f4eaaaf3 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -10,3 +10,5 @@ import 'core-js/es/symbol'; import 'unfetch/polyfill/index'; import 'url-polyfill'; + +import 'whatwg-fetch'; diff --git a/src/services/RedocNormalizedOptions.ts b/src/services/RedocNormalizedOptions.ts index 1bcffb27..668cd7de 100644 --- a/src/services/RedocNormalizedOptions.ts +++ b/src/services/RedocNormalizedOptions.ts @@ -9,6 +9,8 @@ export interface RedocRawOptions { theme?: ThemeInterface; scrollYOffset?: number | string | (() => number); hideHostname?: boolean | string; + enableConsole?: boolean; + additionalHeaders?: object; expandResponses?: string | 'all'; requiredPropsFirst?: boolean | string; sortPropsAlphabetically?: boolean | string; @@ -25,6 +27,11 @@ export interface RedocRawOptions { menuToggle?: boolean | string; jsonSampleExpandLevel?: number | string | 'all'; + providedByName?: string; + providedByUri?: string; + queryParamPrefix?: string; + queryParamSuffix?: string; + unstable_ignoreMimeParameters?: boolean; allowedMdComponents?: Dict; @@ -141,6 +148,12 @@ export class RedocNormalizedOptions { menuToggle: boolean; jsonSampleExpandLevel: number; enumSkipQuotes: boolean; + enableConsole: boolean; + additionalHeaders: object; + providedByName: string; + providedByUri: string; + queryParamPrefix: string; + queryParamSuffix: string; /* tslint:disable-next-line */ unstable_ignoreMimeParameters: boolean; @@ -177,6 +190,12 @@ export class RedocNormalizedOptions { raw.jsonSampleExpandLevel, ); this.enumSkipQuotes = argValueToBoolean(raw.enumSkipQuotes); + this.enableConsole = argValueToBoolean(raw.enableConsole); + this.additionalHeaders = raw.additionalHeaders || {}; + this.providedByName = raw.providedByName || 'Documentation Powered by ReDoc'; + this.providedByUri = raw.providedByUri || 'https://github.com/Rebilly/ReDoc'; + this.queryParamPrefix = raw.queryParamPrefix || '{'; + this.queryParamSuffix = raw.queryParamSuffix || '}'; this.unstable_ignoreMimeParameters = argValueToBoolean(raw.unstable_ignoreMimeParameters); diff --git a/src/services/SearchStore.ts b/src/services/SearchStore.ts index 669d8d05..ac7c57b8 100644 --- a/src/services/SearchStore.ts +++ b/src/services/SearchStore.ts @@ -9,7 +9,7 @@ let worker: new () => Worker; if (IS_BROWSER) { try { // tslint:disable-next-line - worker = require('workerize-loader?inline&fallback=false!./SearchWorker.worker'); + worker = require('workerize-loader?fallback=false!./SearchWorker.worker'); } catch (e) { worker = require('./SearchWorker.worker').default; } diff --git a/src/services/models/Field.ts b/src/services/models/Field.ts index 5302a09f..d1c17697 100644 --- a/src/services/models/Field.ts +++ b/src/services/models/Field.ts @@ -29,9 +29,8 @@ function getDefaultStyleValue(parameterLocation: OpenAPIParameterLocation): Open * Field or Parameter model ready to be used by components */ export class FieldModel { - @observable - expanded: boolean = false; - + @observable expanded: boolean = false; + @observable $value: string = ''; schema: SchemaModel; name: string; required: boolean; @@ -92,4 +91,9 @@ export class FieldModel { toggle() { this.expanded = !this.expanded; } + + @action + setValue(value: string) { + this.$value = value; + } } diff --git a/src/theme.ts b/src/theme.ts index 4b9f37f2..6b20de7b 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -150,6 +150,9 @@ const defaultTheme: ThemeInterface = { codeSample: { backgroundColor: ({ rightPanel }) => darken(0.1, rightPanel.backgroundColor), }, + styledPre: { + maxHeight: '500px', + }, }; export default defaultTheme; @@ -324,6 +327,9 @@ export interface ResolvedThemeInterface { }; extensionsHook?: (name: string, props: any) => string; + styledPre: { + maxHeight: string; + }; } export type primitive = string | number | boolean | undefined | null; diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts new file mode 100644 index 00000000..b332d125 --- /dev/null +++ b/src/utils/fetch.ts @@ -0,0 +1 @@ +//parseparseFetchFetch \ No newline at end of file diff --git a/src/utils/loadAndBundleSpec.ts b/src/utils/loadAndBundleSpec.ts index d2431b8d..e8d3532d 100644 --- a/src/utils/loadAndBundleSpec.ts +++ b/src/utils/loadAndBundleSpec.ts @@ -9,11 +9,20 @@ export async function loadAndBundleSpec(specUrlOrObject: object | string): Promi resolve: { http: { withCredentials: false } }, } as object)) as any; + let v2Specs = spec; if (spec.swagger !== undefined) { - return convertSwagger2OpenAPI(spec); - } else { - return spec; + v2Specs = await convertSwagger2OpenAPI(spec); } + + // we can derefrence the schema here for future use. + // import { cloneDeep } from 'lodash'; + // const derefrencedSpec = await parser.dereference(cloneDeep(spec)); + // const derefed = await parser.dereference(v2Specs, { + // resolve: { http: { withCredentials: false } }, + // } as object); + + return v2Specs; + } export function convertSwagger2OpenAPI(spec: any): Promise { diff --git a/tsconfig.json b/tsconfig.json index 6a327741..466ea081 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,4 +39,4 @@ "./src/**/*.ts?", "demo/*.tsx" ] -} +} \ No newline at end of file diff --git a/tslint.json b/tslint.json index 9c980dcb..feec7e40 100644 --- a/tslint.json +++ b/tslint.json @@ -17,7 +17,7 @@ "quotemark": [true, "single", "avoid-template", "jsx-double"], "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"], "arrow-parens": [true, "ban-single-arg-parens"], - "no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js"], + "no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js", "brace"], "object-literal-key-quotes": [true, "as-needed"], "no-unused-expression": [true, "allow-tagged-template"], "semicolon": [true, "always", "ignore-bound-class-methods"],